use std::collections::HashMap; use super::{Profiles, Verge}; use crate::utils::{config, dirs}; use anyhow::{bail, Result}; use reqwest::header::HeaderMap; use serde::{Deserialize, Serialize}; use serde_yaml::{Mapping, Value}; use tauri::api::process::{Command, CommandChild, CommandEvent}; #[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, } #[derive(Debug)] pub struct Clash { /// maintain the clash config pub config: Mapping, /// some info pub info: ClashInfo, /// clash sidecar pub sidecar: Option, } impl Clash { pub fn new() -> Clash { let config = Clash::read_config(); let info = Clash::get_info(&config); Clash { config, info, sidecar: 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::String("port".to_string()); let key_port_2 = Value::String("mixed-port".to_string()); let key_server = Value::String("external-controller".to_string()); let key_secret = Value::String("secret".to_string()); 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) => Some(val_str.clone()), _ => 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, } } /// run clash sidecar pub fn run_sidecar(&mut self) -> Result<()> { let app_dir = dirs::app_home_dir(); let app_dir = app_dir.as_os_str().to_str().unwrap(); match Command::new_sidecar("clash") { Ok(cmd) => match cmd.args(["-d", app_dir]).spawn() { Ok((mut rx, cmd_child)) => { 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), _ => {} } } }); Ok(()) } Err(err) => bail!(err.to_string()), }, Err(err) => bail!(err.to_string()), } } /// 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()?; self.activate(profiles) } /// 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<()> { for (key, value) in patch.iter() { let value = value.clone(); let key_str = key.as_str().clone().unwrap_or(""); // restart the clash if key_str == "mixed-port" { self.restart_sidecar(profiles)?; let port = if value.is_number() { match value.as_i64().clone() { Some(num) => Some(format!("{num}")), None => None, } } else { match value.as_str().clone() { Some(num) => Some(num.into()), None => None, } }; verge.init_sysproxy(port); } self.config.insert(key.clone(), value); } self.save_config() } /// enable tun mode /// only revise the config and restart the pub fn tun_mode(&mut self, enable: bool) -> Result<()> { let tun_key = Value::String("tun".into()); let tun_val = self.config.get(&tun_key); let mut new_val = Mapping::new(); if tun_val.is_some() && tun_val.as_ref().unwrap().is_mapping() { new_val = tun_val.as_ref().unwrap().as_mapping().unwrap().clone(); } macro_rules! revise { ($map: expr, $key: expr, $val: expr) => { let ret_key = Value::String($key.into()); if $map.contains_key(&ret_key) { $map[&ret_key] = $val; } else { $map.insert(ret_key, $val); } }; } 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, $val); } }; } revise!(new_val, "enable", Value::from(enable)); append!(new_val, "stack", Value::from("gvisor")); append!(new_val, "auto-route", Value::from(true)); append!(new_val, "auto-detect-interface", Value::from(true)); revise!(self.config, "tun", Value::from(new_val)); self.save_config() } /// activate the profile pub fn activate(&self, profiles: &Profiles) -> Result<()> { let temp_path = dirs::profiles_temp_path(); let info = self.info.clone(); let mut config = self.config.clone(); let gen_config = profiles.gen_activate()?; for (key, value) in gen_config.into_iter() { config.insert(key, value); } config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?; tauri::async_runtime::spawn(async move { let server = info.server.clone().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()); for _ in 0..5 { match reqwest::ClientBuilder::new().no_proxy().build() { Ok(client) => match client .put(&server) .headers(headers.clone()) .json(&data) .send() .await { Ok(_) => break, Err(err) => log::error!("failed to activate for `{err}`"), }, Err(err) => log::error!("failed to activate for `{err}`"), } } }); Ok(()) } } 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}"); } } }