Browse Source

webui: add rust UI base code.

Nick Peng 1 year ago
parent
commit
600d1a349f

+ 1 - 0
.gitignore

@@ -4,5 +4,6 @@
 *.pem
 .DS_Store
 *.swp.
+*.a
 systemd/smartdns.service
 test.bin

+ 2 - 0
plugin/smartdns-ui/.gitignore

@@ -0,0 +1,2 @@
+/target
+Cargo.lock

+ 37 - 0
plugin/smartdns-ui/Cargo.toml

@@ -0,0 +1,37 @@
+[package]
+name = "smartdns-ui"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+
+[dependencies]
+libc = "0.2.155"
+ctor = "0.2.8"
+bytes = "1.6.1"
+rusqlite = { version = "0.32.0", features = ["bundled"] }
+hyper = { version = "1.4.1", features = ["full"] }
+hyper-util = { version = "0.1.6", features = ["full"] }
+hyper-tungstenite = "0.14.0"
+tokio = { version = "1.38.1", features = ["full"] }
+serde = { version = "1.0.204", features = ["derive"] }
+tokio-rustls = "0.26.0"
+rustls-pemfile = "2.1.2"
+serde_json = "1.0.120"
+http-body-util = "0.1.2"
+getopts = "0.2.21"
+url = "2.5.2"
+jsonwebtoken = "9"
+matchit = "0.8.4"
+futures = "0.3.30"
+socket2 = "0.5.7"
+
+[features]
+build-release = []
+
+[dev-dependencies]
+reqwest = {version = "0.12.5", features = ["blocking"]}
+tungstenite = "0.23.0"
+tokio-tungstenite = "0.23.1"
+tempfile = "3.10.0"

+ 50 - 0
plugin/smartdns-ui/Makefile

@@ -0,0 +1,50 @@
+
+# Copyright (C) 2018-2023 Ruilin Peng (Nick) <[email protected]>.
+#
+# smartdns is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# smartdns is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+BIN=smartdns-ui
+SMARTDNS_SRC_DIR=../../src
+
+ifdef DEBUG
+CARGO_BUILD_TYPE=
+CARGO_BUILD_PATH=target/debug
+SMARTDNS_BUILD_TYPE=DEBUG=1
+else
+CARGO_BUILD_TYPE=--release
+CARGO_BUILD_PATH=target/release
+SMARTDNS_BUILD_TYPE=
+endif
+
+.PHONY: all clean $(BIN)
+
+all: $(BIN)
+
+test-prepare:
+	$(MAKE) -C $(SMARTDNS_SRC_DIR) libsmartdns-test.a
+
+smartdns:
+	$(MAKE) -C $(SMARTDNS_SRC_DIR) $(SMARTDNS_BUILD_TYPE)
+
+$(BIN): smartdns
+	MAKEFLAGS= cargo build $(CARGO_BUILD_TYPE) --features "build-release"
+	cp $(CARGO_BUILD_PATH)/libsmartdns_ui.so target/ 
+
+test: test-prepare
+	MAKEFLAGS= cargo test
+
+clean:
+	cargo clean
+	$(MAKE) -C $(SMARTDNS_SRC_DIR) clean
+	rm -rf target/libsmartdns_ui.so

+ 25 - 0
plugin/smartdns-ui/build.rs

@@ -0,0 +1,25 @@
+
+use std::env;
+
+fn link_smartdns_lib() {
+    let curr_source_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
+    let smartdns_src_dir = format!("{}/../../src", curr_source_dir);
+    let smartdns_lib_file = format!("{}/libsmartdns-test.a", smartdns_src_dir);
+
+    /*
+    to run tests, please run the following command:
+    make test-prepare
+    */
+    if std::path::Path::new(&smartdns_lib_file).exists() && !cfg!(feature = "build-release") {
+        println!("cargo:rerun-if-changed={}", smartdns_lib_file);
+        println!("cargo:rustc-link-lib=static=smartdns-test");
+        println!("cargo:rustc-link-lib=ssl");
+        println!("cargo:rustc-link-lib=crypto");
+        println!("cargo:rustc-link-search=native={}", smartdns_src_dir);
+        println!("cargo:warning=link smartdns-test library");
+    }
+}
+
+fn main() {
+    link_smartdns_lib();
+}

+ 263 - 0
plugin/smartdns-ui/src/data_server.rs

