mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-11-01 18:30:51 +08:00
Merge pull request #326 from mofeng-git/dev
feat(extensions): 恢复 GOSTC 支持并保留 EasyFrp
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ sudo apt install ./one-kvm_0.x.x_<arch>.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 \
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<GostcConfig>('/extensions/gostc/config', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
|
||||
updateEasyfrp: (config: EasyfrpConfigUpdate) =>
|
||||
request<EasyfrpConfig>('/extensions/easyfrp/config', {
|
||||
method: 'PATCH',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 客户端连接穿透服务',
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -446,12 +446,14 @@ const extensions = ref<ExtensionsStatus | null>(null)
|
||||
const extensionsLoading = ref(false)
|
||||
const extensionLogs = ref<Record<string, string[]>>({
|
||||
ttyd: [],
|
||||
gostc: [],
|
||||
easyfrp: [],
|
||||
easytier: [],
|
||||
frpc: [],
|
||||
})
|
||||
const showLogs = ref<Record<string, boolean>>({
|
||||
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<ExtensionConfigId, 'ttyd'>
|
||||
|
||||
const extensionBusy = ref<Record<ExtensionConfigId, boolean>>({
|
||||
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, () => {
|
||||
|
||||
<!-- Remote Access Section -->
|
||||
<div v-show="activeSection === 'ext-remote-access'" class="settings-section-services">
|
||||
<Card>
|
||||
<CardHeader class="settings-card-heading"
|
||||
><CardTitle :help="t('extensions.gostc.desc')">{{
|
||||
t('extensions.gostc.title')
|
||||
}}</CardTitle>
|
||||
<div class="settings-card-actions">
|
||||
<div class="settings-service-actions" v-if="extensions?.gostc?.available">
|
||||
<Button
|
||||
variant="outline"
|
||||
:aria-label="t('extensions.start') + ' ' + t('extensions.gostc.title')"
|
||||
:disabled="
|
||||
extensionsLoading ||
|
||||
!!gostcValidationMessage ||
|
||||
isExtRunning(extensions?.gostc?.status) ||
|
||||
extensionBusy.gostc
|
||||
"
|
||||
size="sm"
|
||||
@click="startExtension('gostc')"
|
||||
>
|
||||
<Play class="size-4 mr-1" />
|
||||
{{ t('extensions.start') }}
|
||||
</Button>
|
||||
<Button
|
||||
:aria-label="t('extensions.stop') + ' ' + t('extensions.gostc.title')"
|
||||
:disabled="
|
||||
extensionsLoading ||
|
||||
!isExtRunning(extensions?.gostc?.status) ||
|
||||
extensionBusy.gostc
|
||||
"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="stopExtension('gostc')"
|
||||
>
|
||||
<Square class="size-4 mr-1" />
|
||||
{{ t('extensions.stop') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="settings-save-action" v-if="extensions?.gostc?.available">
|
||||
<Button
|
||||
:disabled="
|
||||
loading ||
|
||||
isExtRunning(extensions?.gostc?.status) ||
|
||||
extensionBusy.gostc
|
||||
"
|
||||
@click="saveExtensionConfig('gostc')"
|
||||
>
|
||||
<Loader2 v-if="extensionBusy.gostc" class="size-4 mr-2 animate-spin" /><Save v-else class="size-4 mr-2" />{{ t('common.save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div></CardHeader
|
||||
>
|
||||
<CardContent class="space-y-4">
|
||||
<div
|
||||
v-if="!extensions?.gostc?.available"
|
||||
class="text-sm text-muted-foreground bg-muted p-3 rounded-md"
|
||||
>
|
||||
{{
|
||||
t('extensions.binaryNotFound', {
|
||||
path: isWindows ? 'gostc.exe' : '/usr/bin/gostc',
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<template v-else>
|
||||
<!-- Status and controls -->
|
||||
|
||||
<!-- Config -->
|
||||
<div class="settings-field-list grid gap-4">
|
||||
<div class="settings-toggle flex items-center justify-between">
|
||||
<Label>{{ t('extensions.autoStart') }}</Label>
|
||||
<Switch
|
||||
v-model="extConfig.gostc.enabled"
|
||||
:disabled="isExtRunning(extensions?.gostc?.status)"
|
||||
/>
|
||||
</div>
|
||||
<div class="settings-field grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.gostc.addr') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<Input
|
||||
v-model="extConfig.gostc.addr"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:placeholder="t('extensions.gostc.addrPlaceholder')"
|
||||
:disabled="isExtRunning(extensions?.gostc?.status)"
|
||||
/>
|
||||
<p
|
||||
v-if="extConfig.gostc.enabled && !extConfig.gostc.addr.trim()"
|
||||
class="text-xs text-destructive"
|
||||
>
|
||||
{{ t('extensions.gostc.addrRequired') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||
<Label class="sm:text-right">{{ t('extensions.gostc.key') }}</Label>
|
||||
<div class="sm:col-span-3 space-y-1">
|
||||
<Input
|
||||
v-model="extConfig.gostc.key"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
:placeholder="t('extensions.gostc.keyPlaceholder')"
|
||||
:disabled="isExtRunning(extensions?.gostc?.status)"
|
||||
/>
|
||||
<p
|
||||
v-if="extConfig.gostc.enabled && !extConfig.gostc.key"
|
||||
class="text-xs text-destructive"
|
||||
>
|
||||
{{ t('extensions.gostc.keyRequired') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-toggle flex items-center justify-between">
|
||||
<Label>{{ t('extensions.gostc.tls') }}</Label>
|
||||
<Switch
|
||||
v-model="extConfig.gostc.tls"
|
||||
:disabled="isExtRunning(extensions?.gostc?.status)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Logs -->
|
||||
<div class="space-y-2">
|
||||
<Collapsible
|
||||
v-model:open="showLogs.gostc"
|
||||
@update:open="(open) => open && refreshExtensionLogs('gostc')"
|
||||
>
|
||||
<CollapsibleTrigger as-child
|
||||
><Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="gap-2 text-muted-foreground"
|
||||
><ChevronRight
|
||||
:class="[
|
||||
'size-4 transition-transform',
|
||||
showLogs.gostc ? 'rotate-90' : '',
|
||||
]"
|
||||
/>{{ t('extensions.viewLogs') }}</Button
|
||||
></CollapsibleTrigger
|
||||
>
|
||||
<CollapsibleContent class="space-y-2 pt-2">
|
||||
<pre
|
||||
class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono"
|
||||
>{{
|
||||
(extensionLogs.gostc || []).join('\n') || t('extensions.noLogs')
|
||||
}}</pre
|
||||
>
|
||||
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('gostc')">
|
||||
<RefreshCw class="size-3 mr-1" />
|
||||
{{ t('common.refresh') }}
|
||||
</Button>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</template>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader class="settings-card-heading"
|
||||
><CardTitle :help="t('extensions.easyfrp.desc')">{{
|
||||
|
||||
Reference in New Issue
Block a user