mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-12 10:34:24 +08:00
feat: 为 EasyTier 添加完整 TOML 配置支持
- 新增快速配置与完整配置模式,兼容旧版配置 - 统一扩展配置校验及受保护临时文件生命周期 - 支持通过 easytier-core -c 加载 TOML 配置 - 复用前端配置模式编辑组件并更新中英文文案与类型 - 补充配置校验、启动参数、文件权限及清理测试
This commit is contained in:
@@ -1,15 +1,15 @@
|
|||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::path::PathBuf;
|
use std::path::Path;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use tempfile::TempDir;
|
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
use tokio::process::{Child, Command};
|
use tokio::process::{Child, Command};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use toml_edit::DocumentMut;
|
|
||||||
|
|
||||||
|
use super::protected_config::ProtectedConfigFile;
|
||||||
use super::types::*;
|
use super::types::*;
|
||||||
|
use super::validation::{validate_easytier_config, validate_frpc_config, validate_gostc_config};
|
||||||
use crate::events::EventBus;
|
use crate::events::EventBus;
|
||||||
|
|
||||||
const LOG_BUFFER_SIZE: usize = 200;
|
const LOG_BUFFER_SIZE: usize = 200;
|
||||||
@@ -27,12 +27,12 @@ const TTYD_TCP_PORT: &str = "7681";
|
|||||||
struct ExtensionProcess {
|
struct ExtensionProcess {
|
||||||
child: Child,
|
child: Child,
|
||||||
logs: Arc<RwLock<VecDeque<String>>>,
|
logs: Arc<RwLock<VecDeque<String>>>,
|
||||||
_temp_dir: Option<TempDir>,
|
_config_file: Option<ProtectedConfigFile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ExtensionLaunch {
|
struct ExtensionLaunch {
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
temp_dir: Option<TempDir>,
|
config_file: Option<ProtectedConfigFile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ExtensionManager {
|
pub struct ExtensionManager {
|
||||||
@@ -83,24 +83,12 @@ impl ExtensionManager {
|
|||||||
match id {
|
match id {
|
||||||
ExtensionId::Ttyd => config.ttyd.enabled,
|
ExtensionId::Ttyd => config.ttyd.enabled,
|
||||||
ExtensionId::Gostc => {
|
ExtensionId::Gostc => {
|
||||||
config.gostc.enabled
|
config.gostc.enabled && validate_gostc_config(&config.gostc).is_ok()
|
||||||
&& !config.gostc.key.is_empty()
|
|
||||||
&& !config.gostc.addr.trim().is_empty()
|
|
||||||
}
|
}
|
||||||
ExtensionId::Easytier => {
|
ExtensionId::Easytier => {
|
||||||
config.easytier.enabled && !config.easytier.network_name.is_empty()
|
config.easytier.enabled && validate_easytier_config(&config.easytier).is_ok()
|
||||||
}
|
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ExtensionId::Frpc => config.frpc.enabled && validate_frpc_config(&config.frpc).is_ok(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +191,7 @@ impl ExtensionManager {
|
|||||||
ExtensionProcess {
|
ExtensionProcess {
|
||||||
child,
|
child,
|
||||||
logs,
|
logs,
|
||||||
_temp_dir: launch.temp_dir,
|
_config_file: launch.config_file,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
drop(processes);
|
drop(processes);
|
||||||
@@ -286,12 +274,7 @@ impl ExtensionManager {
|
|||||||
|
|
||||||
ExtensionId::Gostc => {
|
ExtensionId::Gostc => {
|
||||||
let c = &config.gostc;
|
let c = &config.gostc;
|
||||||
if c.addr.trim().is_empty() {
|
validate_gostc_config(c)?;
|
||||||
return Err("GOSTC server address is required".into());
|
|
||||||
}
|
|
||||||
if c.key.is_empty() {
|
|
||||||
return Err("GOSTC client key is required".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut args = Vec::new();
|
let mut args = Vec::new();
|
||||||
|
|
||||||
@@ -307,25 +290,59 @@ impl ExtensionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ExtensionId::Easytier => {
|
ExtensionId::Easytier => {
|
||||||
let c = &config.easytier;
|
return Self::build_easytier_launch(&config.easytier).await;
|
||||||
if c.network_name.is_empty() {
|
|
||||||
return Err("EasyTier network name is required".into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExtensionId::Frpc => {
|
||||||
|
return Self::build_frpc_launch(&config.frpc).await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ExtensionLaunch {
|
||||||
|
args,
|
||||||
|
config_file: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_easytier_launch(config: &EasytierConfig) -> Result<ExtensionLaunch, String> {
|
||||||
|
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<String> {
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"--network-name".to_string(),
|
"--network-name".to_string(),
|
||||||
c.network_name.clone(),
|
config.network_name.clone(),
|
||||||
"--network-secret".to_string(),
|
"--network-secret".to_string(),
|
||||||
c.network_secret.clone(),
|
config.network_secret.clone(),
|
||||||
];
|
];
|
||||||
|
|
||||||
for peer in &c.peer_urls {
|
for peer in &config.peer_urls {
|
||||||
if !peer.is_empty() {
|
if !peer.is_empty() {
|
||||||
args.extend(["--peers".to_string(), peer.clone()]);
|
args.extend(["--peers".to_string(), peer.clone()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref ip) = c.virtual_ip {
|
if let Some(ref ip) = config.virtual_ip {
|
||||||
if !ip.is_empty() {
|
if !ip.is_empty() {
|
||||||
args.extend(["-i".to_string(), ip.clone()]);
|
args.extend(["-i".to_string(), ip.clone()]);
|
||||||
} else {
|
} else {
|
||||||
@@ -338,65 +355,23 @@ impl ExtensionManager {
|
|||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
|
||||||
ExtensionId::Frpc => {
|
|
||||||
return Self::build_frpc_launch(&config.frpc).await;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ExtensionLaunch {
|
|
||||||
args,
|
|
||||||
temp_dir: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn build_frpc_launch(config: &FrpcConfig) -> Result<ExtensionLaunch, String> {
|
async fn build_frpc_launch(config: &FrpcConfig) -> Result<ExtensionLaunch, String> {
|
||||||
|
validate_frpc_config(config)?;
|
||||||
|
|
||||||
let config_text = match config.config_mode {
|
let config_text = match config.config_mode {
|
||||||
FrpcConfigMode::Quick => Self::build_frpc_quick_toml(config)?,
|
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 =
|
let config_file =
|
||||||
tempfile::tempdir().map_err(|e| format!("Failed to create FRPC config dir: {}", e))?;
|
ProtectedConfigFile::create("FRPC", "frpc.toml", config_text.as_str()).await?;
|
||||||
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))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ExtensionLaunch {
|
Ok(ExtensionLaunch {
|
||||||
args: vec!["-c".to_string(), Self::path_to_arg(&config_path)],
|
args: vec!["-c".to_string(), Self::path_to_arg(config_file.path())],
|
||||||
temp_dir: Some(temp_dir),
|
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::<DocumentMut>()
|
|
||||||
.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<String, String> {
|
fn build_frpc_quick_toml(config: &FrpcConfig) -> Result<String, String> {
|
||||||
if config.proxy_name.trim().is_empty() {
|
if config.proxy_name.trim().is_empty() {
|
||||||
return Err("FRPC proxy name is required".into());
|
return Err("FRPC proxy name is required".into());
|
||||||
@@ -480,7 +455,7 @@ impl ExtensionManager {
|
|||||||
serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
|
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()
|
path.to_string_lossy().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,3 +578,107 @@ impl ExtensionManager {
|
|||||||
futures::future::join_all(stop_futures).await;
|
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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
mod manager;
|
mod manager;
|
||||||
|
mod protected_config;
|
||||||
mod software;
|
mod software;
|
||||||
mod types;
|
mod types;
|
||||||
|
mod validation;
|
||||||
|
|
||||||
pub use manager::ExtensionManager;
|
pub use manager::ExtensionManager;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -8,3 +10,7 @@ pub use manager::TTYD_SOCKET_PATH;
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub use manager::TTYD_TCP_ADDR;
|
pub use manager::TTYD_TCP_ADDR;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
pub(crate) use validation::{
|
||||||
|
validate_easytier_config, validate_extension_config, validate_frpc_config,
|
||||||
|
validate_gostc_config,
|
||||||
|
};
|
||||||
|
|||||||
95
src/extensions/protected_config.rs
Normal file
95
src/extensions/protected_config.rs
Normal file
@@ -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<Self, String> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,11 +103,25 @@ impl Default for GostcConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[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)]
|
#[serde(default)]
|
||||||
#[derive(Default)]
|
|
||||||
pub struct EasytierConfig {
|
pub struct EasytierConfig {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
pub config_mode: EasytierConfigMode,
|
||||||
pub network_name: String,
|
pub network_name: String,
|
||||||
#[serde(skip_serializing_if = "String::is_empty")]
|
#[serde(skip_serializing_if = "String::is_empty")]
|
||||||
pub network_secret: String,
|
pub network_secret: String,
|
||||||
@@ -115,6 +129,8 @@ pub struct EasytierConfig {
|
|||||||
pub peer_urls: Vec<String>,
|
pub peer_urls: Vec<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub virtual_ip: Option<String>,
|
pub virtual_ip: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "String::is_empty")]
|
||||||
|
pub custom_toml: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
@@ -260,3 +276,26 @@ pub struct ExtensionLogs {
|
|||||||
pub id: ExtensionId,
|
pub id: ExtensionId,
|
||||||
pub logs: Vec<String>,
|
pub logs: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
119
src/extensions/validation.rs
Normal file
119
src/extensions/validation.rs
Normal file
@@ -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::<DocumentMut>().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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,76 +4,19 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use toml_edit::DocumentMut;
|
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
use crate::error::{AppError, Result};
|
use crate::error::{AppError, Result};
|
||||||
use crate::extensions::{
|
use crate::extensions::{
|
||||||
EasytierConfig, EasytierInfo, ExtensionId, ExtensionInfo, ExtensionLogs, ExtensionsStatus,
|
validate_easytier_config, validate_extension_config, validate_frpc_config,
|
||||||
FrpProxyType, FrpcConfig, FrpcConfigMode, FrpcInfo, GostcConfig, GostcInfo, TtydConfig,
|
validate_gostc_config, EasytierConfig, EasytierConfigMode, EasytierInfo, ExtensionId,
|
||||||
TtydInfo,
|
ExtensionInfo, ExtensionLogs, ExtensionsStatus, FrpProxyType, FrpcConfig, FrpcConfigMode,
|
||||||
|
FrpcInfo, GostcConfig, GostcInfo, TtydConfig, TtydInfo,
|
||||||
};
|
};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
fn validate_gostc_enabled(config: &GostcConfig) -> Result<()> {
|
fn bad_request(validation: std::result::Result<(), String>) -> Result<()> {
|
||||||
if config.addr.trim().is_empty() {
|
validation.map_err(AppError::BadRequest)
|
||||||
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::<DocumentMut>().map_err(|e| {
|
|
||||||
AppError::BadRequest(format!("FRPC full configuration is not valid TOML: {}", e))
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_extensions(State(state): State<Arc<AppState>>) -> Json<ExtensionsStatus> {
|
pub async fn list_extensions(State(state): State<Arc<AppState>>) -> Json<ExtensionsStatus> {
|
||||||
@@ -131,6 +74,8 @@ pub async fn start_extension(
|
|||||||
let config = state.config.get();
|
let config = state.config.get();
|
||||||
let mgr = &state.extensions;
|
let mgr = &state.extensions;
|
||||||
|
|
||||||
|
bad_request(validate_extension_config(ext_id, &config.extensions))?;
|
||||||
|
|
||||||
mgr.start(ext_id, &config.extensions)
|
mgr.start(ext_id, &config.extensions)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::Internal)?;
|
.map_err(AppError::Internal)?;
|
||||||
@@ -200,10 +145,12 @@ pub struct GostcConfigUpdate {
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct EasytierConfigUpdate {
|
pub struct EasytierConfigUpdate {
|
||||||
pub enabled: Option<bool>,
|
pub enabled: Option<bool>,
|
||||||
|
pub config_mode: Option<EasytierConfigMode>,
|
||||||
pub network_name: Option<String>,
|
pub network_name: Option<String>,
|
||||||
pub network_secret: Option<String>,
|
pub network_secret: Option<String>,
|
||||||
pub peer_urls: Option<Vec<String>>,
|
pub peer_urls: Option<Vec<String>>,
|
||||||
pub virtual_ip: Option<String>,
|
pub virtual_ip: Option<String>,
|
||||||
|
pub custom_toml: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
@@ -284,7 +231,7 @@ pub async fn update_gostc_config(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if next_gostc.enabled {
|
if next_gostc.enabled {
|
||||||
validate_gostc_enabled(&next_gostc)?;
|
bad_request(validate_gostc_config(&next_gostc))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
state
|
state
|
||||||
@@ -321,6 +268,9 @@ pub async fn update_easytier_config(
|
|||||||
if let Some(enabled) = req.enabled {
|
if let Some(enabled) = req.enabled {
|
||||||
next_easytier.enabled = 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 {
|
if let Some(ref name) = req.network_name {
|
||||||
next_easytier.network_name = name.clone();
|
next_easytier.network_name = name.clone();
|
||||||
}
|
}
|
||||||
@@ -333,9 +283,12 @@ pub async fn update_easytier_config(
|
|||||||
if req.virtual_ip.is_some() {
|
if req.virtual_ip.is_some() {
|
||||||
next_easytier.virtual_ip = req.virtual_ip.clone();
|
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 {
|
if next_easytier.enabled || matches!(next_easytier.config_mode, EasytierConfigMode::Full) {
|
||||||
validate_easytier_enabled(&next_easytier)?;
|
bad_request(validate_easytier_config(&next_easytier))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
state
|
state
|
||||||
@@ -414,7 +367,7 @@ pub async fn update_frpc_config(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if next_frpc.enabled || matches!(next_frpc.config_mode, FrpcConfigMode::Full) {
|
if next_frpc.enabled || matches!(next_frpc.config_mode, FrpcConfigMode::Full) {
|
||||||
validate_frpc_enabled(&next_frpc)?;
|
bad_request(validate_frpc_config(&next_frpc))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
state
|
state
|
||||||
|
|||||||
49
web/src/components/ExtensionConfigModeEditor.vue
Normal file
49
web/src/components/ExtensionConfigModeEditor.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { ButtonGroup } from '@/components/ui/button-group'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
disabled: boolean
|
||||||
|
quickLabel: string
|
||||||
|
fullLabel: string
|
||||||
|
fullConfigHint: string
|
||||||
|
fullConfigRequired: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const mode = defineModel<'quick' | 'full'>('mode', { required: true })
|
||||||
|
const config = defineModel<string>('config', { required: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ButtonGroup class="grid w-full grid-cols-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
:variant="mode === 'quick' ? 'default' : 'outline'"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="mode = 'quick'"
|
||||||
|
>
|
||||||
|
{{ quickLabel }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
:variant="mode === 'full' ? 'default' : 'outline'"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="mode = 'full'"
|
||||||
|
>
|
||||||
|
{{ fullLabel }}
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<slot v-if="mode === 'quick'" name="quick" />
|
||||||
|
<div v-else class="space-y-1">
|
||||||
|
<Textarea
|
||||||
|
v-model="config"
|
||||||
|
class="min-h-[300px] font-mono text-xs"
|
||||||
|
spellcheck="false"
|
||||||
|
:disabled="disabled"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground">{{ fullConfigHint }}</p>
|
||||||
|
<p v-if="!config.trim()" class="text-xs text-destructive">{{ fullConfigRequired }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1030,6 +1030,10 @@ export default {
|
|||||||
easytier: {
|
easytier: {
|
||||||
title: 'Easytier Network',
|
title: 'Easytier Network',
|
||||||
desc: 'P2P VPN networking via EasyTier',
|
desc: 'P2P VPN networking via EasyTier',
|
||||||
|
quickConfig: 'Quick Config',
|
||||||
|
fullConfig: 'Full Config',
|
||||||
|
fullConfigHint: 'Paste the full easytier.toml configuration file here',
|
||||||
|
fullConfigRequired: 'Enter the full easytier.toml configuration',
|
||||||
networkName: 'Network Name',
|
networkName: 'Network Name',
|
||||||
networkNameRequired: 'Enter the EasyTier network name',
|
networkNameRequired: 'Enter the EasyTier network name',
|
||||||
networkSecret: 'Network Secret',
|
networkSecret: 'Network Secret',
|
||||||
|
|||||||
@@ -1029,6 +1029,10 @@ export default {
|
|||||||
easytier: {
|
easytier: {
|
||||||
title: 'Easytier 组网',
|
title: 'Easytier 组网',
|
||||||
desc: '通过 EasyTier 实现 P2P VPN 组网',
|
desc: '通过 EasyTier 实现 P2P VPN 组网',
|
||||||
|
quickConfig: '快速配置',
|
||||||
|
fullConfig: '完整配置',
|
||||||
|
fullConfigHint: '可在此粘贴完整的 easytier.toml 配置文件',
|
||||||
|
fullConfigRequired: '请填写完整 easytier.toml 配置',
|
||||||
networkName: '网络名称',
|
networkName: '网络名称',
|
||||||
networkNameRequired: '请填写 EasyTier 网络名称',
|
networkNameRequired: '请填写 EasyTier 网络名称',
|
||||||
networkSecret: '网络密钥',
|
networkSecret: '网络密钥',
|
||||||
|
|||||||
@@ -194,12 +194,19 @@ export interface GostcConfig {
|
|||||||
tls: boolean;
|
tls: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum EasytierConfigMode {
|
||||||
|
Quick = "quick",
|
||||||
|
Full = "full",
|
||||||
|
}
|
||||||
|
|
||||||
export interface EasytierConfig {
|
export interface EasytierConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
config_mode: EasytierConfigMode;
|
||||||
network_name: string;
|
network_name: string;
|
||||||
network_secret: string;
|
network_secret: string;
|
||||||
peer_urls: string[];
|
peer_urls: string[];
|
||||||
virtual_ip?: string;
|
virtual_ip?: string;
|
||||||
|
custom_toml: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FrpcConfigMode {
|
export enum FrpcConfigMode {
|
||||||
@@ -430,10 +437,12 @@ export interface ComputerUseStartRequest {
|
|||||||
|
|
||||||
export interface EasytierConfigUpdate {
|
export interface EasytierConfigUpdate {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
config_mode?: EasytierConfigMode;
|
||||||
network_name?: string;
|
network_name?: string;
|
||||||
network_secret?: string;
|
network_secret?: string;
|
||||||
peer_urls?: string[];
|
peer_urls?: string[];
|
||||||
virtual_ip?: string;
|
virtual_ip?: string;
|
||||||
|
custom_toml?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExtensionStatus =
|
export type ExtensionStatus =
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ import type {
|
|||||||
OtgNetworkStatus,
|
OtgNetworkStatus,
|
||||||
WatchdogConfigResponse,
|
WatchdogConfigResponse,
|
||||||
} from '@/types/generated'
|
} from '@/types/generated'
|
||||||
import { FrpProxyType, FrpcConfigMode } from '@/types/generated'
|
import { EasytierConfigMode, FrpProxyType, FrpcConfigMode } from '@/types/generated'
|
||||||
import { toConfigFps } from '@/lib/fps'
|
import { toConfigFps } from '@/lib/fps'
|
||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
import { useFeatureVisibility } from '@/composables/useFeatureVisibility'
|
import { useFeatureVisibility } from '@/composables/useFeatureVisibility'
|
||||||
@@ -67,6 +67,7 @@ import LanguageToggleButton from '@/components/LanguageToggleButton.vue'
|
|||||||
import TerminalDialog from '@/components/TerminalDialog.vue'
|
import TerminalDialog from '@/components/TerminalDialog.vue'
|
||||||
import TotpSettingsCard from '@/components/TotpSettingsCard.vue'
|
import TotpSettingsCard from '@/components/TotpSettingsCard.vue'
|
||||||
import VideoInputFields from '@/components/VideoInputFields.vue'
|
import VideoInputFields from '@/components/VideoInputFields.vue'
|
||||||
|
import ExtensionConfigModeEditor from '@/components/ExtensionConfigModeEditor.vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
@@ -76,7 +77,6 @@ import { Separator } from '@/components/ui/separator'
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
import { ButtonGroup } from '@/components/ui/button-group'
|
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia } from '@/components/ui/empty'
|
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia } from '@/components/ui/empty'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
@@ -427,7 +427,15 @@ const showTerminalDialog = ref(false)
|
|||||||
const extConfig = ref({
|
const extConfig = ref({
|
||||||
ttyd: { enabled: false, shell: '/bin/bash' },
|
ttyd: { enabled: false, shell: '/bin/bash' },
|
||||||
gostc: { enabled: false, addr: '', key: '', tls: true },
|
gostc: { enabled: false, addr: '', key: '', tls: true },
|
||||||
easytier: { enabled: false, network_name: '', network_secret: '', peer_urls: [] as string[], virtual_ip: '' },
|
easytier: {
|
||||||
|
enabled: false,
|
||||||
|
config_mode: EasytierConfigMode.Quick,
|
||||||
|
network_name: '',
|
||||||
|
network_secret: '',
|
||||||
|
peer_urls: [] as string[],
|
||||||
|
virtual_ip: '',
|
||||||
|
custom_toml: '',
|
||||||
|
},
|
||||||
frpc: {
|
frpc: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
config_mode: FrpcConfigMode.Quick,
|
config_mode: FrpcConfigMode.Quick,
|
||||||
@@ -453,6 +461,10 @@ const gostcValidationMessage = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const easytierValidationMessage = computed(() => {
|
const easytierValidationMessage = computed(() => {
|
||||||
|
if (extConfig.value.easytier.config_mode === EasytierConfigMode.Full) {
|
||||||
|
if (!extConfig.value.easytier.custom_toml?.trim()) return t('extensions.easytier.fullConfigRequired')
|
||||||
|
return ''
|
||||||
|
}
|
||||||
if (!extConfig.value.easytier.network_name?.trim()) return t('extensions.easytier.networkNameRequired')
|
if (!extConfig.value.easytier.network_name?.trim()) return t('extensions.easytier.networkNameRequired')
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
@@ -1629,10 +1641,12 @@ async function loadExtensions() {
|
|||||||
const easytier = extensions.value.easytier.config
|
const easytier = extensions.value.easytier.config
|
||||||
extConfig.value.easytier = {
|
extConfig.value.easytier = {
|
||||||
enabled: easytier.enabled,
|
enabled: easytier.enabled,
|
||||||
|
config_mode: easytier.config_mode || EasytierConfigMode.Quick,
|
||||||
network_name: easytier.network_name,
|
network_name: easytier.network_name,
|
||||||
network_secret: easytier.network_secret,
|
network_secret: easytier.network_secret,
|
||||||
peer_urls: easytier.peer_urls || [],
|
peer_urls: easytier.peer_urls || [],
|
||||||
virtual_ip: easytier.virtual_ip || '',
|
virtual_ip: easytier.virtual_ip || '',
|
||||||
|
custom_toml: easytier.custom_toml || '',
|
||||||
}
|
}
|
||||||
const frpc = extensions.value.frpc.config
|
const frpc = extensions.value.frpc.config
|
||||||
extConfig.value.frpc = {
|
extConfig.value.frpc = {
|
||||||
@@ -1690,6 +1704,7 @@ async function refreshExtensionLogs(id: ExtensionConfigId) {
|
|||||||
async function saveExtensionConfig(id: ExtensionConfigId) {
|
async function saveExtensionConfig(id: ExtensionConfigId) {
|
||||||
if (id !== 'ttyd') {
|
if (id !== 'ttyd') {
|
||||||
const shouldValidate = extConfig.value[id].enabled
|
const shouldValidate = extConfig.value[id].enabled
|
||||||
|
|| (id === 'easytier' && extConfig.value.easytier.config_mode === EasytierConfigMode.Full)
|
||||||
|| (id === 'frpc' && extConfig.value.frpc.config_mode === FrpcConfigMode.Full)
|
|| (id === 'frpc' && extConfig.value.frpc.config_mode === FrpcConfigMode.Full)
|
||||||
if (shouldValidate && !validateExtensionConfig(id)) return
|
if (shouldValidate && !validateExtensionConfig(id)) return
|
||||||
}
|
}
|
||||||
@@ -4516,6 +4531,16 @@ watch(isWindows, () => {
|
|||||||
<Label>{{ t('extensions.autoStart') }}</Label>
|
<Label>{{ t('extensions.autoStart') }}</Label>
|
||||||
<Switch v-model="extConfig.easytier.enabled" :disabled="isExtRunning(extensions?.easytier?.status)" />
|
<Switch v-model="extConfig.easytier.enabled" :disabled="isExtRunning(extensions?.easytier?.status)" />
|
||||||
</div>
|
</div>
|
||||||
|
<ExtensionConfigModeEditor
|
||||||
|
v-model:mode="extConfig.easytier.config_mode"
|
||||||
|
v-model:config="extConfig.easytier.custom_toml"
|
||||||
|
:disabled="isExtRunning(extensions?.easytier?.status)"
|
||||||
|
:quick-label="t('extensions.easytier.quickConfig')"
|
||||||
|
:full-label="t('extensions.easytier.fullConfig')"
|
||||||
|
:full-config-hint="t('extensions.easytier.fullConfigHint')"
|
||||||
|
:full-config-required="t('extensions.easytier.fullConfigRequired')"
|
||||||
|
>
|
||||||
|
<template #quick>
|
||||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||||
<Label class="sm:text-right">{{ t('extensions.easytier.networkName') }}</Label>
|
<Label class="sm:text-right">{{ t('extensions.easytier.networkName') }}</Label>
|
||||||
<div class="sm:col-span-3 space-y-1">
|
<div class="sm:col-span-3 space-y-1">
|
||||||
@@ -4548,6 +4573,8 @@ watch(isWindows, () => {
|
|||||||
<Input v-model="extConfig.easytier.virtual_ip" placeholder="10.0.0.1/24" :disabled="isExtRunning(extensions?.easytier?.status)" />
|
<Input v-model="extConfig.easytier.virtual_ip" placeholder="10.0.0.1/24" :disabled="isExtRunning(extensions?.easytier?.status)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
</ExtensionConfigModeEditor>
|
||||||
</div>
|
</div>
|
||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -4621,25 +4648,16 @@ watch(isWindows, () => {
|
|||||||
<Label>{{ t('extensions.autoStart') }}</Label>
|
<Label>{{ t('extensions.autoStart') }}</Label>
|
||||||
<Switch v-model="extConfig.frpc.enabled" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
<Switch v-model="extConfig.frpc.enabled" :disabled="isExtRunning(extensions?.frpc?.status)" />
|
||||||
</div>
|
</div>
|
||||||
<ButtonGroup class="grid w-full grid-cols-2">
|
<ExtensionConfigModeEditor
|
||||||
<Button
|
v-model:mode="extConfig.frpc.config_mode"
|
||||||
type="button"
|
v-model:config="extConfig.frpc.custom_toml"
|
||||||
:variant="frpcQuickMode ? 'default' : 'outline'"
|
|
||||||
:disabled="isExtRunning(extensions?.frpc?.status)"
|
:disabled="isExtRunning(extensions?.frpc?.status)"
|
||||||
@click="extConfig.frpc.config_mode = FrpcConfigMode.Quick"
|
:quick-label="t('extensions.frpc.quickConfig')"
|
||||||
|
:full-label="t('extensions.frpc.fullConfig')"
|
||||||
|
:full-config-hint="t('extensions.frpc.fullConfigHint')"
|
||||||
|
:full-config-required="t('extensions.frpc.fullConfigRequired')"
|
||||||
>
|
>
|
||||||
{{ t('extensions.frpc.quickConfig') }}
|
<template #quick>
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
:variant="!frpcQuickMode ? 'default' : 'outline'"
|
|
||||||
:disabled="isExtRunning(extensions?.frpc?.status)"
|
|
||||||
@click="extConfig.frpc.config_mode = FrpcConfigMode.Full"
|
|
||||||
>
|
|
||||||
{{ t('extensions.frpc.fullConfig') }}
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
<template v-if="frpcQuickMode">
|
|
||||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||||
<Label class="sm:text-right">{{ t('extensions.frpc.proxyType') }}</Label>
|
<Label class="sm:text-right">{{ t('extensions.frpc.proxyType') }}</Label>
|
||||||
<div class="sm:col-span-3">
|
<div class="sm:col-span-3">
|
||||||
@@ -4709,16 +4727,7 @@ watch(isWindows, () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="space-y-1">
|
</ExtensionConfigModeEditor>
|
||||||
<Textarea
|
|
||||||
v-model="extConfig.frpc.custom_toml"
|
|
||||||
class="min-h-[300px] font-mono text-xs"
|
|
||||||
spellcheck="false"
|
|
||||||
:disabled="isExtRunning(extensions?.frpc?.status)"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-muted-foreground">{{ t('extensions.frpc.fullConfigHint') }}</p>
|
|
||||||
<p v-if="!extConfig.frpc.custom_toml?.trim()" class="text-xs text-destructive">{{ t('extensions.frpc.fullConfigRequired') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Collapsible v-model:open="showLogs.frpc" @update:open="open => open && refreshExtensionLogs('frpc')">
|
<Collapsible v-model:open="showLogs.frpc" @update:open="open => open && refreshExtensionLogs('frpc')">
|
||||||
|
|||||||
Reference in New Issue
Block a user