@@ -0,0 +1,263 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use crate::db::*;
+use crate::dns_log;
+use crate::smartdns::*;
+
+use std::error::Error;
+use std::sync::{Arc, Mutex, RwLock};
+use std::thread;
+use tokio::sync::mpsc;
+use tokio::time::{interval_at, Duration, Instant};
+
+#[derive(Clone)]
+pub struct DataServerConfig {
+    pub data_root: String,
+    pub max_log_age_ms: u32,
+}
+
+impl DataServerConfig {
+    pub fn new() -> Self {
+        DataServerConfig {
+            data_root: Plugin::dns_conf_data_dir() + "/ui.db",
+            // max_log_age_ms: 7 * 24 * 60 * 60 * 1000,
+            max_log_age_ms: 60 * 60 * 1000,
+        }
+    }
+}
+
+pub struct DataServerControl {
+    data_server: Arc<DataServer>,
+    server_thread: Mutex<Option<thread::JoinHandle<()>>>,
+}
+
+impl DataServerControl {
+    pub fn new() -> Self {
+        DataServerControl {
+            data_server: Arc::new(DataServer::new()),
+            server_thread: Mutex::new(None),
+        }
+    }
+
+    pub fn get_data_server(&self) -> Arc<DataServer> {
+        Arc::clone(&self.data_server)
+    }
+
+    pub fn start_data_server(&self, conf: &DataServerConfig) -> Result<(), Box<dyn Error>> {
+        let inner_clone = Arc::clone(&self.data_server);
+
+        let ret = inner_clone.init_server(conf);
+        if let Err(e) = ret {
+            return Err(e);
+        }
+
+        let server_thread = thread::spawn(move || {
+            let ret = DataServer::data_server_loop(inner_clone);
+            if let Err(e) = ret {
+                dns_log!(LogLevel::ERROR, "data server error: {}", e);
+                Plugin::smartdns_exit(1);
+            }
+        });
+
+        *self.server_thread.lock().unwrap() = Some(server_thread);
+        Ok(())
+    }
+
+    pub fn stop_data_server(&self) {
+        self.data_server.stop_data_server();
+        let _server_thread = self.server_thread.lock().unwrap().take();
+        if let Some(server_thread) = _server_thread {
+            server_thread.join().unwrap();
+        }
+    }
+
+    pub fn send_request(&self, request: &mut DnsRequest) -> Result<(), Box<dyn Error>> {
+        if let Some(tx) = self.data_server.data_tx.as_ref() {
+            tx.try_send(request.clone())?;
+        }
+        Ok(())
+    }
+}
+
+impl Drop for DataServerControl {
+    fn drop(&mut self) {
+        self.stop_data_server();
+    }
+}
+
+pub struct DataServer {
+    conf: RwLock<DataServerConfig>,
+    notify_tx: Option<mpsc::Sender<()>>,
+    notify_rx: Mutex<Option<mpsc::Receiver<()>>>,
+    data_tx: Option<mpsc::Sender<DnsRequest>>,
+    data_rx: Mutex<Option<mpsc::Receiver<DnsRequest>>>,
+    db: DB,
+}
+
+impl DataServer {
+    pub fn new() -> Self {
+        let mut plugin = DataServer {
+            conf: RwLock::new(DataServerConfig::new()),
+            notify_tx: None,
+            notify_rx: Mutex::new(None),
+            data_tx: None,
+            data_rx: Mutex::new(None),
+            db: DB::new(),
+        };
+
+        let (tx, rx) = mpsc::channel(100);
+        plugin.notify_tx = Some(tx);
+        plugin.notify_rx = Mutex::new(Some(rx));
+
+        let (tx, rx) = mpsc::channel(4096);
+        plugin.data_tx = Some(tx);
+        plugin.data_rx = Mutex::new(Some(rx));
+
+        plugin
+    }
+
+    fn init_server(&self, conf: &DataServerConfig) -> Result<(), Box<dyn Error>> {
+        let mut conf_clone = self.conf.write().unwrap();
+        *conf_clone = conf.clone();
+        dns_log!(LogLevel::INFO, "open db: {}", conf_clone.data_root);
+        let ret = self.db.open(&conf_clone.data_root);
+        if let Err(e) = ret {
+            dns_log!(LogLevel::ERROR, "open db error: {}", e);
+            return Err(e);
+        }
+        Ok(())
+    }
+
+    pub fn get_domain_list(
+        &self,
+        param: &DomainListGetParam,
+    ) -> Result<Vec<DomainData>, Box<dyn Error>> {
+        self.db.get_domain_list(param)
+    }
+
+    pub fn get_domain_list_count(&self) -> u32 {
+        self.db.get_domain_list_count()
+    }
+
+    pub fn delete_domain_by_id(&self, id: u64) -> Result<u64, Box<dyn Error>> {
+        self.db.delete_domain_by_id(id)
+    }
+
+    pub fn delete_domain_before_timestamp(&self, timestamp: u64) -> Result<u64, Box<dyn Error>> {
+        self.db.delete_domain_before_timestamp(timestamp)
+    }
+
+    pub fn get_client_list(&self) -> Result<Vec<ClientData>, Box<dyn Error>> {
+        self.db.get_client_list()
+    }
+
+    pub fn insert_domain(&self, data: &DomainData) -> Result<(), Box<dyn Error>> {
+        self.db.insert_domain(data)
+    }
+
+    async fn data_server_handle(this: Arc<DataServer>, req: DnsRequest) {
+        let domain_data = DomainData {
+            id: 0,
+            domain: req.get_domain(),
+            domain_type: req.get_qtype(),
+            client: req.get_remote_addr(),
+            domain_group: req.get_group_name(),
+            reply_code: req.get_rcode(),
+            timestamp: req.get_query_time(),
+        };
+        dns_log!(
+            LogLevel::DEBUG,
+            "insert domain:{}, type:{}",
+            domain_data.domain,
+            domain_data.domain_type
+        );
+
+        let ret = this.insert_domain(&domain_data);
+        if let Err(e) = ret {
+            dns_log!(LogLevel::ERROR, "insert domain error: {}", e);
+        }
+    }
+
+    async fn hourly_work(this: Arc<DataServer>) {
+        dns_log!(LogLevel::INFO, "start hourly work");
+        let now = get_utc_time_ms();
+
+        let ret = this
+            .delete_domain_before_timestamp(now - this.conf.read().unwrap().max_log_age_ms as u64);
+        if let Err(e) = ret {
+            dns_log!(
+                LogLevel::WARN,
+                "delete domain before timestamp error: {}",
+                e
+            );
+        }
+    }
+
+    #[tokio::main]
+    async fn data_server_loop(this: Arc<DataServer>) -> Result<(), Box<dyn Error>> {
+        let mut rx: mpsc::Receiver<()>;
+        let mut data_rx: mpsc::Receiver<DnsRequest>;
+
+        {
+            let mut _rx = this.notify_rx.lock().unwrap();
+            rx = _rx.take().unwrap();
+            let mut _rx = this.data_rx.lock().unwrap();
+            data_rx = _rx.take().unwrap();
+        }
+
+        let start = Instant::now() + Duration::from_secs(60);
+        let mut hour_timer = interval_at(start, Duration::from_secs(60 * 60));
+
+        loop {
+            tokio::select! {
+                _ = rx.recv() => {
+                    break;
+                }
+                _ = hour_timer.tick() => {
+                    DataServer::hourly_work(this.clone()).await;
+                }
+                res = data_rx.recv() => {
+                    match res {
+                        Some(req) => {
+                            DataServer::data_server_handle(this.clone(), req).await;
+                        }
+                        None => {
+                            continue;
+                        }
+                    }
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn stop_data_server(&self) {
+        if let Some(tx) = self.notify_tx.as_ref().cloned() {
+            let t = thread::spawn(move || {
+                let rt = tokio::runtime::Runtime::new().unwrap();
+                rt.block_on(async move {
+                    _ = tx.send(()).await;
+                });
+            });
+
+            let _ = t.join();
+        }
+    }
+}

+ 458 - 0
plugin/smartdns-ui/src/db.rs

@@ -0,0 +1,458 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use crate::dns_log;
+use crate::smartdns::*;
+use std::error::Error;
+use std::fs;
+use std::sync::Mutex;
+
+use rusqlite::{Connection, OpenFlags, Result};
+
+pub struct DB {
+    conn: Mutex<Option<Connection>>,
+}
+
+pub struct ClientData {
+    pub id: u32,
+    pub client_ip: String,
+}
+
+pub struct ConfigData {
+    pub key: String,
+    pub value: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct DomainData {
+    pub id: u64,
+    pub timestamp: u64,
+    pub domain: String,
+    pub domain_type: u32,
+    pub client: String,
+    pub domain_group: String,
+    pub reply_code: u16,
+}
+
+pub struct DomainListGetParam {
+    pub id: Option<u64>,
+    pub order: Option<String>,
+    pub page_num: u32,
+    pub page_size: u32,
+    pub domain: Option<String>,
+    pub domain_type: Option<u32>,
+    pub client: Option<String>,
+    pub domain_group: Option<String>,
+    pub reply_code: Option<u16>,
+    pub timestamp_before: Option<u64>,
+    pub timestamp_after: Option<u64>,
+}
+
+impl DomainListGetParam {
+    pub fn new() -> Self {
+        DomainListGetParam {
+            id: None,
+            page_num: 1,
+            order: None,
+            page_size: 10,
+            domain: None,
+            domain_type: None,
+            client: None,
+            domain_group: None,
+            reply_code: None,
+            timestamp_before: None,
+            timestamp_after: None,
+        }
+    }
+}
+
+impl DB {
+    pub fn new() -> Self {
+        DB {
+            conn: Mutex::new(None),
+        }
+    }
+
+    fn init_db(&self, conn: &Connection) -> Result<()> {
+        conn.execute(
+            "CREATE TABLE IF NOT EXISTS domain (
+                id INTEGER PRIMARY KEY AUTOINCREMENT,
+                timestamp BIGINT NOT NULL,
+                domain TEXT NOT NULL,
+                domain_type INTEGER NOT NULL,
+                client TEXT NOT NULL,
+                domain_group TEXT NOT NULL,
+                reply_code INTEGER NOT NULL
+            )",
+            [],
+        )?;
+
+        conn.execute(
+            "
+        CREATE TABLE IF NOT EXISTS client (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            client_ip TEXT NOT NULL
+        )",
+            [],
+        )?;
+
+        conn.execute(
+            "CREATE TABLE IF NOT EXISTS config (
+                key TEXT PRIMARY KEY,
+                value TEXT NOT NULL
+            )",
+            [],
+        )?;
+
+        Ok(())
+    }
+
+    pub fn open(&self, path: &str) -> Result<(), Box<dyn Error>> {
+        let ruconn: std::result::Result<Connection, rusqlite::Error> =
+            Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_WRITE);
+        let mut conn = self.conn.lock().unwrap();
+        if let Err(_) = ruconn {
+            let ruconn = Connection::open_with_flags(
+                path,
+                OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
+            )?;
+
+            let ret = self.init_db(&ruconn);
+            if let Err(e) = ret {
+                _ = ruconn.close();
+                fs::remove_file(path)?;
+                return Err(Box::new(e));
+            }
+
+            *conn = Some(ruconn);
+        } else {
+            *conn = Some(ruconn.unwrap());
+        }
+
+        conn.as_ref()
+            .unwrap()
+            .execute("PRAGMA synchronous = OFF", [])?;
+        conn.as_ref()
+            .unwrap()
+            .query_row("PRAGMA journal_mode = WAL", [], |_| Ok(()))?;
+        Ok(())
+    }
+
+    pub fn insert_config(&self, conf: &ConfigData) -> Result<(), Box<dyn Error>> {
+        let conn = self.conn.lock().unwrap();
+        if conn.as_ref().is_none() {
+            return Ok(());
+        }
+
+        let conn = conn.as_ref().unwrap();
+        let mut stmt = conn
+            .prepare("INSERT OR REPLACE INTO config (key, value) VALUES (?1, ?2)")
+            .unwrap();
+        let ret = stmt.execute(&[&conf.key, &conf.value]);
+
+        if let Err(e) = ret {
+            return Err(Box::new(e));
+        }
+
+        Ok(())
+    }
+
+    pub fn get_config(&self, key: &str) -> Result<Option<String>, Box<dyn Error>> {
+        let conn = self.conn.lock().unwrap();
+        if conn.as_ref().is_none() {
+            return Ok(None);
+        }
+
+        let conn = conn.as_ref().unwrap();
+        let mut stmt = conn
+            .prepare("SELECT value FROM config WHERE key = ?")
+            .unwrap();
+        let rows = stmt.query_map(&[&key], |row| Ok(row.get(0)?));
+
+        if let Ok(rows) = rows {
+            for row in rows {
+                if let Ok(row) = row {
+                    return Ok(Some(row));
+                }
+            }
+        }
+
+        Ok(None)
+    }
+
+    pub fn insert_domain(&self, data: &DomainData) -> Result<(), Box<dyn Error>> {
+        let conn = self.conn.lock().unwrap();
+        if conn.as_ref().is_none() {
+            return Ok(());
+        }
+
+        let conn = conn.as_ref().unwrap();
+        let mut stmt = conn.prepare("INSERT INTO domain (timestamp, domain, domain_type, client, domain_group, reply_code) VALUES (?1, ?2, ?3, ?4, ?5, ?6)").unwrap();
+        let ret = stmt.execute(&[
+            &data.timestamp.to_string(),
+            &data.domain,
+            &data.domain_type.to_string(),
+            &data.client,
+            &data.domain_group,
+            &data.reply_code.to_string(),
+        ]);
+
+        if let Err(e) = ret {
+            return Err(Box::new(e));
+        }
+
+        Ok(())
+    }
+
+    pub fn get_domain_list_count(&self) -> u32 {
+        let conn = self.conn.lock().unwrap();
+        if conn.as_ref().is_none() {
+            return 0;
+        }
+
+        let conn = conn.as_ref().unwrap();
+
+        let mut stmt = conn.prepare("SELECT COUNT(*) FROM domain").unwrap();
+        let rows = stmt.query_map([], |row| Ok(row.get(0)?));
+
+        if let Ok(rows) = rows {
+            for row in rows {
+                if let Ok(row) = row {
+                    return row;
+                }
+            }
+        }
+
+        0
+    }
+
+    pub fn delete_domain_by_id(&self, id: u64) -> Result<u64, Box<dyn Error>> {
+        let conn = self.conn.lock().unwrap();
+        if conn.as_ref().is_none() {
+            return Ok(0);
+        }
+
+        let conn = conn.as_ref().unwrap();
+
+        let ret = conn.execute("DELETE FROM domain WHERE id = ?", &[&id]);
+
+        if let Err(e) = ret {
+            return Err(Box::new(e));
+        }
+
+        Ok(ret.unwrap() as u64)
+    }
+
+    pub fn delete_domain_before_timestamp(&self, timestamp: u64) -> Result<u64, Box<dyn Error>> {
+        let conn = self.conn.lock().unwrap();
+        if conn.as_ref().is_none() {
+            return Ok(0);
+        }
+
+        let conn = conn.as_ref().unwrap();
+
+        let ret = conn.execute("DELETE FROM domain WHERE timestamp <= ?", &[&timestamp]);
+
+        if let Err(e) = ret {
+            return Err(Box::new(e));
+        }
+
+        Ok(ret.unwrap() as u64)
+    }
+
+    pub fn get_domain_list(
+        &self,
+        param: &DomainListGetParam,
+    ) -> Result<Vec<DomainData>, Box<dyn Error>> {
+        let mut ret = Vec::new();
+        let conn = self.conn.lock().unwrap();
+        if conn.as_ref().is_none() {
+            return Ok(ret);
+        }
+
+        let mut sql_where = String::new();
+        let mut sql_order = String::new();
+        let mut sql_param: Vec<String> = Vec::new();
+
+        if let Some(v) = &param.id {
+            if !sql_where.is_empty() {
+                sql_where.push_str(" AND ");
+            }
+            sql_where.push_str("id = ?");
+            sql_param.push(v.to_string());
+        }
+
+        if let Some(v) = &param.domain {
+            if !sql_where.is_empty() {
+                sql_where.push_str(" AND ");
+            }
+            sql_where.push_str("domain = ?");
+            sql_param.push(v.to_string());
+        }
+
+        if let Some(v) = &param.domain_type {
+            if !sql_where.is_empty() {
+                sql_where.push_str(" AND ");
+            }
+            sql_where.push_str("domain_type = ?");
+            sql_param.push(v.to_string());
+        }
+
+        if let Some(v) = &param.client {
+            if !sql_where.is_empty() {
+                sql_where.push_str(" AND ");
+            }
+            sql_where.push_str("client = ?");
+            sql_param.push(v.clone());
+        }
+
+        if let Some(v) = &param.domain_group {
+            if !sql_where.is_empty() {
+                sql_where.push_str(" AND ");
+            }
+            sql_where.push_str("domain_group = ?");
+            sql_param.push(v.clone());
+        }
+
+        if let Some(v) = &param.reply_code {
+            if !sql_where.is_empty() {
+                sql_where.push_str(" AND ");
+            }
+            sql_where.push_str("reply_code = ?");
+            sql_param.push(v.to_string());
+        }
+
+        if let Some(v) = &param.timestamp_before {
+            if !sql_where.is_empty() {
+                sql_where.push_str(" AND ");
+            }
+            sql_where.push_str("timestamp <= ?");
+            sql_param.push(v.to_string());
+        }
+
+        if let Some(v) = &param.timestamp_after {
+            if !sql_where.is_empty() {
+                sql_where.push_str(" AND ");
+            }
+            sql_where.push_str("timestamp >= ?");
+            sql_param.push(v.to_string());
+        }
+
+        if let Some(v) = &param.order {
+            if v.eq_ignore_ascii_case("asc") {
+                sql_order.push_str(" ORDER BY id ASC");
+            } else if v.eq_ignore_ascii_case("desc") {
+                sql_order.push_str(" ORDER BY id DESC");
+            } else {
+                return Err("order param error".into());
+            }
+        } else {
+            sql_order.push_str(" ORDER BY id DESC");
+        }
+
+        let mut sql = String::new();
+        sql.push_str("SELECT id, timestamp, domain, domain_type, client, domain_group, reply_code FROM domain");
+
+        if !sql_where.is_empty() {
+            sql.push_str(" WHERE ");
+            sql.push_str(sql_where.as_str());
+        }
+
+        sql.push_str(sql_order.as_str());
+        sql.push_str(" LIMIT ? OFFSET ?");
+
+        sql_param.push(param.page_size.to_string());
+        sql_param.push(((param.page_num - 1) * param.page_size).to_string());
+
+        let conn = conn.as_ref().unwrap();
+        let stmt = conn.prepare(&sql);
+
+        if let Err(e) = stmt {
+            dns_log!(LogLevel::ERROR, "get_domain_list error: {}", e);
+            return Err("get_domain_list error".into());
+        }
+
+        let mut stmt = stmt.unwrap();
+
+        let rows = stmt.query_map(rusqlite::params_from_iter(sql_param), |row| {
+            Ok(DomainData {
+                id: row.get(0)?,
+                timestamp: row.get(1)?,
+                domain: row.get(2)?,
+                domain_type: row.get(3)?,
+                client: row.get(4)?,
+                domain_group: row.get(5)?,
+                reply_code: row.get(6)?,
+            })
+        });
+
+        if let Ok(rows) = rows {
+            for row in rows {
+                if let Ok(row) = row {
+                    ret.push(row);
+                }
+            }
+        }
+
+        Ok(ret)
+    }
+
+    pub fn get_client_list(&self) -> Result<Vec<ClientData>, Box<dyn Error>> {
+        let conn = self.conn.lock().unwrap();
+        if conn.as_ref().is_none() {
+            return Err("db is not open".into());
+        }
+
+        let conn = conn.as_ref().unwrap();
+        let mut ret = Vec::new();
+        let mut stmt = conn.prepare("SELECT id, client_ip FROM client").unwrap();
+        let rows = stmt.query_map([], |row| {
+            Ok(ClientData {
+                id: row.get(0)?,
+                client_ip: row.get(1)?,
+            })
+        });
+
+        if let Ok(rows) = rows {
+            for row in rows {
+                if let Ok(row) = row {
+                    ret.push(row);
+                }
+            }
+        }
+
+        Ok(ret)
+    }
+
+    pub fn close(&self) {
+        let mut conn = self.conn.lock().unwrap();
+        if conn.as_ref().is_none() {
+            return;
+        }
+
+        if let Some(t) = conn.take() {
+            let _ = t.close();
+        }
+    }
+}
+
+impl Drop for DB {
+    fn drop(&mut self) {
+        self.close();
+    }
+}

+ 353 - 0
plugin/smartdns-ui/src/http_api_msg.rs

@@ -0,0 +1,353 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use crate::db::*;
+use crate::smartdns::LogLevel;
+use serde_json::json;
+use std::error::Error;
+
+#[derive(Debug)]
+pub struct AuthUser {
+    pub user: String,
+    pub password: String,
+}
+
+#[derive(Debug)]
+pub struct TokenResponse {
+    pub token: String,
+    pub expires_in: String,
+}
+
+pub fn api_msg_parse_auth(data: &str) -> Result<AuthUser, Box<dyn Error>> {
+    let v: serde_json::Value = serde_json::from_str(data)?;
+    let user = v["user"].as_str();
+    if user.is_none() {
+        return Err("user not found".into());
+    }
+    let password = v["password"].as_str();
+    if password.is_none() {
+        return Err("password not found".into());
+    }
+
+    Ok(AuthUser {
+        user: user.unwrap().to_string(),
+        password: password.unwrap().to_string(),
+    })
+}
+
+pub fn api_msg_gen_auth_login(auth: &AuthUser) -> String {
+    let json_str = json!({
+        "user": auth.user,
+        "password": auth.password,
+    });
+
+    json_str.to_string()
+}
+
+pub fn api_msg_parse_count(data: &str) -> Result<i64, Box<dyn Error>> {
+    let v: serde_json::Value = serde_json::from_str(data)?;
+    let count = v["count"].as_i64();
+    if count.is_none() {
+        return Err("count not found".into());
+    }
+
+    Ok(count.unwrap())
+}
+
+pub fn api_msg_gen_count(count: i64) -> String {
+    let json_str = json!({
+        "count": count,
+    });
+
+    json_str.to_string()
+}
+
+pub fn api_msg_parse_domain(data: &str) -> Result<DomainData, Box<dyn Error>> {
+    let v: serde_json::Value = serde_json::from_str(data)?;
+    let id = v["id"].as_u64();
+    if id.is_none() {
+        return Err("id not found".into());
+    }
+    let timestamp = v["timestamp"].as_u64();
+    if timestamp.is_none() {
+        return Err("timestamp not found".into());
+    }
+    let domain = v["domain"].as_str();
+    if domain.is_none() {
+        return Err("domain not found".into());
+    }
+    let domain_type = v["domain-type"].as_u64();
+    if domain_type.is_none() {
+        return Err("domain-type not found".into());
+    }
+    let client = v["client"].as_str();
+    if client.is_none() {
+        return Err("client not found".into());
+    }
+    let domain_group = v["domain-group"].as_str();
+    if domain_group.is_none() {
+        return Err("domain-group not found".into());
+    }
+    let reply_code = v["reply-code"].as_u64();
+    if reply_code.is_none() {
+        return Err("reply-code not found".into());
+    }
+
+    Ok(DomainData {
+        id: id.unwrap(),
+        timestamp: timestamp.unwrap(),
+        domain: domain.unwrap().to_string(),
+        domain_type: domain_type.unwrap() as u32,
+        client: client.unwrap().to_string(),
+        domain_group: domain_group.unwrap().to_string(),
+        reply_code: reply_code.unwrap() as u16,
+    })
+}
+
+pub fn api_msg_gen_domain(domain: &DomainData) -> String {
+    let json_str = json!({
+        "id": domain.id,
+        "timestamp": domain.timestamp,
+        "domain": domain.domain,
+        "domain-type": domain.domain_type,
+        "client": domain.client,
+        "domain-group": domain.domain_group,
+        "reply-code": domain.reply_code,
+    });
+
+    json_str.to_string()
+}
+
+pub fn api_msg_parse_domain_list(data: &str) -> Result<Vec<DomainData>, Box<dyn Error>> {
+    let v: serde_json::Value = serde_json::from_str(data)?;
+    let list_count = v["list-count"].as_u64();
+    if list_count.is_none() {
+        return Err("list-count not found".into());
+    }
+    let list_count = list_count.unwrap();
+    let mut domain_list = Vec::new();
+    for i in 0..list_count {
+        let domain = &v["domian-list"][i as usize];
+        let id = domain["id"].as_u64();
+        if id.is_none() {
+            return Err("id not found".into());
+        }
+        let timestamp = domain["timestamp"].as_u64();
+        if timestamp.is_none() {
+            return Err("timestamp not found".into());
+        }
+        let domain_str = domain["domain"].as_str();
+        if domain_str.is_none() {
+            return Err("domain not found".into());
+        }
+        let domain_type = domain["domain-type"].as_u64();
+        if domain_type.is_none() {
+            return Err("domain-type not found".into());
+        }
+        let client = domain["client"].as_str();
+        if client.is_none() {
+            return Err("client not found".into());
+        }
+        let domain_group = domain["domain-group"].as_str();
+        if domain_group.is_none() {
+            return Err("domain-group not found".into());
+        }
+        let reply_code = domain["reply-code"].as_u64();
+        if reply_code.is_none() {
+            return Err("reply-code not found".into());
+        }
+
+        domain_list.push(DomainData {
+            id: id.unwrap(),
+            timestamp: timestamp.unwrap(),
+            domain: domain_str.unwrap().to_string(),
+            domain_type: domain_type.unwrap() as u32,
+            client: client.unwrap().to_string(),
+            domain_group: domain_group.unwrap().to_string(),
+            reply_code: reply_code.unwrap() as u16,
+        });
+    }
+
+    Ok(domain_list)
+}
+
+pub fn api_msg_gen_domain_list(domain_list: Vec<DomainData>, total_page: u32) -> String {
+    let json_str = json!({
+        "list-count": domain_list.len(),
+        "total-page": total_page,
+        "domian-list":
+            domain_list
+                .iter()
+                .map(|x| {
+                    let s = json!({
+                        "id": x.id,
+                        "timestamp": x.timestamp,
+                        "domain": x.domain,
+                        "domain-type": x.domain_type,
+                        "client": x.client,
+                        "domain-group": x.domain_group,
+                        "reply-code": x.reply_code,
+                    });
+                    s
+                })
+                .collect::<Vec<serde_json::Value>>()
+
+    });
+
+    json_str.to_string()
+}
+
+pub fn api_msg_gen_client_list(client_list: Vec<ClientData>) -> String {
+    let json_str = json!({
+        "list-count": client_list.len(),
+        "client-list":
+            client_list
+                .iter()
+                .map(|x| {
+                    let s = json!({
+                        "id": x.id,
+                        "client-ip": x.client_ip,
+                    });
+                    s
+                })
+                .collect::<Vec<serde_json::Value>>()
+
+    });
+
+    json_str.to_string()
+}
+
+pub fn api_msg_auth_token(token: &str, expired: &str) -> String {
+    let json_str = json!({
+        "token": token,
+        "expires-in": expired,
+    });
+
+    json_str.to_string()
+}
+
+pub fn api_msg_parse_auth_token(data: &str) -> Result<TokenResponse, Box<dyn Error>> {
+    let v: serde_json::Value = serde_json::from_str(data)?;
+    let token = v["token"].as_str();
+    if token.is_none() {
+        return Err("token not found".into());
+    }
+    let expired = v["expires-in"].as_str();
+    if expired.is_none() {
+        return Err("expires-in not found".into());
+    }
+
+    Ok(TokenResponse {
+        token: token.unwrap().to_string(),
+        expires_in: expired.unwrap().to_string(),
+    })
+}
+
+pub fn api_msg_gen_cache_number(cache_number: i32) -> String {
+    let json_str = json!({
+        "cache-number": cache_number,
+    });
+
+    json_str.to_string()
+}
+
+pub fn api_msg_parse_cache_number(data: &str) -> Result<i32, Box<dyn Error>> {
+    let v: serde_json::Value = serde_json::from_str(data)?;
+    let cache_number = v["cache-number"].as_i64();
+    if cache_number.is_none() {
+        return Err("cache-number not found".into());
+    }
+
+    Ok(cache_number.unwrap() as i32)
+}
+
+pub fn api_msg_error(msg: &str) -> String {
+    let json_str = json!({
+        "error": msg,
+    });
+
+    json_str.to_string()
+}
+
+pub fn api_msg_parse_error(data: &str) -> Result<String, Box<dyn Error>> {
+    let v: serde_json::Value = serde_json::from_str(data)?;
+    let error = v["error"].as_str();
+    if error.is_none() {
+        return Err("error not found".into());
+    }
+
+    Ok(error.unwrap().to_string())
+}
+
+pub fn api_msg_parse_loglevel(data: &str) -> Result<LogLevel, Box<dyn Error>> {
+    let v: serde_json::Value = serde_json::from_str(data)?;
+    let loglevel = v["log-level"].as_str();
+    if loglevel.is_none() {
+        return Err("loglevel not found".into());
+    }
+
+    let loglevel = loglevel.unwrap();
+    match loglevel {
+        "debug" => Ok(LogLevel::DEBUG),
+        "info" => Ok(LogLevel::INFO),
+        "notice" => Ok(LogLevel::NOTICE),
+        "warn" => Ok(LogLevel::WARN),
+        "error" => Ok(LogLevel::ERROR),
+        "fatal" => Ok(LogLevel::FATAL),
+        _ => Err("loglevel not found".into()),
+    }
+}
+
+pub fn api_msg_gen_loglevel(loglevel: LogLevel) -> String {
+    let loglevel = match loglevel {
+        LogLevel::DEBUG => "debug",
+        LogLevel::INFO => "info",
+        LogLevel::NOTICE => "notice",
+        LogLevel::WARN => "warn",
+        LogLevel::ERROR => "error",
+        LogLevel::FATAL => "fatal",
+    };
+    let json_str = json!({
+        "log-level": loglevel,
+    });
+
+    json_str.to_string()
+}
+
+pub fn api_msg_gen_version(smartdns_version: &str, ui_version: &str) -> String {
+    let json_str = json!({
+        "smartdns": smartdns_version,
+        "smartdns-ui": ui_version,
+    });
+
+    json_str.to_string()
+}
+
+pub fn api_msg_parse_version(data: &str) -> Result<(String, String), Box<dyn Error>> {
+    let v: serde_json::Value = serde_json::from_str(data)?;
+    let smartdns = v["smartdns"].as_str();
+    if smartdns.is_none() {
+        return Err("smartdns not found".into());
+    }
+    let ui = v["smartdns-ui"].as_str();
+    if ui.is_none() {
+        return Err("ui not found".into());
+    }
+
+    Ok((smartdns.unwrap().to_string(), ui.unwrap().to_string()))
+}

+ 85 - 0
plugin/smartdns-ui/src/http_error.rs

@@ -0,0 +1,85 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use std::string::FromUtf8Error;
+
+use crate::http_api_msg::*;
+use bytes::Bytes;
+use http_body_util::Full;
+use hyper::{Response, StatusCode};
+
+#[derive(Debug)]
+pub struct HttpError {
+    pub code: StatusCode,
+    pub msg: String,
+}
+
+impl HttpError {
+    pub fn new(code: StatusCode, msg: String) -> Self {
+        HttpError {
+            code: code,
+            msg: msg.to_string(),
+        }
+    }
+
+    pub fn to_response(&self) -> Response<Full<Bytes>> {
+        let bytes = Bytes::from(api_msg_error(&self.msg));
+        let mut response = Response::new(Full::new(bytes));
+        response
+            .headers_mut()
+            .insert("Content-Type", "application/json".parse().unwrap());
+        *response.status_mut() = self.code;
+        response
+    }
+}
+
+impl From<hyper::Error> for HttpError {
+    fn from(err: hyper::Error) -> HttpError {
+        HttpError {
+            code: StatusCode::INTERNAL_SERVER_ERROR,
+            msg: format!("Hyper error: {}", err),
+        }
+    }
+}
+
+impl From<FromUtf8Error> for HttpError {
+    fn from(err: FromUtf8Error) -> HttpError {
+        HttpError {
+            code: StatusCode::BAD_REQUEST,
+            msg: format!("FromUtf8Error: {}", err),
+        }
+    }
+}
+
+impl From<std::io::Error> for HttpError {
+    fn from(err: std::io::Error) -> HttpError {
+        HttpError {
+            code: StatusCode::INTERNAL_SERVER_ERROR,
+            msg: format!("IO error: {}", err),
+        }
+    }
+}
+
+impl From<Box<dyn std::error::Error>> for HttpError {
+    fn from(err: Box<dyn std::error::Error>) -> HttpError {
+        HttpError {
+            code: StatusCode::INTERNAL_SERVER_ERROR,
+            msg: format!("Error: {}", err),
+        }
+    }
+}

+ 104 - 0
plugin/smartdns-ui/src/http_jwt.rs

@@ -0,0 +1,104 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct JwtClaims {
+    pub user: String,
+    pub ip: String,
+    pub exp: u64,
+}
+
+pub struct Jwt {
+    user: String,
+    secret: String,
+    ip: String,
+    expired_in: u32,
+}
+
+pub struct TokenInfo {
+    pub token: String,
+    pub expire: String,
+}
+
+impl Jwt {
+    pub fn new(user: &str, secret: &str, ip: &str, expired_in: u32) -> Self {
+        Jwt {
+            user: user.to_string(),
+            secret: secret.to_string(),
+            ip: ip.to_string(),
+            expired_in: expired_in,
+        }
+    }
+
+    pub fn refresh_token(&self, token: &str) -> Result<TokenInfo, String> {
+        if !self.is_token_valid(token) {
+            return Err("token is invalid".to_string());
+        }
+        
+        Ok(self.encode_token())
+    }
+
+    pub fn encode_token(&self) -> TokenInfo {
+        let calims = JwtClaims {
+            user: self.user.clone(),
+            ip: self.ip.clone(),
+            exp: jsonwebtoken::get_current_timestamp() + self.expired_in as u64,
+        };
+        let token = encode(
+            &Header::default(),
+            &calims,
+            &EncodingKey::from_secret(self.secret.as_ref()),
+        );
+
+        let exp = self.expired_in.to_string();
+        TokenInfo {
+            token: token.unwrap(),
+            expire: exp,
+        }
+    }
+
+    pub fn is_token_valid(&self, token: &str) -> bool {
+        let calim = self.decode_token(token);
+        if self.decode_token(token).is_err() {
+            return false;
+        }
+
+        let calim = calim.unwrap();
+
+        if calim.user != self.user || calim.ip != self.ip {
+            return false;
+        }
+
+        true
+    }
+
+    pub fn decode_token(&self, token: &str) -> Result<JwtClaims, String> {
+        let calims = decode::<JwtClaims>(
+            &token,
+            &DecodingKey::from_secret(self.secret.as_ref()),
+            &Validation::default(),
+        );
+        match calims {
+            Ok(c) => Ok(c.claims),
+            Err(e) => Err(e.to_string()),
+        }
+    }
+}

+ 588 - 0
plugin/smartdns-ui/src/http_server.rs

@@ -0,0 +1,588 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use crate::data_server::*;
+use crate::dns_log;
+use crate::http_api_msg::*;
+use crate::http_jwt::*;
+use crate::http_server_api::*;
+use crate::smartdns;
+use crate::smartdns::*;
+
+use bytes::Bytes;
+use http_body_util::Full;
+use hyper::body;
+use hyper::server::conn::http1;
+use hyper::StatusCode;
+use hyper::{service::service_fn, Request, Response};
+use hyper_util::rt::TokioIo;
+use rustls_pemfile;
+use std::convert::Infallible;
+use std::error::Error;
+use std::fs::Metadata;
+use std::io::BufReader;
+use std::net::SocketAddr;
+use std::path::PathBuf;
+use std::path::{Component, Path};
+use std::sync::{Arc, Mutex};
+use std::thread;
+use std::time::Duration;
+use tokio::fs::read;
+use tokio::net::TcpListener;
+use tokio::net::TcpStream;
+use tokio::sync::mpsc;
+use tokio_rustls::{rustls, TlsAcceptor};
+
+#[derive(Clone)]
+pub struct HttpServerConfig {
+    pub http_ip: String,
+    pub http_root: String,
+    pub user: String,
+    pub password: String,
+    pub token_expired_time: u32,
+}
+
+impl HttpServerConfig {
+    pub fn new() -> Self {
+        HttpServerConfig {
+            http_ip: "http://0.0.0.0:8080".to_string(),
+            http_root: "/usr/local/shared/smartdns/wwww".to_string(),
+            user: "admin".to_string(),
+            password: "password".to_string(),
+            token_expired_time: 600,
+        }
+    }
+}
+
+pub struct HttpServerControl {
+    http_server: Arc<HttpServer>,
+    server_thread: Option<thread::JoinHandle<()>>,
+}
+
+#[allow(dead_code)]
+impl HttpServerControl {
+    pub fn new() -> Self {
+        HttpServerControl {
+            http_server: Arc::new(HttpServer::new()),
+            server_thread: None,
+        }
+    }
+
+    pub fn get_http_server(&self) -> Arc<HttpServer> {
+        Arc::clone(&self.http_server)
+    }
+
+    pub fn start_http_server(
+        &mut self,
+        conf: &HttpServerConfig,
+        data_server: Arc<DataServer>,
+    ) -> Result<(), Box<dyn Error>> {
+        dns_log!(LogLevel::INFO, "start smartdns-ui server.");
+
+        let inner_clone = Arc::clone(&self.http_server);
+        let ret = inner_clone.set_conf(conf);
+        if let Err(e) = ret {
+            return Err(e);
+        }
+
+        let ret = inner_clone.set_data_server(data_server);
+        if let Err(e) = ret {
+            return Err(e);
+        }
+
+        let (tx, rx) = std::sync::mpsc::channel::<i32>();
+
+        let server_thread = thread::spawn(move || {
+            let ret = HttpServer::http_server_loop(inner_clone, &tx);
+            if let Err(e) = ret {
+                _ = tx.send(0);
+                dns_log!(LogLevel::ERROR, "http server error: {}", e);
+                Plugin::smartdns_exit(1);
+            }
+            dns_log!(LogLevel::INFO, "http server exit.");
+        });
+
+        rx.recv().unwrap();
+
+        self.server_thread = Some(server_thread);
+        Ok(())
+    }
+
+    pub fn stop_http_server(&mut self) {
+        if self.server_thread.is_none() {
+            return;
+        }
+
+        dns_log!(LogLevel::INFO, "stop smartdns-ui server.");
+
+        self.http_server.stop_http_server();
+
+        if let Some(server_thread) = self.server_thread.take() {
+            server_thread.join().unwrap();
+        }
+
+        self.server_thread = None;
+    }
+}
+
+impl Drop for HttpServerControl {
+    fn drop(&mut self) {
+        self.stop_http_server();
+    }
+}
+
+pub struct HttpServer {
+    conf: Mutex<HttpServerConfig>,
+    notify_tx: Option<mpsc::Sender<()>>,
+    notify_rx: Mutex<Option<mpsc::Receiver<()>>>,
+    data_server: Mutex<Arc<DataServer>>,
+    api: API,
+    local_addr: Mutex<Option<SocketAddr>>,
+    mime_map: std::collections::HashMap<&'static str, &'static str>,
+}
+
+#[allow(dead_code)]
+impl HttpServer {
+    fn new() -> Self {
+        let mut plugin = HttpServer {
+            conf: Mutex::new(HttpServerConfig::new()),
+            notify_tx: None,
+            notify_rx: Mutex::new(None),
+            data_server: Mutex::new(Arc::new(DataServer::new())),
+            api: API::new(),
+            local_addr: Mutex::new(None),
+            mime_map: std::collections::HashMap::from([
+                ("htm", "text/html"),
+                ("html", "text/html"),
+                ("js", "text/javascript"),
+                ("css", "text/css"),
+                ("json", "application/json"),
+                ("png", "image/png"),
+                ("gif", "image/gif"),
+                ("jpeg", "image/jpeg"),
+                ("svg", "image/svg+xml"),
+                ("tar", "application/x-tar"),
+                ("zip", "application/zip"),
+                ("txt", "text/plain"),
+                ("conf", "text/plain"),
+                ("ico", "application/octet-stream"),
+                ("xml", "text/xml"),
+                ("mpeg", "video/mpeg"),
+                ("mp3", "audio/mpeg"),
+            ]),
+        };
+
+        let (tx, rx) = mpsc::channel(100);
+        plugin.notify_tx = Some(tx);
+        plugin.notify_rx = Mutex::new(Some(rx));
+
+        plugin
+    }
+
+    pub fn get_conf(&self) -> HttpServerConfig {
+        let conf = self.conf.lock().unwrap();
+        conf.clone()
+    }
+
+    pub fn get_local_addr(&self) -> Option<SocketAddr> {
+        let local_addr = self.local_addr.lock().unwrap();
+        local_addr.clone()
+    }
+
+    fn set_conf(&self, conf: &HttpServerConfig) -> Result<(), Box<dyn Error>> {
+        let mut conf_clone = self.conf.lock().unwrap();
+        *conf_clone = conf.clone();
+        dns_log!(LogLevel::INFO, "http server URI: {}", conf_clone.http_ip);
+        dns_log!(
+            LogLevel::INFO,
+            "http server www root: {}",
+            conf_clone.http_root
+        );
+        Ok(())
+    }
+
+    fn set_data_server(&self, data_server: Arc<DataServer>) -> Result<(), Box<dyn Error>> {
+        let mut _data_server = self.data_server.lock().unwrap();
+        *_data_server = data_server.clone();
+        Ok(())
+    }
+
+    pub fn get_data_server(&self) -> Arc<DataServer> {
+        let data_server = self.data_server.lock().unwrap();
+        Arc::clone(&*data_server)
+    }
+
+    pub fn auth_token_is_valid(&self, req: &Request<body::Incoming>) -> bool {
+        let token = req.headers().get("Authorization");
+        if token.is_none() {
+            return false;
+        }
+
+        let token = token.unwrap().to_str().unwrap();
+        let conf = self.conf.lock().unwrap();
+        let jwt = Jwt::new(&conf.user, &conf.password, "", conf.token_expired_time);
+
+        if !jwt.is_token_valid(token) {
+            return false;
+        }
+        true
+    }
+
+    async fn server_handle_http_api_request(
+        this: Arc<HttpServer>,
+        req: Request<body::Incoming>,
+        _path: PathBuf,
+    ) -> Result<Response<Full<Bytes>>, Infallible> {
+        let error_response = |code: StatusCode, msg: &str| {
+            let bytes = Bytes::from(api_msg_error(msg));
+            let mut response = Response::new(Full::new(bytes));
+            response
+                .headers_mut()
+                .insert("Content-Type", "application/json".parse().unwrap());
+            response
+                .headers_mut()
+                .insert("Cache-Control", "no-cache".parse().unwrap());
+            *response.status_mut() = code;
+            Ok(response)
+        };
+
+        dns_log!(LogLevel::DEBUG, "api request: {:?}", req.uri());
+        match this.api.get_router(req.method(), req.uri().path()) {
+            Some((router, param)) => {
+                if router.auth && !this.auth_token_is_valid(&req) {
+                    return error_response(StatusCode::UNAUTHORIZED, "Please login.");
+                }
+
+                if router.method != req.method() {
+                    return error_response(StatusCode::METHOD_NOT_ALLOWED, "Method Not Allowed");
+                }
+
+                let resp = (router.handler)(this.clone(), param, req).await;
+                match resp {
+                    Ok(resp) => {
+                        let mut resp = resp;
+                        if resp.headers().get("Content-Type").is_none() {
+                            resp.headers_mut()
+                                .insert("Content-Type", "application/json".parse().unwrap());
+                        }
+
+                        if resp.headers().get("Cache-Control").is_none() {
+                            resp.headers_mut()
+                                .insert("Cache-Control", "no-cache".parse().unwrap());
+                        }
+                        Ok(resp)
+                    }
+                    Err(e) => Ok(e.to_response()),
+                }
+            }
+            None => error_response(StatusCode::NOT_FOUND, "API not found."),
+        }
+    }
+
+    pub fn get_mime_type(&self, file: &str) -> String {
+        let ext = file.split('.').last().unwrap();
+        if let Some(mime) = self.mime_map.get(ext) {
+            return mime.to_string();
+        }
+
+        "application/octet-stream".to_string()
+    }
+
+    async fn server_handle_http_request(
+        this: Arc<HttpServer>,
+        req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, Infallible> {
+        let path = PathBuf::from(req.uri().path());
+        let www_root = {
+            let conf = this.conf.lock().unwrap();
+            PathBuf::from(conf.http_root.clone())
+        };
+
+        let mut path = normalize_path(path.as_path());
+        if path.starts_with("/") {
+            path = path.strip_prefix("/").unwrap().to_path_buf();
+        }
+
+        if path.starts_with("api/") {
+            let ret = HttpServer::server_handle_http_api_request(this, req, path.clone()).await;
+            if let Err(e) = ret {
+                dns_log!(LogLevel::ERROR, "api request error: {:?}", e);
+                let mut response = Response::new(Full::new(Bytes::from("Internal Server Error")));
+                *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
+                return ret;
+            }
+
+            return ret;
+        }
+
+        dns_log!(LogLevel::DEBUG, "page request: {:?}", req.uri());
+        let mut filepath = www_root.join(path);
+        let mut path = req.uri().path().to_string();
+
+        if !filepath.exists() {
+            filepath = www_root.join("index.html");
+            path = format!("{}/index.html", path);
+        }
+
+        if filepath.is_dir() {
+            filepath = filepath.join("index.html");
+            path = format!("{}/index.html", path);
+        }
+
+        let mut file_meta: Option<Metadata> = None;
+        let fn_get_etag = |meta: &Metadata| -> String {
+            let modify_time = meta.modified();
+            if let Err(_) = modify_time {
+                return "".to_string();
+            }
+            format!(
+                "{:x}-{:?}",
+                meta.len(),
+                modify_time
+                    .unwrap()
+                    .duration_since(std::time::UNIX_EPOCH)
+                    .unwrap()
+                    .as_secs()
+            )
+        };
+
+        if filepath.exists() {
+            let meta = filepath.metadata();
+            if let Ok(meta) = meta {
+                file_meta = Some(meta);
+            }
+        }
+
+        let if_none_match = req.headers().get("If-None-Match");
+        if if_none_match.is_some() && file_meta.is_some() {
+            let etag = fn_get_etag(&file_meta.as_ref().unwrap());
+            if etag == if_none_match.unwrap().to_str().unwrap() {
+                let mut response = Response::new(Full::new(Bytes::from("")));
+                *response.status_mut() = StatusCode::NOT_MODIFIED;
+                return Ok(response);
+            }
+        }
+
+        match read(filepath).await {
+            Ok(contents) => {
+                let bytes = Bytes::from(contents);
+                let bytes_len = bytes.len();
+                let mut response = Response::new(Full::new(bytes));
+                let header = response.headers_mut();
+                header.insert("Content-Length", bytes_len.to_string().parse().unwrap());
+                header.insert("Content-Type", this.get_mime_type(&path).parse().unwrap());
+
+                if file_meta.as_ref().is_some() {
+                    let etag = fn_get_etag(&file_meta.as_ref().unwrap());
+                    header.insert("ETag", etag.parse().unwrap());
+                }
+                *response.status_mut() = StatusCode::OK;
+                Ok(response)
+            }
+            Err(_) => {
+                let bytes = Bytes::from("Not Found");
+                let mut response = Response::new(Full::new(bytes));
+                *response.status_mut() = StatusCode::NOT_FOUND;
+                Ok(response)
+            }
+        }
+    }
+
+    async fn http_server_handle_conn(this: Arc<HttpServer>, stream: TcpStream) {
+        let io = TokioIo::new(stream);
+
+        let handle_func = move |req| HttpServer::server_handle_http_request(this.clone(), req);
+
+        tokio::task::spawn(async move {
+            let conn = http1::Builder::new()
+                .serve_connection(io, service_fn(handle_func))
+                .with_upgrades()
+                .await;
+            if let Err(err) = conn {
+                dns_log!(LogLevel::DEBUG, "Error serving connection: {:?}", err);
+                return;
+            }
+        });
+    }
+
+    async fn https_server_handle_conn(
+        this: Arc<HttpServer>,
+        stream: tokio_rustls::server::TlsStream<TcpStream>,
+    ) {
+        let io = TokioIo::new(stream);
+
+        let handle_func = move |req| HttpServer::server_handle_http_request(this.clone(), req);
+
+        tokio::task::spawn(async move {
+            let conn = http1::Builder::new()
+                .serve_connection(io, service_fn(handle_func))
+                .with_upgrades()
+                .await;
+            if let Err(err) = conn {
+                dns_log!(LogLevel::DEBUG, "Error serving connection: {:?}", err);
+                return;
+            }
+        });
+    }
+
+    async fn handle_tls_accept(this: Arc<HttpServer>, acceptor: TlsAcceptor, stream: TcpStream) {
+        tokio::task::spawn(async move {
+            let acceptor_future = acceptor.accept(stream);
+            let stream_ssl_tmout =
+                tokio::time::timeout(tokio::time::Duration::from_secs(60), acceptor_future).await;
+            if let Err(e) = stream_ssl_tmout {
+                dns_log!(LogLevel::DEBUG, "tls accept timeout. {}", e);
+                return;
+            }
+
+            let stream_ret = stream_ssl_tmout.unwrap();
+            if let Err(e) = stream_ret {
+                dns_log!(LogLevel::DEBUG, "tls accept error: {}", e);
+                return;
+            }
+
+            let stream_ssl = stream_ret.unwrap();
+            HttpServer::https_server_handle_conn(this, stream_ssl).await;
+        });
+    }
+
+    #[tokio::main]
+    async fn http_server_loop(
+        this: Arc<HttpServer>,
+        kickoff_tx: &std::sync::mpsc::Sender<i32>,
+    ) -> Result<(), Box<dyn Error>> {
+        let addr: String;
+        let mut rx: mpsc::Receiver<()>;
+
+        {
+            let conf = this.conf.lock().unwrap();
+            addr = format!("{}", conf.http_ip);
+            let mut _rx = this.notify_rx.lock().unwrap();
+            rx = _rx.take().unwrap();
+        }
+
+        let url = addr.parse::<url::Url>()?;
+        let mut acceptor = None;
+        if url.scheme() == "https" {
+            let cert_info = smartdns::Plugin::smartdns_get_cert()?;
+
+            dns_log!(
+                LogLevel::DEBUG,
+                "cert: {}, key: {}",
+                cert_info.cert,
+                cert_info.key
+            );
+            let cert_chain: Result<Vec<rustls::pki_types::CertificateDer<'_>>, _> =
+                rustls_pemfile::certs(&mut BufReader::new(std::fs::File::open(cert_info.cert)?))
+                    .collect();
+            let cert_chain = cert_chain.unwrap_or_else(|_| Vec::new());
+            let key_der = rustls_pemfile::private_key(&mut BufReader::new(std::fs::File::open(
+                cert_info.key,
+            )?))?
+            .unwrap();
+
+            let config = rustls::ServerConfig::builder()
+                .with_no_client_auth()
+                .with_single_cert(cert_chain, key_der)?;
+            acceptor = Some(TlsAcceptor::from(Arc::new(config)));
+        }
+        let host = url.host_str().unwrap_or("0.0.0.0");
+        let port = url.port().unwrap_or(80);
+        let sock_addr = format!("{}:{}", host, port).parse::<SocketAddr>()?;
+
+        let listner = TcpListener::bind(sock_addr).await?;
+        let addr = listner.local_addr()?;
+
+        *this.local_addr.lock().unwrap() = Some(addr);
+        dns_log!(LogLevel::INFO, "http server listen at {}", url);
+
+        _ = kickoff_tx.send(0);
+        loop {
+            tokio::select! {
+                _ = rx.recv() => {
+                    break;
+                }
+                res = listner.accept() => {
+                    match res {
+                        Ok((stream, _)) => {
+                            let sock_ref = socket2::SockRef::from(&stream);
+
+                            let mut ka = socket2::TcpKeepalive::new();
+                            ka = ka.with_time(Duration::from_secs(30));
+                            ka = ka.with_interval(Duration::from_secs(10));
+                            sock_ref.set_tcp_keepalive(&ka)?;
+                            sock_ref.set_nonblocking(true)?;
+                            if acceptor.is_some() {
+                                let acceptor = acceptor.clone().unwrap().clone();
+                                let this_clone = this.clone();
+                                HttpServer::handle_tls_accept(this_clone, acceptor, stream).await;
+                            } else {
+                                HttpServer::http_server_handle_conn(this.clone(), stream).await;
+                            }
+                        }
+                        Err(e) => {
+                            dns_log!(LogLevel::ERROR, "accept error: {}", e);
+                        }
+                    }
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn stop_http_server(&self) {
+        if let Some(tx) = self.notify_tx.as_ref().cloned() {
+            let t = thread::spawn(move || {
+                let rt = tokio::runtime::Runtime::new().unwrap();
+                rt.block_on(async move {
+                    _ = tx.send(()).await;
+                });
+            });
+
+            let _ = t.join();
+        }
+    }
+}
+
+pub fn normalize_path(path: &Path) -> PathBuf {
+    let mut components = path.components().peekable();
+    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
+        components.next();
+        PathBuf::from(c.as_os_str())
+    } else {
+        PathBuf::new()
+    };
+
+    for component in components {
+        match component {
+            Component::Prefix(..) => unreachable!(),
+            Component::RootDir => {
+                ret.push(component.as_os_str());
+            }
+            Component::CurDir => {}
+            Component::ParentDir => {
+                ret.pop();
+            }
+            Component::Normal(c) => {
+                ret.push(c);
+            }
+        }
+    }
+    ret
+}

+ 563 - 0
plugin/smartdns-ui/src/http_server_api.rs

@@ -0,0 +1,563 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use crate::db::*;
+use crate::http_api_msg::*;
+use crate::http_error::*;
+use crate::http_jwt::*;
+use crate::http_server::*;
+use crate::http_server_log_stream;
+use crate::smartdns;
+use crate::smartdns::*;
+use crate::Plugin;
+
+use bytes::Bytes;
+use http_body_util::BodyExt;
+use http_body_util::Full;
+use hyper::{body, Method, Request, Response, StatusCode};
+use matchit::Router;
+use std::collections::HashMap;
+use std::future::Future;
+use std::pin::Pin;
+use std::sync::Arc;
+use url::form_urlencoded;
+
+type APIRouteFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
+type APIRouterFun = fn(
+    this: Arc<HttpServer>,
+    param: APIRouteParam,
+    req: Request<body::Incoming>,
+) -> APIRouteFuture<'static, Result<Response<Full<Bytes>>, HttpError>>;
+type APIRouteParam = HashMap<String, String>;
+
+pub struct APIRouter {
+    pub method: Method,
+    pub auth: bool,
+    pub handler: APIRouterFun,
+}
+
+pub struct API {
+    router: Router<std::collections::HashMap<Method, APIRouter>>,
+}
+
+macro_rules! APIRoute {
+    ( $fn:path) => {
+        |r, p, h| Box::pin($fn(r, p, h))
+    };
+}
+
+#[allow(dead_code)]
+impl API {
+    #[rustfmt::skip]
+    pub fn new() -> Self {
+        let mut api = API {
+            router: Router::new(),
+        };
+
+        api.register(Method::PUT, "/api/service/restart",  true, APIRoute!(API::api_service_restart));
+        api.register(Method::PUT, "/api/cache/flush",  true, APIRoute!(API::api_cache_flush));
+        api.register(Method::GET, "/api/cache/count",  true, APIRoute!(API::api_cache_count));
+        api.register(Method::POST, "/api/auth/login",  false, APIRoute!(API::api_auth_login));
+        api.register(Method::POST, "/api/auth/refresh",  true, APIRoute!(API::api_auth_refresh));
+        api.register(Method::GET, "/api/domain",  true, APIRoute!(API::api_domain_get_list));
+        api.register(Method::DELETE, "/api/domain",  true, APIRoute!(API::api_domain_delete_list));
+        api.register(Method::GET, "/api/domain/count",  true, APIRoute!(API::api_domain_get_list_count));
+        api.register(Method::GET, "/api/domain/{id}",  true, APIRoute!(API::api_domain_get_by_id));
+        api.register(Method::DELETE, "/api/domain/{id}",  true, APIRoute!(API::api_domain_delete_by_id));
+        api.register(Method::GET, "/api/client", true, APIRoute!(API::api_client_get_list));
+        api.register(Method::GET, "/api/log/stream", true, APIRoute!(API::api_log_stream));
+        api.register(Method::PUT, "/api/log/level", true, APIRoute!(API::api_log_set_level));
+        api.register(Method::GET, "/api/log/level", true, APIRoute!(API::api_log_get_level));
+        api.register(Method::GET, "/api/server/version", false, APIRoute!(API::api_server_version));
+        api
+    }
+
+    pub fn register(&mut self, method: Method, path: &str, auth: bool, handler: APIRouterFun) {
+        let route_data = APIRouter {
+            method: method.clone(),
+            auth: auth,
+            handler: handler,
+        };
+
+        let mut m = self.router.at_mut(path);
+        if m.is_err() {
+            let map_new = std::collections::HashMap::new();
+            _ = self.router.insert(path, map_new);
+            m = self.router.at_mut(path);
+            if m.is_err() {
+                return;
+            }
+        }
+
+        let m = m.unwrap();
+        let mutmethod_map = m.value;
+        mutmethod_map.insert(method, route_data);
+    }
+
+    pub fn get_router(&self, method: &Method, path: &str) -> Option<(&APIRouter, APIRouteParam)> {
+        let m = self.router.at(path);
+        if m.is_err() {
+            return None;
+        }
+
+        let m = m.unwrap();
+        let method_map = m.value;
+        let route_data = method_map.get(method);
+        if route_data.is_none() {
+            return None;
+        }
+
+        let route_data = route_data.unwrap();
+        let mut param = APIRouteParam::new();
+
+        m.params.iter().for_each(|(k, v)| {
+            let v = v.to_string();
+            param.insert(k.to_string(), v);
+        });
+        Some((route_data, param))
+    }
+
+    fn get_params(req: &Request<body::Incoming>) -> HashMap<String, String> {
+        let b = req.uri().query().unwrap_or("").to_string();
+        form_urlencoded::parse(b.as_ref())
+            .into_owned()
+            .collect::<HashMap<String, String>>()
+    }
+
+    fn params_parser_value<T: std::str::FromStr>(v: Option<&String>) -> Option<T> {
+        if v.is_none() {
+            return None;
+        }
+        let v = v.unwrap();
+
+        match T::from_str(&v) {
+            Ok(value) => Some(value),
+            Err(_) => None,
+        }
+    }
+
+    fn params_get_value<T: std::str::FromStr>(
+        params: &HashMap<String, String>,
+        key: &str,
+    ) -> Option<T> {
+        let v = params.get(key);
+        if v.is_none() {
+            return None;
+        }
+
+        let v = v.unwrap();
+        API::params_parser_value(Some(v))
+    }
+
+    fn params_get_value_default<T: std::str::FromStr>(
+        params: &HashMap<String, String>,
+        key: &str,
+        default: T,
+    ) -> Result<T, HttpError> {
+        let v = params.get(key);
+        if v.is_none() {
+            return Ok(default);
+        }
+        let v = v.unwrap();
+        match v.parse::<T>() {
+            Ok(v) => return Ok(v),
+            Err(_) => {
+                return Err(HttpError::new(
+                    StatusCode::BAD_REQUEST,
+                    format!("Invalid parameter: {}", key),
+                ));
+            }
+        }
+    }
+
+    pub fn response_error(code: StatusCode, msg: &str) -> Result<Response<Full<Bytes>>, HttpError> {
+        let bytes = Bytes::from(api_msg_error(msg));
+        let mut response = Response::new(Full::new(bytes));
+        response
+            .headers_mut()
+            .insert("Content-Type", "application/json".parse().unwrap());
+        *response.status_mut() = code;
+        Ok(response)
+    }
+
+    pub fn response_build(
+        code: StatusCode,
+        body: String,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let mut response = Response::new(Full::new(Bytes::from(body)));
+        response
+            .headers_mut()
+            .insert("Content-Type", "application/json".parse().unwrap());
+        *response.status_mut() = code;
+        Ok(response)
+    }
+
+    async fn api_auth_refresh(
+        this: Arc<HttpServer>,
+        _param: APIRouteParam,
+        req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let token = req.headers().get("Authorization");
+        if token.is_none() {
+            return API::response_error(
+                StatusCode::UNAUTHORIZED,
+                "Incorrect username or password.",
+            );
+        }
+
+        let conf = this.get_conf();
+
+        let jtw = Jwt::new(
+            &conf.user.as_str(),
+            conf.password.as_str(),
+            "",
+            conf.token_expired_time,
+        );
+
+        let token = token.unwrap().to_str().unwrap();
+        let token_new = jtw.refresh_token(token);
+        if token_new.is_err() {
+            return API::response_error(
+                StatusCode::UNAUTHORIZED,
+                "Incorrect username or password.",
+            );
+        }
+        let token_new = token_new.unwrap();
+        API::response_build(
+            StatusCode::OK,
+            api_msg_auth_token(&token_new.token, &token_new.expire),
+        )
+    }
+
+    /// Login
+    /// API: POST /api/auth/login
+    ///     body:
+    /// {
+    ///   "user": "admin"
+    ///   "password": "password"
+    /// }
+    async fn api_auth_login(
+        this: Arc<HttpServer>,
+        _param: APIRouteParam,
+        req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let whole_body = String::from_utf8(req.into_body().collect().await?.to_bytes().into())?;
+        let userinfo = api_msg_parse_auth(whole_body.as_str());
+        if let Err(e) = userinfo {
+            return API::response_error(StatusCode::BAD_REQUEST, e.to_string().as_str());
+        }
+
+        let conf = this.get_conf();
+        let userinfo = userinfo.unwrap();
+
+        if userinfo.user != conf.user || userinfo.password != conf.password {
+            return API::response_error(
+                StatusCode::UNAUTHORIZED,
+                "Incorrect username or password.",
+            );
+        }
+
+        let jtw = Jwt::new(
+            userinfo.user.as_str(),
+            conf.password.as_str(),
+            "",
+            conf.token_expired_time,
+        );
+        let token = jtw.encode_token();
+        API::response_build(
+            StatusCode::OK,
+            api_msg_auth_token(&token.token, &token.expire),
+        )
+    }
+
+    /// Restart the service <br>
+    /// API: PUT /api/service/restart
+    ///
+    async fn api_service_restart(
+        _this: Arc<HttpServer>,
+        _param: APIRouteParam,
+        _req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let mut response = Response::new(Full::new(Bytes::from("")));
+        response
+            .headers_mut()
+            .insert("Content-Type", "application/json".parse().unwrap());
+        *response.status_mut() = StatusCode::NO_CONTENT;
+        Plugin::smartdns_restart();
+        Ok(response)
+    }
+
+    /// Get the number of cache <br>
+    /// API: GET /api/cache/count
+    ///
+    async fn api_cache_count(
+        _this: Arc<HttpServer>,
+        _param: APIRouteParam,
+        _req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        API::response_build(
+            StatusCode::OK,
+            api_msg_gen_cache_number(Plugin::dns_cache_total_num()),
+        )
+    }
+
+    /// Flush the cache <br>
+    /// API: PUT /api/cache/flush
+    ///
+    async fn api_cache_flush(
+        _this: Arc<HttpServer>,
+        _param: APIRouteParam,
+        _req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        Plugin::dns_cache_flush();
+        API::response_build(
+            StatusCode::OK,
+            api_msg_gen_cache_number(Plugin::dns_cache_total_num()),
+        )
+    }
+
+    /// Get the number of domain list <br>
+    /// API: GET /api/domain/count
+    ///
+    async fn api_domain_get_list_count(
+        this: Arc<HttpServer>,
+        _param: APIRouteParam,
+        _req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let data_server = this.get_data_server();
+        let count = data_server.get_domain_list_count();
+        let body = api_msg_gen_count(count as i64);
+
+        API::response_build(StatusCode::OK, body)
+    }
+
+    /// Get the domain by id <br>
+    /// API: GET /api/domain/{id}
+    async fn api_domain_get_by_id(
+        this: Arc<HttpServer>,
+        param: APIRouteParam,
+        _req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let id = API::params_parser_value(param.get("id"));
+        if id.is_none() {
+            return API::response_error(StatusCode::BAD_REQUEST, "Invalid parameter.");
+        }
+
+        let id = id.unwrap();
+        let mut get_param = DomainListGetParam::new();
+        get_param.id = Some(id);
+
+        let data_server = this.get_data_server();
+        let domain_list: Vec<DomainData> = data_server.get_domain_list(&get_param)?;
+        if domain_list.len() == 0 {
+            return API::response_error(StatusCode::NOT_FOUND, "Not found");
+        }
+        let body = api_msg_gen_domain(&domain_list[0]);
+
+        API::response_build(StatusCode::OK, body)
+    }
+
+    /// Delete the domain by id <br>
+    /// API: DELETE /api/domain/{id}
+    ///
+    async fn api_domain_delete_by_id(
+        this: Arc<HttpServer>,
+        param: APIRouteParam,
+        _req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let id = API::params_parser_value(param.get("id"));
+        if id.is_none() {
+            return API::response_error(StatusCode::BAD_REQUEST, "Invalid parameter.");
+        }
+
+        let id = id.unwrap();
+        let data_server = this.get_data_server();
+        let ret = data_server.delete_domain_by_id(id);
+        if let Err(e) = ret {
+            return API::response_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string().as_str());
+        }
+
+        if ret.unwrap() == 0 {
+            return API::response_error(StatusCode::NOT_FOUND, "Not found");
+        }
+
+        API::response_build(StatusCode::NO_CONTENT, "".to_string())
+    }
+
+    /// Get the domain list <br>
+    /// API: GET /api/domain <br>
+    ///   parameter: <br>
+    ///     page_num: u32: Page number <br>
+    ///     page_size: u32: Page size <br>
+    ///     domain: String: Domain <br>
+    ///     domain_type: String: Domain type <br>
+    ///     domain_group: String: Domain group <br>
+    ///     client: String: Client <br>
+    ///     reply_code: String: Reply code <br>
+    ///
+    ///
+    async fn api_domain_get_list(
+        this: Arc<HttpServer>,
+        _param: APIRouteParam,
+        req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let params = API::get_params(&req);
+
+        let page_num = API::params_get_value_default(&params, "page_num", 1 as u32)?;
+        let page_size = API::params_get_value_default(&params, "page_size", 10 as u32)?;
+        if page_num == 0 || page_size == 0 {
+            return API::response_error(
+                StatusCode::BAD_REQUEST,
+                "Invalid parameter: page_num or page_size",
+            );
+        }
+
+        let domain = API::params_get_value(&params, "domain");
+        let domain_type = API::params_get_value(&params, "domain_type");
+        let domain_group = API::params_get_value(&params, "domain_group");
+        let client = API::params_get_value(&params, "client");
+        let reply_code = API::params_get_value(&params, "reply_code");
+        let order = API::params_get_value(&params, "order");
+        let timestamp_after = API::params_get_value(&params, "timestamp_after");
+        let timestamp_before = API::params_get_value(&params, "timestamp_before");
+
+        let mut param = DomainListGetParam::new();
+        param.page_num = page_num;
+        param.page_size = page_size;
+        param.domain = domain;
+        param.domain_type = domain_type;
+        param.domain_group = domain_group;
+        param.client = client;
+        param.reply_code = reply_code;
+        param.order = order;
+        param.timestamp_after = timestamp_after;
+        param.timestamp_before = timestamp_before;
+
+        let data_server = this.get_data_server();
+        let domain_list: Vec<DomainData> = data_server.get_domain_list(&param)?;
+        let list_count = data_server.get_domain_list_count();
+        let mut total_page = list_count / page_size;
+        if list_count % page_size != 0 {
+            total_page += 1;
+        }
+        let body = api_msg_gen_domain_list(domain_list, total_page);
+
+        API::response_build(StatusCode::OK, body)
+    }
+
+    /// Delete the domain list before timestamp <br>
+    /// API: DELETE /api/domain <br>
+    ///   parameter: <br>
+    ///     timestamp: u64: Unix timestamp <br>
+    ///
+    async fn api_domain_delete_list(
+        this: Arc<HttpServer>,
+        _param: APIRouteParam,
+        req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let params = API::get_params(&req);
+        let timestamp_before = API::params_get_value(&params, "timestamp_before");
+        if timestamp_before.is_none() {
+            return API::response_error(StatusCode::BAD_REQUEST, "Invalid parameter.");
+        }
+
+        let timestamp_before = timestamp_before.unwrap();
+        let data_server = this.get_data_server();
+        let ret = data_server.delete_domain_before_timestamp(timestamp_before);
+        if let Err(e) = ret {
+            return API::response_error(StatusCode::INTERNAL_SERVER_ERROR, e.to_string().as_str());
+        }
+
+        if *ret.as_ref().unwrap() == 0 {
+            return API::response_error(StatusCode::NOT_FOUND, "Not found");
+        }
+
+        let body = api_msg_gen_count(ret.unwrap() as i64);
+        API::response_build(StatusCode::OK, body)
+    }
+
+    async fn api_client_get_list(
+        this: Arc<HttpServer>,
+        _param: APIRouteParam,
+        _req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let data_server = this.get_data_server();
+        let client_list: Vec<ClientData> = data_server.get_client_list()?;
+        let body = api_msg_gen_client_list(client_list);
+
+        API::response_build(StatusCode::OK, body)
+    }
+
+    async fn api_log_stream(
+        _this: Arc<HttpServer>,
+        _param: APIRouteParam,
+        mut req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        if hyper_tungstenite::is_upgrade_request(&req) {
+            let (response, websocket) = hyper_tungstenite::upgrade(&mut req, None)
+                .map_err(|e| HttpError::new(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
+
+            tokio::spawn(async move {
+                if let Err(e) = http_server_log_stream::serve_log_stream(websocket).await {
+                    eprintln!("Error in websocket connection: {e}");
+                }
+            });
+
+            Ok(response)
+        } else {
+            return API::response_error(StatusCode::BAD_REQUEST, "Need websocket upgrade.");
+        }
+    }
+
+    async fn api_log_set_level(
+        _this: Arc<HttpServer>,
+        _param: APIRouteParam,
+        _req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let whole_body = String::from_utf8(_req.into_body().collect().await?.to_bytes().into())?;
+        let level = api_msg_parse_loglevel(whole_body.as_str());
+        if let Err(e) = level {
+            return API::response_error(StatusCode::BAD_REQUEST, e.to_string().as_str());
+        }
+
+        let level = level.unwrap();
+        dns_log_set_level(level);
+        API::response_build(StatusCode::NO_CONTENT, "".to_string())
+    }
+
+    async fn api_log_get_level(
+        _this: Arc<HttpServer>,
+        _param: APIRouteParam,
+        _req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let level = dns_log_get_level();
+        let msg = api_msg_gen_loglevel(level);
+        API::response_build(StatusCode::OK, msg)
+    }
+
+    async  fn api_server_version(
+        _this: Arc<HttpServer>,
+        _param: APIRouteParam,
+        _req: Request<body::Incoming>,
+    ) -> Result<Response<Full<Bytes>>, HttpError> {
+        let server_version = &smartdns::smartdns_version();
+        let ui_version = &smartdns::smartdns_ui_version();
+        let msg = api_msg_gen_version(server_version, ui_version);
+        API::response_build(StatusCode::OK, msg)
+    }
+}

+ 48 - 0
plugin/smartdns-ui/src/http_server_log_stream.rs

@@ -0,0 +1,48 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use futures::stream::StreamExt;
+use hyper_tungstenite::{tungstenite, HyperWebsocket};
+use tungstenite::Message;
+
+type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
+
+pub async fn serve_log_stream(websocket: HyperWebsocket) -> Result<(), Error> {
+    let mut websocket = websocket.await?;
+    loop {
+        tokio::select! {
+            msg = websocket.next() => {
+                let message = msg.ok_or("websocket closed")??;
+                match message {
+                    Message::Text(_msg) => {}
+                    Message::Binary(_msg) => {}
+                    Message::Ping(_msg) => {}
+                    Message::Pong(_msg) => {}
+                    Message::Close(_msg) => {
+                        break;
+                    }
+                    Message::Frame(_msg) => {
+                        unreachable!();
+                    }
+                }
+            }
+        }
+    }
+
+    Ok(())
+}

+ 55 - 0
plugin/smartdns-ui/src/lib.rs

@@ -0,0 +1,55 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+pub mod data_server;
+pub mod db;
+pub mod http_api_msg;
+pub mod http_error;
+pub mod http_jwt;
+pub mod http_server;
+pub mod http_server_api;
+pub mod http_server_log_stream;
+pub mod plugin;
+pub mod smartdns;
+
+use ctor::ctor;
+#[cfg(not(test))]
+use plugin::*;
+use smartdns::*;
+
+#[cfg(not(test))]
+fn lib_init_ops() {
+    let ops: Box<dyn SmartdnsOperations> = Box::new(SmartdnsPlugin::new());
+    unsafe {
+        PLUGIN.set_operation(ops);
+    }
+}
+
+#[cfg(test)]
+fn lib_init_smartdns_lib() {
+    smartdns::dns_log_set_level(LogLevel::DEBUG);
+}
+
+#[ctor]
+fn lib_init() {
+    #[cfg(not(test))]
+    lib_init_ops();
+
+    #[cfg(test)]
+    lib_init_smartdns_lib();
+}

+ 166 - 0
plugin/smartdns-ui/src/plugin.rs

@@ -0,0 +1,166 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use crate::data_server::*;
+use crate::dns_log;
+use crate::http_server::*;
+use crate::smartdns::*;
+
+use getopts::Options;
+use std::error::Error;
+use std::sync::Arc;
+
+pub struct SmartdnsPlugin {
+    http_server_ctl: HttpServerControl,
+    http_conf: HttpServerConfig,
+
+    data_server_ctl: Arc<DataServerControl>,
+    data_conf: DataServerConfig,
+}
+
+impl SmartdnsPlugin {
+    pub fn new() -> Self {
+        SmartdnsPlugin {
+            http_server_ctl: HttpServerControl::new(),
+            http_conf: HttpServerConfig::new(),
+
+            data_server_ctl: Arc::new(DataServerControl::new()),
+            data_conf: DataServerConfig::new(),
+        }
+    }
+
+    pub fn get_http_server(&self) -> Arc<HttpServer> {
+        self.http_server_ctl.get_http_server()
+    }
+
+    pub fn get_data_server(&self) -> Arc<DataServer> {
+        self.data_server_ctl.get_data_server()
+    }
+
+    fn parser_args(&mut self, args: &Vec<String>) -> Result<(), Box<dyn Error>> {
+        let mut opts = Options::new();
+        opts.optopt("i", "ip", "http ip", "IP");
+        opts.optopt("r", "www-root", "http www root", "PATH");
+        opts.optopt("", "data-dir", "http data dir", "PATH");
+        opts.optopt("", "token-expire", "http token expire time", "TIME");
+
+        if args.len() <= 0 {
+            return Ok(());
+        }
+
+        let matches = match opts.parse(&args[1..]) {
+            Ok(m) => m,
+            Err(f) => {
+                return Err(Box::new(f));
+            }
+        };
+
+        let www_root =
+            Plugin::dns_conf_plugin_config("smartdns-ui.www-root", "/usr/share/smartdns/www");
+        self.http_conf.http_root = www_root;
+
+        let user = Plugin::dns_conf_plugin_config("smartdns-ui.user", "");
+        let password = Plugin::dns_conf_plugin_config("smartdns-ui.password", "");
+        let ip = Plugin::dns_conf_plugin_config("smartdns-ui.ip", "");
+        if user.len() > 0  {
+            self.http_conf.user = user;
+        }
+
+        if  password.len() > 0 {
+            self.http_conf.password = password;
+        }
+
+        if ip.len() > 0 {
+            self.http_conf.http_ip = ip;
+        }
+
+        if let Some(ip) = matches.opt_str("i") {
+            self.http_conf.http_ip = ip;
+        }
+
+        if let Some(root) = matches.opt_str("r") {
+            self.http_conf.http_root = root;
+        }
+        dns_log!(LogLevel::INFO, "www root: {}", self.http_conf.http_root);
+
+        if let Some(token_expire) = matches.opt_str("token-expire") {
+            let v = token_expire.parse::<u32>();
+            if let Err(e) = v {
+                dns_log!(
+                    LogLevel::ERROR,
+                    "parse token expire time error: {}",
+                    e.to_string()
+                );
+                return Err(Box::new(e));
+            }
+            self.http_conf.token_expired_time = v.unwrap();
+        }
+
+        if let Some(data_dir) = matches.opt_str("data-dir") {
+            self.data_conf.data_root = data_dir;
+        }
+
+        Ok(())
+    }
+
+    pub fn start(&mut self, args: &Vec<String>) -> Result<(), Box<dyn Error>> {
+        self.parser_args(args)?;
+        self.data_server_ctl.start_data_server(&self.data_conf)?;
+        self.http_server_ctl
+            .start_http_server(&self.http_conf, self.data_server_ctl.get_data_server())?;
+
+        Ok(())
+    }
+
+    pub fn stop(&mut self) {
+        self.http_server_ctl.stop_http_server();
+        self.data_server_ctl.stop_data_server();
+    }
+
+    pub fn query_complete(&self, request: &mut DnsRequest) {
+        let ret = self.data_server_ctl.send_request(request);
+        if let Err(e) = ret {
+            dns_log!(
+                LogLevel::ERROR,
+                "send data to data server error: {}",
+                e.to_string()
+            );
+            return;
+        }
+    }
+}
+
+impl Drop for SmartdnsPlugin {
+    fn drop(&mut self) {
+        self.stop();
+    }
+}
+
+impl SmartdnsOperations for SmartdnsPlugin {
+    fn server_query_complete(&self, request: &mut DnsRequest) {
+        return self.query_complete(request);
+    }
+
+    fn server_init(&mut self, args: &Vec<String>) -> Result<(), Box<dyn Error>> {
+        return self.start(args);
+    }
+
+    fn server_exit(&mut self) {
+        self.stop();
+    }
+}

+ 537 - 0
plugin/smartdns-ui/src/smartdns.rs

@@ -0,0 +1,537 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+extern crate libc;
+use std::error::Error;
+use std::ffi::CString;
+
+pub use smartdns_c::LogLevel;
+
+#[macro_export]
+macro_rules! dns_log {
+    ($level:expr, $($arg:tt)*) => {
+        if $crate::smartdns::dns_can_log($level) {
+            $crate::smartdns::dns_log_out($level, file!(), line!(), &format!($($arg)*));
+        }
+    };
+}
+pub fn dns_can_log(level: LogLevel) -> bool {
+    unsafe { smartdns_c::smartdns_plugin_can_log(level) != 0 }
+}
+
+pub fn dns_log_set_level(level: LogLevel) {
+    unsafe {
+        smartdns_c::smartdns_plugin_log_setlevel(level);
+    }
+}
+
+pub fn dns_log_get_level() -> LogLevel {
+    unsafe { smartdns_c::smartdns_plugin_log_getlevel() }
+}
+
+pub fn dns_log_out(level: LogLevel, file: &str, line: u32, message: &str) {
+    let filename_only = std::path::Path::new(file)
+        .file_name()
+        .and_then(|s| s.to_str())
+        .unwrap();
+    let file_cstring = CString::new(filename_only).expect("Failed to convert to CString");
+    let message_cstring = CString::new(message).expect("Failed to convert to CString");
+
+    unsafe {
+        smartdns_c::smartdns_plugin_log(
+            level,
+            file_cstring.as_ptr(),
+            line,
+            std::ptr::null(),
+            message_cstring.as_ptr(),
+        );
+    }
+}
+
+mod smartdns_c {
+
+    #[repr(C)]
+    #[derive(Copy, Clone, Debug, PartialEq)]
+    #[allow(dead_code)]
+    pub enum LogLevel {
+        DEBUG = 0,
+        INFO = 1,
+        NOTICE = 2,
+        WARN = 3,
+        ERROR = 4,
+        FATAL = 5,
+    }
+
+    #[repr(C)]
+    pub struct _SmartdnsOperations {
+        pub server_recv: Option<
+            extern "C" fn(
+                packet: *mut _DnsPacket,
+                inpacket: *mut u8,
+                inpacket_len: libc::c_int,
+                local: *mut libc::sockaddr_storage,
+                local_len: libc::socklen_t,
+                from: *mut libc::sockaddr_storage,
+                from_len: libc::socklen_t,
+            ) -> libc::c_int,
+        >,
+        pub server_query_complete: Option<extern "C" fn(request: *mut _DnsRequest)>,
+    }
+
+    #[repr(C)]
+    pub struct _DnsPlugin {
+        _dummy: [u8; 0],
+    }
+
+    #[repr(C)]
+    pub struct _DnsRequest {
+        _dummy: [u8; 0],
+    }
+
+    #[repr(C)]
+    pub struct _DnsPacket {
+        _dummy: [u8; 0],
+    }
+
+    extern "C" {
+        pub fn dns_plugin_get_argc(plugin: *mut _DnsPlugin) -> i32;
+        pub fn dns_plugin_get_argv(plugin: *mut _DnsPlugin) -> *const *const libc::c_char;
+        pub fn dns_server_request_get_group_name(request: *mut _DnsRequest) -> *const libc::c_char;
+        pub fn dns_server_request_get_domain(request: *mut _DnsRequest) -> *const libc::c_char;
+        pub fn dns_server_request_get_qtype(request: *mut _DnsRequest) -> i32;
+        pub fn dns_server_request_get_qclass(request: *mut _DnsRequest) -> i32;
+        pub fn dns_server_request_get_id(request: *mut _DnsRequest) -> u16;
+        pub fn dns_server_request_get_rcode(request: *mut _DnsRequest) -> i32;
+        pub fn dns_server_request_get_query_time(request: *mut _DnsRequest) -> u64;
+        pub fn dns_server_request_get_remote_addr(
+            request: *mut _DnsRequest,
+        ) -> *const libc::sockaddr_storage;
+
+        pub fn dns_server_request_get_local_addr(
+            request: *mut _DnsRequest,
+        ) -> *const libc::sockaddr_storage;
+
+        pub fn get_host_by_addr(
+            host: *mut libc::c_char,
+            maxsize: i32,
+            addr: *const libc::sockaddr_storage,
+        ) -> *const libc::c_char;
+
+        pub fn dns_server_request_get(request: *mut _DnsRequest);
+
+        pub fn dns_server_request_put(request: *mut _DnsRequest);
+
+        pub fn smartdns_operations_register(operations: *const _SmartdnsOperations) -> i32;
+        pub fn smartdns_operations_unregister(operations: *const _SmartdnsOperations) -> i32;
+
+        pub fn smartdns_exit(status: i32);
+
+        pub fn smartdns_restart();
+
+        pub fn smartdns_get_cert(key: *mut libc::c_char, cert: *mut libc::c_char) -> i32;
+
+        pub fn dns_cache_flush();
+
+        pub fn dns_cache_total_num() -> i32;
+
+        pub fn smartdns_plugin_log(
+            level: LogLevel,
+            file: *const libc::c_char,
+            line: u32,
+            func: *const libc::c_char,
+            msg: *const libc::c_char,
+        );
+
+        pub fn smartdns_plugin_log_setlevel(level: LogLevel);
+
+        pub fn smartdns_plugin_log_getlevel() -> LogLevel;
+
+        pub fn smartdns_plugin_can_log(level: LogLevel) -> i32;
+
+        pub fn dns_conf_get_cache_dir() -> *const libc::c_char;
+
+        pub fn dns_conf_get_data_dir() -> *const libc::c_char;
+
+        pub fn smartdns_plugin_get_config(key: *const libc::c_char) -> *const libc::c_char;
+
+        pub fn smartdns_plugin_clear_all_config();
+
+        pub fn smartdns_server_run(file: *const libc::c_char) -> i32;
+
+        pub fn smartdns_server_stop();
+
+        pub fn get_utc_time_ms() -> u64;
+
+        pub fn smartdns_version() -> *const libc::c_char;
+    }
+}
+
+pub fn smartdns_version() -> String {
+    unsafe {
+        let version = smartdns_c::smartdns_version();
+        std::ffi::CStr::from_ptr(version)
+            .to_string_lossy()
+            .into_owned()
+    }
+}
+
+pub fn smartdns_ui_version() -> String {
+    env!("CARGO_PKG_VERSION").to_string()
+}
+
+pub fn smartdns_server_run(file: &str) -> Result<(), Box<dyn Error>> {
+    let file = CString::new(file).expect("Failed to convert to CString");
+    let ret: i32;
+    unsafe {
+        ret = smartdns_c::smartdns_server_run(file.as_ptr());
+    };
+
+    if ret != 0 {
+        return Err("smartdns server run error".into());
+    }
+
+    Ok(())
+}
+
+pub fn smartdns_server_stop() {
+    unsafe {
+        smartdns_c::smartdns_server_stop();
+    }
+}
+
+pub fn get_utc_time_ms() -> u64 {
+    unsafe { smartdns_c::get_utc_time_ms() }
+}
+
+static SMARTDNS_OPS: smartdns_c::_SmartdnsOperations = smartdns_c::_SmartdnsOperations {
+    server_recv: None,
+    server_query_complete: Some(dns_request_complete),
+};
+
+#[no_mangle]
+extern "C" fn dns_request_complete(request: *mut smartdns_c::_DnsRequest) {
+    unsafe {
+        let ops = PLUGIN.ops.as_ref();
+        if let None = ops {
+            return;
+        }
+
+        let ops = ops.unwrap();
+        let mut req = DnsRequest::new(request);
+        ops.server_query_complete(&mut req);
+    }
+}
+
+#[no_mangle]
+extern "C" fn dns_plugin_init(plugin: *mut smartdns_c::_DnsPlugin) -> i32 {
+    unsafe {
+        PLUGIN.parser_args(plugin).unwrap();
+        smartdns_c::smartdns_operations_register(&SMARTDNS_OPS);
+        let ret = PLUGIN.ops.as_mut().unwrap().server_init(PLUGIN.get_args());
+        if let Err(e) = ret {
+            dns_log!(LogLevel::ERROR, "server init error: {}", e);
+            return -1;
+        }
+    }
+
+    return 0;
+}
+
+#[no_mangle]
+extern "C" fn dns_plugin_exit(_plugin: *mut smartdns_c::_DnsPlugin) -> i32 {
+    unsafe {
+        smartdns_c::smartdns_operations_unregister(&SMARTDNS_OPS);
+        PLUGIN.ops.as_mut().unwrap().server_exit();
+    }
+    return 0;
+}
+
+pub struct DnsRequest {
+    request: *mut smartdns_c::_DnsRequest,
+}
+
+#[allow(dead_code)]
+impl DnsRequest {
+    fn new(request: *mut smartdns_c::_DnsRequest) -> DnsRequest {
+        unsafe {
+            smartdns_c::dns_server_request_get(request);
+        }
+
+        DnsRequest { request }
+    }
+
+    fn put_ref(&mut self) {
+        unsafe {
+            smartdns_c::dns_server_request_put(self.request);
+            self.request = std::ptr::null_mut();
+        }
+    }
+
+    pub fn get_group_name(&self) -> String {
+        unsafe {
+            let group_name = smartdns_c::dns_server_request_get_group_name(self.request);
+            std::ffi::CStr::from_ptr(group_name)
+                .to_string_lossy()
+                .into_owned()
+        }
+    }
+
+    pub fn get_domain(&self) -> String {
+        unsafe {
+            let domain = smartdns_c::dns_server_request_get_domain(self.request);
+            std::ffi::CStr::from_ptr(domain)
+                .to_string_lossy()
+                .into_owned()
+        }
+    }
+
+    pub fn get_qtype(&self) -> u32 {
+        unsafe { smartdns_c::dns_server_request_get_qtype(self.request) as u32 }
+    }
+
+    pub fn get_qclass(&self) -> i32 {
+        unsafe { smartdns_c::dns_server_request_get_qclass(self.request) }
+    }
+
+    pub fn get_id(&self) -> u16 {
+        unsafe { smartdns_c::dns_server_request_get_id(self.request) }
+    }
+
+    pub fn get_rcode(&self) -> u16 {
+        unsafe { smartdns_c::dns_server_request_get_rcode(self.request) as u16 }
+    }
+
+    pub fn get_query_time(&self) -> u64 {
+        unsafe { smartdns_c::dns_server_request_get_query_time(self.request) }
+    }
+
+    pub fn get_remote_addr(&self) -> String {
+        unsafe {
+            let addr = smartdns_c::dns_server_request_get_remote_addr(self.request);
+            let addr =
+                std::mem::transmute::<*const libc::sockaddr_storage, *const libc::sockaddr>(addr);
+            let mut buf = [0u8; 1024];
+            let retstr = smartdns_c::get_host_by_addr(
+                buf.as_mut_ptr(),
+                buf.len() as i32,
+                addr as *const libc::sockaddr_storage,
+            );
+            if retstr.is_null() {
+                return String::new();
+            }
+
+            let addr = std::ffi::CStr::from_ptr(retstr)
+                .to_string_lossy()
+                .into_owned();
+            addr
+        }
+    }
+
+    pub fn get_local_addr(&self) -> String {
+        unsafe {
+            let addr = smartdns_c::dns_server_request_get_local_addr(self.request);
+            let addr =
+                std::mem::transmute::<*const libc::sockaddr_storage, *const libc::sockaddr>(addr);
+            let mut buf = [0u8; 1024];
+            let retstr = smartdns_c::get_host_by_addr(
+                buf.as_mut_ptr(),
+                buf.len() as i32,
+                addr as *const libc::sockaddr_storage,
+            );
+            if retstr.is_null() {
+                return String::new();
+            }
+
+            let addr = std::ffi::CStr::from_ptr(retstr)
+                .to_string_lossy()
+                .into_owned();
+            addr
+        }
+    }
+}
+
+impl Drop for DnsRequest {
+    fn drop(&mut self) {
+        self.put_ref();
+    }
+}
+
+impl Clone for DnsRequest {
+    fn clone(&self) -> Self {
+        unsafe {
+            smartdns_c::dns_server_request_get(self.request);
+        }
+
+        DnsRequest {
+            request: self.request,
+        }
+    }
+}
+
+unsafe impl Send for DnsRequest {}
+
+pub trait SmartdnsOperations {
+    fn server_query_complete(&self, request: &mut DnsRequest);
+    fn server_init(&mut self, args: &Vec<String>) -> Result<(), Box<dyn Error>>;
+    fn server_exit(&mut self);
+}
+
+pub static mut PLUGIN: Plugin = Plugin {
+    args: Vec::new(),
+    ops: None,
+};
+
+pub struct Plugin {
+    args: Vec<String>,
+    ops: Option<Box<dyn SmartdnsOperations>>,
+}
+
+pub struct SmartdnsCert {
+    pub key: String,
+    pub cert: String,
+    pub password: String,
+}
+
+#[allow(dead_code)]
+impl Plugin {
+    pub fn get_args(&self) -> &Vec<String> {
+        &self.args
+    }
+
+    pub fn set_operation(&mut self, ops: Box<dyn SmartdnsOperations>) {
+        self.ops = Some(ops);
+    }
+
+    pub fn smartdns_exit(status: i32) {
+        unsafe {
+            smartdns_c::smartdns_exit(status);
+        }
+    }
+
+    pub fn smartdns_restart() {
+        unsafe {
+            smartdns_c::smartdns_restart();
+        }
+    }
+
+    pub fn smartdns_get_cert() -> Result<SmartdnsCert, String> {
+        unsafe {
+            let mut key = [0u8; 4096];
+            let mut cert = [0u8; 4096];
+            let ret = smartdns_c::smartdns_get_cert(
+                key.as_mut_ptr() as *mut libc::c_char,
+                cert.as_mut_ptr() as *mut libc::c_char,
+            );
+            if ret != 0 {
+                return Err("get cert error".to_string());
+            }
+
+            let key = std::ffi::CStr::from_ptr(key.as_ptr() as *const libc::c_char)
+                .to_string_lossy()
+                .into_owned();
+            let cert = std::ffi::CStr::from_ptr(cert.as_ptr() as *const libc::c_char)
+                .to_string_lossy()
+                .into_owned();
+            Ok(SmartdnsCert {
+                key,
+                cert,
+                password: "".to_string(),
+            })
+        }
+    }
+
+    pub fn dns_cache_flush() {
+        unsafe {
+            smartdns_c::dns_cache_flush();
+        }
+    }
+
+    pub fn dns_cache_total_num() -> i32 {
+        unsafe { smartdns_c::dns_cache_total_num() }
+    }
+
+    #[allow(dead_code)]
+    pub fn dns_conf_cache_dir() -> String {
+        unsafe {
+            let cache_dir = smartdns_c::dns_conf_get_cache_dir();
+            std::ffi::CStr::from_ptr(cache_dir)
+                .to_string_lossy()
+                .into_owned()
+        }
+    }
+
+    #[allow(dead_code)]
+    pub fn dns_conf_data_dir() -> String {
+        unsafe {
+            let data_dir = smartdns_c::dns_conf_get_data_dir();
+            std::ffi::CStr::from_ptr(data_dir)
+                .to_string_lossy()
+                .into_owned()
+        }
+    }
+
+    #[allow(dead_code)]
+    pub fn dns_conf_plugin_config(key: &str, default: &str) -> String {
+        let key = CString::new(key).expect("Failed to convert to CString");
+        unsafe {
+            let value = smartdns_c::smartdns_plugin_get_config(key.as_ptr());
+            if value.is_null() {
+                return default.to_string();
+            }
+
+            std::ffi::CStr::from_ptr(value)
+                .to_string_lossy()
+                .into_owned()
+        }
+    }
+
+    #[allow(dead_code)]
+    pub fn dns_conf_plugin_clear_all_config() {
+        unsafe {
+            smartdns_c::smartdns_plugin_clear_all_config();
+        }
+    }
+
+    fn parser_args(&mut self, plugin: *mut smartdns_c::_DnsPlugin) -> Result<(), String> {
+        let argc = unsafe { smartdns_c::dns_plugin_get_argc(plugin) };
+        let args: Vec<String> = unsafe {
+            let argv = smartdns_c::dns_plugin_get_argv(plugin);
+            let mut args = Vec::new();
+            for i in 0..argc {
+                let arg = std::ffi::CStr::from_ptr(*argv.offset(i as isize))
+                    .to_string_lossy()
+                    .into_owned();
+                args.push(arg);
+            }
+            args
+        };
+
+        self.args = args;
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_dns_log() {
+        dns_log!(LogLevel::DEBUG, "test log");
+    }
+}

+ 140 - 0
plugin/smartdns-ui/tests/common/client.rs

@@ -0,0 +1,140 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use http::uri;
+use reqwest;
+use smartdns_ui::http_api_msg;
+use std::{error::Error, net::TcpStream};
+use tungstenite::*;
+
+pub struct TestClient {
+    url: String,
+    token: Option<http_api_msg::TokenResponse>,
+}
+
+impl TestClient {
+    pub fn new(url: &String) -> Self {
+        let client = TestClient {
+            url: url.clone(),
+            token: None,
+        };
+
+        client
+    }
+
+    pub fn login(&mut self, user: &str, password: &str) -> Result<String, Box<dyn Error>> {
+        let url = self.url.clone() + "/api/auth/login";
+        let body = http_api_msg::api_msg_gen_auth_login(&http_api_msg::AuthUser {
+            user: user.to_string(),
+            password: password.to_string(),
+        });
+        let client = reqwest::blocking::Client::new();
+        let resp = client.post(&url).body(body).send()?;
+        let text = resp.text()?;
+
+        let token = http_api_msg::api_msg_parse_auth_token(&text)?;
+        self.token = Some(token);
+        Ok(text)
+    }
+
+    fn prep_request(
+        &self,
+        method: reqwest::Method,
+        path: &str,
+    ) -> Result<reqwest::blocking::RequestBuilder, Box<dyn Error>> {
+        let url = self.url.clone() + path;
+        let client = reqwest::blocking::ClientBuilder::new()
+            .danger_accept_invalid_certs(true)
+            .build()?;
+        let mut req = client.request(method, url);
+        if let Some(token) = &self.token {
+            if self.token.is_some() {
+                req = req.header("Authorization", format!("{}", token.token));
+            }
+        }
+        Ok(req)
+    }
+
+    pub fn get(&self, path: &str) -> Result<(i32, String), Box<dyn Error>> {
+        let req = self.prep_request(reqwest::Method::GET, path)?;
+        let resp = req.send()?;
+        let status = resp.status().as_u16();
+        let text = resp.text()?;
+        Ok((status as i32, text))
+    }
+
+    pub fn delete(&self, path: &str) -> Result<(i32, String), Box<dyn Error>> {
+        let req = self.prep_request(reqwest::Method::DELETE, path)?;
+        let resp = req.send()?;
+        let status = resp.status().as_u16();
+        let text = resp.text()?;
+        Ok((status as i32, text))
+    }
+
+    pub fn put(&self, path: &str, body: &str) -> Result<(i32, String), Box<dyn Error>> {
+        let req = self.prep_request(reqwest::Method::PUT, path)?;
+        let resp = req.body(body.to_string()).send()?;
+        let status = resp.status().as_u16();
+        let text = resp.text()?;
+        Ok((status as i32, text))
+    }
+
+    pub fn post(&self, path: &str, body: &str) -> Result<(i32, String), Box<dyn Error>> {
+        let req = self.prep_request(reqwest::Method::POST, path)?;
+        let resp = req.body(body.to_string()).send()?;
+        let status = resp.status().as_u16();
+        let text = resp.text()?;
+        Ok((status as i32, text))
+    }
+
+    pub fn websocket(
+        &self,
+        path: &str,
+    ) -> Result<WebSocket<stream::MaybeTlsStream<TcpStream>>, Box<dyn Error>> {
+        let url = self.url.clone() + path;
+        let uri: http::Uri = url.parse()?;
+        let mut parts = uri.into_parts();
+        parts.scheme = Some("ws".parse().unwrap());
+        let uri = uri::Uri::from_parts(parts).unwrap();
+        let mut request_builder = tungstenite::ClientRequestBuilder::new(uri);
+
+        if let Some(token) = &self.token {
+            if self.token.is_some() {
+                request_builder =
+                    request_builder.with_header("Authorization", format!("{}", token.token));
+            }
+        }
+
+        request_builder = request_builder
+            .with_header("Upgrade", "websocket")
+            .with_header("Sec-WebSocket-Version", "13")
+            .with_header("Connection", "keep-alive, Upgrade");
+
+        let ret = tungstenite::connect(request_builder);
+        if let Err(e) = ret {
+            println!("websocket connect error: {:?}", e.to_string());
+            return Err(Box::new(e));
+        }
+        let (socket, _) = ret.unwrap();
+        Ok(socket)
+    }
+}
+
+impl Drop for TestClient {
+    fn drop(&mut self) {}
+}

+ 24 - 0
plugin/smartdns-ui/tests/common/mod.rs

@@ -0,0 +1,24 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+mod server;
+mod client;
+
+pub use server::*;
+pub use client::*;

+ 336 - 0
plugin/smartdns-ui/tests/common/server.rs

@@ -0,0 +1,336 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use smartdns_ui::db::*;
+use smartdns_ui::dns_log;
+use smartdns_ui::plugin::*;
+use smartdns_ui::smartdns::*;
+use std::io::Write;
+use tempfile::TempDir;
+
+static INSTANCE_LOCK: std::sync::RwLock<()> = std::sync::RwLock::new(());
+
+pub struct InstanceLockGuard<'a> {
+    _read_guard: Option<std::sync::RwLockReadGuard<'a, ()>>,
+    _write_guard: Option<std::sync::RwLockWriteGuard<'a, ()>>,
+}
+
+impl<'a> InstanceLockGuard<'a> {
+    pub fn new_read_guard() -> Self {
+        Self {
+            _read_guard: Some(INSTANCE_LOCK.read().unwrap()),
+            _write_guard: None,
+        }
+    }
+
+    pub fn new_write_guard() -> Self {
+        Self {
+            _read_guard: None,
+            _write_guard: Some(INSTANCE_LOCK.write().unwrap()),
+        }
+    }
+}
+
+#[allow(dead_code)]
+struct TestSmartDnsConfigItem {
+    pub key: String,
+    pub value: String,
+}
+
+pub struct TestSmartDnsServer {
+    confs: Vec<TestSmartDnsConfigItem>,
+    is_started: bool,
+    workdir: String,
+    thread: Option<std::thread::JoinHandle<()>>,
+}
+
+impl TestSmartDnsServer {
+    pub fn new() -> Self {
+        let mut server = TestSmartDnsServer {
+            confs: Vec::new(),
+            is_started: false,
+            workdir: "/tmp/smartdns-test.conf".to_string(),
+            thread: None,
+        };
+
+        server.add_conf("bind", ":66603");
+        server.add_conf("log-level", "debug");
+        server.add_conf("log-num", "0");
+        server.add_conf("cache-persist", "no");
+
+        server
+    }
+
+    pub fn set_workdir(&mut self, workdir: &str) {
+        self.workdir = workdir.to_string();
+    }
+
+    pub fn add_conf(&mut self, key: &str, value: &str) {
+        self.confs.push(TestSmartDnsConfigItem {
+            key: key.to_string(),
+            value: value.to_string(),
+        });
+    }
+
+    fn gen_conf_file(&self) -> std::io::Result<String> {
+        let file = self.workdir.clone() + "/smartdns.conf";
+        let mut f = std::fs::File::create(&file)?;
+        for conf in self.confs.iter() {
+            f.write_all(format!("{} {}\n", conf.key, conf.value).as_bytes())?;
+        }
+        Ok(file)
+    }
+
+    pub fn start(&mut self) -> Result<(), Box<dyn std::error::Error>> {
+        let conf_file = self.gen_conf_file()?;
+        let t = std::thread::spawn(move || {
+            dns_log!(LogLevel::ERROR, "smartdns server run start...");
+            smartdns_ui::smartdns::smartdns_server_run(&conf_file).unwrap();
+            dns_log!(LogLevel::ERROR, "smartdns server run exit...");
+        });
+        self.thread = Some(t);
+        self.is_started = true;
+        dns_log!(LogLevel::ERROR, "smartdns_server_run");
+        Ok(())
+    }
+
+    pub fn stop(&mut self) {
+        if !self.is_started {
+            return;
+        }
+        self.is_started = false;
+        smartdns_ui::smartdns::smartdns_server_stop();
+        if self.thread.is_none() {
+            return;
+        }
+        let _ = self.thread.take().unwrap().join();
+    }
+}
+
+impl Drop for TestSmartDnsServer {
+    fn drop(&mut self) {
+        self.stop();
+    }
+}
+
+pub struct TestServer {
+    dns_server: TestSmartDnsServer,
+    dns_server_enable: bool,
+    plugin: SmartdnsPlugin,
+    args: Vec<String>,
+    workdir: String,
+    temp_dir: TempDir,
+    www_root: String,
+    is_started: bool,
+    ip: String,
+    is_https: bool,
+    log_level: LogLevel,
+    old_log_level: LogLevel,
+    one_instance: bool,
+    instance_lock_guard: Option<InstanceLockGuard<'static>>,
+}
+
+impl TestServer {
+    pub fn new() -> Self {
+        let mut server = TestServer {
+            dns_server: TestSmartDnsServer::new(),
+            dns_server_enable: false,
+            plugin: SmartdnsPlugin::new(),
+            args: Vec::new(),
+            workdir: String::new(),
+            temp_dir: TempDir::with_prefix("smartdns-ui-").unwrap(),
+            www_root: String::new(),
+            is_started: false,
+            ip: "http://127.0.0.1:0".to_string(),
+            is_https: false,
+            log_level: LogLevel::INFO,
+            old_log_level: LogLevel::INFO,
+            one_instance: false,
+            instance_lock_guard: None,
+        };
+
+        server.workdir = server.temp_dir.path().to_str().unwrap().to_string();
+        server.dns_server.set_workdir(&server.workdir);
+        server
+    }
+
+    fn setup_default_args(&mut self) {
+        self.args.insert(0, "--ip".to_string());
+        self.args.insert(1, self.ip.clone());
+
+        self.args.insert(0, "--data-dir".to_string());
+        self.args.insert(1, self.workdir.clone() + "/data.db");
+
+        self.args.insert(0, "--www-root".to_string());
+        self.www_root = self.workdir.clone() + "/www";
+        self.args.insert(1, self.www_root.clone());
+
+        self.args.insert(0, "smartdns-ui".to_string());
+        dns_log!(LogLevel::INFO, "workdir: {}", self.workdir);
+    }
+
+    pub fn get_url(&self, path: &str) -> String {
+        self.ip.clone() + path
+    }
+
+    pub fn get_host(&self) -> String {
+        self.ip.clone()
+    }
+
+    pub fn get_www_root(&self) -> &String {
+        &self.www_root
+    }
+
+    fn create_workdir(&self) -> std::io::Result<()> {
+        std::fs::create_dir_all(&self.workdir)?;
+        std::fs::create_dir_all(&self.www_root)?;
+        Ok(())
+    }
+
+    fn remove_workdir(&self) -> std::io::Result<()> {
+        let r = std::fs::remove_dir_all(&self.workdir);
+        return r;
+    }
+
+    pub fn add_mock_server_conf(&mut self, key: &str, value: &str) {
+        self.dns_server.add_conf(key, value);
+    }
+
+    pub fn enable_mock_server(&mut self) {
+        self.dns_server_enable = true;
+        self.set_one_instance(true);
+    }
+
+    pub fn add_args(&mut self, args: Vec<String>) {
+        for arg in args.iter() {
+            self.args.push(arg.clone());
+        }
+    }
+
+    pub fn new_mock_domain_record(&self) -> DomainData {
+        DomainData {
+            id: 0,
+            timestamp: smartdns_ui::smartdns::get_utc_time_ms(),
+            domain: "example.com".to_string(),
+            domain_type: 1,
+            client: "127.0.0.1".to_string(),
+            domain_group: "default".to_string(),
+            reply_code: 0,
+        }
+    }
+
+    pub fn add_domain_record(
+        &mut self,
+        record: &DomainData,
+    ) -> Result<(), Box<dyn std::error::Error>> {
+        self.plugin.get_data_server().insert_domain(record)
+    }
+
+    pub fn set_log_level(&mut self, level: LogLevel) {
+        self.log_level = level;
+    }
+
+    fn init_server(&mut self) -> Result<(), Box<dyn std::error::Error>> {
+        self.create_workdir()?;
+
+        self.old_log_level = smartdns_ui::smartdns::dns_log_get_level();
+        smartdns_ui::smartdns::dns_log_set_level(self.log_level);
+
+        Ok(())
+    }
+
+    pub fn set_https(&mut self, enable: bool) {
+        self.is_https = enable;
+        if enable {
+            self.ip = "https://127.0.0.1:0".to_string();
+        } else {
+            self.ip = "http://127.0.0.1:0".to_string();
+        }
+    }
+
+    pub fn set_one_instance(&mut self, one_instance: bool) {
+        self.one_instance = one_instance;
+    }
+
+    pub fn start(&mut self) -> Result<(), Box<dyn std::error::Error>> {
+        if self.one_instance {
+            self.instance_lock_guard = Some(InstanceLockGuard::new_write_guard());
+            if self.dns_server_enable {
+                let ret = self.dns_server.start();
+                if let Err(e) = ret {
+                    dns_log!(LogLevel::ERROR, "start dns server failed: {:?}", e);
+                    return Err(e);
+                }
+            }
+        } else {
+            self.instance_lock_guard = Some(InstanceLockGuard::new_read_guard());
+        }
+
+        self.setup_default_args();
+
+        dns_log!(LogLevel::INFO, "TestServer start");
+        let ret = self.init_server();
+        if let Err(e) = ret {
+            dns_log!(LogLevel::ERROR, "init server failed: {:?}", e);
+            return Err(e);
+        }
+
+        let result = self.plugin.start(&self.args);
+        if let Err(e) = result {
+            dns_log!(LogLevel::ERROR, "start error: {:?}", e);
+            return Err(e);
+        }
+
+        let addr = self.plugin.get_http_server().get_local_addr();
+        if addr.is_none() {
+            return Err(Box::new(std::io::Error::new(
+                std::io::ErrorKind::Other,
+                "get local addr failed",
+            )));
+        }
+
+        let addr = addr.unwrap();
+        if self.is_https {
+            self.ip = format!("https://{}:{}", addr.ip(), addr.port());
+        } else {
+            self.ip = format!("http://{}:{}", addr.ip(), addr.port());
+        }
+        self.is_started = true;
+        Ok(())
+    }
+
+    pub fn stop(&mut self) {
+        if !self.is_started {
+            return;
+        }
+        dns_log!(LogLevel::INFO, "TestServer stop");
+        self.plugin.stop();
+        self.is_started = false;
+        self.one_instance = false;
+        smartdns_ui::smartdns::dns_log_set_level(self.old_log_level);
+        self.dns_server.stop();
+        self.instance_lock_guard = None;
+    }
+}
+
+impl Drop for TestServer {
+    fn drop(&mut self) {
+        self.stop();
+        let _ = self.remove_workdir();
+    }
+}

+ 96 - 0
plugin/smartdns-ui/tests/httpserver_test.rs

@@ -0,0 +1,96 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+mod common;
+
+use smartdns_ui::smartdns::LogLevel;
+use std::{fs::File, io::Write};
+
+#[test]
+fn test_http_server_indexhtml() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+    let www_root = server.get_www_root();
+
+    let mut index_html_file = File::create(www_root.clone() + "/index.html").unwrap();
+    let content = "Hello, world!";
+    index_html_file.write_all(content.as_bytes()).unwrap();
+
+    let client = common::TestClient::new(&server.get_host());
+    let c = client.get("/");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 200);
+    assert_eq!(body, content);
+}
+
+#[test]
+fn test_http_server_somehtml() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+    let www_root = server.get_www_root();
+
+    let mut index_html_file = File::create(www_root.clone() + "/index.html").unwrap();
+    let content = "Hello, world!";
+    index_html_file.write_all(content.as_bytes()).unwrap();
+
+    let mut some_html_file = File::create(www_root.clone() + "/some.html").unwrap();
+    let some_content = "Some index file!";
+    some_html_file.write_all(some_content.as_bytes()).unwrap();
+
+    let client = common::TestClient::new(&server.get_host());
+    let c = client.get("/some.html");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 200);
+    assert_eq!(body, some_content);
+}
+
+#[test]
+fn test_http_server_redirect_indexhtml() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+    let www_root = server.get_www_root();
+
+    let mut index_html_file = File::create(www_root.clone() + "/index.html").unwrap();
+    let content = "Hello, world!";
+    index_html_file.write_all(content.as_bytes()).unwrap();
+
+    let client = common::TestClient::new(&server.get_host());
+    let c = client.get("/some.html");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 200);
+    assert_eq!(body, content);
+}
+
+#[test]
+fn test_http_server_404() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+
+    let client = common::TestClient::new(&server.get_host());
+    let c = client.get("/index.html");
+    assert!(c.is_ok());
+    let (code, _) = c.unwrap();
+    assert_eq!(code, 404);
+}

