From f95ddd594eec4bcfa57ea6cb7356ce92ac2c8be4 Mon Sep 17 00:00:00 2001
From: GyDi <zzzgydi@gmail.com>
Date: Tue, 22 Nov 2022 14:49:37 +0800
Subject: [PATCH] feat: guard the mixed-port and external-controller

---
 src-tauri/src/cmds.rs           |   4 +-
 src-tauri/src/config/clash.rs   | 234 ++++++++++++++++++++------------
 src-tauri/src/config/prfitem.rs |   3 +-
 src-tauri/src/core/clash_api.rs |  18 +--
 src-tauri/src/core/sysopt.rs    |  37 ++---
 src-tauri/src/feat.rs           |   2 +-
 6 files changed, 164 insertions(+), 134 deletions(-)

diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs
index fb05e43..e245796 100644
--- a/src-tauri/src/cmds.rs
+++ b/src-tauri/src/cmds.rs
@@ -119,8 +119,8 @@ pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult
 }
 
 #[tauri::command]
-pub fn get_clash_info() -> CmdResult<ClashInfoN> {
-    wrap_err!(Config::clash().latest().get_info())
+pub fn get_clash_info() -> CmdResult<ClashInfo> {
+    Ok(Config::clash().latest().get_client_info())
 }
 
 #[tauri::command]
diff --git a/src-tauri/src/config/clash.rs b/src-tauri/src/config/clash.rs
index 549ccfc..142f096 100644
--- a/src-tauri/src/config/clash.rs
+++ b/src-tauri/src/config/clash.rs
@@ -2,6 +2,10 @@ use crate::utils::{dirs, help};
 use anyhow::Result;
 use serde::{Deserialize, Serialize};
 use serde_yaml::{Mapping, Value};
+use std::{
+    net::{IpAddr, Ipv4Addr, SocketAddr},
+    str::FromStr,
+};
 
 #[derive(Default, Debug, Clone)]
 pub struct IClashTemp(pub Mapping);
@@ -9,7 +13,7 @@ pub struct IClashTemp(pub Mapping);
 impl IClashTemp {
     pub fn new() -> Self {
         match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) {
-            Ok(map) => Self(map),
+            Ok(map) => Self(Self::guard(map)),
             Err(err) => {
                 log::error!(target: "app", "{err}");
                 Self::template()
@@ -27,7 +31,16 @@ impl IClashTemp {
         map.insert("external-controller".into(), "127.0.0.1:9090".into());
         map.insert("secret".into(), "".into());
 
-        Self(map)
+        Self(Self::guard(map))
+    }
+
+    fn guard(mut config: Mapping) -> Mapping {
+        let port = Self::guard_mixed_port(&config);
+        let ctrl = Self::guard_server_ctrl(&config);
+
+        config.insert("mixed-port".into(), port.into());
+        config.insert("external-controller".into(), ctrl.into());
+        config
     }
 
     pub fn patch_config(&mut self, patch: Mapping) {
@@ -44,109 +57,150 @@ impl IClashTemp {
         )
     }
 
-    pub fn get_info(&self) -> Result<ClashInfoN> {
-        Ok(ClashInfoN::from(&self.0))
-    }
-}
-
-#[derive(Default, Debug, Clone, Deserialize, Serialize)]
-pub struct ClashInfoN {
-    /// clash sidecar status
-    pub status: String,
-    /// clash core port
-    pub port: Option<String>,
-    /// same as `external-controller`
-    pub server: Option<String>,
-    /// clash secret
-    pub secret: Option<String>,
-}
+    // pub fn get_info(&self) -> ClashInfo {
+    //     self.1.clone()
+    // }
 
-impl ClashInfoN {
-    /// parse the clash's config.yaml
-    /// get some information
-    pub fn from(config: &Mapping) -> ClashInfoN {
-        let key_port_1 = Value::from("mixed-port");
-        let key_port_2 = Value::from("port");
-        let key_server = Value::from("external-controller");
-        let key_secret = Value::from("secret");
+    pub fn get_mixed_port(&self) -> u16 {
+        Self::guard_mixed_port(&self.0)
+    }
 
-        let mut status: u32 = 0;
+    pub fn get_client_info(&self) -> ClashInfo {
+        let config = &self.0;
 
-        let port = match config.get(&key_port_1) {
-            Some(value) => match value {
+        ClashInfo {
+            port: Self::guard_mixed_port(&config),
+            server: Self::guard_client_ctrl(&config),
+            secret: config.get("secret").and_then(|value| match value {
                 Value::String(val_str) => Some(val_str.clone()),
+                Value::Bool(val_bool) => Some(val_bool.to_string()),
                 Value::Number(val_num) => Some(val_num.to_string()),
-                _ => {
-                    status |= 0b1;
-                    None
-                }
-            },
-            _ => {
-                status |= 0b10;
-                None
-            }
-        };
-        let port = match port {
-            Some(_) => port,
-            None => match config.get(&key_port_2) {
-                Some(value) => match value {
-                    Value::String(val_str) => Some(val_str.clone()),
-                    Value::Number(val_num) => Some(val_num.to_string()),
-                    _ => {
-                        status |= 0b100;
-                        None
-                    }
-                },
-                _ => {
-                    status |= 0b1000;
-                    None
-                }
-            },
-        };
+                _ => None,
+            }),
+        }
+    }
+
+    pub fn guard_mixed_port(config: &Mapping) -> u16 {
+        let mut port = config
+            .get("mixed-port")
+            .and_then(|value| match value {
+                Value::String(val_str) => val_str.parse().ok(),
+                Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
+                _ => None,
+            })
+            .unwrap_or(7890);
+        if port == 0 {
+            port = 7890;
+        }
+        port
+    }
 
-        // `external-controller` could be
-        // "127.0.0.1:9090" or ":9090"
-        let server = match config.get(&key_server) {
-            Some(value) => match value.as_str() {
+    pub fn guard_server_ctrl(config: &Mapping) -> String {
+        config
+            .get("external-controller")
+            .and_then(|value| match value.as_str() {
                 Some(val_str) => {
-                    if val_str.starts_with(":") {
-                        Some(format!("127.0.0.1{val_str}"))
-                    } else if val_str.starts_with("0.0.0.0:") {
-                        Some(format!("127.0.0.1:{}", &val_str[8..]))
-                    } else if val_str.starts_with("[::]:") {
-                        Some(format!("127.0.0.1:{}", &val_str[5..]))
-                    } else {
-                        Some(val_str.into())
-                    }
+                    let val_str = val_str.trim();
+
+                    let val = match val_str.starts_with(":") {
+                        true => format!("127.0.0.1{val_str}"),
+                        false => val_str.to_owned(),
+                    };
+
+                    SocketAddr::from_str(val.as_str())
+                        .ok()
+                        .map(|s| s.to_string())
                 }
-                None => {
-                    status |= 0b10000;
-                    None
+                None => None,
+            })
+            .unwrap_or("127.0.0.1:9090".into())
+    }
+
+    pub fn guard_client_ctrl(config: &Mapping) -> String {
+        let value = Self::guard_server_ctrl(config);
+        match SocketAddr::from_str(value.as_str()) {
+            Ok(mut socket) => {
+                if socket.ip().is_unspecified() {
+                    socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
                 }
-            },
-            None => {
-                status |= 0b100000;
-                None
+                socket.to_string()
             }
-        };
+            Err(_) => "127.0.0.1:9090".into(),
+        }
+    }
+}
 
-        let secret = match config.get(&key_secret) {
-            Some(value) => match value {
-                Value::String(val_str) => Some(val_str.clone()),
-                Value::Bool(val_bool) => Some(val_bool.to_string()),
-                Value::Number(val_num) => Some(val_num.to_string()),
-                _ => None,
-            },
-            _ => None,
-        };
+#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+pub struct ClashInfo {
+    /// clash core port
+    pub port: u16,
+    /// same as `external-controller`
+    pub server: String,
+    /// clash secret
+    pub secret: Option<String>,
+}
 
-        ClashInfoN {
-            status: format!("{status}"),
+#[test]
+fn test_clash_info() {
+    fn get_case<T: Into<Value>, D: Into<Value>>(mp: T, ec: D) -> ClashInfo {
+        let mut map = Mapping::new();
+        map.insert("mixed-port".into(), mp.into());
+        map.insert("external-controller".into(), ec.into());
+
+        IClashTemp(IClashTemp::guard(map)).get_client_info()
+    }
+
+    fn get_result<S: Into<String>>(port: u16, server: S) -> ClashInfo {
+        ClashInfo {
             port,
-            server,
-            secret,
+            server: server.into(),
+            secret: None,
         }
     }
+
+    assert_eq!(
+        IClashTemp(IClashTemp::guard(Mapping::new())).get_client_info(),
+        get_result(7890, "127.0.0.1:9090")
+    );
+
+    assert_eq!(get_case("", ""), get_result(7890, "127.0.0.1:9090"));
+
+    assert_eq!(get_case(65537, ""), get_result(1, "127.0.0.1:9090"));
+
+    assert_eq!(
+        get_case(8888, "127.0.0.1:8888"),
+        get_result(8888, "127.0.0.1:8888")
+    );
+
+    assert_eq!(
+        get_case(8888, "   :98888 "),
+        get_result(8888, "127.0.0.1:9090")
+    );
+
+    assert_eq!(
+        get_case(8888, "0.0.0.0:8080  "),
+        get_result(8888, "127.0.0.1:8080")
+    );
+
+    assert_eq!(
+        get_case(8888, "0.0.0.0:8080"),
+        get_result(8888, "127.0.0.1:8080")
+    );
+
+    assert_eq!(
+        get_case(8888, "[::]:8080"),
+        get_result(8888, "127.0.0.1:8080")
+    );
+
+    assert_eq!(
+        get_case(8888, "192.168.1.1:8080"),
+        get_result(8888, "192.168.1.1:8080")
+    );
+
+    assert_eq!(
+        get_case(8888, "192.168.1.1:80800"),
+        get_result(8888, "127.0.0.1:9090")
+    );
 }
 
 #[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs
index 8bf8cce..0bfeb37 100644
--- a/src-tauri/src/config/prfitem.rs
+++ b/src-tauri/src/config/prfitem.rs
@@ -194,8 +194,7 @@ impl PrfItem {
 
         // 使用软件自己的代理
         if self_proxy {
-            let port = Config::clash().data().get_info()?.port;
-            let port = port.ok_or(anyhow::anyhow!("failed to get clash info port"))?;
+            let port = Config::clash().data().get_mixed_port();
 
             let proxy_scheme = format!("http://127.0.0.1:{port}");
 
diff --git a/src-tauri/src/core/clash_api.rs b/src-tauri/src/core/clash_api.rs
index 2261d1d..eab08b4 100644
--- a/src-tauri/src/core/clash_api.rs
+++ b/src-tauri/src/core/clash_api.rs
@@ -38,25 +38,15 @@ pub async fn patch_configs(config: &Mapping) -> Result<()> {
 
 /// 根据clash info获取clash服务地址和请求头
 fn clash_client_info() -> Result<(String, HeaderMap)> {
-    let info = { Config::clash().data().get_info()? };
-
-    if info.server.is_none() {
-        let status = &info.status;
-        if info.port.is_none() {
-            bail!("failed to parse config.yaml file with status {status}");
-        } else {
-            bail!("failed to parse the server with status {status}");
-        }
-    }
+    let client = { Config::clash().data().get_client_info() };
 
-    let server = info.server.unwrap();
-    let server = format!("http://{server}");
+    let server = format!("http://{}", client.server);
 
     let mut headers = HeaderMap::new();
     headers.insert("Content-Type", "application/json".parse()?);
 
-    if let Some(secret) = info.secret.as_ref() {
-        let secret = format!("Bearer {}", secret.clone()).parse()?;
+    if let Some(secret) = client.secret {
+        let secret = format!("Bearer {}", secret).parse()?;
         headers.insert("Authorization", secret);
     }
 
diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs
index 152ce58..5eed1e5 100644
--- a/src-tauri/src/core/sysopt.rs
+++ b/src-tauri/src/core/sysopt.rs
@@ -1,5 +1,5 @@
 use crate::{config::Config, log_err};
-use anyhow::{anyhow, bail, Result};
+use anyhow::{anyhow, Result};
 use auto_launch::{AutoLaunch, AutoLaunchBuilder};
 use once_cell::sync::OnceCell;
 use parking_lot::Mutex;
@@ -43,13 +43,7 @@ impl Sysopt {
 
     /// init the sysproxy
     pub fn init_sysproxy(&self) -> Result<()> {
-        let port = { Config::clash().latest().get_info()?.port };
-
-        if port.is_none() {
-            bail!("clash port is none");
-        }
-
-        let port = port.unwrap().parse::<u16>()?;
+        let port = { Config::clash().latest().get_mixed_port() };
 
         let (enable, bypass) = {
             let verge = Config::verge();
@@ -263,23 +257,16 @@ impl Sysopt {
 
                 log::debug!(target: "app", "try to guard the system proxy");
 
-                if let Ok(info) = { Config::clash().latest().get_info() } {
-                    match info.port.unwrap_or("".into()).parse::<u16>() {
-                        Ok(port) => {
-                            let sysproxy = Sysproxy {
-                                enable: true,
-                                host: "127.0.0.1".into(),
-                                port,
-                                bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
-                            };
-
-                            log_err!(sysproxy.set_system_proxy());
-                        }
-                        Err(_) => {
-                            log::error!(target: "app", "failed to parse clash port in guard proxy")
-                        }
-                    }
-                }
+                let port = { Config::clash().latest().get_mixed_port() };
+
+                let sysproxy = Sysproxy {
+                    enable: true,
+                    host: "127.0.0.1".into(),
+                    port,
+                    bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
+                };
+
+                log_err!(sysproxy.set_system_proxy());
             }
 
             let mut state = guard_state.lock().await;
diff --git a/src-tauri/src/feat.rs b/src-tauri/src/feat.rs
index 00d2ae2..4fc7be9 100644
--- a/src-tauri/src/feat.rs
+++ b/src-tauri/src/feat.rs
@@ -156,7 +156,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
                 if let Some(port) = mixed_port.clone().unwrap().as_u64() {
                     if !port_scanner::local_port_available(port as u16) {
                         Config::clash().discard();
-                        bail!("the port not available");
+                        bail!("port already in use");
                     }
                 }
             }
-- 
GitLab