diff --git a/README.en.md b/README.en.md index 497aca04..668d1172 100644 --- a/README.en.md +++ b/README.en.md @@ -72,7 +72,7 @@ Replace the version and architecture in the filename with your actual file name. Images: - **one-kvm** — main app + ttyd -- **one-kvm-full** — same plus optional extras (e.g. gostc, easytier-core) +- **one-kvm-full** — same plus optional extras (gostc, easyfrpc, easytier-core, easytier-cli, frpc) Example: diff --git a/README.md b/README.md index 234402c8..00b123b5 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ sudo apt install ./one-kvm_0.x.x_.deb ### 使用 Docker -镜像分为 **one-kvm**(One-KVM 主程序 + ttyd)与 **one-kvm-full**(另含 gostc、easytier-core 等可选扩展),按需选用。 +镜像分为 **one-kvm**(One-KVM 主程序 + ttyd)与 **one-kvm-full**(另含 gostc、easyfrpc、easytier-core、easytier-cli、frpc 等可选扩展),按需选用。 ```bash docker run --name one-kvm -itd \ diff --git a/build/Dockerfile.runtime-full b/build/Dockerfile.runtime-full index f869b90f..2d8358e5 100644 --- a/build/Dockerfile.runtime-full +++ b/build/Dockerfile.runtime-full @@ -52,7 +52,7 @@ RUN if [ -n "$APT_MIRROR" ]; then \ COPY --chmod=755 init.sh /init.sh # Copy binaries (these are placed by the build script) -COPY --chmod=755 one-kvm ttyd easyfrpc easytier-core easytier-cli frpc /usr/bin/ +COPY --chmod=755 one-kvm ttyd gostc easyfrpc easytier-core easytier-cli frpc /usr/bin/ # Copy ventoy resources if they exist COPY ventoy/ /etc/one-kvm/ventoy/ diff --git a/build/download-remote-tools.py b/build/download-remote-tools.py index 78f494fd..fedbbf1e 100644 --- a/build/download-remote-tools.py +++ b/build/download-remote-tools.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Fetch the pinned EasyFrp, EasyTier and frp clients for full Docker images.""" +"""Fetch the pinned GOSTC, EasyFrp, EasyTier and frp clients for full Docker images.""" import argparse import hashlib import json diff --git a/build/package-docker.sh b/build/package-docker.sh index 9b58b69e..893d3510 100755 --- a/build/package-docker.sh +++ b/build/package-docker.sh @@ -118,7 +118,7 @@ while [[ $# -gt 0 ]]; do echo " # Build for current platform and load locally" echo " $0 --platform linux/arm64 --load" echo "" - echo " # Build full image (includes EasyFrp + EasyTier + frpc)" + echo " # Build full image (includes GOSTC + EasyFrp + EasyTier + frpc)" echo " $0 --variant full --platform linux/arm64 --load" echo "" echo " # Build and push single platform" diff --git a/build/remote-access.lock.json b/build/remote-access.lock.json index 93182ae9..5a0c45de 100644 --- a/build/remote-access.lock.json +++ b/build/remote-access.lock.json @@ -1,5 +1,28 @@ { "tools": { + "gostc": { + "version": "v2.0.9", + "binaries": [ + "gostc" + ], + "assets": { + "arm": { + "archive": "gostc_linux_arm_7.tar.gz", + "url": "https://github.com/SianHH/gostc-open/releases/download/v2.0.9/gostc_linux_arm_7.tar.gz", + "sha256": "abca04c0503245ad5238d4482111b4a0ca2959daf460cd2abba47f3fce292a6b" + }, + "aarch64": { + "archive": "gostc_linux_arm64_v8.0.tar.gz", + "url": "https://github.com/SianHH/gostc-open/releases/download/v2.0.9/gostc_linux_arm64_v8.0.tar.gz", + "sha256": "2a100487a4f3ccd27ad82132c4f8e6ac253c9696428f0d3fd4c53170e1c3682e" + }, + "x86_64": { + "archive": "gostc_linux_amd64_v1.tar.gz", + "url": "https://github.com/SianHH/gostc-open/releases/download/v2.0.9/gostc_linux_amd64_v1.tar.gz", + "sha256": "6285506b54bfece2af2636ceacd4e40fefb28fcfbe1b7ba072854b08166549d7" + } + } + }, "easyfrp": { "version": "v3.1.1", "binaries": [ diff --git a/src/extensions/manager.rs b/src/extensions/manager.rs index e0e98bd2..ee2e0440 100644 --- a/src/extensions/manager.rs +++ b/src/extensions/manager.rs @@ -9,7 +9,9 @@ use tokio::sync::RwLock; use super::protected_config::ProtectedConfigFile; use super::types::*; -use super::validation::{validate_easyfrp_config, validate_easytier_config, validate_frpc_config}; +use super::validation::{ + validate_easyfrp_config, validate_easytier_config, validate_frpc_config, validate_gostc_config, +}; use crate::events::EventBus; const LOG_BUFFER_SIZE: usize = 200; @@ -82,6 +84,9 @@ impl ExtensionManager { fn is_enabled_for_config(id: ExtensionId, config: &ExtensionsConfig) -> bool { match id { ExtensionId::Ttyd => config.ttyd.enabled, + ExtensionId::Gostc => { + config.gostc.enabled && validate_gostc_config(&config.gostc).is_ok() + } ExtensionId::Easyfrp => { config.easyfrp.enabled && validate_easyfrp_config(&config.easyfrp).is_ok() } @@ -267,6 +272,20 @@ impl ExtensionManager { args } + ExtensionId::Gostc => { + let c = &config.gostc; + validate_gostc_config(c)?; + + // gostc defaults to TLS, so disabling it must be explicit. + let mut args = vec![format!("--tls={}", c.tls)]; + + args.extend(["-addr".to_string(), c.addr.trim().to_string()]); + + args.extend(["-key".to_string(), c.key.clone()]); + + args + } + ExtensionId::Easyfrp => { let c = &config.easyfrp; validate_easyfrp_config(c)?; @@ -570,6 +589,54 @@ impl ExtensionManager { mod tests { use super::*; + #[tokio::test] + async fn gostc_launch_explicitly_sets_tls_and_preserves_key_argument() { + let mut config = ExtensionsConfig::default(); + config.gostc.addr = " example.com:443 ".to_string(); + config.gostc.key = "key with spaces".to_string(); + for tls in [true, false] { + config.gostc.tls = tls; + let launch = ExtensionManager::new() + .build_launch(ExtensionId::Gostc, &config) + .await + .unwrap(); + assert_eq!( + launch.args, + [ + format!("--tls={tls}"), + "-addr".to_string(), + "example.com:443".to_string(), + "-key".to_string(), + "key with spaces".to_string(), + ] + ); + } + } + + #[tokio::test] + async fn gostc_invalid_config_cannot_launch_or_autostart() { + let mut config = ExtensionsConfig::default(); + config.gostc.enabled = true; + assert!(!ExtensionManager::is_enabled_for_config( + ExtensionId::Gostc, + &config + )); + assert!(ExtensionManager::new() + .build_launch(ExtensionId::Gostc, &config) + .await + .is_err()); + config.gostc.addr = "example.com".to_string(); + config.gostc.key = "client-key".to_string(); + assert!(ExtensionManager::is_enabled_for_config( + ExtensionId::Gostc, + &config + )); + assert!(!ExtensionManager::is_enabled_for_config( + ExtensionId::Easyfrp, + &config + )); + } + #[tokio::test] async fn easyfrp_launch_uses_control_urls_as_separate_arguments() { let mut config = ExtensionsConfig::default(); diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs index 3fcb695d..f0fda28f 100644 --- a/src/extensions/mod.rs +++ b/src/extensions/mod.rs @@ -12,5 +12,5 @@ pub use manager::TTYD_TCP_ADDR; pub use types::*; pub(crate) use validation::{ validate_easyfrp_config, validate_easytier_config, validate_extension_config, - validate_frpc_config, + validate_frpc_config, validate_gostc_config, }; diff --git a/src/extensions/software_linux.rs b/src/extensions/software_linux.rs index db924db8..672e4fef 100644 --- a/src/extensions/software_linux.rs +++ b/src/extensions/software_linux.rs @@ -5,6 +5,7 @@ use super::ExtensionId; pub fn default_binary_path(id: ExtensionId) -> &'static str { match id { ExtensionId::Ttyd => "/usr/bin/ttyd", + ExtensionId::Gostc => "/usr/bin/gostc", ExtensionId::Easyfrp => "/usr/bin/easyfrpc", ExtensionId::Easytier => "/usr/bin/easytier-core", ExtensionId::Frpc => "/usr/bin/frpc", diff --git a/src/extensions/software_windows.rs b/src/extensions/software_windows.rs index 6e4db753..d3694ead 100644 --- a/src/extensions/software_windows.rs +++ b/src/extensions/software_windows.rs @@ -5,6 +5,7 @@ use super::ExtensionId; pub fn default_binary_path(id: ExtensionId) -> &'static str { match id { ExtensionId::Ttyd => "ttyd.win32.exe", + ExtensionId::Gostc => "gostc.exe", ExtensionId::Easyfrp => "easyfrpc.exe", ExtensionId::Easytier => "easytier-core.exe", ExtensionId::Frpc => "frpc.exe", diff --git a/src/extensions/types.rs b/src/extensions/types.rs index 08e90f27..bb3bc5bb 100644 --- a/src/extensions/types.rs +++ b/src/extensions/types.rs @@ -8,6 +8,7 @@ use super::software; #[serde(rename_all = "lowercase")] pub enum ExtensionId { Ttyd, + Gostc, Easyfrp, Easytier, Frpc, @@ -19,7 +20,13 @@ impl ExtensionId { } pub fn all() -> &'static [ExtensionId] { - &[Self::Ttyd, Self::Easyfrp, Self::Easytier, Self::Frpc] + &[ + Self::Ttyd, + Self::Gostc, + Self::Easyfrp, + Self::Easytier, + Self::Frpc, + ] } } @@ -27,6 +34,7 @@ impl std::fmt::Display for ExtensionId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Ttyd => write!(f, "ttyd"), + Self::Gostc => write!(f, "gostc"), Self::Easyfrp => write!(f, "easyfrp"), Self::Easytier => write!(f, "easytier"), Self::Frpc => write!(f, "frpc"), @@ -40,6 +48,7 @@ impl std::str::FromStr for ExtensionId { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "ttyd" => Ok(Self::Ttyd), + "gostc" => Ok(Self::Gostc), "easyfrp" => Ok(Self::Easyfrp), "easytier" => Ok(Self::Easytier), "frpc" => Ok(Self::Frpc), @@ -80,6 +89,28 @@ impl Default for TtydConfig { } } +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct GostcConfig { + pub enabled: bool, + pub addr: String, + #[serde(skip_serializing_if = "String::is_empty")] + pub key: String, + pub tls: bool, +} + +impl Default for GostcConfig { + fn default() -> Self { + Self { + enabled: false, + addr: String::new(), + key: String::new(), + tls: true, + } + } +} + #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default)] @@ -203,6 +234,7 @@ impl Default for FrpcConfig { #[serde(default)] pub struct ExtensionsConfig { pub ttyd: TtydConfig, + pub gostc: GostcConfig, pub easyfrp: EasyfrpConfig, pub easytier: EasytierConfig, pub frpc: FrpcConfig, @@ -223,6 +255,14 @@ pub struct TtydInfo { pub config: TtydConfig, } +#[typeshare] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GostcInfo { + pub available: bool, + pub status: ExtensionStatus, + pub config: GostcConfig, +} + #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EasyfrpInfo { @@ -251,6 +291,7 @@ pub struct FrpcInfo { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExtensionsStatus { pub ttyd: TtydInfo, + pub gostc: GostcInfo, pub easyfrp: EasyfrpInfo, pub easytier: EasytierInfo, pub frpc: FrpcInfo, @@ -268,11 +309,43 @@ mod tests { use super::{EasytierConfig, EasytierConfigMode, ExtensionsConfig}; #[test] - fn replacing_legacy_gostc_requires_new_easyfrp_configuration() { + fn existing_easyfrp_config_leaves_gostc_disabled_with_tls_default() { + let config: ExtensionsConfig = serde_json::from_str( + r#"{"easyfrp":{"enabled":true,"control_urls":["wss://example.com/key"]}}"#, + ) + .unwrap(); + assert!(!config.gostc.enabled); + assert!(config.gostc.tls); + assert!(config.easyfrp.enabled); + assert_eq!(config.easyfrp.control_urls, ["wss://example.com/key"]); + } + + #[test] + fn gostc_and_easyfrp_configs_round_trip_independently() { + let config: ExtensionsConfig = serde_json::from_str( + r#"{"gostc":{"enabled":true,"addr":"old.example.com","key":"old-key","tls":false},"easyfrp":{"enabled":true,"control_urls":["wss://example.com/new-key"]}}"#, + ).unwrap(); + let saved = serde_json::to_string(&config).unwrap(); + let restored: ExtensionsConfig = serde_json::from_str(&saved).unwrap(); + assert!(restored.gostc.enabled); + assert!(!restored.gostc.tls); + assert_eq!(restored.gostc.key, "old-key"); + assert!(restored.easyfrp.enabled); + assert_eq!(restored.easyfrp.control_urls, ["wss://example.com/new-key"]); + } + + #[test] + fn legacy_gostc_configuration_is_preserved_independently_of_easyfrp() { let config: ExtensionsConfig = serde_json::from_str( r#"{"gostc":{"enabled":true,"addr":"old.example.com","key":"old-key","tls":true}}"#, ) .unwrap(); + assert!(config.gostc.enabled); + assert_eq!(config.gostc.addr, "old.example.com"); + assert_eq!(config.gostc.key, "old-key"); + assert!(config.gostc.tls); + let saved = serde_json::to_value(&config).unwrap(); + assert_eq!(saved["gostc"]["key"], "old-key"); assert!(!config.easyfrp.enabled); assert!(config.easyfrp.control_urls.is_empty()); } diff --git a/src/extensions/validation.rs b/src/extensions/validation.rs index 31180100..49e05c83 100644 --- a/src/extensions/validation.rs +++ b/src/extensions/validation.rs @@ -2,7 +2,7 @@ use toml_edit::DocumentMut; use super::types::{ EasyfrpConfig, EasytierConfig, EasytierConfigMode, ExtensionId, ExtensionsConfig, FrpProxyType, - FrpcConfig, FrpcConfigMode, + FrpcConfig, FrpcConfigMode, GostcConfig, }; pub(crate) fn validate_extension_config( @@ -11,12 +11,18 @@ pub(crate) fn validate_extension_config( ) -> Result<(), String> { match id { ExtensionId::Ttyd => Ok(()), + ExtensionId::Gostc => validate_gostc_config(&config.gostc), ExtensionId::Easyfrp => validate_easyfrp_config(&config.easyfrp), 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_easyfrp_config(config: &EasyfrpConfig) -> Result<(), String> { if config.control_urls.is_empty() { return Err("EasyFrp control URL is required".to_string()); @@ -101,6 +107,18 @@ fn validate_full_toml(extension_name: &str, config: &str) -> Result<(), String> mod tests { use super::*; + #[test] + fn gostc_requires_both_address_and_key() { + let mut config = GostcConfig::default(); + assert!(validate_gostc_config(&config).is_err()); + config.addr = "example.com".to_string(); + assert!(validate_gostc_config(&config).is_err()); + config.key = "client-key".to_string(); + assert!(validate_gostc_config(&config).is_ok()); + config.addr = " \t ".to_string(); + assert!(validate_gostc_config(&config).is_err()); + } + #[test] fn easyfrp_accepts_documented_control_protocols() { for scheme in ["ws", "wss", "http", "https", "txt"] { diff --git a/src/web/handlers/extensions.rs b/src/web/handlers/extensions.rs index e798ca71..b049f59e 100644 --- a/src/web/handlers/extensions.rs +++ b/src/web/handlers/extensions.rs @@ -9,9 +9,10 @@ use typeshare::typeshare; use crate::error::{AppError, Result}; use crate::extensions::{ validate_easyfrp_config, validate_easytier_config, validate_extension_config, - validate_frpc_config, EasyfrpConfig, EasyfrpInfo, EasytierConfig, EasytierConfigMode, - EasytierInfo, ExtensionId, ExtensionInfo, ExtensionLogs, ExtensionsStatus, FrpProxyType, - FrpcConfig, FrpcConfigMode, FrpcInfo, TtydConfig, TtydInfo, + validate_frpc_config, validate_gostc_config, EasyfrpConfig, EasyfrpInfo, EasytierConfig, + EasytierConfigMode, EasytierInfo, ExtensionId, ExtensionInfo, ExtensionLogs, ExtensionsStatus, + FrpProxyType, FrpcConfig, FrpcConfigMode, FrpcInfo, GostcConfig, GostcInfo, TtydConfig, + TtydInfo, }; use crate::state::AppState; @@ -29,6 +30,11 @@ pub async fn list_extensions(State(state): State>) -> Json, } +#[typeshare] +#[derive(Debug, Deserialize)] +pub struct GostcConfigUpdate { + pub enabled: Option, + pub addr: Option, + pub key: Option, + pub tls: Option, +} + #[typeshare] #[derive(Debug, Deserialize)] pub struct EasyfrpConfigUpdate { @@ -207,6 +222,54 @@ pub async fn update_ttyd_config( Ok(Json(new_config.extensions.ttyd.clone())) } +pub async fn update_gostc_config( + State(state): State>, + Json(req): Json, +) -> Result> { + let current_config = state.config.get(); + let was_enabled = current_config.extensions.gostc.enabled; + let mut next_gostc = current_config.extensions.gostc.clone(); + + if let Some(enabled) = req.enabled { + next_gostc.enabled = enabled; + } + if let Some(ref addr) = req.addr { + next_gostc.addr = addr.clone(); + } + if let Some(ref key) = req.key { + next_gostc.key = key.clone(); + } + if let Some(tls) = req.tls { + next_gostc.tls = tls; + } + + if next_gostc.enabled { + bad_request(validate_gostc_config(&next_gostc))?; + } + + state + .config + .update(|config| { + config.extensions.gostc = next_gostc.clone(); + }) + .await?; + + let new_config = state.config.get(); + let is_enabled = new_config.extensions.gostc.enabled; + + if was_enabled && !is_enabled { + state.extensions.stop(ExtensionId::Gostc).await.ok(); + } else if !was_enabled && is_enabled && state.extensions.check_available(ExtensionId::Gostc) { + state + .extensions + .start(ExtensionId::Gostc, &new_config.extensions) + .await + .ok(); + } + + Ok(Json(new_config.extensions.gostc.clone())) +} + pub async fn update_easyfrp_config( State(state): State>, Json(req): Json, diff --git a/src/web/routes.rs b/src/web/routes.rs index 9b4059d8..b502b314 100644 --- a/src/web/routes.rs +++ b/src/web/routes.rs @@ -252,6 +252,10 @@ pub fn create_router(state: Arc) -> Router { "/extensions/ttyd/config", patch(handlers::extensions::update_ttyd_config), ) + .route( + "/extensions/gostc/config", + patch(handlers::extensions::update_gostc_config), + ) .route( "/extensions/easyfrp/config", patch(handlers::extensions::update_easyfrp_config), diff --git a/web/src/api/config.ts b/web/src/api/config.ts index 23d13b06..9f496c7a 100644 --- a/web/src/api/config.ts +++ b/web/src/api/config.ts @@ -26,6 +26,8 @@ import type { ExtensionLogs, TtydConfig, TtydConfigUpdate, + GostcConfig, + GostcConfigUpdate, EasyfrpConfig, EasyfrpConfigUpdate, EasytierConfig, @@ -201,6 +203,12 @@ export const extensionsApi = { body: JSON.stringify(config), }), + updateGostc: (config: GostcConfigUpdate) => + request('/extensions/gostc/config', { + method: 'PATCH', + body: JSON.stringify(config), + }), + updateEasyfrp: (config: EasyfrpConfigUpdate) => request('/extensions/easyfrp/config', { method: 'PATCH', diff --git a/web/src/i18n/en-US.ts b/web/src/i18n/en-US.ts index 7ac2cef5..90d044e8 100644 --- a/web/src/i18n/en-US.ts +++ b/web/src/i18n/en-US.ts @@ -1128,6 +1128,17 @@ export default { openInNewTab: 'Open in New Tab', shell: 'Shell', }, + gostc: { + title: 'GOSTC NAT Traversal', + desc: 'NAT traversal via GOSTC', + addr: 'Server Address', + addrPlaceholder: 'Hostname or IP (required)', + addrRequired: 'Enter the GOSTC server address', + key: 'Client Key', + keyPlaceholder: 'Client key (required)', + keyRequired: 'Enter the GOSTC client key', + tls: 'Enable TLS', + }, easyfrp: { title: 'EasyFrp NAT Traversal', desc: 'Connect to a tunnel service with the EasyFrp client', diff --git a/web/src/i18n/zh-CN.ts b/web/src/i18n/zh-CN.ts index 91b88bf6..7c292fcc 100644 --- a/web/src/i18n/zh-CN.ts +++ b/web/src/i18n/zh-CN.ts @@ -1127,6 +1127,17 @@ export default { openInNewTab: '在新标签页打开', shell: 'Shell', }, + gostc: { + title: 'GOSTC 内网穿透', + desc: '通过 GOSTC 实现内网穿透', + addr: '服务器地址', + addrPlaceholder: '主机名或 IP(必填)', + addrRequired: '请填写 GOSTC 服务器地址', + key: '客户端密钥', + keyPlaceholder: '客户端密钥(必填)', + keyRequired: '请填写 GOSTC 客户端密钥', + tls: '启用 TLS', + }, easyfrp: { title: 'EasyFrp 内网穿透', desc: '使用 EasyFrp 客户端连接穿透服务', diff --git a/web/src/types/generated.ts b/web/src/types/generated.ts index 8c58bb95..e1470bbc 100644 --- a/web/src/types/generated.ts +++ b/web/src/types/generated.ts @@ -197,6 +197,13 @@ export interface TtydConfig { shell: string; } +export interface GostcConfig { + enabled: boolean; + addr: string; + key: string; + tls: boolean; +} + export interface EasyfrpConfig { enabled: boolean; control_urls: string[]; @@ -251,6 +258,7 @@ export interface FrpcConfig { export interface ExtensionsConfig { ttyd: TtydConfig; + gostc: GostcConfig; easyfrp: EasyfrpConfig; easytier: EasytierConfig; frpc: FrpcConfig; @@ -436,6 +444,7 @@ export interface ExtensionInfo { export enum ExtensionId { Ttyd = "ttyd", + Gostc = "gostc", Easyfrp = "easyfrp", Easytier = "easytier", Frpc = "frpc", @@ -452,6 +461,12 @@ export interface TtydInfo { config: TtydConfig; } +export interface GostcInfo { + available: boolean; + status: ExtensionStatus; + config: GostcConfig; +} + export interface EasyfrpInfo { available: boolean; status: ExtensionStatus; @@ -466,6 +481,7 @@ export interface FrpcInfo { export interface ExtensionsStatus { ttyd: TtydInfo; + gostc: GostcInfo; easyfrp: EasyfrpInfo; easytier: EasytierInfo; frpc: FrpcInfo; @@ -488,6 +504,13 @@ export interface FrpcConfigUpdate { custom_toml?: string; } +export interface GostcConfigUpdate { + enabled?: boolean; + addr?: string; + key?: string; + tls?: boolean; +} + export interface EasyfrpConfigUpdate { enabled?: boolean; control_urls?: string[]; diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue index 615a90a1..c207eb95 100644 --- a/web/src/views/SettingsView.vue +++ b/web/src/views/SettingsView.vue @@ -446,12 +446,14 @@ const extensions = ref(null) const extensionsLoading = ref(false) const extensionLogs = ref>({ ttyd: [], + gostc: [], easyfrp: [], easytier: [], frpc: [], }) const showLogs = ref>({ ttyd: false, + gostc: false, easyfrp: false, easytier: false, frpc: false, @@ -461,6 +463,7 @@ const showTerminalDialog = ref(false) const extConfig = ref({ ttyd: { enabled: false, shell: '/bin/bash' }, + gostc: { enabled: false, addr: '', key: '', tls: true }, easyfrp: { enabled: false, control_urls: [] as string[] }, easytier: { enabled: false, @@ -489,6 +492,12 @@ const extConfig = ref({ }, }) +const gostcValidationMessage = computed(() => { + if (!extConfig.value.gostc.addr.trim()) return t('extensions.gostc.addrRequired') + if (!extConfig.value.gostc.key) return t('extensions.gostc.keyRequired') + return '' +}) + const easyfrpUrlsText = computed({ get: () => extConfig.value.easyfrp.control_urls.join('\n'), set: (value: string) => { @@ -1771,6 +1780,11 @@ async function loadExtensions(only?: ExtensionConfigId) { enabled: ttyd.enabled, shell: ttyd.shell, } + if (!only || only === 'gostc') + extConfig.value.gostc = { + ...extensions.value.gostc.config, + key: extensions.value.gostc.config.key || '', + } if (!only || only === 'easyfrp') extConfig.value.easyfrp = { ...extensions.value.easyfrp.config } const easytier = extensions.value.easytier.config @@ -1809,11 +1823,12 @@ async function loadExtensions(only?: ExtensionConfigId) { } } -type ExtensionConfigId = 'ttyd' | 'easyfrp' | 'easytier' | 'frpc' +type ExtensionConfigId = 'ttyd' | 'gostc' | 'easyfrp' | 'easytier' | 'frpc' type ValidatedExtensionConfigId = Exclude const extensionBusy = ref>({ ttyd: false, + gostc: false, easyfrp: false, easytier: false, frpc: false, @@ -1855,6 +1870,8 @@ async function refreshExtensionLogs(id: ExtensionConfigId) { async function persistExtensionConfig(id: ExtensionConfigId) { if (id === 'ttyd') { await extensionsApi.updateTtyd(extConfig.value.ttyd) + } else if (id === 'gostc') { + await extensionsApi.updateGostc(extConfig.value.gostc) } else if (id === 'easyfrp') { await extensionsApi.updateEasyfrp(extConfig.value.easyfrp) } else if (id === 'easytier') { @@ -2084,7 +2101,9 @@ function showValidationError(message: string): boolean { function validateExtensionConfig(id: ValidatedExtensionConfigId): boolean { let message = '' - if (id === 'easyfrp') { + if (id === 'gostc') { + message = gostcValidationMessage.value + } else if (id === 'easyfrp') { message = easyfrpValidationMessage.value } else if (id === 'easytier') { message = easytierValidationMessage.value @@ -4747,6 +4766,162 @@ watch(isWindows, () => {
+ + {{ + t('extensions.gostc.title') + }} +
+
+ + +
+
+ +
+
+ +
+ {{ + t('extensions.binaryNotFound', { + path: isWindows ? 'gostc.exe' : '/usr/bin/gostc', + }) + }} +
+ +
+
+ {{