+ 356 - 0
plugin/smartdns-ui/tests/restapi_test.rs

@@ -0,0 +1,356 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+mod common;
+
+use reqwest;
+use serde_json::json;
+use smartdns_ui::{http_api_msg, http_jwt::JwtClaims, smartdns::LogLevel};
+
+#[tokio::test]
+async fn test_rest_api_login() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+
+    let c = reqwest::Client::new();
+    let body = json!({
+        "user": "admin",
+        "password": "password",
+    });
+
+    let res = c
+        .post(server.get_url("/api/auth/login"))
+        .body(body.to_string())
+        .send()
+        .await
+        .unwrap();
+    let code = res.status();
+    let body = res.text().await.unwrap();
+    println!("res: {}", body);
+    assert_eq!(code, 200);
+
+    let result = http_api_msg::api_msg_parse_auth_token(&body);
+    assert!(result.is_ok());
+    let token = result.unwrap();
+    assert!(!token.token.is_empty());
+    let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
+    validation.insecure_disable_signature_validation();
+    let calims = jsonwebtoken::decode::<JwtClaims>(
+        &token.token,
+        &jsonwebtoken::DecodingKey::from_secret(&[]),
+        &validation,
+    );
+    println!("calims: {:?}", calims);
+    assert_eq!(token.expires_in, "600");
+    assert!(calims.is_ok());
+    let calims = calims.unwrap();
+    let calims = calims.claims;
+    assert_eq!(calims.user, "admin");
+}
+
+#[tokio::test]
+async fn test_rest_api_login_incorrect() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+
+    let c = reqwest::Client::new();
+    let body = json!({
+        "user": "admin",
+        "password": "wrongpassword",
+    });
+
+    let res = c
+        .post(server.get_url("/api/auth/login"))
+        .body(body.to_string())
+        .send()
+        .await
+        .unwrap();
+    let code = res.status();
+    let body = res.text().await.unwrap();
+    println!("res: {}", body);
+    assert_eq!(code, 401);
+
+    let result = http_api_msg::api_msg_parse_error(&body);
+    assert!(result.is_ok());
+    assert_eq!(result.unwrap(), "Incorrect username or password.");
+}
+
+#[test]
+fn test_rest_api_cache_count() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+
+    let mut client = common::TestClient::new(&server.get_host());
+    let res = client.login("admin", "password");
+    assert!(res.is_ok());
+    let c = client.get("/api/cache/count");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 200);
+    let count = http_api_msg::api_msg_parse_cache_number(&body);
+    assert!(count.is_ok());
+    assert_eq!(count.unwrap(), 0);
+}
+
+#[test]
+fn test_rest_api_auth_refresh() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+
+    let mut client = common::TestClient::new(&server.get_host());
+    let res = client.login("admin", "password");
+    assert!(res.is_ok());
+    let c = client.post("/api/auth/refresh", "");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 200);
+    let token = http_api_msg::api_msg_parse_auth_token(&body);
+    assert!(token.is_ok());
+    let token = token.unwrap();
+    assert!(!token.token.is_empty());
+    assert_eq!(token.expires_in, "600");
+    println!("token: {:?}", token);
+}
+
+#[test]
+fn test_rest_api_no_permission() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+
+    let client = common::TestClient::new(&server.get_host());
+    let c = client.get("/api/cache/count");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 401);
+    println!("body: {}", body);
+    let error_msg = http_api_msg::api_msg_parse_error(&body);
+    assert!(error_msg.is_ok());
+    assert_eq!(error_msg.unwrap(), "Please login.");
+}
+
+#[test]
+fn test_rest_api_404() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+
+    let mut client = common::TestClient::new(&server.get_host());
+    let res = client.login("admin", "password");
+    assert!(res.is_ok());
+    let c = client.post("/api/404", "");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 404);
+    let error_msg = http_api_msg::api_msg_parse_error(&body);
+    assert!(error_msg.is_ok());
+    assert_eq!(error_msg.unwrap(), "API not found.");
+}
+
+#[test]
+fn test_rest_api_log_stream() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+
+    let mut client = common::TestClient::new(&server.get_host());
+    let res = client.login("admin", "password");
+    assert!(res.is_ok());
+    let socket = client.websocket("/api/log/stream");
+    assert!(socket.is_ok());
+    let mut socket = socket.unwrap();
+
+    _ = socket.send(tungstenite::Message::Text("aaaa".to_string()));
+    _ = socket.close(None);
+}
+
+#[test]
+fn test_rest_api_log_level() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    server.set_one_instance(true);
+    assert!(server.start().is_ok());
+
+    let mut client = common::TestClient::new(&server.get_host());
+    let res = client.login("admin", "password");
+    assert!(res.is_ok());
+
+    let c = client.get("/api/log/level");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 200);
+    let log_level = http_api_msg::api_msg_parse_loglevel(&body);
+    assert!(log_level.is_ok());
+    assert_eq!(log_level.unwrap(), LogLevel::DEBUG);
+
+    let level_msg = http_api_msg::api_msg_gen_loglevel(LogLevel::ERROR);
+    let c = client.put("/api/log/level", level_msg.as_str());
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 204);
+    println!("body: {}", body);
+
+    assert_eq!(smartdns_ui::smartdns::dns_log_get_level(), LogLevel::ERROR);
+}
+
+#[test]
+fn test_rest_api_get_domain() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+
+    let record = server.new_mock_domain_record();
+    for i in 0..1024 {
+        let mut record = record.clone();
+        record.domain = format!("{}.com", i);
+        assert!(server.add_domain_record(&record).is_ok());
+    }
+
+    let mut client = common::TestClient::new(&server.get_host());
+    let res = client.login("admin", "password");
+    assert!(res.is_ok());
+
+    let c = client.get("/api/domain/count");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 200);
+    let count = http_api_msg::api_msg_parse_count(&body);
+    assert!(count.is_ok());
+    assert_eq!(count.unwrap(), 1024);
+
+    let c = client.get("/api/domain?page_num=11&page_size=10&order=asc");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 200);
+    let result = http_api_msg::api_msg_parse_domain_list(&body);
+    assert!(result.is_ok());
+    let result = result.unwrap();
+    assert_eq!(result.len(), 10);
+    assert_eq!(result[0].id, 101);
+    assert_eq!(result[0].domain, "100.com");
+}
+
+#[test]
+fn test_rest_api_get_by_id() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+
+    let record = server.new_mock_domain_record();
+    for i in 0..1024 {
+        let mut record = record.clone();
+        record.domain = format!("{}.com", i);
+        assert!(server.add_domain_record(&record).is_ok());
+    }
+
+    let mut client = common::TestClient::new(&server.get_host());
+    let res = client.login("admin", "password");
+    assert!(res.is_ok());
+
+    let c = client.get("/api/domain/1000");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 200);
+    let result = http_api_msg::api_msg_parse_domain(&body);
+    assert!(result.is_ok());
+    let result = result.unwrap();
+    assert_eq!(result.id, 1000);
+    assert_eq!(result.domain, "999.com");
+}
+
+#[test]
+fn test_rest_api_delete_domain_by_id() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    assert!(server.start().is_ok());
+
+    let record = server.new_mock_domain_record();
+    for i in 0..1024 {
+        let mut record = record.clone();
+        record.domain = format!("{}.com", i);
+        assert!(server.add_domain_record(&record).is_ok());
+    }
+
+    let mut client = common::TestClient::new(&server.get_host());
+    let res = client.login("admin", "password");
+    assert!(res.is_ok());
+
+    let c = client.delete("/api/domain/1000");
+    assert!(c.is_ok());
+    let (code, _) = c.unwrap();
+    assert_eq!(code, 204);
+
+    let c = client.get("/api/domain/1000");
+    assert!(c.is_ok());
+    let (code, _) = c.unwrap();
+    assert_eq!(code, 404);
+
+    let c = client.get("/api/domain/count");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 200);
+    let count = http_api_msg::api_msg_parse_count(&body);
+    assert!(count.is_ok());
+    assert_eq!(count.unwrap(), 1023);
+}
+
+#[test]
+fn test_rest_api_server_version() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    server.enable_mock_server();
+    assert!(server.start().is_ok());
+
+    let client = common::TestClient::new(&server.get_host());
+
+    let c = client.get("/api/server/version");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 200);
+    let version = http_api_msg::api_msg_parse_version(&body);
+    assert!(version.is_ok());
+    let version = version.unwrap();
+    assert_eq!(version.0, smartdns_ui::smartdns::smartdns_version());
+    assert_eq!(version.1, env!("CARGO_PKG_VERSION"));
+}
+
+#[test]
+fn test_rest_api_https_server() {
+    let mut server = common::TestServer::new();
+    server.set_log_level(LogLevel::DEBUG);
+    server.enable_mock_server();
+    server.set_https(true);
+    assert!(server.start().is_ok());
+
+    let client = common::TestClient::new(&server.get_host());
+
+    let c = client.get("/api/server/version");
+    assert!(c.is_ok());
+    let (code, body) = c.unwrap();
+    assert_eq!(code, 200);
+    let version = http_api_msg::api_msg_parse_version(&body);
+    assert!(version.is_ok());
+    let version = version.unwrap();
+    assert_eq!(version.0, smartdns_ui::smartdns::smartdns_version());
+    assert_eq!(version.1, env!("CARGO_PKG_VERSION"));
+}
+

