feat: 新增 frp 远程访问扩展

This commit is contained in:
mofeng-git
2026-06-13 16:05:34 +08:00
parent 4b65eebd5d
commit 5de7ecd4c5
12 changed files with 828 additions and 69 deletions

View File

@@ -4,12 +4,14 @@ use axum::{
};
use serde::Deserialize;
use std::sync::Arc;
use toml_edit::DocumentMut;
use typeshare::typeshare;
use crate::error::{AppError, Result};
use crate::extensions::{
EasytierConfig, EasytierInfo, ExtensionId, ExtensionInfo, ExtensionLogs, ExtensionsStatus,
GostcConfig, GostcInfo, TtydConfig, TtydInfo,
FrpProxyType, FrpcConfig, FrpcConfigMode, FrpcInfo, GostcConfig, GostcInfo, TtydConfig,
TtydInfo,
};
use crate::state::AppState;
@@ -34,6 +36,46 @@ fn validate_easytier_enabled(config: &EasytierConfig) -> Result<()> {
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> {
let config = state.config.get();
let mgr = &state.extensions;
@@ -54,6 +96,11 @@ pub async fn list_extensions(State(state): State<Arc<AppState>>) -> Json<Extensi
status: mgr.status(ExtensionId::Easytier).await,
config: config.extensions.easytier.clone(),
},
frpc: FrpcInfo {
available: mgr.check_available(ExtensionId::Frpc),
status: mgr.status(ExtensionId::Frpc).await,
config: config.extensions.frpc.clone(),
},
})
}
@@ -159,6 +206,25 @@ pub struct EasytierConfigUpdate {
pub virtual_ip: Option<String>,
}
#[typeshare]
#[derive(Debug, Deserialize)]
pub struct FrpcConfigUpdate {
pub enabled: Option<bool>,
pub config_mode: Option<FrpcConfigMode>,
pub proxy_name: Option<String>,
pub proxy_type: Option<FrpProxyType>,
pub server_addr: Option<String>,
pub server_port: Option<u16>,
pub token: Option<String>,
pub local_ip: Option<String>,
pub local_port: Option<u16>,
pub remote_port: Option<Option<u16>>,
pub custom_domain: Option<Option<String>>,
pub secret_key: Option<String>,
pub tls: Option<bool>,
pub custom_toml: Option<String>,
}
pub async fn update_ttyd_config(
State(state): State<Arc<AppState>>,
Json(req): Json<TtydConfigUpdate>,
@@ -295,3 +361,81 @@ pub async fn update_easytier_config(
Ok(Json(new_config.extensions.easytier.clone()))
}
pub async fn update_frpc_config(
State(state): State<Arc<AppState>>,
Json(req): Json<FrpcConfigUpdate>,
) -> Result<Json<FrpcConfig>> {
let current_config = state.config.get();
let was_enabled = current_config.extensions.frpc.enabled;
let mut next_frpc = current_config.extensions.frpc.clone();
if let Some(enabled) = req.enabled {
next_frpc.enabled = enabled;
}
if let Some(config_mode) = req.config_mode {
next_frpc.config_mode = config_mode;
}
if let Some(ref proxy_name) = req.proxy_name {
next_frpc.proxy_name = proxy_name.clone();
}
if let Some(proxy_type) = req.proxy_type {
next_frpc.proxy_type = proxy_type;
}
if let Some(ref addr) = req.server_addr {
next_frpc.server_addr = addr.clone();
}
if let Some(port) = req.server_port {
next_frpc.server_port = port;
}
if let Some(ref token) = req.token {
next_frpc.token = token.clone();
}
if let Some(ref local_ip) = req.local_ip {
next_frpc.local_ip = local_ip.clone();
}
if let Some(local_port) = req.local_port {
next_frpc.local_port = local_port;
}
if let Some(remote_port) = req.remote_port {
next_frpc.remote_port = remote_port;
}
if let Some(custom_domain) = req.custom_domain {
next_frpc.custom_domain = custom_domain;
}
if let Some(ref secret_key) = req.secret_key {
next_frpc.secret_key = secret_key.clone();
}
if let Some(tls) = req.tls {
next_frpc.tls = tls;
}
if let Some(ref custom_toml) = req.custom_toml {
next_frpc.custom_toml = custom_toml.clone();
}
if next_frpc.enabled || matches!(next_frpc.config_mode, FrpcConfigMode::Full) {
validate_frpc_enabled(&next_frpc)?;
}
state
.config
.update(|config| {
config.extensions.frpc = next_frpc.clone();
})
.await?;
let new_config = state.config.get();
let is_enabled = new_config.extensions.frpc.enabled;
if was_enabled && !is_enabled {
state.extensions.stop(ExtensionId::Frpc).await.ok();
} else if !was_enabled && is_enabled && state.extensions.check_available(ExtensionId::Frpc) {
state
.extensions
.start(ExtensionId::Frpc, &new_config.extensions)
.await
.ok();
}
Ok(Json(new_config.extensions.frpc.clone()))
}

View File

@@ -205,6 +205,10 @@ pub fn create_router(state: Arc<AppState>) -> Router {
"/extensions/easytier/config",
patch(handlers::extensions::update_easytier_config),
)
.route(
"/extensions/frpc/config",
patch(handlers::extensions::update_frpc_config),
)
// Terminal (ttyd) reverse proxy - WebSocket and HTTP
.route("/terminal", get(handlers::terminal::terminal_index))
.route("/terminal/", get(handlers::terminal::terminal_index))