use super::{PrfEnhancedResult, Profiles, Verge, VergeConfig}; use crate::log_if_err; use crate::utils::{config, dirs, help}; use anyhow::{bail, Result}; use reqwest::header::HeaderMap; use serde::{Deserialize, Serialize}; use serde_yaml::{Mapping, Value}; use std::{collections::HashMap, time::Duration}; use tauri::api::process::{Command, CommandChild, CommandEvent}; use tauri::Window; use tokio::time::sleep; #[derive(Default, Debug, Clone, Deserialize, Serialize)] pub struct ClashInfo { /// clash sidecar status pub status: String, /// clash core port pub port: Option, /// same as `external-controller` pub server: Option, /// clash secret pub secret: Option, } pub struct Clash { /// maintain the clash config pub config: Mapping, /// some info pub info: ClashInfo, /// clash sidecar pub sidecar: Option, /// save the main window pub window: Option, } impl Clash { pub fn new() -> Clash { let config = Clash::read_config(); let info = Clash::get_info(&config); Clash { config, info, sidecar: None, window: None, } } /// get clash config fn read_config() -> Mapping { config::read_yaml::(dirs::clash_path()) } /// save the clash config fn save_config(&self) -> Result<()> { config::save_yaml( dirs::clash_path(), &self.config, Some("# Default Config For Clash Core\n\n"), ) } /// parse the clash's config.yaml /// get some information fn get_info(clash_config: &Mapping) -> ClashInfo { let key_port_1 = Value::from("port"); let key_port_2 = Value::from("mixed-port"); let key_server = Value::from("external-controller"); let key_secret = Value::from("secret"); let port = match clash_config.get(&key_port_1) { Some(value) => match value { Value::String(val_str) => Some(val_str.clone()), Value::Number(val_num) => Some(val_num.to_string()), _ => None, }, _ => None, }; let port = match port { Some(_) => port, None => match clash_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()), _ => None, }, _ => None, }, }; let server = match clash_config.get(&key_server) { Some(value) => match value { Value::String(val_str) => { // `external-controller` could be // "127.0.0.1:9090" or ":9090" // Todo: maybe it could support single port let server = val_str.clone(); let server = match server.starts_with(":") { true => format!("127.0.0.1{server}"), false => server, }; Some(server) } _ => None, }, _ => None, }; let secret = match clash_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, }; ClashInfo { status: "init".into(), port, server, secret, } } /// save the main window pub fn set_window(&mut self, win: Option) { self.window = win; } /// run clash sidecar pub fn run_sidecar(&mut self, profiles: &Profiles, delay: bool) -> Result<()> { let app_dir = dirs::app_home_dir(); let app_dir = app_dir.as_os_str().to_str().unwrap(); let cmd = Command::new_sidecar("clash")?; let (mut rx, cmd_child) = cmd.args(["-d", app_dir]).spawn()?; self.sidecar = Some(cmd_child); // clash log tauri::async_runtime::spawn(async move { while let Some(event) = rx.recv().await { match event { CommandEvent::Stdout(line) => log::info!("[clash]: {}", line), CommandEvent::Stderr(err) => log::error!("[clash]: {}", err), _ => {} } } }); // activate profile log_if_err!(self.activate(&profiles)); log_if_err!(self.activate_enhanced(&profiles, delay, true)); Ok(()) } /// drop clash sidecar pub fn drop_sidecar(&mut self) -> Result<()> { if let Some(sidecar) = self.sidecar.take() { sidecar.kill()?; } Ok(()) } /// restart clash sidecar /// should reactivate profile after restart pub fn restart_sidecar(&mut self, profiles: &mut Profiles) -> Result<()> { self.update_config(); self.drop_sidecar()?; self.run_sidecar(profiles, false) } /// update the clash info pub fn update_config(&mut self) { self.config = Clash::read_config(); self.info = Clash::get_info(&self.config); } /// patch update the clash config pub fn patch_config( &mut self, patch: Mapping, verge: &mut Verge, profiles: &mut Profiles, ) -> Result<()> { let mix_port_key = Value::from("mixed-port"); let mut port = None; for (key, value) in patch.into_iter() { let value = value.clone(); // check whether the mix_port is changed if key == mix_port_key { if value.is_number() { port = value.as_i64().as_ref().map(|n| n.to_string()); } else { port = value.as_str().as_ref().map(|s| s.to_string()); } } self.config.insert(key.clone(), value); } self.save_config()?; if let Some(port) = port { self.restart_sidecar(profiles)?; verge.init_sysproxy(Some(port)); } Ok(()) } /// revise the `tun` and `dns` config fn _tun_mode(mut config: Mapping, enable: bool) -> Mapping { macro_rules! revise { ($map: expr, $key: expr, $val: expr) => { let ret_key = Value::String($key.into()); $map.insert(ret_key, Value::from($val)); }; } // if key not exists then append value macro_rules! append { ($map: expr, $key: expr, $val: expr) => { let ret_key = Value::String($key.into()); if !$map.contains_key(&ret_key) { $map.insert(ret_key, Value::from($val)); } }; } // tun config let tun_val = config.get(&Value::from("tun")); let mut new_tun = Mapping::new(); if tun_val.is_some() && tun_val.as_ref().unwrap().is_mapping() { new_tun = tun_val.as_ref().unwrap().as_mapping().unwrap().clone(); } revise!(new_tun, "enable", enable); if enable { append!(new_tun, "stack", "gvisor"); append!(new_tun, "dns-hijack", vec!["198.18.0.2:53"]); append!(new_tun, "auto-route", true); append!(new_tun, "auto-detect-interface", true); } revise!(config, "tun", new_tun); // dns config let dns_val = config.get(&Value::from("dns")); let mut new_dns = Mapping::new(); if dns_val.is_some() && dns_val.as_ref().unwrap().is_mapping() { new_dns = dns_val.as_ref().unwrap().as_mapping().unwrap().clone(); } // 借鉴cfw的默认配置 revise!(new_dns, "enable", enable); if enable { append!(new_dns, "enhanced-mode", "fake-ip"); append!( new_dns, "nameserver", vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"] ); append!(new_dns, "fallback", vec![] as Vec<&str>); #[cfg(target_os = "windows")] append!( new_dns, "fake-ip-filter", vec![ "dns.msftncsi.com", "www.msftncsi.com", "www.msftconnecttest.com" ] ); } revise!(config, "dns", new_dns); config } /// activate the profile /// generate a new profile to the temp_dir /// then put the path to the clash core fn _activate(info: ClashInfo, config: Mapping, window: Option) -> Result<()> { let verge_config = VergeConfig::new(); let tun_enable = verge_config.enable_tun_mode.unwrap_or(false); let config = Clash::_tun_mode(config, tun_enable); let temp_path = dirs::profiles_temp_path(); config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?; tauri::async_runtime::spawn(async move { if info.server.is_none() { return; } let server = info.server.unwrap(); let server = format!("http://{server}/configs"); let mut headers = HeaderMap::new(); headers.insert("Content-Type", "application/json".parse().unwrap()); if let Some(secret) = info.secret.as_ref() { let secret = format!("Bearer {}", secret.clone()).parse().unwrap(); headers.insert("Authorization", secret); } let mut data = HashMap::new(); data.insert("path", temp_path.as_os_str().to_str().unwrap()); // retry 5 times for _ in 0..5 { match reqwest::ClientBuilder::new().no_proxy().build() { Ok(client) => { let builder = client.put(&server).headers(headers.clone()).json(&data); match builder.send().await { Ok(resp) => { if resp.status() != 204 { log::error!("failed to activate clash for status \"{}\"", resp.status()); } // emit the window to update something if let Some(window) = window { window.emit("verge://refresh-clash-config", "yes").unwrap(); } // do not retry break; } Err(err) => log::error!("failed to activate for `{err}`"), } } Err(err) => log::error!("failed to activate for `{err}`"), } sleep(Duration::from_millis(500)).await; } }); Ok(()) } /// enhanced profiles mode /// - (sync) refresh config if enhance chain is null /// - (async) enhanced config pub fn activate_enhanced(&self, profiles: &Profiles, delay: bool, skip: bool) -> Result<()> { if self.window.is_none() { bail!("failed to get the main window"); } let event_name = help::get_uid("e"); let event_name = format!("enhanced-cb-{event_name}"); // generate the payload let payload = profiles.gen_enhanced(event_name.clone())?; let info = self.info.clone(); // do not run enhanced if payload.chain.len() == 0 { if skip { return Ok(()); } let mut config = self.config.clone(); let filter_data = Clash::strict_filter(payload.current); for (key, value) in filter_data.into_iter() { config.insert(key, value); } return Clash::_activate(info, config, self.window.clone()); } let window = self.window.clone().unwrap(); let window_move = self.window.clone(); window.once(&event_name, move |event| { if let Some(result) = event.payload() { let result: PrfEnhancedResult = serde_json::from_str(result).unwrap(); if let Some(data) = result.data { let mut config = Clash::read_config(); let filter_data = Clash::loose_filter(data); // loose filter for (key, value) in filter_data.into_iter() { config.insert(key, value); } log_if_err!(Clash::_activate(info, config, window_move)); log::info!("profile enhanced status {}", result.status); } result.error.map(|err| log::error!("{err}")); } }); tauri::async_runtime::spawn(async move { // wait the window setup during resolve app if delay { sleep(Duration::from_secs(2)).await; } window.emit("script-handler", payload).unwrap(); }); Ok(()) } /// activate the profile /// auto activate enhanced profile pub fn activate(&self, profiles: &Profiles) -> Result<()> { let data = profiles.gen_activate()?; let data = Clash::strict_filter(data); let info = self.info.clone(); let mut config = self.config.clone(); for (key, value) in data.into_iter() { config.insert(key, value); } Clash::_activate(info, config, self.window.clone()) } /// only 5 default fields available (clash config fields) /// convert to lowercase fn strict_filter(config: Mapping) -> Mapping { // Only the following fields are allowed: // proxies/proxy-providers/proxy-groups/rule-providers/rules let valid_keys = vec![ "proxies", "proxy-providers", "proxy-groups", "rules", "rule-providers", ]; let mut new_config = Mapping::new(); for (key, value) in config.into_iter() { key.as_str().map(|key_str| { // change to lowercase let mut key_str = String::from(key_str); key_str.make_ascii_lowercase(); // filter if valid_keys.contains(&&*key_str) { new_config.insert(Value::String(key_str), value); } }); } new_config } /// more clash config fields available /// convert to lowercase fn loose_filter(config: Mapping) -> Mapping { // all of these can not be revised by script or merge // http/https/socks port should be under control let not_allow = vec![ "port", "socks-port", "mixed-port", "allow-lan", "mode", "external-controller", "secret", "log-level", ]; let mut new_config = Mapping::new(); for (key, value) in config.into_iter() { key.as_str().map(|key_str| { // change to lowercase let mut key_str = String::from(key_str); key_str.make_ascii_lowercase(); // filter if !not_allow.contains(&&*key_str) { new_config.insert(Value::String(key_str), value); } }); } new_config } } impl Default for Clash { fn default() -> Self { Clash::new() } } impl Drop for Clash { fn drop(&mut self) { if let Err(err) = self.drop_sidecar() { log::error!("{err}"); } } }