+ 18 - 5
src/Makefile

@@ -14,9 +14,13 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-BIN=smartdns 
+BIN=smartdns
+SMARTDNS_LIB=libsmartdns.a
+SMARTDNS_TEST_LIB=libsmartdns-test.a
 OBJS_LIB=$(patsubst %.c,%.o,$(wildcard lib/*.c))
-OBJS_MAIN=$(patsubst %.c,%.o,$(wildcard *.c))
+OBJS_MAIN=$(filter-out main.o, $(patsubst %.c,%.o,$(wildcard *.c)))
+TEST_OBJS=$(patsubst %.o,%_test.o,$(OBJS_MAIN) $(OBJS_LIB))
+MAIN_OBJ = main.o
 OBJS=$(OBJS_MAIN) $(OBJS_LIB)
 
 # cflags
@@ -71,11 +75,20 @@ endif
 
 all: $(BIN)
 
-$(BIN) : $(OBJS)
-	$(CC) $(OBJS) -o $@ $(LDFLAGS)
+%_test.o: %.c
+	$(CC) -DTEST $(CFLAGS) -c $< -o $@
+
+$(BIN) : $(MAIN_OBJ) $(SMARTDNS_LIB)
+	$(CC) $^ -o $@ $(LDFLAGS)
+
+$(SMARTDNS_TEST_LIB): $(TEST_OBJS)
+	$(AR) rcs $@ $^
+
+$(SMARTDNS_LIB): $(OBJS)
+	$(AR) rcs $@ $^
 
 clang-tidy:
 	clang-tidy -p=. $(OBJS_MAIN:.o=.c) -- $(CFLAGS)
 
 clean:
-	$(RM) $(OBJS) $(BIN)
+	$(RM) $(OBJS) $(BIN) $(SMARTDNS_LIB) $(MAIN_OBJ) $(SMARTDNS_TEST_LIB) $(TEST_OBJS)

+ 106 - 1
src/dns_conf.c

@@ -121,6 +121,7 @@ char dns_conf_ca_file[DNS_MAX_PATH];
 char dns_conf_ca_path[DNS_MAX_PATH];
 
 char dns_conf_cache_file[DNS_MAX_PATH];
+char dns_conf_data_dir[DNS_MAX_PATH];
 int dns_conf_cache_persist = 2;
 int dns_conf_cache_checkpoint_time = DNS_DEFAULT_CHECKPOINT_TIME;
 
@@ -191,6 +192,7 @@ static void _config_ip_iter_free(radix_node_t *node, void *cbctx);
 static int _config_nftset_setvalue(struct dns_nftset_names *nftsets, const char *nftsetvalue);
 static int _config_client_rule_flag_set(const char *ip_cidr, unsigned int flag, unsigned int is_clear);
 static int _config_client_rule_group_add(const char *client, const char *group_name);
+static void _config_plugin_table_conf_destroy(void);
 
 #define group_member(m) ((void *)offsetof(struct dns_conf_group, m))
 
@@ -5779,6 +5781,39 @@ static struct dns_conf_plugin *_config_get_plugin(const char *file)
 	return NULL;
 }
 
+static struct dns_conf_plugin_conf *_config_get_plugin_conf(const char *key)
+{
+	uint32_t hash = 0;
+	struct dns_conf_plugin_conf *conf = NULL;
+
+	hash = hash_string(key);
+	hash_for_each_possible(dns_conf_plugin_table.plugins_conf, conf, node, hash)
+	{
+		if (strncmp(conf->key, key, DNS_MAX_PATH) != 0) {
+			continue;
+		}
+
+		return conf;
+	}
+
+	return NULL;
+}
+
+const char *dns_conf_get_plugin_conf(const char *key)
+{
+	struct dns_conf_plugin_conf *conf = _config_get_plugin_conf(key);
+	if (conf == NULL) {
+		return NULL;
+	}
+
+	return conf->value;
+}
+
+void dns_conf_clear_all_plugin_conf(void)
+{
+	_config_plugin_table_conf_destroy();
+}
+
 static int _config_plugin(void *data, int argc, char *argv[])
 {
 #ifdef BUILD_STATIC
@@ -5836,6 +5871,36 @@ errout:
 	return -1;
 }
 
+static int _config_plugin_conf_add(const char *key, const char *value)
+{
+	uint32_t hash = 0;
+	struct dns_conf_plugin_conf *conf = NULL;
+
+	if (key == NULL || value == NULL) {
+		tlog(TLOG_ERROR, "invalid parameter.");
+		goto errout;
+	}
+
+	conf = _config_get_plugin_conf(key);
+	if (conf == NULL) {
+
+		hash = hash_string(key);
+		conf = malloc(sizeof(*conf));
+		if (conf == NULL) {
+			goto errout;
+		}
+		memset(conf, 0, sizeof(*conf));
+		safe_strncpy(conf->key, key, sizeof(conf->key) - 1);
+		hash_add(dns_conf_plugin_table.plugins_conf, &conf->node, hash);
+	}
+	safe_strncpy(conf->value, value, sizeof(conf->value) - 1);
+
+	return 0;
+
+errout:
+	return -1;
+}
+
 int dns_server_check_update_hosts(void)
 {
 	struct stat statbuf;
@@ -5986,6 +6051,7 @@ static struct config_item _config_item[] = {
 	CONF_SSIZE("cache-size", &dns_conf_cachesize, -1, CONF_INT_MAX),
 	CONF_SSIZE("cache-mem-size", &dns_conf_cache_max_memsize, 0, CONF_INT_MAX),
 	CONF_CUSTOM("cache-file", _config_option_parser_filepath, (char *)&dns_conf_cache_file),
+	CONF_CUSTOM("data-dir", _config_option_parser_filepath, (char *)&dns_conf_data_dir),
 	CONF_YESNO("cache-persist", &dns_conf_cache_persist),
 	CONF_INT("cache-checkpoint-time", &dns_conf_cache_checkpoint_time, 0, 3600 * 24 * 7),
 	CONF_YESNO_FUNC("prefetch-domain", _dns_conf_group_yesno, group_member(dns_prefetch)),
@@ -6061,7 +6127,18 @@ static struct config_item _config_item[] = {
 	CONF_END(),
 };
 
-static int _conf_printf(const char *file, int lineno, int ret)
+static int _conf_value_handler(const char *key, const char *value)
+{
+	if (strstr(key, ".") == NULL) {
+		return -1;
+	}
+
+	_config_plugin_conf_add(key, value);
+
+	return 0;
+}
+
+static int _conf_printf(const char *key, const char *value, const char *file, int lineno, int ret)
 {
 	switch (ret) {
 	case CONF_RET_ERR:
@@ -6071,6 +6148,10 @@ static int _conf_printf(const char *file, int lineno, int ret)
 		return -1;
 		break;
 	case CONF_RET_NOENT:
+		if (_conf_value_handler(key, value) == 0) {
+			return 0;
+		}
+
 		tlog(TLOG_WARN, "unsupported config at '%s:%d'.", file, lineno);
 		return -1;
 		break;
@@ -6214,6 +6295,15 @@ const char *dns_conf_get_cache_dir(void)
 	return dns_conf_cache_file;
 }
 
+const char *dns_conf_get_data_dir(void)
+{
+	if (dns_conf_data_dir[0] == '\0') {
+		return SMARTDNS_DATA_DIR;
+	}
+
+	return dns_conf_data_dir;
+}
+
 static int _dns_server_load_conf_init(void)
 {
 	dns_conf_client_rule.rule = New_Radix();
@@ -6238,6 +6328,7 @@ static int _dns_server_load_conf_init(void)
 	hash_init(dns_ip_set_name_table.names);
 	hash_init(dns_conf_srv_record_table.srv);
 	hash_init(dns_conf_plugin_table.plugins);
+	hash_init(dns_conf_plugin_table.plugins_conf);
 
 	if (_config_current_group_push_default() != 0) {
 		tlog(TLOG_ERROR, "init default group failed.");
@@ -6309,6 +6400,19 @@ static void _config_plugin_table_destroy(void)
 	}
 }
 
+static void _config_plugin_table_conf_destroy(void)
+{
+	struct dns_conf_plugin_conf *plugin_conf = NULL;
+	struct hlist_node *tmp = NULL;
+	unsigned long i = 0;
+
+	hash_for_each_safe(dns_conf_plugin_table.plugins_conf, i, tmp, plugin_conf, node)
+	{
+		hlist_del_init(&plugin_conf->node);
+		free(plugin_conf);
+	}
+}
+
 void dns_server_load_exit(void)
 {
 	_config_rule_group_destroy();
@@ -6321,6 +6425,7 @@ void dns_server_load_exit(void)
 	_config_proxy_table_destroy();
 	_config_srv_record_table_destroy();
 	_config_plugin_table_destroy();
+	_config_plugin_table_conf_destroy();
 
 	dns_conf_server_num = 0;
 	dns_server_bind_destroy();

+ 16 - 0
src/dns_conf.h

@@ -68,6 +68,7 @@ extern "C" {
 #define SMARTDNS_LOG_FILE "/var/log/smartdns/smartdns.log"
 #define SMARTDNS_AUDIT_FILE "/var/log/smartdns/smartdns-audit.log"
 #define SMARTDNS_CACHE_FILE "/var/cache/smartdns/smartdns.cache"
+#define SMARTDNS_DATA_DIR "/var/lib/smartdns"
 #define SMARTDNS_TMP_CACHE_FILE "/tmp/smartdns.cache"
 #define SMARTDNS_DEBUG_DIR "/tmp/smartdns"
 #define DNS_RESOLV_FILE "/etc/resolv.conf"
@@ -661,8 +662,16 @@ struct dns_conf_plugin {
 	int argc;
 	int args_len;
 };
+
+struct dns_conf_plugin_conf {
+	struct hlist_node node;
+	char key[MAX_KEY_LEN];
+	char value[MAX_LINE_LEN];
+};
+
 struct dns_conf_plugin_table {
 	DECLARE_HASHTABLE(plugins, 4);
+	DECLARE_HASHTABLE(plugins_conf, 4);
 };
 extern struct dns_conf_plugin_table dns_conf_plugin_table;
 
@@ -695,6 +704,7 @@ extern char dns_conf_ca_file[DNS_MAX_PATH];
 extern char dns_conf_ca_path[DNS_MAX_PATH];
 
 extern char dns_conf_cache_file[DNS_MAX_PATH];
+extern char dns_conf_var_libdir[DNS_MAX_PATH];
 extern int dns_conf_cache_persist;
 extern int dns_conf_cache_checkpoint_time;
 
@@ -757,10 +767,16 @@ struct dns_conf_group *dns_server_get_default_rule_group(void);
 
 struct client_roue_group_mac *dns_server_rule_group_mac_get(const uint8_t mac[6]);
 
+const char *dns_conf_get_plugin_conf(const char *key);
+
+void dns_conf_clear_all_plugin_conf(void);
+
 extern int config_additional_file(void *data, int argc, char *argv[]);
 
 const char *dns_conf_get_cache_dir(void);
 
+const char *dns_conf_get_data_dir(void);
+
 #ifdef __cplusplus
 }
 #endif

+ 26 - 0
src/dns_plugin.c

@@ -18,6 +18,7 @@
 
 #include "dns_plugin.h"
 
+#include "dns_conf.h"
 #include "include/conf.h"
 #include "include/hashtable.h"
 #include "include/list.h"
@@ -343,3 +344,28 @@ void smartdns_plugin_log(smartdns_log_level level, const char *file, int line, c
 {
 	tlog_ext((tlog_level)level, file, line, func, NULL, "%s", msg);
 }
+
+int smartdns_plugin_can_log(smartdns_log_level level)
+{
+	return tlog_getlevel() <= (tlog_level)level;
+}
+
+void smartdns_plugin_log_setlevel(smartdns_log_level level)
+{
+	tlog_setlevel((tlog_level)level);
+}
+
+int smartdns_plugin_log_getlevel(void)
+{
+	return tlog_getlevel();
+}
+
+const char *smartdns_plugin_get_config(const char *key)
+{
+	return dns_conf_get_plugin_conf(key);
+}
+
+void smartdns_plugin_clear_all_config(void)
+{
+	dns_conf_clear_all_plugin_conf();
+}

+ 10 - 0
src/dns_plugin.h

@@ -66,6 +66,16 @@ const char **dns_plugin_get_argv(struct dns_plugin *plugin);
 
 void smartdns_plugin_log(smartdns_log_level level, const char *file, int line, const char *func, const char *msg);
 
+int smartdns_plugin_can_log(smartdns_log_level level);
+
+void smartdns_plugin_log_setlevel(smartdns_log_level level);
+
+int smartdns_plugin_log_getlevel(void);
+
+const char *smartdns_plugin_get_config(const char *key);
+
+void smartdns_plugin_clear_all_config(void);
+
 int smartdns_plugin_func_server_recv(struct dns_packet *packet, unsigned char *inpacket, int inpacket_len,
 									 struct sockaddr_storage *local, socklen_t local_len, struct sockaddr_storage *from,
 									 socklen_t from_len);

+ 14 - 1
src/dns_server.c

@@ -348,6 +348,7 @@ struct dns_request {
 	atomic_t do_callback;
 	atomic_t adblock;
 	atomic_t soa_num;
+	atomic_t plugin_complete_called;
 
 	/* send original raw packet to server/client like proxy */
 	int passthrough;
@@ -390,6 +391,8 @@ struct dns_request {
 	int has_cname_loop;
 
 	void *private_data;
+
+	uint64_t query_time;
 };
 
 /* dns server data */
@@ -2905,7 +2908,10 @@ static void _dns_server_request_release_complete(struct dns_request *request, in
 	}
 
 	atomic_inc(&request->refcnt);
-	smartdns_plugin_func_server_complete_request(request);
+	if (atomic_inc_return(&request->plugin_complete_called) == 1) {
+		smartdns_plugin_func_server_complete_request(request);
+	}
+
 	if (atomic_dec_return(&request->refcnt) > 0) {
 		/* plugin may hold request. */
 		return;
@@ -2964,6 +2970,11 @@ int dns_server_request_get_qclass(struct dns_request *request)
 	return request->qclass;
 }
 
+uint64_t dns_server_request_get_query_time(struct dns_request *request)
+{
+	return request->query_time;
+}
+
 int dns_server_request_get_id(struct dns_request *request)
 {
 	return request->id;
@@ -3079,6 +3090,7 @@ static struct dns_request *_dns_server_new_request(void)
 	atomic_set(&request->refcnt, 0);
 	atomic_set(&request->notified, 0);
 	atomic_set(&request->do_callback, 0);
+	atomic_set(&request->plugin_complete_called, 0);
 	request->ping_time = -1;
 	request->prefetch = 0;
 	request->dualstack_selection = 0;
@@ -3090,6 +3102,7 @@ static struct dns_request *_dns_server_new_request(void)
 	request->conf = dns_server_get_default_rule_group();
 	request->check_order_list = &dns_conf_default_check_orders;
 	request->response_mode = dns_conf_default_response_mode;
+	request->query_time = get_utc_time_ms();
 	INIT_LIST_HEAD(&request->list);
 	INIT_LIST_HEAD(&request->pending_list);
 	INIT_LIST_HEAD(&request->check_list);

+ 2 - 0
src/dns_server.h

@@ -87,6 +87,8 @@ int dns_server_request_get_id(struct dns_request *request);
 
 int dns_server_request_get_rcode(struct dns_request *request);
 
+uint64_t dns_server_request_get_query_time(struct dns_request *request);
+
 void dns_server_request_get(struct dns_request *request);
 
 void dns_server_request_put(struct dns_request *request);

+ 2 - 2
src/include/conf.h

@@ -21,7 +21,7 @@
 
 #include <unistd.h>
 
-#define MAX_LINE_LEN 8192
+#define MAX_LINE_LEN 4096
 #define MAX_KEY_LEN 64
 #define CONF_INT_MAX (~(1 << 31))
 #define CONF_INT_MIN (1 << 31)
@@ -204,7 +204,7 @@ extern int conf_enum(const char *item, void *data, int argc, char *argv[]);
  *
  */
 
-typedef int(conf_error_handler)(const char *file, int lineno, int ret);
+typedef int(conf_error_handler)(const char *key, const char *value, const char *file, int lineno, int ret);
 
 int conf_parse_key_values(char *line, int *key_num, char **keys, char **values);
 

+ 17 - 9
src/lib/conf.c

@@ -52,6 +52,7 @@ static char *get_dir_name(char *path)
 const char *conf_get_conf_fullpath(const char *path, char *fullpath, size_t path_len)
 {
 	char file_path_dir[PATH_MAX];
+	const char *conf_file = NULL;
 
 	if (path_len < 1) {
 		return NULL;
@@ -62,7 +63,13 @@ const char *conf_get_conf_fullpath(const char *path, char *fullpath, size_t path
 		return fullpath;
 	}
 
-	strncpy(file_path_dir, conf_get_conf_file(), PATH_MAX - 1);
+	conf_file = conf_get_conf_file();
+	if (conf_file == NULL) {
+		strncpy(fullpath, path, path_len);
+		return fullpath;
+	}
+
+	strncpy(file_path_dir, conf_file, PATH_MAX - 1);
 	file_path_dir[PATH_MAX - 1] = 0;
 	get_dir_name(file_path_dir);
 	if (file_path_dir[0] == '\0') {
@@ -448,7 +455,7 @@ static int conf_parse_args(char *key, char *value, int *argc, char **argv)
 
 void load_exit(void) {}
 
-static int load_conf_printf(const char *file, int lineno, int ret)
+static int load_conf_printf(const char *key, const char *value, const char *file, int lineno, int ret)
 {
 	if (ret != CONF_RET_OK) {
 		printf("process config file '%s' failed at line %d.", file, lineno);
@@ -504,10 +511,9 @@ static int load_conf_file(const char *file, struct config_item *items, conf_erro
 		}
 
 		/* comment in wrap line, skip */
-		if (is_last_line_wrap && read_len > 0) {
-			if (*(line + line_len) == '#') {
-				continue;
-			}
+		if (*(line + line_len) == '#') {
+			line_len = 0;
+			continue;
 		}
 
 		/* trim prefix spaces in wrap line */
@@ -547,7 +553,7 @@ static int load_conf_file(const char *file, struct config_item *items, conf_erro
 
 		/* if field format is not key = value, error */
 		if (filed_num != 2 && filed_num != 1) {
-			handler(file, line_no, CONF_RET_BADCONF);
+			handler(NULL, NULL, file, line_no, CONF_RET_BADCONF);
 			goto errout;
 		}
 
@@ -580,7 +586,7 @@ static int load_conf_file(const char *file, struct config_item *items, conf_erro
 			current_conf_file = file;
 			current_conf_lineno = line_no;
 			call_ret = items[i].item_func(items[i].item, items[i].data, argc, argv);
-			ret = handler(file, line_no, call_ret);
+			ret = handler(key, value, file, line_no, call_ret);
 			if (ret != 0) {
 				conf_getopt_reset();
 				goto errout;
@@ -597,7 +603,9 @@ static int load_conf_file(const char *file, struct config_item *items, conf_erro
 		}
 
 		if (is_func_found == 0) {
-			handler(file, line_no, CONF_RET_NOENT);
+			if (handler(key, value, file, line_no, CONF_RET_NOENT) != 0) {
+				goto errout;
+			}
 		}
 	}
 

+ 24 - 0
src/main.c

@@ -0,0 +1,24 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2024 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "smartdns.h"
+
+int main(int argc, char *argv[])
+{
+    return smartdns_main(argc, argv);
+}

+ 107 - 3
src/smartdns.c

@@ -113,6 +113,14 @@ static void _smartdns_get_version(char *str_ver, int str_ver_len)
 #endif
 }
 
+const char *smartdns_version() {
+	static char str_ver[256] = {0};
+	if (str_ver[0] == 0) {
+		_smartdns_get_version(str_ver, sizeof(str_ver));
+	}
+	return str_ver;
+}
+
 static void _show_version(void)
 {
 	char str_ver[256] = {0};
@@ -401,6 +409,28 @@ static int _smartdns_create_cert(void)
 	return 0;
 }
 
+int smartdns_get_cert(char *key, char *cert)
+{
+	if (dns_conf_need_cert == 0) {
+		dns_conf_need_cert = 1;
+	}
+
+	if (_smartdns_create_cert() != 0) {
+		tlog(TLOG_WARN, "generate ssl cert and key file failed. %s", strerror(errno));
+		return -1;
+	}
+
+	if (key != NULL) {
+		safe_strncpy(key, dns_conf_bind_ca_key_file, PATH_MAX);
+	}
+
+	if (cert != NULL) {
+		safe_strncpy(cert, dns_conf_bind_ca_file, PATH_MAX);
+	}
+
+	return 0;
+}
+
 static int _smartdns_init_ssl(void)
 {
 #if OPENSSL_API_COMPAT < 0x10100000L
@@ -729,6 +759,37 @@ static int _smartdns_create_cache_dir(void)
 	return 0;
 }
 
+static int _smartdns_create_datadir(void)
+{
+	uid_t uid = 0;
+	gid_t gid = 0;
+	struct stat sb;
+	char data_dir[PATH_MAX] = {0};
+	int unused __attribute__((unused)) = 0;
+
+	safe_strncpy(data_dir, dns_conf_get_data_dir(), PATH_MAX);
+	dir_name(data_dir);
+
+	if (get_uid_gid(&uid, &gid) != 0) {
+		return -1;
+	}
+
+	mkdir(data_dir, 0750);
+	if (stat(data_dir, &sb) == 0 && sb.st_uid == uid && sb.st_gid == gid && (sb.st_mode & 0700) == 0700) {
+		return 0;
+	}
+
+	if (chown(data_dir, uid, gid) != 0) {
+		if (dns_conf_cache_file[0] == '\0') {
+			safe_strncpy(dns_conf_cache_file, SMARTDNS_DATA_DIR, sizeof(dns_conf_cache_file));
+		}
+	}
+
+	unused = chmod(data_dir, 0750);
+	unused = chown(dns_conf_get_data_dir(), uid, gid);
+	return 0;
+}
+
 static int _set_rlimit(void)
 {
 	struct rlimit value;
@@ -742,6 +803,7 @@ static int _smartdns_init_pre(void)
 {
 	_smartdns_create_logdir();
 	_smartdns_create_cache_dir();
+	_smartdns_create_datadir();
 
 	_set_rlimit();
 
@@ -869,8 +931,8 @@ void smartdns_exit(int status)
 
 void smartdns_restart(void)
 {
-	dns_server_stop();
 	exit_restart = 1;
+	dns_server_stop();
 }
 
 static int smartdns_enter_monitor_mode(int argc, char *argv[], int no_deamon)
@@ -914,11 +976,11 @@ static void smartdns_test_notify_func(int fd_notify, uint64_t retval)
 		close_all_fd(fd_notify);                                                                                       \
 	}
 
-int smartdns_main(int argc, char *argv[], int fd_notify, int no_close_allfds)
+int smartdns_test_main(int argc, char *argv[], int fd_notify, int no_close_allfds)
 #else
 #define smartdns_test_notify(retval)
 #define smartdns_close_allfds() close_all_fd(-1)
-int main(int argc, char *argv[])
+int smartdns_main(int argc, char *argv[])
 #endif
 {
 	int ret = 0;
@@ -1123,3 +1185,45 @@ errout:
 	_smartdns_exit();
 	return ret;
 }
+
+int smartdns_server_run(const char *config_file)
+{
+	int ret = -1;
+
+	ret = dns_server_load_conf(config_file);
+	if (ret != 0) {
+		fprintf(stderr, "load config failed.\n");
+		goto errout;
+	}
+
+	ret = _smartdns_init_pre();
+	if (ret != 0) {
+		fprintf(stderr, "init failed.\n");
+		goto errout;
+	}
+
+	ret = _smartdns_init();
+	if (ret != 0) {
+		fprintf(stderr, "init failed.\n");
+		goto errout;
+	}
+
+	ret = _smartdns_run();
+	if (ret != 0) {
+		fprintf(stderr, "run failed.\n");
+		goto errout;
+	}
+	
+	_smartdns_exit();
+	tlog(TLOG_INFO, "smartdns exit...");
+	return ret;
+errout:
+	_smartdns_exit();
+	return -1;
+}
+
+int smartdns_server_stop(void)
+{
+	dns_server_stop();
+	return 0;
+}

+ 11 - 1
src/smartdns.h

@@ -27,13 +27,23 @@ void smartdns_exit(int status);
 
 void smartdns_restart(void);
 
+int smartdns_get_cert(char *key, char *cert);
+
+int smartdns_main(int argc, char *argv[]);
+
+int smartdns_server_run(const char *config_file);
+
+int smartdns_server_stop(void);
+
+const char *smartdns_version(void);
+
 #ifdef TEST
 
 typedef void (*smartdns_post_func)(void *arg);
 
 int smartdns_reg_post_func(smartdns_post_func func, void *arg);
 
-int smartdns_main(int argc, char *argv[], int fd_notify, int no_close_allfds);
+int smartdns_test_main(int argc, char *argv[], int fd_notify, int no_close_allfds);
 
 #endif
 

+ 34 - 1
src/util.c

@@ -218,6 +218,18 @@ int drop_root_privilege(void)
 	return 0;
 }
 
+unsigned long long get_utc_time_ms(void)
+{
+    struct timeval tv;
+    gettimeofday(&tv, NULL);
+
+    unsigned long long millisecondsSinceEpoch =
+        (unsigned long long)(tv.tv_sec) * 1000 +
+        (unsigned long long)(tv.tv_usec) / 1000;
+
+	return millisecondsSinceEpoch;
+}
+
 char *dir_name(char *path)
 {
 	if (strstr(path, "/") == NULL) {
@@ -691,6 +703,7 @@ int check_is_ipv6(const char *ip)
 
 	return 0;
 }
+
 int check_is_ipaddr(const char *ip)
 {
 	if (strstr(ip, ".")) {
@@ -1343,6 +1356,12 @@ int generate_cert_key(const char *key_path, const char *cert_path, const char *s
 	key_file = BIO_new_file(key_path, "wb");
 	cert_file = BIO_new_file(cert_path, "wb");
 	cert = X509_new();
+	if (cert == NULL) {
+		goto out;
+	}
+
+	X509_set_version(cert, 2);
+
 #if (OPENSSL_VERSION_NUMBER >= 0x30000000L)
 	pkey = EVP_RSA_gen(RSA_KEY_LENGTH);
 #else
@@ -1385,10 +1404,24 @@ int generate_cert_key(const char *key_path, const char *cert_path, const char *s
 		if (cert_ext == NULL) {
 			goto out;
 		}
-		X509_add_ext(cert, cert_ext, -1);
+		ret = X509_add_ext(cert, cert_ext, -1);
 	}
 
 	X509_set_issuer_name(cert, name);
+
+	// Add X509v3 extensions
+    cert_ext = X509V3_EXT_conf_nid(NULL, NULL, NID_basic_constraints, "CA:FALSE");
+    ret = X509_add_ext(cert, cert_ext, -1);
+    X509_EXTENSION_free(cert_ext);
+
+	cert_ext = X509V3_EXT_conf_nid(NULL, NULL, NID_key_usage, "digitalSignature,keyEncipherment");
+    X509_add_ext(cert, cert_ext, -1);
+    X509_EXTENSION_free(cert_ext);
+
+    cert_ext = X509V3_EXT_conf_nid(NULL, NULL, NID_subject_key_identifier, "hash");
+    X509_add_ext(cert, cert_ext, -1);
+    X509_EXTENSION_free(cert_ext);
+
 	X509_sign(cert, pkey, EVP_sha256());
 
 	ret = PEM_write_bio_PrivateKey(key_file, pkey, NULL, NULL, 0, NULL, NULL);

+ 2 - 0
src/util.h

@@ -55,6 +55,8 @@ void bug_ext(const char *file, int line, const char *func, const char *errfmt, .
 
 unsigned long get_tick_count(void);
 
+unsigned long long get_utc_time_ms(void);
+
 char *dir_name(char *path);
 
 int get_uid_gid(uid_t *uid, gid_t *gid);

+ 9 - 9
test/Makefile

@@ -14,19 +14,15 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+SMARTDNS_SRC_DIR=../src
+
 BIN=test.bin
-CFLAGS += -I../src -I../src/include
-CFLAGS += -DTEST
-CFLAGS += -g -Wall -Wstrict-prototypes -fno-omit-frame-pointer -Wstrict-aliasing -funwind-tables -Wmissing-prototypes -Wshadow -Wextra -Wno-unused-parameter -Wno-implicit-fallthrough
+SMARTDNS_TEST_LIB=$(SMARTDNS_SRC_DIR)/libsmartdns-test.a
 
 CXXFLAGS += -g
 CXXFLAGS += -DTEST
 CXXFLAGS += -I./ -I../src -I../src/include
 
-OBJS_LIB=$(patsubst %.c,%.o,$(wildcard ../src/lib/*.c))
-OBJS_MAIN=$(patsubst %.c,%.o,$(wildcard ../src/*.c))
-OBJS = $(OBJS_LIB) $(OBJS_MAIN)
-
 TEST_SOURCES := $(wildcard *.cc) $(wildcard */*.cc) $(wildcard */*/*.cc)
 TEST_OBJECTS := $(patsubst %.cc, %.o, $(TEST_SOURCES))
 OBJS += $(TEST_OBJECTS)
@@ -37,11 +33,15 @@ LDFLAGS += -lssl -lcrypto -lpthread -ldl -lgtest -lstdc++ -lm
 
 all: $(BIN)
 
-$(BIN) : $(OBJS)
-	$(CC) $(OBJS) -o $@ $(LDFLAGS)
+$(BIN) : $(OBJS) $(SMARTDNS_TEST_LIB)
+	$(CC) $^ -o $@ $(LDFLAGS)
 
 test: $(BIN)
 	./$(BIN)
 
+$(SMARTDNS_TEST_LIB):
+	$(MAKE) -C $(SMARTDNS_SRC_DIR) libsmartdns-test.a
+
 clean:
 	$(RM) $(OBJS) $(BIN)
+	$(MAKE) -C $(SMARTDNS_SRC_DIR) clean

+ 2 - 3
test/server.cc

@@ -392,7 +392,7 @@ cache-persist no
 
 			smartdns_reg_post_func(Server::StartPost, this);
 			dns_ping_cap_force_enable = 1;
-			smartdns_main(args.size(), argv, fds[1], 0);
+			smartdns_test_main(args.size(), argv, fds[1], 0);
 			_exit(1);
 		} else if (pid < 0) {
 			return false;
@@ -407,8 +407,7 @@ cache-persist no
 
 			smartdns_reg_post_func(Server::StartPost, this);
 			dns_ping_cap_force_enable = 1;
-			smartdns_main(args.size(), argv, fds[1], 1);
-			dns_ping_cap_force_enable = 0;
+			smartdns_test_main(args.size(), argv, fds[1], 1);
 			smartdns_reg_post_func(nullptr, nullptr);
 		});
 	} else {