From ce1712ff2e3c68e5cf2acf4ea320838583d8fe4f Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Thu, 30 Jul 2026 21:26:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BA=20EasyTier=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=AE=8C=E6=95=B4=20TOML=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增快速配置与完整配置模式,兼容旧版配置 - 统一扩展配置校验及受保护临时文件生命周期 - 支持通过 easytier-core -c 加载 TOML 配置 - 复用前端配置模式编辑组件并更新中英文文案与类型 - 补充配置校验、启动参数、文件权限及清理测试 --- src/extensions/manager.rs | 271 +++++++++++------- src/extensions/mod.rs | 6 + src/extensions/protected_config.rs | 95 ++++++ src/extensions/types.rs | 43 ++- src/extensions/validation.rs | 119 ++++++++ src/web/handlers/extensions.rs | 87 ++---- .../components/ExtensionConfigModeEditor.vue | 49 ++++ web/src/i18n/en-US.ts | 4 + web/src/i18n/zh-CN.ts | 4 + web/src/types/generated.ts | 9 + web/src/views/SettingsView.vue | 259 +++++++++-------- 11 files changed, 656 insertions(+), 290 deletions(-) create mode 100644 src/extensions/protected_config.rs create mode 100644 src/extensions/validation.rs create mode 100644 web/src/components/ExtensionConfigModeEditor.vue diff --git a/src/extensions/manager.rs b/src/extensions/manager.rs index 89477d15..46e2dc10 100644 --- a/src/extensions/manager.rs +++ b/src/extensions/manager.rs @@ -1,15 +1,15 @@ use std::collections::{HashMap, VecDeque}; -use std::path::PathBuf; +use std::path::Path; use std::process::Stdio; use std::sync::Arc; -use tempfile::TempDir; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::{Child, Command}; use tokio::sync::RwLock; -use toml_edit::DocumentMut; +use super::protected_config::ProtectedConfigFile; use super::types::*; +use super::validation::{validate_easytier_config, validate_frpc_config, validate_gostc_config}; use crate::events::EventBus; const LOG_BUFFER_SIZE: usize = 200; @@ -27,12 +27,12 @@ const TTYD_TCP_PORT: &str = "7681"; struct ExtensionProcess { child: Child, logs: Arc>>, - _temp_dir: Option, + _config_file: Option, } struct ExtensionLaunch { args: Vec, - temp_dir: Option, + config_file: Option, } pub struct ExtensionManager { @@ -83,24 +83,12 @@ impl ExtensionManager { match id { ExtensionId::Ttyd => config.ttyd.enabled, ExtensionId::Gostc => { - config.gostc.enabled - && !config.gostc.key.is_empty() - && !config.gostc.addr.trim().is_empty() + config.gostc.enabled && validate_gostc_config(&config.gostc).is_ok() } ExtensionId::Easytier => { - config.easytier.enabled && !config.easytier.network_name.is_empty() - } - ExtensionId::Frpc => { - config.frpc.enabled - && match config.frpc.config_mode { - FrpcConfigMode::Quick => { - !config.frpc.proxy_name.trim().is_empty() - && !config.frpc.server_addr.trim().is_empty() - && !config.frpc.token.is_empty() - } - FrpcConfigMode::Full => !config.frpc.custom_toml.trim().is_empty(), - } + config.easytier.enabled && validate_easytier_config(&config.easytier).is_ok() } + ExtensionId::Frpc => config.frpc.enabled && validate_frpc_config(&config.frpc).is_ok(), } } @@ -203,7 +191,7 @@ impl ExtensionManager { ExtensionProcess { child, logs, - _temp_dir: launch.temp_dir, + _config_file: launch.config_file, }, ); drop(processes); @@ -286,12 +274,7 @@ impl ExtensionManager { ExtensionId::Gostc => { let c = &config.gostc; - if c.addr.trim().is_empty() { - return Err("GOSTC server address is required".into()); - } - if c.key.is_empty() { - return Err("GOSTC client key is required".into()); - } + validate_gostc_config(c)?; let mut args = Vec::new(); @@ -307,35 +290,7 @@ impl ExtensionManager { } ExtensionId::Easytier => { - let c = &config.easytier; - if c.network_name.is_empty() { - return Err("EasyTier network name is required".into()); - } - - let mut args = vec![ - "--network-name".to_string(), - c.network_name.clone(), - "--network-secret".to_string(), - c.network_secret.clone(), - ]; - - for peer in &c.peer_urls { - if !peer.is_empty() { - args.extend(["--peers".to_string(), peer.clone()]); - } - } - - if let Some(ref ip) = c.virtual_ip { - if !ip.is_empty() { - args.extend(["-i".to_string(), ip.clone()]); - } else { - args.push("-d".to_string()); - } - } else { - args.push("-d".to_string()); - } - - args + return Self::build_easytier_launch(&config.easytier).await; } ExtensionId::Frpc => { @@ -345,58 +300,78 @@ impl ExtensionManager { Ok(ExtensionLaunch { args, - temp_dir: None, + config_file: None, }) } + async fn build_easytier_launch(config: &EasytierConfig) -> Result { + validate_easytier_config(config)?; + + match config.config_mode { + EasytierConfigMode::Quick => Ok(ExtensionLaunch { + args: Self::build_easytier_quick_args(config), + config_file: None, + }), + EasytierConfigMode::Full => { + let config_file = ProtectedConfigFile::create( + "EasyTier", + "easytier.toml", + config.custom_toml.as_str(), + ) + .await?; + + Ok(ExtensionLaunch { + args: vec!["-c".to_string(), Self::path_to_arg(config_file.path())], + config_file: Some(config_file), + }) + } + } + } + + fn build_easytier_quick_args(config: &EasytierConfig) -> Vec { + let mut args = vec![ + "--network-name".to_string(), + config.network_name.clone(), + "--network-secret".to_string(), + config.network_secret.clone(), + ]; + + for peer in &config.peer_urls { + if !peer.is_empty() { + args.extend(["--peers".to_string(), peer.clone()]); + } + } + + if let Some(ref ip) = config.virtual_ip { + if !ip.is_empty() { + args.extend(["-i".to_string(), ip.clone()]); + } else { + args.push("-d".to_string()); + } + } else { + args.push("-d".to_string()); + } + + args + } + async fn build_frpc_launch(config: &FrpcConfig) -> Result { + validate_frpc_config(config)?; + let config_text = match config.config_mode { FrpcConfigMode::Quick => Self::build_frpc_quick_toml(config)?, - FrpcConfigMode::Full => Self::validate_frpc_full_toml(config)?.to_string(), + FrpcConfigMode::Full => config.custom_toml.clone(), }; - let temp_dir = - tempfile::tempdir().map_err(|e| format!("Failed to create FRPC config dir: {}", e))?; - let config_path = temp_dir.path().join("frpc.toml"); - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(temp_dir.path(), std::fs::Permissions::from_mode(0o700)) - .map_err(|e| format!("Failed to protect FRPC config dir: {}", e))?; - } - - tokio::fs::write(&config_path, config_text) - .await - .map_err(|e| format!("Failed to write FRPC config: {}", e))?; - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - tokio::fs::set_permissions(&config_path, std::fs::Permissions::from_mode(0o600)) - .await - .map_err(|e| format!("Failed to protect FRPC config: {}", e))?; - } + let config_file = + ProtectedConfigFile::create("FRPC", "frpc.toml", config_text.as_str()).await?; Ok(ExtensionLaunch { - args: vec!["-c".to_string(), Self::path_to_arg(&config_path)], - temp_dir: Some(temp_dir), + args: vec!["-c".to_string(), Self::path_to_arg(config_file.path())], + config_file: Some(config_file), }) } - fn validate_frpc_full_toml(config: &FrpcConfig) -> Result<&str, String> { - let trimmed = config.custom_toml.trim(); - if trimmed.is_empty() { - return Err("FRPC full configuration is required".into()); - } - - trimmed - .parse::() - .map_err(|e| format!("FRPC full configuration is not valid TOML: {}", e))?; - - Ok(config.custom_toml.as_str()) - } - fn build_frpc_quick_toml(config: &FrpcConfig) -> Result { if config.proxy_name.trim().is_empty() { return Err("FRPC proxy name is required".into()); @@ -480,7 +455,7 @@ impl ExtensionManager { serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string()) } - fn path_to_arg(path: &PathBuf) -> String { + fn path_to_arg(path: &Path) -> String { path.to_string_lossy().to_string() } @@ -603,3 +578,107 @@ impl ExtensionManager { futures::future::join_all(stop_futures).await; } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn easytier_launch_revalidates_full_configuration() { + let config = EasytierConfig { + config_mode: EasytierConfigMode::Full, + custom_toml: "instance_name = [".to_string(), + ..Default::default() + }; + + let error = ExtensionManager::build_easytier_launch(&config) + .await + .err() + .expect("invalid full configuration should fail launch validation"); + assert!(error.starts_with("EasyTier full configuration is not valid TOML:")); + } + + #[test] + fn easytier_quick_mode_keeps_command_line_arguments() { + let config = EasytierConfig { + network_name: "one-kvm".to_string(), + network_secret: "secret".to_string(), + peer_urls: vec![ + "tcp://peer-one:11010".to_string(), + String::new(), + "udp://peer-two:11010".to_string(), + ], + virtual_ip: Some("10.20.30.40/24".to_string()), + ..Default::default() + }; + + assert_eq!( + ExtensionManager::build_easytier_quick_args(&config), + vec![ + "--network-name", + "one-kvm", + "--network-secret", + "secret", + "--peers", + "tcp://peer-one:11010", + "--peers", + "udp://peer-two:11010", + "-i", + "10.20.30.40/24", + ] + ); + } + + #[tokio::test] + async fn easytier_full_mode_uses_protected_temporary_config() { + let config_text = "instance_name = \"one-kvm\"\n"; + let config = EasytierConfig { + config_mode: EasytierConfigMode::Full, + network_name: "ignored-quick-network".to_string(), + custom_toml: config_text.to_string(), + ..Default::default() + }; + + let launch = ExtensionManager::build_easytier_launch(&config) + .await + .expect("full EasyTier launch should build"); + assert_eq!(launch.args[0], "-c"); + + let config_path = std::path::PathBuf::from(&launch.args[1]); + assert_eq!( + config_path.file_name().and_then(|name| name.to_str()), + Some("easytier.toml") + ); + assert_eq!( + tokio::fs::read_to_string(&config_path).await.unwrap(), + config_text + ); + + drop(launch); + assert!(!config_path.exists()); + } + + #[test] + fn easytier_auto_start_uses_fields_for_selected_mode() { + let mut config = ExtensionsConfig::default(); + config.easytier.enabled = true; + config.easytier.network_name = "quick-network".to_string(); + assert!(ExtensionManager::is_enabled_for_config( + ExtensionId::Easytier, + &config + )); + + config.easytier.config_mode = EasytierConfigMode::Full; + assert!(!ExtensionManager::is_enabled_for_config( + ExtensionId::Easytier, + &config + )); + + config.easytier.network_name.clear(); + config.easytier.custom_toml = "instance_name = \"one-kvm\"".to_string(); + assert!(ExtensionManager::is_enabled_for_config( + ExtensionId::Easytier, + &config + )); + } +} diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs index 7f599dab..19ce6845 100644 --- a/src/extensions/mod.rs +++ b/src/extensions/mod.rs @@ -1,6 +1,8 @@ mod manager; +mod protected_config; mod software; mod types; +mod validation; pub use manager::ExtensionManager; #[cfg(unix)] @@ -8,3 +10,7 @@ pub use manager::TTYD_SOCKET_PATH; #[cfg(windows)] pub use manager::TTYD_TCP_ADDR; pub use types::*; +pub(crate) use validation::{ + validate_easytier_config, validate_extension_config, validate_frpc_config, + validate_gostc_config, +}; diff --git a/src/extensions/protected_config.rs b/src/extensions/protected_config.rs new file mode 100644 index 00000000..e44972b5 --- /dev/null +++ b/src/extensions/protected_config.rs @@ -0,0 +1,95 @@ +use std::path::{Path, PathBuf}; + +use tempfile::TempDir; + +pub(crate) struct ProtectedConfigFile { + _temp_dir: TempDir, + path: PathBuf, +} + +impl ProtectedConfigFile { + pub(crate) async fn create( + extension_name: &str, + file_name: &str, + contents: &str, + ) -> Result { + let temp_dir = tempfile::tempdir().map_err(|error| { + format!("Failed to create {} config dir: {}", extension_name, error) + })?; + let path = temp_dir.path().join(file_name); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + std::fs::set_permissions(temp_dir.path(), std::fs::Permissions::from_mode(0o700)) + .map_err(|error| { + format!("Failed to protect {} config dir: {}", extension_name, error) + })?; + } + + tokio::fs::write(&path, contents) + .await + .map_err(|error| format!("Failed to write {} config: {}", extension_name, error))?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + tokio::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) + .await + .map_err(|error| { + format!("Failed to protect {} config: {}", extension_name, error) + })?; + } + + Ok(Self { + _temp_dir: temp_dir, + path, + }) + } + + pub(crate) fn path(&self) -> &Path { + &self.path + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn protects_and_cleans_up_config_file() { + let config = + ProtectedConfigFile::create("Test extension", "extension.toml", "enabled = true\n") + .await + .unwrap(); + let path = config.path().to_path_buf(); + + assert_eq!( + tokio::fs::read_to_string(&path).await.unwrap(), + "enabled = true\n" + ); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + assert_eq!( + std::fs::metadata(path.parent().unwrap()) + .unwrap() + .permissions() + .mode() + & 0o777, + 0o700 + ); + assert_eq!( + std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, + 0o600 + ); + } + + drop(config); + assert!(!path.exists()); + } +} diff --git a/src/extensions/types.rs b/src/extensions/types.rs index e0e2c9ca..45839d0f 100644 --- a/src/extensions/types.rs +++ b/src/extensions/types.rs @@ -103,11 +103,25 @@ impl Default for GostcConfig { } #[typeshare] -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum EasytierConfigMode { + Quick, + Full, +} + +impl Default for EasytierConfigMode { + fn default() -> Self { + Self::Quick + } +} + +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default)] -#[derive(Default)] pub struct EasytierConfig { pub enabled: bool, + pub config_mode: EasytierConfigMode, pub network_name: String, #[serde(skip_serializing_if = "String::is_empty")] pub network_secret: String, @@ -115,6 +129,8 @@ pub struct EasytierConfig { pub peer_urls: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub virtual_ip: Option, + #[serde(skip_serializing_if = "String::is_empty")] + pub custom_toml: String, } #[typeshare] @@ -260,3 +276,26 @@ pub struct ExtensionLogs { pub id: ExtensionId, pub logs: Vec, } + +#[cfg(test)] +mod tests { + use super::{EasytierConfig, EasytierConfigMode}; + + #[test] + fn legacy_easytier_config_defaults_to_quick_mode() { + let config: EasytierConfig = serde_json::from_str( + r#"{ + "enabled": true, + "network_name": "legacy-network", + "network_secret": "secret", + "peer_urls": ["tcp://example.com:11010"], + "virtual_ip": "10.10.10.2/24" + }"#, + ) + .expect("legacy EasyTier config should deserialize"); + + assert_eq!(config.config_mode, EasytierConfigMode::Quick); + assert!(config.custom_toml.is_empty()); + assert_eq!(config.network_name, "legacy-network"); + } +} diff --git a/src/extensions/validation.rs b/src/extensions/validation.rs new file mode 100644 index 00000000..9da0c722 --- /dev/null +++ b/src/extensions/validation.rs @@ -0,0 +1,119 @@ +use toml_edit::DocumentMut; + +use super::types::{ + EasytierConfig, EasytierConfigMode, ExtensionId, ExtensionsConfig, FrpProxyType, FrpcConfig, + FrpcConfigMode, GostcConfig, +}; + +pub(crate) fn validate_extension_config( + id: ExtensionId, + config: &ExtensionsConfig, +) -> Result<(), String> { + match id { + ExtensionId::Ttyd => Ok(()), + ExtensionId::Gostc => validate_gostc_config(&config.gostc), + ExtensionId::Easytier => validate_easytier_config(&config.easytier), + ExtensionId::Frpc => validate_frpc_config(&config.frpc), + } +} + +pub(crate) fn validate_gostc_config(config: &GostcConfig) -> Result<(), String> { + require_non_empty(config.addr.trim(), "GOSTC server address is required")?; + require_non_empty(config.key.as_str(), "GOSTC client key is required") +} + +pub(crate) fn validate_easytier_config(config: &EasytierConfig) -> Result<(), String> { + match config.config_mode { + EasytierConfigMode::Quick => require_non_empty( + config.network_name.trim(), + "EasyTier network name is required", + ), + EasytierConfigMode::Full => validate_full_toml("EasyTier", config.custom_toml.as_str()), + } +} + +pub(crate) fn validate_frpc_config(config: &FrpcConfig) -> Result<(), String> { + match config.config_mode { + FrpcConfigMode::Quick => { + require_non_empty(config.proxy_name.trim(), "FRPC proxy name is required")?; + require_non_empty(config.server_addr.trim(), "FRPC server address is required")?; + require_non_empty(config.token.as_str(), "FRPC token is required")?; + require_non_empty(config.local_ip.trim(), "FRPC local IP is required")?; + + if matches!(config.proxy_type, FrpProxyType::Tcp | FrpProxyType::Udp) + && config.remote_port.is_none() + { + return Err("FRPC remote port is required for TCP/UDP proxies".to_string()); + } + + Ok(()) + } + FrpcConfigMode::Full => validate_full_toml("FRPC", config.custom_toml.as_str()), + } +} + +fn require_non_empty(value: &str, message: &str) -> Result<(), String> { + if value.is_empty() { + Err(message.to_string()) + } else { + Ok(()) + } +} + +fn validate_full_toml(extension_name: &str, config: &str) -> Result<(), String> { + let trimmed = config.trim(); + if trimmed.is_empty() { + return Err(format!("{} full configuration is required", extension_name)); + } + + trimmed.parse::().map_err(|error| { + format!( + "{} full configuration is not valid TOML: {}", + extension_name, error + ) + })?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validates_easytier_full_configuration() { + let mut config = EasytierConfig { + config_mode: EasytierConfigMode::Full, + ..Default::default() + }; + + assert_eq!( + validate_easytier_config(&config).unwrap_err(), + "EasyTier full configuration is required" + ); + + config.custom_toml = "instance_name = [".to_string(); + assert!(validate_easytier_config(&config) + .unwrap_err() + .starts_with("EasyTier full configuration is not valid TOML:")); + + config.custom_toml = "instance_name = \"one-kvm\"".to_string(); + assert!(validate_easytier_config(&config).is_ok()); + } + + #[test] + fn validates_frpc_through_the_same_entry_point() { + let mut config = FrpcConfig { + config_mode: FrpcConfigMode::Full, + ..Default::default() + }; + + assert_eq!( + validate_frpc_config(&config).unwrap_err(), + "FRPC full configuration is required" + ); + + config.custom_toml = "serverAddr = \"frps.example.com\"".to_string(); + assert!(validate_frpc_config(&config).is_ok()); + } +} diff --git a/src/web/handlers/extensions.rs b/src/web/handlers/extensions.rs index 963364f7..6b4e9019 100644 --- a/src/web/handlers/extensions.rs +++ b/src/web/handlers/extensions.rs @@ -4,76 +4,19 @@ use axum::{ }; use serde::Deserialize; use std::sync::Arc; -use toml_edit::DocumentMut; use typeshare::typeshare; use crate::error::{AppError, Result}; use crate::extensions::{ - EasytierConfig, EasytierInfo, ExtensionId, ExtensionInfo, ExtensionLogs, ExtensionsStatus, - FrpProxyType, FrpcConfig, FrpcConfigMode, FrpcInfo, GostcConfig, GostcInfo, TtydConfig, - TtydInfo, + validate_easytier_config, validate_extension_config, validate_frpc_config, + validate_gostc_config, EasytierConfig, EasytierConfigMode, EasytierInfo, ExtensionId, + ExtensionInfo, ExtensionLogs, ExtensionsStatus, FrpProxyType, FrpcConfig, FrpcConfigMode, + FrpcInfo, GostcConfig, GostcInfo, TtydConfig, TtydInfo, }; use crate::state::AppState; -fn validate_gostc_enabled(config: &GostcConfig) -> Result<()> { - if config.addr.trim().is_empty() { - return Err(AppError::BadRequest( - "GOSTC server address is required".into(), - )); - } - if config.key.is_empty() { - return Err(AppError::BadRequest("GOSTC client key is required".into())); - } - Ok(()) -} - -fn validate_easytier_enabled(config: &EasytierConfig) -> Result<()> { - if config.network_name.trim().is_empty() { - return Err(AppError::BadRequest( - "EasyTier network name is required".into(), - )); - } - Ok(()) -} - -fn validate_frpc_enabled(config: &FrpcConfig) -> Result<()> { - match config.config_mode { - FrpcConfigMode::Quick => { - if config.proxy_name.trim().is_empty() { - return Err(AppError::BadRequest("FRPC proxy name is required".into())); - } - if config.server_addr.trim().is_empty() { - return Err(AppError::BadRequest( - "FRPC server address is required".into(), - )); - } - if config.token.is_empty() { - return Err(AppError::BadRequest("FRPC token is required".into())); - } - if config.local_ip.trim().is_empty() { - return Err(AppError::BadRequest("FRPC local IP is required".into())); - } - if matches!(config.proxy_type, FrpProxyType::Tcp | FrpProxyType::Udp) - && config.remote_port.is_none() - { - return Err(AppError::BadRequest( - "FRPC remote port is required for TCP/UDP proxies".into(), - )); - } - } - FrpcConfigMode::Full => { - let toml = config.custom_toml.trim(); - if toml.is_empty() { - return Err(AppError::BadRequest( - "FRPC full configuration is required".into(), - )); - } - toml.parse::().map_err(|e| { - AppError::BadRequest(format!("FRPC full configuration is not valid TOML: {}", e)) - })?; - } - } - Ok(()) +fn bad_request(validation: std::result::Result<(), String>) -> Result<()> { + validation.map_err(AppError::BadRequest) } pub async fn list_extensions(State(state): State>) -> Json { @@ -131,6 +74,8 @@ pub async fn start_extension( let config = state.config.get(); let mgr = &state.extensions; + bad_request(validate_extension_config(ext_id, &config.extensions))?; + mgr.start(ext_id, &config.extensions) .await .map_err(AppError::Internal)?; @@ -200,10 +145,12 @@ pub struct GostcConfigUpdate { #[derive(Debug, Deserialize)] pub struct EasytierConfigUpdate { pub enabled: Option, + pub config_mode: Option, pub network_name: Option, pub network_secret: Option, pub peer_urls: Option>, pub virtual_ip: Option, + pub custom_toml: Option, } #[typeshare] @@ -284,7 +231,7 @@ pub async fn update_gostc_config( } if next_gostc.enabled { - validate_gostc_enabled(&next_gostc)?; + bad_request(validate_gostc_config(&next_gostc))?; } state @@ -321,6 +268,9 @@ pub async fn update_easytier_config( if let Some(enabled) = req.enabled { next_easytier.enabled = enabled; } + if let Some(config_mode) = req.config_mode { + next_easytier.config_mode = config_mode; + } if let Some(ref name) = req.network_name { next_easytier.network_name = name.clone(); } @@ -333,9 +283,12 @@ pub async fn update_easytier_config( if req.virtual_ip.is_some() { next_easytier.virtual_ip = req.virtual_ip.clone(); } + if let Some(ref custom_toml) = req.custom_toml { + next_easytier.custom_toml = custom_toml.clone(); + } - if next_easytier.enabled { - validate_easytier_enabled(&next_easytier)?; + if next_easytier.enabled || matches!(next_easytier.config_mode, EasytierConfigMode::Full) { + bad_request(validate_easytier_config(&next_easytier))?; } state @@ -414,7 +367,7 @@ pub async fn update_frpc_config( } if next_frpc.enabled || matches!(next_frpc.config_mode, FrpcConfigMode::Full) { - validate_frpc_enabled(&next_frpc)?; + bad_request(validate_frpc_config(&next_frpc))?; } state diff --git a/web/src/components/ExtensionConfigModeEditor.vue b/web/src/components/ExtensionConfigModeEditor.vue new file mode 100644 index 00000000..b5a7861d --- /dev/null +++ b/web/src/components/ExtensionConfigModeEditor.vue @@ -0,0 +1,49 @@ + + +