mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-11-01 18:30:51 +08:00
feat(extensions): 恢复 GOSTC 支持并保留 EasyFrp
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<Self, Self::Err> {
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
@@ -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<Arc<AppState>>) -> Json<Extensi
|
||||
status: mgr.status(ExtensionId::Ttyd).await,
|
||||
config: config.extensions.ttyd.clone(),
|
||||
},
|
||||
gostc: GostcInfo {
|
||||
available: mgr.check_available(ExtensionId::Gostc),
|
||||
status: mgr.status(ExtensionId::Gostc).await,
|
||||
config: config.extensions.gostc.clone(),
|
||||
},
|
||||
easyfrp: EasyfrpInfo {
|
||||
available: mgr.check_available(ExtensionId::Easyfrp),
|
||||
status: mgr.status(ExtensionId::Easyfrp).await,
|
||||
@@ -132,6 +138,15 @@ pub struct TtydConfigUpdate {
|
||||
pub shell: Option<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GostcConfigUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub addr: Option<String>,
|
||||
pub key: Option<String>,
|
||||
pub tls: Option<bool>,
|
||||
}
|
||||
|
||||
#[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<Arc<AppState>>,
|
||||
Json(req): Json<GostcConfigUpdate>,
|
||||
) -> Result<Json<GostcConfig>> {
|
||||
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<Arc<AppState>>,
|
||||
Json(req): Json<EasyfrpConfigUpdate>,
|
||||
|
||||
@@ -252,6 +252,10 @@ pub fn create_router(state: Arc<AppState>) -> 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),
|
||||
|
||||
Reference in New Issue
Block a user