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:
@@ -72,7 +72,7 @@ Replace the version and architecture in the filename with your actual file name.
|
|||||||
Images:
|
Images:
|
||||||
|
|
||||||
- **one-kvm** — main app + ttyd
|
- **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:
|
Example:
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ sudo apt install ./one-kvm_0.x.x_<arch>.deb
|
|||||||
|
|
||||||
### 使用 Docker
|
### 使用 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
|
```bash
|
||||||
docker run --name one-kvm -itd \
|
docker run --name one-kvm -itd \
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ RUN if [ -n "$APT_MIRROR" ]; then \
|
|||||||
COPY --chmod=755 init.sh /init.sh
|
COPY --chmod=755 init.sh /init.sh
|
||||||
|
|
||||||
# Copy binaries (these are placed by the build script)
|
# 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 resources if they exist
|
||||||
COPY ventoy/ /etc/one-kvm/ventoy/
|
COPY ventoy/ /etc/one-kvm/ventoy/
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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 argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
echo " # Build for current platform and load locally"
|
echo " # Build for current platform and load locally"
|
||||||
echo " $0 --platform linux/arm64 --load"
|
echo " $0 --platform linux/arm64 --load"
|
||||||
echo ""
|
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 " $0 --variant full --platform linux/arm64 --load"
|
||||||
echo ""
|
echo ""
|
||||||
echo " # Build and push single platform"
|
echo " # Build and push single platform"
|
||||||
|
|||||||
@@ -1,5 +1,28 @@
|
|||||||
{
|
{
|
||||||
"tools": {
|
"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": {
|
"easyfrp": {
|
||||||
"version": "v3.1.1",
|
"version": "v3.1.1",
|
||||||
"binaries": [
|
"binaries": [
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ use tokio::sync::RwLock;
|
|||||||
|
|
||||||
use super::protected_config::ProtectedConfigFile;
|
use super::protected_config::ProtectedConfigFile;
|
||||||
use super::types::*;
|
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;
|
use crate::events::EventBus;
|
||||||
|
|
||||||
const LOG_BUFFER_SIZE: usize = 200;
|
const LOG_BUFFER_SIZE: usize = 200;
|
||||||
@@ -82,6 +84,9 @@ impl ExtensionManager {
|
|||||||
fn is_enabled_for_config(id: ExtensionId, config: &ExtensionsConfig) -> bool {
|
fn is_enabled_for_config(id: ExtensionId, config: &ExtensionsConfig) -> bool {
|
||||||
match id {
|
match id {
|
||||||
ExtensionId::Ttyd => config.ttyd.enabled,
|
ExtensionId::Ttyd => config.ttyd.enabled,
|
||||||
|
ExtensionId::Gostc => {
|
||||||
|
config.gostc.enabled && validate_gostc_config(&config.gostc).is_ok()
|
||||||
|
}
|
||||||
ExtensionId::Easyfrp => {
|
ExtensionId::Easyfrp => {
|
||||||
config.easyfrp.enabled && validate_easyfrp_config(&config.easyfrp).is_ok()
|
config.easyfrp.enabled && validate_easyfrp_config(&config.easyfrp).is_ok()
|
||||||
}
|
}
|
||||||
@@ -267,6 +272,20 @@ impl ExtensionManager {
|
|||||||
args
|
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 => {
|
ExtensionId::Easyfrp => {
|
||||||
let c = &config.easyfrp;
|
let c = &config.easyfrp;
|
||||||
validate_easyfrp_config(c)?;
|
validate_easyfrp_config(c)?;
|
||||||
@@ -570,6 +589,54 @@ impl ExtensionManager {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[tokio::test]
|
||||||
async fn easyfrp_launch_uses_control_urls_as_separate_arguments() {
|
async fn easyfrp_launch_uses_control_urls_as_separate_arguments() {
|
||||||
let mut config = ExtensionsConfig::default();
|
let mut config = ExtensionsConfig::default();
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ pub use manager::TTYD_TCP_ADDR;
|
|||||||
pub use types::*;
|
pub use types::*;
|
||||||
pub(crate) use validation::{
|
pub(crate) use validation::{
|
||||||
validate_easyfrp_config, validate_easytier_config, validate_extension_config,
|
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 {
|
pub fn default_binary_path(id: ExtensionId) -> &'static str {
|
||||||
match id {
|
match id {
|
||||||
ExtensionId::Ttyd => "/usr/bin/ttyd",
|
ExtensionId::Ttyd => "/usr/bin/ttyd",
|
||||||
|
ExtensionId::Gostc => "/usr/bin/gostc",
|
||||||
ExtensionId::Easyfrp => "/usr/bin/easyfrpc",
|
ExtensionId::Easyfrp => "/usr/bin/easyfrpc",
|
||||||
ExtensionId::Easytier => "/usr/bin/easytier-core",
|
ExtensionId::Easytier => "/usr/bin/easytier-core",
|
||||||
ExtensionId::Frpc => "/usr/bin/frpc",
|
ExtensionId::Frpc => "/usr/bin/frpc",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use super::ExtensionId;
|
|||||||
pub fn default_binary_path(id: ExtensionId) -> &'static str {
|
pub fn default_binary_path(id: ExtensionId) -> &'static str {
|
||||||
match id {
|
match id {
|
||||||
ExtensionId::Ttyd => "ttyd.win32.exe",
|
ExtensionId::Ttyd => "ttyd.win32.exe",
|
||||||
|
ExtensionId::Gostc => "gostc.exe",
|
||||||
ExtensionId::Easyfrp => "easyfrpc.exe",
|
ExtensionId::Easyfrp => "easyfrpc.exe",
|
||||||
ExtensionId::Easytier => "easytier-core.exe",
|
ExtensionId::Easytier => "easytier-core.exe",
|
||||||
ExtensionId::Frpc => "frpc.exe",
|
ExtensionId::Frpc => "frpc.exe",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use super::software;
|
|||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ExtensionId {
|
pub enum ExtensionId {
|
||||||
Ttyd,
|
Ttyd,
|
||||||
|
Gostc,
|
||||||
Easyfrp,
|
Easyfrp,
|
||||||
Easytier,
|
Easytier,
|
||||||
Frpc,
|
Frpc,
|
||||||
@@ -19,7 +20,13 @@ impl ExtensionId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn all() -> &'static [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 {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Ttyd => write!(f, "ttyd"),
|
Self::Ttyd => write!(f, "ttyd"),
|
||||||
|
Self::Gostc => write!(f, "gostc"),
|
||||||
Self::Easyfrp => write!(f, "easyfrp"),
|
Self::Easyfrp => write!(f, "easyfrp"),
|
||||||
Self::Easytier => write!(f, "easytier"),
|
Self::Easytier => write!(f, "easytier"),
|
||||||
Self::Frpc => write!(f, "frpc"),
|
Self::Frpc => write!(f, "frpc"),
|
||||||
@@ -40,6 +48,7 @@ impl std::str::FromStr for ExtensionId {
|
|||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.to_lowercase().as_str() {
|
match s.to_lowercase().as_str() {
|
||||||
"ttyd" => Ok(Self::Ttyd),
|
"ttyd" => Ok(Self::Ttyd),
|
||||||
|
"gostc" => Ok(Self::Gostc),
|
||||||
"easyfrp" => Ok(Self::Easyfrp),
|
"easyfrp" => Ok(Self::Easyfrp),
|
||||||
"easytier" => Ok(Self::Easytier),
|
"easytier" => Ok(Self::Easytier),
|
||||||
"frpc" => Ok(Self::Frpc),
|
"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]
|
#[typeshare]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -203,6 +234,7 @@ impl Default for FrpcConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct ExtensionsConfig {
|
pub struct ExtensionsConfig {
|
||||||
pub ttyd: TtydConfig,
|
pub ttyd: TtydConfig,
|
||||||
|
pub gostc: GostcConfig,
|
||||||
pub easyfrp: EasyfrpConfig,
|
pub easyfrp: EasyfrpConfig,
|
||||||
pub easytier: EasytierConfig,
|
pub easytier: EasytierConfig,
|
||||||
pub frpc: FrpcConfig,
|
pub frpc: FrpcConfig,
|
||||||
@@ -223,6 +255,14 @@ pub struct TtydInfo {
|
|||||||
pub config: TtydConfig,
|
pub config: TtydConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GostcInfo {
|
||||||
|
pub available: bool,
|
||||||
|
pub status: ExtensionStatus,
|
||||||
|
pub config: GostcConfig,
|
||||||
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct EasyfrpInfo {
|
pub struct EasyfrpInfo {
|
||||||
@@ -251,6 +291,7 @@ pub struct FrpcInfo {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ExtensionsStatus {
|
pub struct ExtensionsStatus {
|
||||||
pub ttyd: TtydInfo,
|
pub ttyd: TtydInfo,
|
||||||
|
pub gostc: GostcInfo,
|
||||||
pub easyfrp: EasyfrpInfo,
|
pub easyfrp: EasyfrpInfo,
|
||||||
pub easytier: EasytierInfo,
|
pub easytier: EasytierInfo,
|
||||||
pub frpc: FrpcInfo,
|
pub frpc: FrpcInfo,
|
||||||
@@ -268,11 +309,43 @@ mod tests {
|
|||||||
use super::{EasytierConfig, EasytierConfigMode, ExtensionsConfig};
|
use super::{EasytierConfig, EasytierConfigMode, ExtensionsConfig};
|
||||||
|
|
||||||
#[test]
|
#[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(
|
let config: ExtensionsConfig = serde_json::from_str(
|
||||||
r#"{"gostc":{"enabled":true,"addr":"old.example.com","key":"old-key","tls":true}}"#,
|
r#"{"gostc":{"enabled":true,"addr":"old.example.com","key":"old-key","tls":true}}"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.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.enabled);
|
||||||
assert!(config.easyfrp.control_urls.is_empty());
|
assert!(config.easyfrp.control_urls.is_empty());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use toml_edit::DocumentMut;
|
|||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
EasyfrpConfig, EasytierConfig, EasytierConfigMode, ExtensionId, ExtensionsConfig, FrpProxyType,
|
EasyfrpConfig, EasytierConfig, EasytierConfigMode, ExtensionId, ExtensionsConfig, FrpProxyType,
|
||||||
FrpcConfig, FrpcConfigMode,
|
FrpcConfig, FrpcConfigMode, GostcConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn validate_extension_config(
|
pub(crate) fn validate_extension_config(
|
||||||
@@ -11,12 +11,18 @@ pub(crate) fn validate_extension_config(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
match id {
|
match id {
|
||||||
ExtensionId::Ttyd => Ok(()),
|
ExtensionId::Ttyd => Ok(()),
|
||||||
|
ExtensionId::Gostc => validate_gostc_config(&config.gostc),
|
||||||
ExtensionId::Easyfrp => validate_easyfrp_config(&config.easyfrp),
|
ExtensionId::Easyfrp => validate_easyfrp_config(&config.easyfrp),
|
||||||
ExtensionId::Easytier => validate_easytier_config(&config.easytier),
|
ExtensionId::Easytier => validate_easytier_config(&config.easytier),
|
||||||
ExtensionId::Frpc => validate_frpc_config(&config.frpc),
|
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> {
|
pub(crate) fn validate_easyfrp_config(config: &EasyfrpConfig) -> Result<(), String> {
|
||||||
if config.control_urls.is_empty() {
|
if config.control_urls.is_empty() {
|
||||||
return Err("EasyFrp control URL is required".to_string());
|
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 {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn easyfrp_accepts_documented_control_protocols() {
|
fn easyfrp_accepts_documented_control_protocols() {
|
||||||
for scheme in ["ws", "wss", "http", "https", "txt"] {
|
for scheme in ["ws", "wss", "http", "https", "txt"] {
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ use typeshare::typeshare;
|
|||||||
use crate::error::{AppError, Result};
|
use crate::error::{AppError, Result};
|
||||||
use crate::extensions::{
|
use crate::extensions::{
|
||||||
validate_easyfrp_config, validate_easytier_config, validate_extension_config,
|
validate_easyfrp_config, validate_easytier_config, validate_extension_config,
|
||||||
validate_frpc_config, EasyfrpConfig, EasyfrpInfo, EasytierConfig, EasytierConfigMode,
|
validate_frpc_config, validate_gostc_config, EasyfrpConfig, EasyfrpInfo, EasytierConfig,
|
||||||
EasytierInfo, ExtensionId, ExtensionInfo, ExtensionLogs, ExtensionsStatus, FrpProxyType,
|
EasytierConfigMode, EasytierInfo, ExtensionId, ExtensionInfo, ExtensionLogs, ExtensionsStatus,
|
||||||
FrpcConfig, FrpcConfigMode, FrpcInfo, TtydConfig, TtydInfo,
|
FrpProxyType, FrpcConfig, FrpcConfigMode, FrpcInfo, GostcConfig, GostcInfo, TtydConfig,
|
||||||
|
TtydInfo,
|
||||||
};
|
};
|
||||||
use crate::state::AppState;
|
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,
|
status: mgr.status(ExtensionId::Ttyd).await,
|
||||||
config: config.extensions.ttyd.clone(),
|
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 {
|
easyfrp: EasyfrpInfo {
|
||||||
available: mgr.check_available(ExtensionId::Easyfrp),
|
available: mgr.check_available(ExtensionId::Easyfrp),
|
||||||
status: mgr.status(ExtensionId::Easyfrp).await,
|
status: mgr.status(ExtensionId::Easyfrp).await,
|
||||||
@@ -132,6 +138,15 @@ pub struct TtydConfigUpdate {
|
|||||||
pub shell: Option<String>,
|
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]
|
#[typeshare]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct EasyfrpConfigUpdate {
|
pub struct EasyfrpConfigUpdate {
|
||||||
@@ -207,6 +222,54 @@ pub async fn update_ttyd_config(
|
|||||||
Ok(Json(new_config.extensions.ttyd.clone()))
|
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(
|
pub async fn update_easyfrp_config(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(req): Json<EasyfrpConfigUpdate>,
|
Json(req): Json<EasyfrpConfigUpdate>,
|
||||||
|
|||||||
@@ -252,6 +252,10 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
|||||||
"/extensions/ttyd/config",
|
"/extensions/ttyd/config",
|
||||||
patch(handlers::extensions::update_ttyd_config),
|
patch(handlers::extensions::update_ttyd_config),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/extensions/gostc/config",
|
||||||
|
patch(handlers::extensions::update_gostc_config),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/extensions/easyfrp/config",
|
"/extensions/easyfrp/config",
|
||||||
patch(handlers::extensions::update_easyfrp_config),
|
patch(handlers::extensions::update_easyfrp_config),
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import type {
|
|||||||
ExtensionLogs,
|
ExtensionLogs,
|
||||||
TtydConfig,
|
TtydConfig,
|
||||||
TtydConfigUpdate,
|
TtydConfigUpdate,
|
||||||
|
GostcConfig,
|
||||||
|
GostcConfigUpdate,
|
||||||
EasyfrpConfig,
|
EasyfrpConfig,
|
||||||
EasyfrpConfigUpdate,
|
EasyfrpConfigUpdate,
|
||||||
EasytierConfig,
|
EasytierConfig,
|
||||||
@@ -201,6 +203,12 @@ export const extensionsApi = {
|
|||||||
body: JSON.stringify(config),
|
body: JSON.stringify(config),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
updateGostc: (config: GostcConfigUpdate) =>
|
||||||
|
request<GostcConfig>('/extensions/gostc/config', {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(config),
|
||||||
|
}),
|
||||||
|
|
||||||
updateEasyfrp: (config: EasyfrpConfigUpdate) =>
|
updateEasyfrp: (config: EasyfrpConfigUpdate) =>
|
||||||
request<EasyfrpConfig>('/extensions/easyfrp/config', {
|
request<EasyfrpConfig>('/extensions/easyfrp/config', {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
|||||||
@@ -1128,6 +1128,17 @@ export default {
|
|||||||
openInNewTab: 'Open in New Tab',
|
openInNewTab: 'Open in New Tab',
|
||||||
shell: 'Shell',
|
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: {
|
easyfrp: {
|
||||||
title: 'EasyFrp NAT Traversal',
|
title: 'EasyFrp NAT Traversal',
|
||||||
desc: 'Connect to a tunnel service with the EasyFrp client',
|
desc: 'Connect to a tunnel service with the EasyFrp client',
|
||||||
|
|||||||
@@ -1127,6 +1127,17 @@ export default {
|
|||||||
openInNewTab: '在新标签页打开',
|
openInNewTab: '在新标签页打开',
|
||||||
shell: 'Shell',
|
shell: 'Shell',
|
||||||
},
|
},
|
||||||
|
gostc: {
|
||||||
|
title: 'GOSTC 内网穿透',
|
||||||
|
desc: '通过 GOSTC 实现内网穿透',
|
||||||
|
addr: '服务器地址',
|
||||||
|
addrPlaceholder: '主机名或 IP(必填)',
|
||||||
|
addrRequired: '请填写 GOSTC 服务器地址',
|
||||||
|
key: '客户端密钥',
|
||||||
|
keyPlaceholder: '客户端密钥(必填)',
|
||||||
|
keyRequired: '请填写 GOSTC 客户端密钥',
|
||||||
|
tls: '启用 TLS',
|
||||||
|
},
|
||||||
easyfrp: {
|
easyfrp: {
|
||||||
title: 'EasyFrp 内网穿透',
|
title: 'EasyFrp 内网穿透',
|
||||||
desc: '使用 EasyFrp 客户端连接穿透服务',
|
desc: '使用 EasyFrp 客户端连接穿透服务',
|
||||||
|
|||||||
@@ -197,6 +197,13 @@ export interface TtydConfig {
|
|||||||
shell: string;
|
shell: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GostcConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
addr: string;
|
||||||
|
key: string;
|
||||||
|
tls: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EasyfrpConfig {
|
export interface EasyfrpConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
control_urls: string[];
|
control_urls: string[];
|
||||||
@@ -251,6 +258,7 @@ export interface FrpcConfig {
|
|||||||
|
|
||||||
export interface ExtensionsConfig {
|
export interface ExtensionsConfig {
|
||||||
ttyd: TtydConfig;
|
ttyd: TtydConfig;
|
||||||
|
gostc: GostcConfig;
|
||||||
easyfrp: EasyfrpConfig;
|
easyfrp: EasyfrpConfig;
|
||||||
easytier: EasytierConfig;
|
easytier: EasytierConfig;
|
||||||
frpc: FrpcConfig;
|
frpc: FrpcConfig;
|
||||||
@@ -436,6 +444,7 @@ export interface ExtensionInfo {
|
|||||||
|
|
||||||
export enum ExtensionId {
|
export enum ExtensionId {
|
||||||
Ttyd = "ttyd",
|
Ttyd = "ttyd",
|
||||||
|
Gostc = "gostc",
|
||||||
Easyfrp = "easyfrp",
|
Easyfrp = "easyfrp",
|
||||||
Easytier = "easytier",
|
Easytier = "easytier",
|
||||||
Frpc = "frpc",
|
Frpc = "frpc",
|
||||||
@@ -452,6 +461,12 @@ export interface TtydInfo {
|
|||||||
config: TtydConfig;
|
config: TtydConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GostcInfo {
|
||||||
|
available: boolean;
|
||||||
|
status: ExtensionStatus;
|
||||||
|
config: GostcConfig;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EasyfrpInfo {
|
export interface EasyfrpInfo {
|
||||||
available: boolean;
|
available: boolean;
|
||||||
status: ExtensionStatus;
|
status: ExtensionStatus;
|
||||||
@@ -466,6 +481,7 @@ export interface FrpcInfo {
|
|||||||
|
|
||||||
export interface ExtensionsStatus {
|
export interface ExtensionsStatus {
|
||||||
ttyd: TtydInfo;
|
ttyd: TtydInfo;
|
||||||
|
gostc: GostcInfo;
|
||||||
easyfrp: EasyfrpInfo;
|
easyfrp: EasyfrpInfo;
|
||||||
easytier: EasytierInfo;
|
easytier: EasytierInfo;
|
||||||
frpc: FrpcInfo;
|
frpc: FrpcInfo;
|
||||||
@@ -488,6 +504,13 @@ export interface FrpcConfigUpdate {
|
|||||||
custom_toml?: string;
|
custom_toml?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GostcConfigUpdate {
|
||||||
|
enabled?: boolean;
|
||||||
|
addr?: string;
|
||||||
|
key?: string;
|
||||||
|
tls?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EasyfrpConfigUpdate {
|
export interface EasyfrpConfigUpdate {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
control_urls?: string[];
|
control_urls?: string[];
|
||||||
|
|||||||
@@ -446,12 +446,14 @@ const extensions = ref<ExtensionsStatus | null>(null)
|
|||||||
const extensionsLoading = ref(false)
|
const extensionsLoading = ref(false)
|
||||||
const extensionLogs = ref<Record<string, string[]>>({
|
const extensionLogs = ref<Record<string, string[]>>({
|
||||||
ttyd: [],
|
ttyd: [],
|
||||||
|
gostc: [],
|
||||||
easyfrp: [],
|
easyfrp: [],
|
||||||
easytier: [],
|
easytier: [],
|
||||||
frpc: [],
|
frpc: [],
|
||||||
})
|
})
|
||||||
const showLogs = ref<Record<string, boolean>>({
|
const showLogs = ref<Record<string, boolean>>({
|
||||||
ttyd: false,
|
ttyd: false,
|
||||||
|
gostc: false,
|
||||||
easyfrp: false,
|
easyfrp: false,
|
||||||
easytier: false,
|
easytier: false,
|
||||||
frpc: false,
|
frpc: false,
|
||||||
@@ -461,6 +463,7 @@ const showTerminalDialog = ref(false)
|
|||||||
|
|
||||||
const extConfig = ref({
|
const extConfig = ref({
|
||||||
ttyd: { enabled: false, shell: '/bin/bash' },
|
ttyd: { enabled: false, shell: '/bin/bash' },
|
||||||
|
gostc: { enabled: false, addr: '', key: '', tls: true },
|
||||||
easyfrp: { enabled: false, control_urls: [] as string[] },
|
easyfrp: { enabled: false, control_urls: [] as string[] },
|
||||||
easytier: {
|
easytier: {
|
||||||
enabled: false,
|
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({
|
const easyfrpUrlsText = computed({
|
||||||
get: () => extConfig.value.easyfrp.control_urls.join('\n'),
|
get: () => extConfig.value.easyfrp.control_urls.join('\n'),
|
||||||
set: (value: string) => {
|
set: (value: string) => {
|
||||||
@@ -1771,6 +1780,11 @@ async function loadExtensions(only?: ExtensionConfigId) {
|
|||||||
enabled: ttyd.enabled,
|
enabled: ttyd.enabled,
|
||||||
shell: ttyd.shell,
|
shell: ttyd.shell,
|
||||||
}
|
}
|
||||||
|
if (!only || only === 'gostc')
|
||||||
|
extConfig.value.gostc = {
|
||||||
|
...extensions.value.gostc.config,
|
||||||
|
key: extensions.value.gostc.config.key || '',
|
||||||
|
}
|
||||||
if (!only || only === 'easyfrp')
|
if (!only || only === 'easyfrp')
|
||||||
extConfig.value.easyfrp = { ...extensions.value.easyfrp.config }
|
extConfig.value.easyfrp = { ...extensions.value.easyfrp.config }
|
||||||
const easytier = extensions.value.easytier.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'>
|
type ValidatedExtensionConfigId = Exclude<ExtensionConfigId, 'ttyd'>
|
||||||
|
|
||||||
const extensionBusy = ref<Record<ExtensionConfigId, boolean>>({
|
const extensionBusy = ref<Record<ExtensionConfigId, boolean>>({
|
||||||
ttyd: false,
|
ttyd: false,
|
||||||
|
gostc: false,
|
||||||
easyfrp: false,
|
easyfrp: false,
|
||||||
easytier: false,
|
easytier: false,
|
||||||
frpc: false,
|
frpc: false,
|
||||||
@@ -1855,6 +1870,8 @@ async function refreshExtensionLogs(id: ExtensionConfigId) {
|
|||||||
async function persistExtensionConfig(id: ExtensionConfigId) {
|
async function persistExtensionConfig(id: ExtensionConfigId) {
|
||||||
if (id === 'ttyd') {
|
if (id === 'ttyd') {
|
||||||
await extensionsApi.updateTtyd(extConfig.value.ttyd)
|
await extensionsApi.updateTtyd(extConfig.value.ttyd)
|
||||||
|
} else if (id === 'gostc') {
|
||||||
|
await extensionsApi.updateGostc(extConfig.value.gostc)
|
||||||
} else if (id === 'easyfrp') {
|
} else if (id === 'easyfrp') {
|
||||||
await extensionsApi.updateEasyfrp(extConfig.value.easyfrp)
|
await extensionsApi.updateEasyfrp(extConfig.value.easyfrp)
|
||||||
} else if (id === 'easytier') {
|
} else if (id === 'easytier') {
|
||||||
@@ -2084,7 +2101,9 @@ function showValidationError(message: string): boolean {
|
|||||||
|
|
||||||
function validateExtensionConfig(id: ValidatedExtensionConfigId): boolean {
|
function validateExtensionConfig(id: ValidatedExtensionConfigId): boolean {
|
||||||
let message = ''
|
let message = ''
|
||||||
if (id === 'easyfrp') {
|
if (id === 'gostc') {
|
||||||
|
message = gostcValidationMessage.value
|
||||||
|
} else if (id === 'easyfrp') {
|
||||||
message = easyfrpValidationMessage.value
|
message = easyfrpValidationMessage.value
|
||||||
} else if (id === 'easytier') {
|
} else if (id === 'easytier') {
|
||||||
message = easytierValidationMessage.value
|
message = easytierValidationMessage.value
|
||||||
@@ -4747,6 +4766,162 @@ watch(isWindows, () => {
|
|||||||
|
|
||||||
<!-- Remote Access Section -->
|
<!-- Remote Access Section -->
|
||||||
<div v-show="activeSection === 'ext-remote-access'" class="settings-section-services">
|
<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>
|
<Card>
|
||||||
<CardHeader class="settings-card-heading"
|
<CardHeader class="settings-card-heading"
|
||||||
><CardTitle :help="t('extensions.easyfrp.desc')">{{
|
><CardTitle :help="t('extensions.easyfrp.desc')">{{
|
||||||
|
|||||||
Reference in New Issue
Block a user