diff --git a/src-tauri/src_crates/crate_mihomo_api/.gitignore b/src-tauri/src_crates/crate_mihomo_api/.gitignore new file mode 100644 index 00000000..4c49bd78 --- /dev/null +++ b/src-tauri/src_crates/crate_mihomo_api/.gitignore @@ -0,0 +1 @@ +.env diff --git a/src-tauri/src_crates/crate_mihomo_api/Cargo.toml b/src-tauri/src_crates/crate_mihomo_api/Cargo.toml index 53fd9744..b1dbfbfe 100644 --- a/src-tauri/src_crates/crate_mihomo_api/Cargo.toml +++ b/src-tauri/src_crates/crate_mihomo_api/Cargo.toml @@ -2,13 +2,20 @@ name = "mihomo_api" edition = "2024" -[features] -debug = [] - [dependencies] -reqwest = { version = "0.12.15", features = ["json"] } -serde = { version = "1.0.219", features = ["derive"] } +async-trait = "0.1.88" +http-body-util = "0.1.3" +hyper = { version = "1.6.0", features = ["http1", "client"] } +hyper-util = "0.1.11" +hyperlocal = "0.9.1" serde_json = "1.0.140" +tokio = { version = "1.44.1", features = [ + "rt", + "macros", + "rt-multi-thread", + "io-std", +] } [dev-dependencies] -tokio = { version = "1.44.1", features = ["rt", "macros"] } +dotenv = "0.15.0" +lazy_static = "1.5.0" diff --git a/src-tauri/src_crates/crate_mihomo_api/src/lib.rs b/src-tauri/src_crates/crate_mihomo_api/src/lib.rs index 61a2d3c2..5aac4768 100644 --- a/src-tauri/src_crates/crate_mihomo_api/src/lib.rs +++ b/src-tauri/src_crates/crate_mihomo_api/src/lib.rs @@ -1,162 +1,48 @@ -use reqwest::{Method, header::HeaderMap}; -use serde_json::json; -use std::{ - sync::{Arc, Mutex}, - time::Duration, -}; +// impl MihomoManager { +// pub async fn patch_configs(&self, config: serde_json::Value) -> Result<(), String> { +// let url = format!("{}/configs", self.mihomo_server); +// let response = self.send_request(Method::PATCH, url, Some(config)).await?; +// if response["code"] == 204 { +// Ok(()) +// } else { +// Err(response["message"] +// .as_str() +// .unwrap_or("unknown error") +// .to_string()) +// } +// } + +// pub async fn test_proxy_delay( +// &self, +// name: &str, +// test_url: Option, +// timeout: i32, +// ) -> Result { +// let test_url = test_url.unwrap_or("http://cp.cloudflare.com/generate_204".to_string()); +// let url = format!( +// "{}/proxies/{}/delay?url={}&timeout={}", +// self.mihomo_server, name, test_url, timeout +// ); +// let response = self.send_request(Method::GET, url, None).await?; +// Ok(response) +// } + +// pub async fn delete_connection(&self, id: &str) -> Result<(), String> { +// let url = format!("{}/connections/{}", self.mihomo_server, id); +// let response = self.send_request(Method::DELETE, url, None).await?; +// if response["code"] == 204 { +// Ok(()) +// } else { +// Err(response["message"] +// .as_str() +// .unwrap_or("unknown error") +// .to_string()) +// } +// } +// } + pub mod model; -pub use model::{MihomoData, MihomoManager}; - -impl MihomoManager { - pub fn new(mihomo_server: String, headers: HeaderMap) -> Self { - Self { - mihomo_server, - data: Arc::new(Mutex::new(MihomoData { - proxies: serde_json::Value::Null, - providers_proxies: serde_json::Value::Null, - })), - headers, - } - } - - fn update_proxies(&self, proxies: serde_json::Value) { - let mut data = self.data.lock().unwrap(); - data.proxies = proxies; - } - - fn update_providers_proxies(&self, providers_proxies: serde_json::Value) { - let mut data = self.data.lock().unwrap(); - data.providers_proxies = providers_proxies; - } - - pub fn get_mihomo_server(&self) -> String { - self.mihomo_server.clone() - } - - pub fn get_proxies(&self) -> serde_json::Value { - let data = self.data.lock().unwrap(); - data.proxies.clone() - } - - pub fn get_providers_proxies(&self) -> serde_json::Value { - let data = self.data.lock().unwrap(); - data.providers_proxies.clone() - } - - async fn send_request( - &self, - method: Method, - url: String, - data: Option, - ) -> Result { - let client_response = reqwest::ClientBuilder::new() - .default_headers(self.headers.clone()) - .no_proxy() - .timeout(Duration::from_secs(60)) - .build() - .map_err(|e| e.to_string())? - .request(method.clone(), &url) - .json(&data.unwrap_or(json!({}))) - .send() - .await - .map_err(|e| e.to_string())?; - - let response = match method { - Method::PATCH => { - let status = client_response.status(); - if status.as_u16() == 204 { - json!({"code": 204}) - } else { - client_response - .json::() - .await - .map_err(|e| e.to_string())? - } - } - Method::PUT => json!(client_response.text().await.map_err(|e| e.to_string())?), - _ => client_response - .json::() - .await - .map_err(|e| e.to_string())?, - }; - Ok(response) - } - - pub async fn refresh_proxies(&self) -> Result<&Self, String> { - let url = format!("{}/proxies", self.mihomo_server); - let proxies = self.send_request(Method::GET, url, None).await?; - self.update_proxies(proxies); - Ok(self) - } - - pub async fn refresh_providers_proxies(&self) -> Result<&Self, String> { - let url = format!("{}/providers/proxies", self.mihomo_server); - let providers_proxies = self.send_request(Method::GET, url, None).await?; - self.update_providers_proxies(providers_proxies); - Ok(self) - } -} - -impl MihomoManager { - pub async fn is_mihomo_running(&self) -> Result<(), String> { - let url = format!("{}/version", self.mihomo_server); - let _response = self.send_request(Method::GET, url, None).await?; - Ok(()) - } - - pub async fn put_configs_force(&self, clash_config_path: &str) -> Result<(), String> { - let url = format!("{}/configs?force=true", self.mihomo_server); - let payload = serde_json::json!({ - "path": clash_config_path, - }); - let _response = self.send_request(Method::PUT, url, Some(payload)).await?; - Ok(()) - } - - pub async fn patch_configs(&self, config: serde_json::Value) -> Result<(), String> { - let url = format!("{}/configs", self.mihomo_server); - let response = self.send_request(Method::PATCH, url, Some(config)).await?; - if response["code"] == 204 { - Ok(()) - } else { - Err(response["message"] - .as_str() - .unwrap_or("unknown error") - .to_string()) - } - } - - pub async fn test_proxy_delay( - &self, - name: &str, - test_url: Option, - timeout: i32, - ) -> Result { - let test_url = test_url.unwrap_or("http://cp.cloudflare.com/generate_204".to_string()); - let url = format!( - "{}/proxies/{}/delay?url={}&timeout={}", - self.mihomo_server, name, test_url, timeout - ); - let response = self.send_request(Method::GET, url, None).await?; - Ok(response) - } - - pub async fn get_connections(&self) -> Result { - let url = format!("{}/connections", self.mihomo_server); - let response = self.send_request(Method::GET, url, None).await?; - Ok(response) - } - - pub async fn delete_connection(&self, id: &str) -> Result<(), String> { - let url = format!("{}/connections/{}", self.mihomo_server, id); - let response = self.send_request(Method::DELETE, url, None).await?; - if response["code"] == 204 { - Ok(()) - } else { - Err(response["message"] - .as_str() - .unwrap_or("unknown error") - .to_string()) - } - } -} +pub use model::E; +pub use model::MihomoData; +pub use model::MihomoManager; +pub mod sock; diff --git a/src-tauri/src_crates/crate_mihomo_api/src/model.rs b/src-tauri/src_crates/crate_mihomo_api/src/model.rs index 61ab191c..3baedb03 100644 --- a/src-tauri/src_crates/crate_mihomo_api/src/model.rs +++ b/src-tauri/src_crates/crate_mihomo_api/src/model.rs @@ -1,29 +1,58 @@ -use std::sync::{Arc, Mutex}; -use reqwest::header::HeaderMap; +use async_trait::async_trait; +use http_body_util::Full; +use hyper::{Method, body::Bytes}; +use hyper_util::client::legacy::Client; +use hyperlocal::{UnixConnector, Uri}; +use serde_json::Value; +use std::{error::Error, sync::Arc}; +use tokio::sync::Mutex; pub struct MihomoData { pub(crate) proxies: serde_json::Value, pub(crate) providers_proxies: serde_json::Value, } -#[derive(Clone)] +impl Default for MihomoData { + fn default() -> Self { + Self { + proxies: Value::Null, + providers_proxies: Value::Null, + } + } +} + +pub type E = Box; + +#[async_trait] +pub trait MihomoClient: Sized { + async fn set_data_proxies(&self, data: Value); + async fn set_data_providers_proxies(&self, data: Value); + async fn get_data_proxies(&self) -> Value; + async fn get_data_providers_proxies(&self) -> Value; + async fn generate_unix_path(&self, path: &str) -> Uri; + async fn send_request( + &self, + path: &str, + method: Method, + body: Option, + ) -> Result; + async fn is_mihomo_running(&self) -> Result<(), E>; + async fn put_configs_force(&self, clash_config_path: &str) -> Result<(), E>; + async fn patch_configs(&self, config: Value) -> Result<(), E>; + async fn refresh_proxies(&self) -> Result<&Self, E>; + async fn refresh_providers_proxies(&self) -> Result<&Self, E>; + async fn get_connections(&self) -> Result; + async fn delete_connections(&self, id: &str) -> Result<(), E>; + async fn test_proxy_delay( + &self, + name: &str, + test_url: Option, + timeout: i32, + ) -> Result; +} + pub struct MihomoManager { - pub(crate) mihomo_server: String, - pub(crate) data: Arc>, - pub(crate) headers: HeaderMap, + pub(super) socket_path: String, + pub(super) client: Arc>>>, + pub(super) data: Arc>, } - -#[cfg(feature = "debug")] -impl Drop for MihomoData { - fn drop(&mut self) { - println!("Dropping MihomoData"); - } -} - -#[cfg(feature = "debug")] -impl Drop for MihomoManager { - fn drop(&mut self) { - println!("Dropping MihomoManager"); - } - -} \ No newline at end of file diff --git a/src-tauri/src_crates/crate_mihomo_api/src/sock.rs b/src-tauri/src_crates/crate_mihomo_api/src/sock.rs new file mode 100644 index 00000000..00519f81 --- /dev/null +++ b/src-tauri/src_crates/crate_mihomo_api/src/sock.rs @@ -0,0 +1,148 @@ +use crate::model::E; +use async_trait::async_trait; +use http_body_util::{BodyExt, Full}; +use hyper::{ + Method, Request, + body::Bytes, + header::{HeaderName, HeaderValue}, +}; +use hyper_util::client::legacy::Client; +use hyperlocal::{UnixClientExt, Uri}; +use serde_json::Value; +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::{ + MihomoData, + model::{MihomoClient, MihomoManager}, +}; + +impl MihomoManager { + pub fn new(socket_path: String) -> Self { + let client: Client<_, Full> = Client::unix(); + Self { + socket_path, + client: Arc::new(Mutex::new(client)), + data: Arc::new(Mutex::new(MihomoData::default())), + } + } +} + +#[async_trait] +impl MihomoClient for MihomoManager { + async fn set_data_proxies(&self, data: Value) { + self.data.lock().await.proxies = data; + } + + async fn set_data_providers_proxies(&self, data: Value) { + self.data.lock().await.providers_proxies = data; + } + + async fn get_data_proxies(&self) -> Value { + self.data.lock().await.proxies.clone() + } + + async fn get_data_providers_proxies(&self) -> Value { + self.data.lock().await.providers_proxies.clone() + } + + async fn generate_unix_path(&self, path: &str) -> Uri { + Uri::new(self.socket_path.clone(), path).into() + } + + async fn send_request( + &self, + path: &str, + method: Method, + body: Option, + ) -> Result { + let uri = self.generate_unix_path(path).await; + + let mut request_builder = Request::builder().method(method).uri(uri); + + let body_bytes = if let Some(body) = body { + request_builder = request_builder.header( + HeaderName::from_static("Content-Type"), + HeaderValue::from_static("application/json"), + ); + Bytes::from(serde_json::to_vec(&body)?) + } else { + Bytes::new() + }; + + let request = request_builder.body(Full::new(body_bytes))?; + + let response = self.client.lock().await.request(request).await?; + let body_bytes = response.into_body().collect().await?.to_bytes(); + let json_value = serde_json::from_slice(&body_bytes)?; + + Ok(json_value) + } + async fn is_mihomo_running(&self) -> Result<(), E> { + let _ = self.send_request("/version", Method::GET, None).await?; + Ok(()) + } + + async fn put_configs_force(&self, clash_config_path: &str) -> Result<(), E> { + let body = serde_json::json!({ + "path": clash_config_path + }); + let _ = self + .send_request("/configs?force=true", Method::PUT, Some(body)) + .await?; + Ok(()) + } + + async fn patch_configs(&self, config: Value) -> Result<(), E> { + let _ = self + .send_request("/configs", Method::PATCH, Some(config)) + .await?; + Ok(()) + } + + async fn refresh_proxies(&self) -> Result<&Self, E> { + let data = self.send_request("/proxies", Method::GET, None).await?; + self.set_data_proxies(data).await; + Ok(self) + } + + async fn refresh_providers_proxies(&self) -> Result<&Self, E> { + let data = self + .send_request("/providers/proxies", Method::GET, None) + .await?; + self.set_data_providers_proxies(data).await; + Ok(self) + } + + async fn get_connections(&self) -> Result { + let data = self.send_request("/connections", Method::GET, None).await?; + Ok(data) + } + + async fn delete_connections(&self, id: &str) -> Result<(), E> { + let _ = self + .send_request(&format!("/connections/{}", id), Method::DELETE, None) + .await?; + Ok(()) + } + + async fn test_proxy_delay( + &self, + name: &str, + test_url: Option, + timeout: i32, + ) -> Result { + let test_url = test_url.unwrap_or("http://cp.cloudflare.com/generate_204".to_string()); + let data = self + .send_request( + &format!( + "/proxies/{}/delay?url={}&timeout={}", + name, test_url, timeout + ), + Method::GET, + None, + ) + .await?; + Ok(data) + } +} diff --git a/src-tauri/src_crates/crate_mihomo_api/tests/test_mihomo_api.rs b/src-tauri/src_crates/crate_mihomo_api/tests/test_mihomo_api.rs index 80137d22..f8d6df7e 100644 --- a/src-tauri/src_crates/crate_mihomo_api/tests/test_mihomo_api.rs +++ b/src-tauri/src_crates/crate_mihomo_api/tests/test_mihomo_api.rs @@ -1,29 +1,48 @@ -use mihomo_api; -use reqwest::header::HeaderMap; +use dotenv::dotenv; +use mihomo_api::{self, model::MihomoClient}; +use std::env; -#[test] -fn test_mihomo_manager_init() { - let manager = mihomo_api::MihomoManager::new("url".into(), HeaderMap::new()); - assert_eq!(manager.get_proxies(), serde_json::Value::Null); - assert_eq!(manager.get_providers_proxies(), serde_json::Value::Null); +lazy_static::lazy_static! { + static ref LOCAL_SOCK: String = { + // 加载 .env 文件 + dotenv().ok(); + + // 从 .env 或系统环境变量读取 + env::var("LOCAL_SOCK") + .expect("LOCAL_SOCK must be set in .env or environment variables") + }; +} + +#[tokio::test] +async fn test_env() { + assert_eq!(LOCAL_SOCK.to_string(), LOCAL_SOCK.to_string()); +} + +#[tokio::test] +async fn test_mihomo_manager_init() { + let manager = mihomo_api::MihomoManager::new(LOCAL_SOCK.to_string()); + let proxies = manager.get_data_proxies().await; + let providers = manager.get_data_providers_proxies().await; + assert_eq!(proxies, serde_json::Value::Null); + assert_eq!(providers, serde_json::Value::Null); } #[tokio::test] async fn test_refresh_proxies() { - let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new()); + let manager = mihomo_api::MihomoManager::new(LOCAL_SOCK.to_string()); let manager = manager.refresh_proxies().await.unwrap(); - let proxies = manager.get_proxies(); - let providers = manager.get_providers_proxies(); + let proxies = manager.get_data_proxies().await; + let providers = manager.get_data_providers_proxies().await; assert_ne!(proxies, serde_json::Value::Null); assert_eq!(providers, serde_json::Value::Null); } #[tokio::test] async fn test_refresh_providers_proxies() { - let manager = mihomo_api::MihomoManager::new("http://127.0.0.1:9097".into(), HeaderMap::new()); + let manager = mihomo_api::MihomoManager::new(LOCAL_SOCK.to_string()); let manager = manager.refresh_providers_proxies().await.unwrap(); - let proxies = manager.get_proxies(); - let providers = manager.get_providers_proxies(); + let proxies = manager.get_data_proxies().await; + let providers = manager.get_data_providers_proxies().await; assert_eq!(proxies, serde_json::Value::Null); assert_ne!(providers, serde_json::Value::Null); -} \ No newline at end of file +}