mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-28 08:20:49 +08:00
feat(extensions): 使用 EasyFrp 替换 gostc
将扩展配置、接口和设置页切换为 EasyFrp,支持按行填写多个客户端连接 URL,通过 easyfrpc control 启动并校验协议与密钥路径。 旧 gostc 配置不自动启用 EasyFrp,升级后需重新配置;启动日志不再输出命令参数,避免暴露连接信息。 完整 Docker 镜像包含 easyfrpc、easytier-core、easytier-cli 和 frpc,通过独立下载脚本及版本锁定文件校验下载摘要和二进制架构。
This commit is contained in:
@@ -9,7 +9,7 @@ use tokio::sync::RwLock;
|
||||
|
||||
use super::protected_config::ProtectedConfigFile;
|
||||
use super::types::*;
|
||||
use super::validation::{validate_easytier_config, validate_frpc_config, validate_gostc_config};
|
||||
use super::validation::{validate_easyfrp_config, validate_easytier_config, validate_frpc_config};
|
||||
use crate::events::EventBus;
|
||||
|
||||
const LOG_BUFFER_SIZE: usize = 200;
|
||||
@@ -82,8 +82,8 @@ 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()
|
||||
}
|
||||
ExtensionId::Easytier => {
|
||||
config.easytier.enabled && validate_easytier_config(&config.easytier).is_ok()
|
||||
@@ -144,12 +144,7 @@ impl ExtensionManager {
|
||||
|
||||
let launch = self.build_launch(id, config).await?;
|
||||
|
||||
tracing::info!(
|
||||
"Starting extension {}: {} {}",
|
||||
id,
|
||||
id.binary_path().display(),
|
||||
launch.args.join(" ")
|
||||
);
|
||||
tracing::info!("Starting extension {}: {}", id, id.binary_path().display());
|
||||
|
||||
let mut child = Command::new(id.binary_path())
|
||||
.args(&launch.args)
|
||||
@@ -272,17 +267,12 @@ 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()]);
|
||||
ExtensionId::Easyfrp => {
|
||||
let c = &config.easyfrp;
|
||||
validate_easyfrp_config(c)?;
|
||||
|
||||
let mut args = vec!["control".to_string()];
|
||||
args.extend(c.control_urls.iter().map(|url| url.trim().to_string()));
|
||||
args
|
||||
}
|
||||
|
||||
@@ -580,6 +570,27 @@ impl ExtensionManager {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn easyfrp_launch_uses_control_urls_as_separate_arguments() {
|
||||
let mut config = ExtensionsConfig::default();
|
||||
config.easyfrp.control_urls = vec![
|
||||
"wss://example.com/key-one".to_string(),
|
||||
"txt://example.net/key-two".to_string(),
|
||||
];
|
||||
let launch = ExtensionManager::new()
|
||||
.build_launch(ExtensionId::Easyfrp, &config)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
launch.args,
|
||||
[
|
||||
"control",
|
||||
"wss://example.com/key-one",
|
||||
"txt://example.net/key-two"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn easytier_launch_revalidates_full_configuration() {
|
||||
let config = EasytierConfig {
|
||||
|
||||
@@ -11,6 +11,6 @@ pub use manager::TTYD_SOCKET_PATH;
|
||||
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,
|
||||
validate_easyfrp_config, validate_easytier_config, validate_extension_config,
|
||||
validate_frpc_config,
|
||||
};
|
||||
|
||||
@@ -5,7 +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,7 +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,7 +8,7 @@ use super::software;
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ExtensionId {
|
||||
Ttyd,
|
||||
Gostc,
|
||||
Easyfrp,
|
||||
Easytier,
|
||||
Frpc,
|
||||
}
|
||||
@@ -19,7 +19,7 @@ impl ExtensionId {
|
||||
}
|
||||
|
||||
pub fn all() -> &'static [ExtensionId] {
|
||||
&[Self::Ttyd, Self::Gostc, Self::Easytier, Self::Frpc]
|
||||
&[Self::Ttyd, Self::Easyfrp, Self::Easytier, Self::Frpc]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,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,7 +40,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),
|
||||
_ => Err(format!("Unknown extension: {}", s)),
|
||||
@@ -81,25 +81,11 @@ impl Default for TtydConfig {
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct GostcConfig {
|
||||
pub struct EasyfrpConfig {
|
||||
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,
|
||||
}
|
||||
}
|
||||
pub control_urls: Vec<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -217,7 +203,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,
|
||||
}
|
||||
@@ -239,10 +225,10 @@ pub struct TtydInfo {
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GostcInfo {
|
||||
pub struct EasyfrpInfo {
|
||||
pub available: bool,
|
||||
pub status: ExtensionStatus,
|
||||
pub config: GostcConfig,
|
||||
pub config: EasyfrpConfig,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -265,7 +251,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,
|
||||
}
|
||||
@@ -279,7 +265,17 @@ pub struct ExtensionLogs {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{EasytierConfig, EasytierConfigMode};
|
||||
use super::{EasytierConfig, EasytierConfigMode, ExtensionsConfig};
|
||||
|
||||
#[test]
|
||||
fn replacing_legacy_gostc_requires_new_easyfrp_configuration() {
|
||||
let config: ExtensionsConfig = serde_json::from_str(
|
||||
r#"{"gostc":{"enabled":true,"addr":"old.example.com","key":"old-key","tls":true}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!config.easyfrp.enabled);
|
||||
assert!(config.easyfrp.control_urls.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_easytier_config_defaults_to_quick_mode() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use toml_edit::DocumentMut;
|
||||
|
||||
use super::types::{
|
||||
EasytierConfig, EasytierConfigMode, ExtensionId, ExtensionsConfig, FrpProxyType, FrpcConfig,
|
||||
FrpcConfigMode, GostcConfig,
|
||||
EasyfrpConfig, EasytierConfig, EasytierConfigMode, ExtensionId, ExtensionsConfig, FrpProxyType,
|
||||
FrpcConfig, FrpcConfigMode,
|
||||
};
|
||||
|
||||
pub(crate) fn validate_extension_config(
|
||||
@@ -11,15 +11,36 @@ 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());
|
||||
}
|
||||
for value in &config.control_urls {
|
||||
let value = value.trim();
|
||||
if value.chars().any(char::is_whitespace) {
|
||||
return Err("EasyFrp control URL must not contain whitespace".to_string());
|
||||
}
|
||||
let url =
|
||||
reqwest::Url::parse(value).map_err(|_| "EasyFrp control URL is invalid".to_string())?;
|
||||
if !matches!(url.scheme(), "ws" | "wss" | "http" | "https" | "txt")
|
||||
|| url.host_str().is_none()
|
||||
|| url.path().trim_matches('/').is_empty()
|
||||
|| !url.username().is_empty()
|
||||
|| url.password().is_some()
|
||||
|| url.fragment().is_some()
|
||||
{
|
||||
return Err(
|
||||
"EasyFrp requires a ws/wss/http/https/txt URL with a client key path".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn validate_easytier_config(config: &EasytierConfig) -> Result<(), String> {
|
||||
@@ -80,6 +101,36 @@ fn validate_full_toml(extension_name: &str, config: &str) -> Result<(), String>
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn easyfrp_accepts_documented_control_protocols() {
|
||||
for scheme in ["ws", "wss", "http", "https", "txt"] {
|
||||
let config = EasyfrpConfig {
|
||||
enabled: true,
|
||||
control_urls: vec![format!("{scheme}://example.com/client-key")],
|
||||
};
|
||||
assert!(validate_easyfrp_config(&config).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn easyfrp_rejects_missing_keys_and_non_connection_arguments() {
|
||||
for urls in [
|
||||
vec![],
|
||||
vec![""],
|
||||
vec!["--help"],
|
||||
vec!["wss://example.com/"],
|
||||
vec!["file:///tmp/config"],
|
||||
vec!["wss://example.com/key with spaces"],
|
||||
vec!["wss://example.com/key", "invalid"],
|
||||
] {
|
||||
let config = EasyfrpConfig {
|
||||
enabled: true,
|
||||
control_urls: urls.into_iter().map(str::to_string).collect(),
|
||||
};
|
||||
assert!(validate_easyfrp_config(&config).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_easytier_full_configuration() {
|
||||
let mut config = EasytierConfig {
|
||||
|
||||
@@ -8,10 +8,10 @@ use typeshare::typeshare;
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::extensions::{
|
||||
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,
|
||||
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,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
|
||||
@@ -29,10 +29,10 @@ 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,
|
||||
config: config.extensions.easyfrp.clone(),
|
||||
},
|
||||
easytier: EasytierInfo {
|
||||
available: mgr.check_available(ExtensionId::Easytier),
|
||||
@@ -134,11 +134,9 @@ pub struct TtydConfigUpdate {
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GostcConfigUpdate {
|
||||
pub struct EasyfrpConfigUpdate {
|
||||
pub enabled: Option<bool>,
|
||||
pub addr: Option<String>,
|
||||
pub key: Option<String>,
|
||||
pub tls: Option<bool>,
|
||||
pub control_urls: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -209,52 +207,46 @@ pub async fn update_ttyd_config(
|
||||
Ok(Json(new_config.extensions.ttyd.clone()))
|
||||
}
|
||||
|
||||
pub async fn update_gostc_config(
|
||||
pub async fn update_easyfrp_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<GostcConfigUpdate>,
|
||||
) -> Result<Json<GostcConfig>> {
|
||||
Json(req): Json<EasyfrpConfigUpdate>,
|
||||
) -> Result<Json<EasyfrpConfig>> {
|
||||
let current_config = state.config.get();
|
||||
let was_enabled = current_config.extensions.gostc.enabled;
|
||||
let mut next_gostc = current_config.extensions.gostc.clone();
|
||||
let was_enabled = current_config.extensions.easyfrp.enabled;
|
||||
let mut next_easyfrp = current_config.extensions.easyfrp.clone();
|
||||
|
||||
if let Some(enabled) = req.enabled {
|
||||
next_gostc.enabled = enabled;
|
||||
next_easyfrp.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 let Some(ref urls) = req.control_urls {
|
||||
next_easyfrp.control_urls = urls.iter().map(|url| url.trim().to_string()).collect();
|
||||
}
|
||||
|
||||
if next_gostc.enabled {
|
||||
bad_request(validate_gostc_config(&next_gostc))?;
|
||||
if next_easyfrp.enabled {
|
||||
bad_request(validate_easyfrp_config(&next_easyfrp))?;
|
||||
}
|
||||
|
||||
state
|
||||
.config
|
||||
.update(|config| {
|
||||
config.extensions.gostc = next_gostc.clone();
|
||||
config.extensions.easyfrp = next_easyfrp.clone();
|
||||
})
|
||||
.await?;
|
||||
|
||||
let new_config = state.config.get();
|
||||
let is_enabled = new_config.extensions.gostc.enabled;
|
||||
let is_enabled = new_config.extensions.easyfrp.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.stop(ExtensionId::Easyfrp).await.ok();
|
||||
} else if !was_enabled && is_enabled && state.extensions.check_available(ExtensionId::Easyfrp) {
|
||||
state
|
||||
.extensions
|
||||
.start(ExtensionId::Gostc, &new_config.extensions)
|
||||
.start(ExtensionId::Easyfrp, &new_config.extensions)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(Json(new_config.extensions.gostc.clone()))
|
||||
Ok(Json(new_config.extensions.easyfrp.clone()))
|
||||
}
|
||||
|
||||
pub async fn update_easytier_config(
|
||||
|
||||
@@ -259,8 +259,8 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
patch(handlers::extensions::update_ttyd_config),
|
||||
)
|
||||
.route(
|
||||
"/extensions/gostc/config",
|
||||
patch(handlers::extensions::update_gostc_config),
|
||||
"/extensions/easyfrp/config",
|
||||
patch(handlers::extensions::update_easyfrp_config),
|
||||
)
|
||||
.route(
|
||||
"/extensions/easytier/config",
|
||||
|
||||
Reference in New Issue
Block a user