mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 11:42:02 +08:00
feat: 新增 frp 远程访问扩展
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tempfile::TempDir;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::sync::RwLock;
|
||||
use toml_edit::DocumentMut;
|
||||
|
||||
use super::types::*;
|
||||
use crate::events::EventBus;
|
||||
|
||||
const LOG_BUFFER_SIZE: usize = 200;
|
||||
const LOG_BATCH_SIZE: usize = 16;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub const TTYD_SOCKET_PATH: &str = "/var/run/one-kvm/ttyd.sock";
|
||||
@@ -25,6 +27,12 @@ const TTYD_TCP_PORT: &str = "7681";
|
||||
struct ExtensionProcess {
|
||||
child: Child,
|
||||
logs: Arc<RwLock<VecDeque<String>>>,
|
||||
_temp_dir: Option<TempDir>,
|
||||
}
|
||||
|
||||
struct ExtensionLaunch {
|
||||
args: Vec<String>,
|
||||
temp_dir: Option<TempDir>,
|
||||
}
|
||||
|
||||
pub struct ExtensionManager {
|
||||
@@ -82,6 +90,17 @@ impl ExtensionManager {
|
||||
ExtensionId::Easytier => {
|
||||
config.easytier.enabled && !config.easytier.network_name.is_empty()
|
||||
}
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,17 +154,17 @@ impl ExtensionManager {
|
||||
|
||||
self.stop(id).await.ok();
|
||||
|
||||
let args = self.build_args(id, config).await?;
|
||||
let launch = self.build_launch(id, config).await?;
|
||||
|
||||
tracing::info!(
|
||||
"Starting extension {}: {} {}",
|
||||
id,
|
||||
id.binary_path().display(),
|
||||
Self::redact_args_for_log(&args).join(" ")
|
||||
launch.args.join(" ")
|
||||
);
|
||||
|
||||
let mut child = Command::new(id.binary_path())
|
||||
.args(&args)
|
||||
.args(&launch.args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
@@ -172,9 +191,21 @@ impl ExtensionManager {
|
||||
|
||||
let pid = child.id();
|
||||
tracing::info!("Extension {} started with PID {:?}", id, pid);
|
||||
Self::push_log(
|
||||
&logs,
|
||||
format!("Extension {} started with PID {:?}", id, pid),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut processes = self.processes.write().await;
|
||||
processes.insert(id, ExtensionProcess { child, logs });
|
||||
processes.insert(
|
||||
id,
|
||||
ExtensionProcess {
|
||||
child,
|
||||
logs,
|
||||
_temp_dir: launch.temp_dir,
|
||||
},
|
||||
);
|
||||
drop(processes);
|
||||
self.mark_ttyd_status_dirty(id).await;
|
||||
|
||||
@@ -212,22 +243,14 @@ impl ExtensionManager {
|
||||
) {
|
||||
let reader = BufReader::new(reader);
|
||||
let mut lines = reader.lines();
|
||||
let mut local_buffer = Vec::with_capacity(LOG_BATCH_SIZE);
|
||||
|
||||
loop {
|
||||
match lines.next_line().await {
|
||||
Ok(Some(line)) => {
|
||||
tracing::info!("[{}] {}", id, line);
|
||||
local_buffer.push(line);
|
||||
|
||||
if local_buffer.len() >= LOG_BATCH_SIZE {
|
||||
Self::flush_logs(&logs, &mut local_buffer).await;
|
||||
}
|
||||
Self::push_log(&logs, line).await;
|
||||
}
|
||||
Ok(None) => {
|
||||
if !local_buffer.is_empty() {
|
||||
Self::flush_logs(&logs, &mut local_buffer).await;
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -238,29 +261,27 @@ impl ExtensionManager {
|
||||
}
|
||||
}
|
||||
|
||||
async fn flush_logs(logs: &RwLock<VecDeque<String>>, buffer: &mut Vec<String>) {
|
||||
async fn push_log(logs: &RwLock<VecDeque<String>>, line: String) {
|
||||
let mut logs = logs.write().await;
|
||||
for line in buffer.drain(..) {
|
||||
if logs.len() >= LOG_BUFFER_SIZE {
|
||||
logs.pop_front();
|
||||
}
|
||||
logs.push_back(line);
|
||||
if logs.len() >= LOG_BUFFER_SIZE {
|
||||
logs.pop_front();
|
||||
}
|
||||
logs.push_back(line);
|
||||
}
|
||||
|
||||
async fn build_args(
|
||||
async fn build_launch(
|
||||
&self,
|
||||
id: ExtensionId,
|
||||
config: &ExtensionsConfig,
|
||||
) -> Result<Vec<String>, String> {
|
||||
match id {
|
||||
) -> Result<ExtensionLaunch, String> {
|
||||
let args = match id {
|
||||
ExtensionId::Ttyd => {
|
||||
let c = &config.ttyd;
|
||||
|
||||
let mut args = Self::build_ttyd_listen_args().await?;
|
||||
|
||||
args.push(c.shell.clone());
|
||||
Ok(args)
|
||||
args
|
||||
}
|
||||
|
||||
ExtensionId::Gostc => {
|
||||
@@ -282,7 +303,7 @@ impl ExtensionManager {
|
||||
|
||||
args.extend(["-key".to_string(), c.key.clone()]);
|
||||
|
||||
Ok(args)
|
||||
args
|
||||
}
|
||||
|
||||
ExtensionId::Easytier => {
|
||||
@@ -314,9 +335,153 @@ impl ExtensionManager {
|
||||
args.push("-d".to_string());
|
||||
}
|
||||
|
||||
Ok(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> {
|
||||
let config_text = match config.config_mode {
|
||||
FrpcConfigMode::Quick => Self::build_frpc_quick_toml(config)?,
|
||||
FrpcConfigMode::Full => Self::validate_frpc_full_toml(config)?.to_string(),
|
||||
};
|
||||
|
||||
let temp_dir =
|
||||
tempfile::tempdir().map_err(|e| format!("Failed to create FRPC config dir: {}", e))?;
|
||||
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 {
|
||||
args: vec!["-c".to_string(), Self::path_to_arg(&config_path)],
|
||||
temp_dir: Some(temp_dir),
|
||||
})
|
||||
}
|
||||
|
||||
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> {
|
||||
if config.proxy_name.trim().is_empty() {
|
||||
return Err("FRPC proxy name is required".into());
|
||||
}
|
||||
if config.server_addr.trim().is_empty() {
|
||||
return Err("FRPC server address is required".into());
|
||||
}
|
||||
if config.token.is_empty() {
|
||||
return Err("FRPC token is required".into());
|
||||
}
|
||||
if config.local_ip.trim().is_empty() {
|
||||
return Err("FRPC local IP is required".into());
|
||||
}
|
||||
|
||||
let proxy_type = match config.proxy_type {
|
||||
FrpProxyType::Tcp => "tcp",
|
||||
FrpProxyType::Udp => "udp",
|
||||
FrpProxyType::Http => "http",
|
||||
FrpProxyType::Https => "https",
|
||||
FrpProxyType::Stcp => "stcp",
|
||||
FrpProxyType::Sudp => "sudp",
|
||||
FrpProxyType::Xtcp => "xtcp",
|
||||
};
|
||||
|
||||
let mut toml = String::new();
|
||||
toml.push_str(&format!(
|
||||
"serverAddr = {}\nserverPort = {}\n\n",
|
||||
Self::toml_string(config.server_addr.trim()),
|
||||
config.server_port
|
||||
));
|
||||
toml.push_str("[auth]\n");
|
||||
toml.push_str("method = \"token\"\n");
|
||||
toml.push_str(&format!("token = {}\n\n", Self::toml_string(&config.token)));
|
||||
toml.push_str("[transport]\n");
|
||||
toml.push_str("protocol = \"tcp\"\n\n");
|
||||
toml.push_str("[transport.tls]\n");
|
||||
toml.push_str(&format!("enable = {}\n\n", config.tls));
|
||||
toml.push_str("[[proxies]]\n");
|
||||
toml.push_str(&format!(
|
||||
"name = {}\ntype = {}\nlocalIP = {}\nlocalPort = {}\n",
|
||||
Self::toml_string(config.proxy_name.trim()),
|
||||
Self::toml_string(proxy_type),
|
||||
Self::toml_string(config.local_ip.trim()),
|
||||
config.local_port
|
||||
));
|
||||
|
||||
match config.proxy_type {
|
||||
FrpProxyType::Tcp | FrpProxyType::Udp => {
|
||||
let remote_port = config.remote_port.ok_or_else(|| {
|
||||
"FRPC remote port is required for TCP/UDP proxies".to_string()
|
||||
})?;
|
||||
toml.push_str(&format!("remotePort = {}\n", remote_port));
|
||||
}
|
||||
FrpProxyType::Http | FrpProxyType::Https => {
|
||||
if let Some(domain) = config
|
||||
.custom_domain
|
||||
.as_ref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
toml.push_str(&format!(
|
||||
"customDomains = [{}]\n",
|
||||
Self::toml_string(domain)
|
||||
));
|
||||
}
|
||||
}
|
||||
FrpProxyType::Stcp | FrpProxyType::Sudp | FrpProxyType::Xtcp => {
|
||||
if !config.secret_key.is_empty() {
|
||||
toml.push_str(&format!(
|
||||
"secretKey = {}\n",
|
||||
Self::toml_string(&config.secret_key)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(toml)
|
||||
}
|
||||
|
||||
fn toml_string(value: &str) -> String {
|
||||
serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
|
||||
}
|
||||
|
||||
fn path_to_arg(path: &PathBuf) -> String {
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -356,34 +521,6 @@ impl ExtensionManager {
|
||||
])
|
||||
}
|
||||
|
||||
fn redact_args_for_log(args: &[String]) -> Vec<String> {
|
||||
let mut redacted = Vec::with_capacity(args.len());
|
||||
let mut redact_next = false;
|
||||
|
||||
for arg in args {
|
||||
if redact_next {
|
||||
redacted.push("****".to_string());
|
||||
redact_next = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if arg == "-key" || arg == "--key" {
|
||||
redacted.push(arg.clone());
|
||||
redact_next = true;
|
||||
} else if let Some((flag, _)) = arg.split_once('=') {
|
||||
if flag == "-key" || flag == "--key" {
|
||||
redacted.push(format!("{}=****", flag));
|
||||
} else {
|
||||
redacted.push(arg.clone());
|
||||
}
|
||||
} else {
|
||||
redacted.push(arg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
redacted
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn prepare_ttyd_socket() -> Result<(), String> {
|
||||
let socket_path = std::path::Path::new(TTYD_SOCKET_PATH);
|
||||
|
||||
@@ -7,6 +7,7 @@ pub fn default_binary_path(id: ExtensionId) -> &'static str {
|
||||
ExtensionId::Ttyd => "/usr/bin/ttyd",
|
||||
ExtensionId::Gostc => "/usr/bin/gostc",
|
||||
ExtensionId::Easytier => "/usr/bin/easytier-core",
|
||||
ExtensionId::Frpc => "/usr/bin/frpc",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ pub fn default_binary_path(id: ExtensionId) -> &'static str {
|
||||
ExtensionId::Ttyd => "ttyd.win32.exe",
|
||||
ExtensionId::Gostc => "gostc.exe",
|
||||
ExtensionId::Easytier => "easytier-core.exe",
|
||||
ExtensionId::Frpc => "frpc.exe",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ pub enum ExtensionId {
|
||||
Ttyd,
|
||||
Gostc,
|
||||
Easytier,
|
||||
Frpc,
|
||||
}
|
||||
|
||||
impl ExtensionId {
|
||||
@@ -18,7 +19,7 @@ impl ExtensionId {
|
||||
}
|
||||
|
||||
pub fn all() -> &'static [ExtensionId] {
|
||||
&[Self::Ttyd, Self::Gostc, Self::Easytier]
|
||||
&[Self::Ttyd, Self::Gostc, Self::Easytier, Self::Frpc]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +29,7 @@ impl std::fmt::Display for ExtensionId {
|
||||
Self::Ttyd => write!(f, "ttyd"),
|
||||
Self::Gostc => write!(f, "gostc"),
|
||||
Self::Easytier => write!(f, "easytier"),
|
||||
Self::Frpc => write!(f, "frpc"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +42,7 @@ impl std::str::FromStr for ExtensionId {
|
||||
"ttyd" => Ok(Self::Ttyd),
|
||||
"gostc" => Ok(Self::Gostc),
|
||||
"easytier" => Ok(Self::Easytier),
|
||||
"frpc" => Ok(Self::Frpc),
|
||||
_ => Err(format!("Unknown extension: {}", s)),
|
||||
}
|
||||
}
|
||||
@@ -114,6 +117,85 @@ pub struct EasytierConfig {
|
||||
pub virtual_ip: Option<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FrpProxyType {
|
||||
Tcp,
|
||||
Udp,
|
||||
Http,
|
||||
Https,
|
||||
Stcp,
|
||||
Sudp,
|
||||
Xtcp,
|
||||
}
|
||||
|
||||
impl Default for FrpProxyType {
|
||||
fn default() -> Self {
|
||||
Self::Tcp
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FrpcConfigMode {
|
||||
Quick,
|
||||
Full,
|
||||
}
|
||||
|
||||
impl Default for FrpcConfigMode {
|
||||
fn default() -> Self {
|
||||
Self::Quick
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct FrpcConfig {
|
||||
pub enabled: bool,
|
||||
pub config_mode: FrpcConfigMode,
|
||||
pub proxy_name: String,
|
||||
pub proxy_type: FrpProxyType,
|
||||
pub server_addr: String,
|
||||
pub server_port: u16,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub token: String,
|
||||
pub local_ip: String,
|
||||
pub local_port: u16,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub remote_port: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub custom_domain: Option<String>,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub secret_key: String,
|
||||
pub tls: bool,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub custom_toml: String,
|
||||
}
|
||||
|
||||
impl Default for FrpcConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
config_mode: FrpcConfigMode::Quick,
|
||||
proxy_name: String::new(),
|
||||
proxy_type: FrpProxyType::Tcp,
|
||||
server_addr: String::new(),
|
||||
server_port: 7000,
|
||||
token: String::new(),
|
||||
local_ip: "127.0.0.1".to_string(),
|
||||
local_port: 22,
|
||||
remote_port: None,
|
||||
custom_domain: None,
|
||||
secret_key: String::new(),
|
||||
tls: true,
|
||||
custom_toml: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
@@ -121,6 +203,7 @@ pub struct ExtensionsConfig {
|
||||
pub ttyd: TtydConfig,
|
||||
pub gostc: GostcConfig,
|
||||
pub easytier: EasytierConfig,
|
||||
pub frpc: FrpcConfig,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
@@ -154,12 +237,21 @@ pub struct EasytierInfo {
|
||||
pub config: EasytierConfig,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FrpcInfo {
|
||||
pub available: bool,
|
||||
pub status: ExtensionStatus,
|
||||
pub config: FrpcConfig,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExtensionsStatus {
|
||||
pub ttyd: TtydInfo,
|
||||
pub gostc: GostcInfo,
|
||||
pub easytier: EasytierInfo,
|
||||
pub frpc: FrpcInfo,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user