mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-28 08:20:49 +08:00
feat(extensions): 使用 EasyFrp 替换 gostc
将扩展配置、接口和设置页切换为 EasyFrp,支持按行填写多个客户端连接 URL,通过 easyfrpc control 启动并校验协议与密钥路径。 旧 gostc 配置不自动启用 EasyFrp,升级后需重新配置;启动日志不再输出命令参数,避免暴露连接信息。 完整 Docker 镜像包含 easyfrpc、easytier-core、easytier-cli 和 frpc,通过独立下载脚本及版本锁定文件校验下载摘要和二进制架构。
This commit is contained in:
@@ -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 gostc easytier-core /usr/bin/
|
COPY --chmod=755 one-kvm ttyd 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/
|
||||||
|
|||||||
68
build/download-remote-tools.py
Normal file
68
build/download-remote-tools.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fetch the pinned EasyFrp, EasyTier and frp clients for full Docker images."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import urllib.request
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
|
def install(platform, output):
|
||||||
|
architecture = {'linux/amd64': 'x86_64', 'linux/arm64': 'aarch64',
|
||||||
|
'linux/arm/v7': 'arm'}[platform]
|
||||||
|
lock = json.loads(Path(__file__).with_name('remote-access.lock.json').read_text())
|
||||||
|
output.mkdir(parents=True, exist_ok=True)
|
||||||
|
expected = {'arm': (1, 40), 'aarch64': (2, 183), 'x86_64': (2, 62)}[architecture]
|
||||||
|
opener = urllib.request.build_opener(
|
||||||
|
urllib.request.ProxyHandler({'http': os.environ['CURL_PROXY'], 'https': os.environ['CURL_PROXY']})
|
||||||
|
) if os.environ.get('CURL_PROXY') else urllib.request.build_opener()
|
||||||
|
# Validate the entire download before replacing any staged executable.
|
||||||
|
with tempfile.TemporaryDirectory(prefix='one-kvm-remote-tools-') as temporary:
|
||||||
|
staging = Path(temporary)
|
||||||
|
for spec in lock['tools'].values():
|
||||||
|
asset = spec['assets'][architecture]
|
||||||
|
archive = staging / asset['archive']
|
||||||
|
prefix = os.environ.get('GHPROXY_PREFIX', '').rstrip('/')
|
||||||
|
url = prefix + '/' + asset['url'] if prefix else asset['url']
|
||||||
|
with opener.open(url, timeout=120) as response:
|
||||||
|
with archive.open('wb') as stream:
|
||||||
|
shutil.copyfileobj(response, stream)
|
||||||
|
if hashlib.sha256(archive.read_bytes()).hexdigest() != asset['sha256']:
|
||||||
|
raise ValueError('release checksum differs: ' + archive.name)
|
||||||
|
for name in spec['binaries']:
|
||||||
|
if zipfile.is_zipfile(archive):
|
||||||
|
with zipfile.ZipFile(archive) as source:
|
||||||
|
matches = [item for item in source.infolist()
|
||||||
|
if not item.is_dir() and PurePosixPath(item.filename).name == name]
|
||||||
|
if len(matches) != 1:
|
||||||
|
raise ValueError('expected one executable: ' + name)
|
||||||
|
data = source.read(matches[0])
|
||||||
|
else:
|
||||||
|
with tarfile.open(archive, 'r:gz') as source:
|
||||||
|
matches = [item for item in source if item.isfile() and PurePosixPath(item.name).name == name]
|
||||||
|
if len(matches) != 1:
|
||||||
|
raise ValueError('expected one executable: ' + name)
|
||||||
|
data = source.extractfile(matches[0]).read()
|
||||||
|
if (data[:4] != b'\x7fELF' or data[5] != 1
|
||||||
|
or (data[4], struct.unpack('<H', data[18:20])[0]) != expected):
|
||||||
|
raise ValueError('executable architecture differs: ' + name)
|
||||||
|
path = staging / name
|
||||||
|
path.write_bytes(data)
|
||||||
|
path.chmod(0o755)
|
||||||
|
for spec in lock['tools'].values():
|
||||||
|
for name in spec['binaries']:
|
||||||
|
shutil.copy2(staging / name, output / name)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('--platform', required=True, choices=('linux/amd64', 'linux/arm64', 'linux/arm/v7'))
|
||||||
|
parser.add_argument('--output', required=True, type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
install(args.platform, args.output)
|
||||||
@@ -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 gostc + easytier)"
|
echo " # Build full image (includes 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"
|
||||||
@@ -184,21 +184,12 @@ get_tool_urls() {
|
|||||||
case "$platform" in
|
case "$platform" in
|
||||||
"linux/amd64")
|
"linux/amd64")
|
||||||
TTYD_URL="https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64"
|
TTYD_URL="https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64"
|
||||||
GOSTC_URL="https://github.com/SianHH/gostc-open/releases/download/v2.0.9/gostc_linux_amd64_v1.tar.gz"
|
|
||||||
EASYTIER_URL="https://github.com/EasyTier/EasyTier/releases/download/v2.4.5/easytier-linux-x86_64-v2.4.5.zip"
|
|
||||||
EASYTIER_DIR="easytier-linux-x86_64"
|
|
||||||
;;
|
;;
|
||||||
"linux/arm64")
|
"linux/arm64")
|
||||||
TTYD_URL="https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.aarch64"
|
TTYD_URL="https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.aarch64"
|
||||||
GOSTC_URL="https://github.com/SianHH/gostc-open/releases/download/v2.0.9/gostc_linux_arm64_v8.0.tar.gz"
|
|
||||||
EASYTIER_URL="https://github.com/EasyTier/EasyTier/releases/download/v2.4.5/easytier-linux-aarch64-v2.4.5.zip"
|
|
||||||
EASYTIER_DIR="easytier-linux-aarch64"
|
|
||||||
;;
|
;;
|
||||||
"linux/arm/v7")
|
"linux/arm/v7")
|
||||||
TTYD_URL="https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.armhf"
|
TTYD_URL="https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.armhf"
|
||||||
GOSTC_URL="https://github.com/SianHH/gostc-open/releases/download/v2.0.9/gostc_linux_arm_7.tar.gz"
|
|
||||||
EASYTIER_URL="https://github.com/EasyTier/EasyTier/releases/download/v2.4.5/easytier-linux-armv7hf-v2.4.5.zip"
|
|
||||||
EASYTIER_DIR="easytier-linux-armv7hf"
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -231,22 +222,9 @@ download_tools() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$INCLUDE_THIRD_PARTY" = true ]; then
|
if [ "$INCLUDE_THIRD_PARTY" = true ]; then
|
||||||
# gostc
|
python3 "$PROJECT_ROOT/build/download-remote-tools.py" \
|
||||||
if [ ! -f "$staging/gostc" ]; then
|
--platform "$platform" --output "$staging"
|
||||||
dl "$GOSTC_URL" /tmp/gostc.tar.gz
|
|
||||||
tar -xzf /tmp/gostc.tar.gz -C "$staging"
|
|
||||||
chmod +x "$staging/gostc"
|
|
||||||
rm /tmp/gostc.tar.gz
|
|
||||||
fi
|
|
||||||
|
|
||||||
# easytier
|
|
||||||
if [ ! -f "$staging/easytier-core" ]; then
|
|
||||||
dl "$EASYTIER_URL" /tmp/easytier.zip
|
|
||||||
unzip -o /tmp/easytier.zip -d /tmp/easytier
|
|
||||||
cp "/tmp/easytier/$EASYTIER_DIR/easytier-core" "$staging/easytier-core"
|
|
||||||
chmod +x "$staging/easytier-core"
|
|
||||||
rm -rf /tmp/easytier.zip /tmp/easytier
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
74
build/remote-access.lock.json
Normal file
74
build/remote-access.lock.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"easyfrp": {
|
||||||
|
"version": "v3.1.1",
|
||||||
|
"binaries": [
|
||||||
|
"easyfrpc"
|
||||||
|
],
|
||||||
|
"assets": {
|
||||||
|
"arm": {
|
||||||
|
"archive": "easyfrpc_linux_arm_7.tar.gz",
|
||||||
|
"url": "https://github.com/SianHH/easyfrp/releases/download/v3.1.1/easyfrpc_linux_arm_7.tar.gz",
|
||||||
|
"sha256": "98fa5c95a01295b59043d2869251a63249d286813b66544389f80d6fa3b07ae6"
|
||||||
|
},
|
||||||
|
"aarch64": {
|
||||||
|
"archive": "easyfrpc_linux_arm64_v8.0.tar.gz",
|
||||||
|
"url": "https://github.com/SianHH/easyfrp/releases/download/v3.1.1/easyfrpc_linux_arm64_v8.0.tar.gz",
|
||||||
|
"sha256": "cbd6c21fcd4d1921ee86879a4e90f1abffeb49324cbf8b31b1fc4581523b34d9"
|
||||||
|
},
|
||||||
|
"x86_64": {
|
||||||
|
"archive": "easyfrpc_linux_amd64_v1.tar.gz",
|
||||||
|
"url": "https://github.com/SianHH/easyfrp/releases/download/v3.1.1/easyfrpc_linux_amd64_v1.tar.gz",
|
||||||
|
"sha256": "e744618a57305c3a6a866cc2e08ca09da22dad1cd1a4aa5216ab10ebf7ba16ec"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"easytier": {
|
||||||
|
"version": "v2.6.4",
|
||||||
|
"binaries": [
|
||||||
|
"easytier-core",
|
||||||
|
"easytier-cli"
|
||||||
|
],
|
||||||
|
"assets": {
|
||||||
|
"arm": {
|
||||||
|
"archive": "easytier-linux-armv7hf-v2.6.4.zip",
|
||||||
|
"url": "https://github.com/EasyTier/EasyTier/releases/download/v2.6.4/easytier-linux-armv7hf-v2.6.4.zip",
|
||||||
|
"sha256": "af0186ce95ffbe90b0e9dc8df0e9a01563f59e94ea52212651950c34dc35ac37"
|
||||||
|
},
|
||||||
|
"aarch64": {
|
||||||
|
"archive": "easytier-linux-aarch64-v2.6.4.zip",
|
||||||
|
"url": "https://github.com/EasyTier/EasyTier/releases/download/v2.6.4/easytier-linux-aarch64-v2.6.4.zip",
|
||||||
|
"sha256": "f533ec25a7ea714e09f645615012200278058525795cc3bb690ff011aec1a70f"
|
||||||
|
},
|
||||||
|
"x86_64": {
|
||||||
|
"archive": "easytier-linux-x86_64-v2.6.4.zip",
|
||||||
|
"url": "https://github.com/EasyTier/EasyTier/releases/download/v2.6.4/easytier-linux-x86_64-v2.6.4.zip",
|
||||||
|
"sha256": "61b659eaedba658fa66fe47d17e1426cdd77e5d02fa15fed447bb4357c09dfd6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"frp": {
|
||||||
|
"version": "v0.71.1",
|
||||||
|
"binaries": [
|
||||||
|
"frpc"
|
||||||
|
],
|
||||||
|
"assets": {
|
||||||
|
"arm": {
|
||||||
|
"archive": "frp_0.71.1_linux_arm_hf.tar.gz",
|
||||||
|
"url": "https://github.com/fatedier/frp/releases/download/v0.71.1/frp_0.71.1_linux_arm_hf.tar.gz",
|
||||||
|
"sha256": "a40116f15f2a05b7001f985f1f1ccb2570e2aa41329b06de76a82e71495786db"
|
||||||
|
},
|
||||||
|
"aarch64": {
|
||||||
|
"archive": "frp_0.71.1_linux_arm64.tar.gz",
|
||||||
|
"url": "https://github.com/fatedier/frp/releases/download/v0.71.1/frp_0.71.1_linux_arm64.tar.gz",
|
||||||
|
"sha256": "8edf590b0c396fa42f9c147121a76af83cfc2002b08f5aef0ea1008c43482019"
|
||||||
|
},
|
||||||
|
"x86_64": {
|
||||||
|
"archive": "frp_0.71.1_linux_amd64.tar.gz",
|
||||||
|
"url": "https://github.com/fatedier/frp/releases/download/v0.71.1/frp_0.71.1_linux_amd64.tar.gz",
|
||||||
|
"sha256": "1d9875210972a5d1a62b2dc4c48df45ab0af05d3ca1b5e494a014428259df803"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ use tokio::sync::RwLock;
|
|||||||
|
|
||||||
use super::protected_config::ProtectedConfigFile;
|
use super::protected_config::ProtectedConfigFile;
|
||||||
use super::types::*;
|
use super::types::*;
|
||||||
use super::validation::{validate_easytier_config, validate_frpc_config, validate_gostc_config};
|
use super::validation::{validate_easyfrp_config, validate_easytier_config, validate_frpc_config};
|
||||||
use crate::events::EventBus;
|
use crate::events::EventBus;
|
||||||
|
|
||||||
const LOG_BUFFER_SIZE: usize = 200;
|
const LOG_BUFFER_SIZE: usize = 200;
|
||||||
@@ -82,8 +82,8 @@ 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 => {
|
ExtensionId::Easyfrp => {
|
||||||
config.gostc.enabled && validate_gostc_config(&config.gostc).is_ok()
|
config.easyfrp.enabled && validate_easyfrp_config(&config.easyfrp).is_ok()
|
||||||
}
|
}
|
||||||
ExtensionId::Easytier => {
|
ExtensionId::Easytier => {
|
||||||
config.easytier.enabled && validate_easytier_config(&config.easytier).is_ok()
|
config.easytier.enabled && validate_easytier_config(&config.easytier).is_ok()
|
||||||
@@ -144,12 +144,7 @@ impl ExtensionManager {
|
|||||||
|
|
||||||
let launch = self.build_launch(id, config).await?;
|
let launch = self.build_launch(id, config).await?;
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!("Starting extension {}: {}", id, id.binary_path().display());
|
||||||
"Starting extension {}: {} {}",
|
|
||||||
id,
|
|
||||||
id.binary_path().display(),
|
|
||||||
launch.args.join(" ")
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut child = Command::new(id.binary_path())
|
let mut child = Command::new(id.binary_path())
|
||||||
.args(&launch.args)
|
.args(&launch.args)
|
||||||
@@ -272,17 +267,12 @@ impl ExtensionManager {
|
|||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
|
||||||
ExtensionId::Gostc => {
|
ExtensionId::Easyfrp => {
|
||||||
let c = &config.gostc;
|
let c = &config.easyfrp;
|
||||||
validate_gostc_config(c)?;
|
validate_easyfrp_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()]);
|
|
||||||
|
|
||||||
|
let mut args = vec!["control".to_string()];
|
||||||
|
args.extend(c.control_urls.iter().map(|url| url.trim().to_string()));
|
||||||
args
|
args
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,6 +570,27 @@ impl ExtensionManager {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn easyfrp_launch_uses_control_urls_as_separate_arguments() {
|
||||||
|
let mut config = ExtensionsConfig::default();
|
||||||
|
config.easyfrp.control_urls = vec![
|
||||||
|
"wss://example.com/key-one".to_string(),
|
||||||
|
"txt://example.net/key-two".to_string(),
|
||||||
|
];
|
||||||
|
let launch = ExtensionManager::new()
|
||||||
|
.build_launch(ExtensionId::Easyfrp, &config)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
launch.args,
|
||||||
|
[
|
||||||
|
"control",
|
||||||
|
"wss://example.com/key-one",
|
||||||
|
"txt://example.net/key-two"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn easytier_launch_revalidates_full_configuration() {
|
async fn easytier_launch_revalidates_full_configuration() {
|
||||||
let config = EasytierConfig {
|
let config = EasytierConfig {
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ pub use manager::TTYD_SOCKET_PATH;
|
|||||||
pub use manager::TTYD_TCP_ADDR;
|
pub use manager::TTYD_TCP_ADDR;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
pub(crate) use validation::{
|
pub(crate) use validation::{
|
||||||
validate_easytier_config, validate_extension_config, validate_frpc_config,
|
validate_easyfrp_config, validate_easytier_config, validate_extension_config,
|
||||||
validate_gostc_config,
|
validate_frpc_config,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +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::Easytier => "/usr/bin/easytier-core",
|
ExtensionId::Easytier => "/usr/bin/easytier-core",
|
||||||
ExtensionId::Frpc => "/usr/bin/frpc",
|
ExtensionId::Frpc => "/usr/bin/frpc",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +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::Easytier => "easytier-core.exe",
|
ExtensionId::Easytier => "easytier-core.exe",
|
||||||
ExtensionId::Frpc => "frpc.exe",
|
ExtensionId::Frpc => "frpc.exe",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use super::software;
|
|||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ExtensionId {
|
pub enum ExtensionId {
|
||||||
Ttyd,
|
Ttyd,
|
||||||
Gostc,
|
Easyfrp,
|
||||||
Easytier,
|
Easytier,
|
||||||
Frpc,
|
Frpc,
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ impl ExtensionId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn all() -> &'static [ExtensionId] {
|
pub fn all() -> &'static [ExtensionId] {
|
||||||
&[Self::Ttyd, Self::Gostc, Self::Easytier, Self::Frpc]
|
&[Self::Ttyd, Self::Easyfrp, Self::Easytier, Self::Frpc]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ impl std::fmt::Display for ExtensionId {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
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::Easytier => write!(f, "easytier"),
|
Self::Easytier => write!(f, "easytier"),
|
||||||
Self::Frpc => write!(f, "frpc"),
|
Self::Frpc => write!(f, "frpc"),
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,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),
|
||||||
"easytier" => Ok(Self::Easytier),
|
"easytier" => Ok(Self::Easytier),
|
||||||
"frpc" => Ok(Self::Frpc),
|
"frpc" => Ok(Self::Frpc),
|
||||||
_ => Err(format!("Unknown extension: {}", s)),
|
_ => Err(format!("Unknown extension: {}", s)),
|
||||||
@@ -81,25 +81,11 @@ impl Default for TtydConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct GostcConfig {
|
pub struct EasyfrpConfig {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub addr: String,
|
pub control_urls: Vec<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]
|
||||||
@@ -217,7 +203,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 easytier: EasytierConfig,
|
pub easytier: EasytierConfig,
|
||||||
pub frpc: FrpcConfig,
|
pub frpc: FrpcConfig,
|
||||||
}
|
}
|
||||||
@@ -239,10 +225,10 @@ pub struct TtydInfo {
|
|||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct GostcInfo {
|
pub struct EasyfrpInfo {
|
||||||
pub available: bool,
|
pub available: bool,
|
||||||
pub status: ExtensionStatus,
|
pub status: ExtensionStatus,
|
||||||
pub config: GostcConfig,
|
pub config: EasyfrpConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
@@ -265,7 +251,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 easytier: EasytierInfo,
|
pub easytier: EasytierInfo,
|
||||||
pub frpc: FrpcInfo,
|
pub frpc: FrpcInfo,
|
||||||
}
|
}
|
||||||
@@ -279,7 +265,17 @@ pub struct ExtensionLogs {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{EasytierConfig, EasytierConfigMode};
|
use super::{EasytierConfig, EasytierConfigMode, ExtensionsConfig};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replacing_legacy_gostc_requires_new_easyfrp_configuration() {
|
||||||
|
let config: ExtensionsConfig = serde_json::from_str(
|
||||||
|
r#"{"gostc":{"enabled":true,"addr":"old.example.com","key":"old-key","tls":true}}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!config.easyfrp.enabled);
|
||||||
|
assert!(config.easyfrp.control_urls.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn legacy_easytier_config_defaults_to_quick_mode() {
|
fn legacy_easytier_config_defaults_to_quick_mode() {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use toml_edit::DocumentMut;
|
use toml_edit::DocumentMut;
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
EasytierConfig, EasytierConfigMode, ExtensionId, ExtensionsConfig, FrpProxyType, FrpcConfig,
|
EasyfrpConfig, EasytierConfig, EasytierConfigMode, ExtensionId, ExtensionsConfig, FrpProxyType,
|
||||||
FrpcConfigMode, GostcConfig,
|
FrpcConfig, FrpcConfigMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn validate_extension_config(
|
pub(crate) fn validate_extension_config(
|
||||||
@@ -11,15 +11,36 @@ 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::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> {
|
pub(crate) fn validate_easyfrp_config(config: &EasyfrpConfig) -> Result<(), String> {
|
||||||
require_non_empty(config.addr.trim(), "GOSTC server address is required")?;
|
if config.control_urls.is_empty() {
|
||||||
require_non_empty(config.key.as_str(), "GOSTC client key is required")
|
return Err("EasyFrp control URL is required".to_string());
|
||||||
|
}
|
||||||
|
for value in &config.control_urls {
|
||||||
|
let value = value.trim();
|
||||||
|
if value.chars().any(char::is_whitespace) {
|
||||||
|
return Err("EasyFrp control URL must not contain whitespace".to_string());
|
||||||
|
}
|
||||||
|
let url =
|
||||||
|
reqwest::Url::parse(value).map_err(|_| "EasyFrp control URL is invalid".to_string())?;
|
||||||
|
if !matches!(url.scheme(), "ws" | "wss" | "http" | "https" | "txt")
|
||||||
|
|| url.host_str().is_none()
|
||||||
|
|| url.path().trim_matches('/').is_empty()
|
||||||
|
|| !url.username().is_empty()
|
||||||
|
|| url.password().is_some()
|
||||||
|
|| url.fragment().is_some()
|
||||||
|
{
|
||||||
|
return Err(
|
||||||
|
"EasyFrp requires a ws/wss/http/https/txt URL with a client key path".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn validate_easytier_config(config: &EasytierConfig) -> Result<(), String> {
|
pub(crate) fn validate_easytier_config(config: &EasytierConfig) -> Result<(), String> {
|
||||||
@@ -80,6 +101,36 @@ fn validate_full_toml(extension_name: &str, config: &str) -> Result<(), String>
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn easyfrp_accepts_documented_control_protocols() {
|
||||||
|
for scheme in ["ws", "wss", "http", "https", "txt"] {
|
||||||
|
let config = EasyfrpConfig {
|
||||||
|
enabled: true,
|
||||||
|
control_urls: vec![format!("{scheme}://example.com/client-key")],
|
||||||
|
};
|
||||||
|
assert!(validate_easyfrp_config(&config).is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn easyfrp_rejects_missing_keys_and_non_connection_arguments() {
|
||||||
|
for urls in [
|
||||||
|
vec![],
|
||||||
|
vec![""],
|
||||||
|
vec!["--help"],
|
||||||
|
vec!["wss://example.com/"],
|
||||||
|
vec!["file:///tmp/config"],
|
||||||
|
vec!["wss://example.com/key with spaces"],
|
||||||
|
vec!["wss://example.com/key", "invalid"],
|
||||||
|
] {
|
||||||
|
let config = EasyfrpConfig {
|
||||||
|
enabled: true,
|
||||||
|
control_urls: urls.into_iter().map(str::to_string).collect(),
|
||||||
|
};
|
||||||
|
assert!(validate_easyfrp_config(&config).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validates_easytier_full_configuration() {
|
fn validates_easytier_full_configuration() {
|
||||||
let mut config = EasytierConfig {
|
let mut config = EasytierConfig {
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ use typeshare::typeshare;
|
|||||||
|
|
||||||
use crate::error::{AppError, Result};
|
use crate::error::{AppError, Result};
|
||||||
use crate::extensions::{
|
use crate::extensions::{
|
||||||
validate_easytier_config, validate_extension_config, validate_frpc_config,
|
validate_easyfrp_config, validate_easytier_config, validate_extension_config,
|
||||||
validate_gostc_config, EasytierConfig, EasytierConfigMode, EasytierInfo, ExtensionId,
|
validate_frpc_config, EasyfrpConfig, EasyfrpInfo, EasytierConfig, EasytierConfigMode,
|
||||||
ExtensionInfo, ExtensionLogs, ExtensionsStatus, FrpProxyType, FrpcConfig, FrpcConfigMode,
|
EasytierInfo, ExtensionId, ExtensionInfo, ExtensionLogs, ExtensionsStatus, FrpProxyType,
|
||||||
FrpcInfo, GostcConfig, GostcInfo, TtydConfig, TtydInfo,
|
FrpcConfig, FrpcConfigMode, FrpcInfo, TtydConfig, TtydInfo,
|
||||||
};
|
};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
@@ -29,10 +29,10 @@ pub async fn list_extensions(State(state): State<Arc<AppState>>) -> Json<Extensi
|
|||||||
status: mgr.status(ExtensionId::Ttyd).await,
|
status: mgr.status(ExtensionId::Ttyd).await,
|
||||||
config: config.extensions.ttyd.clone(),
|
config: config.extensions.ttyd.clone(),
|
||||||
},
|
},
|
||||||
gostc: GostcInfo {
|
easyfrp: EasyfrpInfo {
|
||||||
available: mgr.check_available(ExtensionId::Gostc),
|
available: mgr.check_available(ExtensionId::Easyfrp),
|
||||||
status: mgr.status(ExtensionId::Gostc).await,
|
status: mgr.status(ExtensionId::Easyfrp).await,
|
||||||
config: config.extensions.gostc.clone(),
|
config: config.extensions.easyfrp.clone(),
|
||||||
},
|
},
|
||||||
easytier: EasytierInfo {
|
easytier: EasytierInfo {
|
||||||
available: mgr.check_available(ExtensionId::Easytier),
|
available: mgr.check_available(ExtensionId::Easytier),
|
||||||
@@ -134,11 +134,9 @@ pub struct TtydConfigUpdate {
|
|||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct GostcConfigUpdate {
|
pub struct EasyfrpConfigUpdate {
|
||||||
pub enabled: Option<bool>,
|
pub enabled: Option<bool>,
|
||||||
pub addr: Option<String>,
|
pub control_urls: Option<Vec<String>>,
|
||||||
pub key: Option<String>,
|
|
||||||
pub tls: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
@@ -209,52 +207,46 @@ 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(
|
pub async fn update_easyfrp_config(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
Json(req): Json<GostcConfigUpdate>,
|
Json(req): Json<EasyfrpConfigUpdate>,
|
||||||
) -> Result<Json<GostcConfig>> {
|
) -> Result<Json<EasyfrpConfig>> {
|
||||||
let current_config = state.config.get();
|
let current_config = state.config.get();
|
||||||
let was_enabled = current_config.extensions.gostc.enabled;
|
let was_enabled = current_config.extensions.easyfrp.enabled;
|
||||||
let mut next_gostc = current_config.extensions.gostc.clone();
|
let mut next_easyfrp = current_config.extensions.easyfrp.clone();
|
||||||
|
|
||||||
if let Some(enabled) = req.enabled {
|
if let Some(enabled) = req.enabled {
|
||||||
next_gostc.enabled = enabled;
|
next_easyfrp.enabled = enabled;
|
||||||
}
|
}
|
||||||
if let Some(ref addr) = req.addr {
|
if let Some(ref urls) = req.control_urls {
|
||||||
next_gostc.addr = addr.clone();
|
next_easyfrp.control_urls = urls.iter().map(|url| url.trim().to_string()).collect();
|
||||||
}
|
|
||||||
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 {
|
if next_easyfrp.enabled {
|
||||||
bad_request(validate_gostc_config(&next_gostc))?;
|
bad_request(validate_easyfrp_config(&next_easyfrp))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
state
|
state
|
||||||
.config
|
.config
|
||||||
.update(|config| {
|
.update(|config| {
|
||||||
config.extensions.gostc = next_gostc.clone();
|
config.extensions.easyfrp = next_easyfrp.clone();
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let new_config = state.config.get();
|
let new_config = state.config.get();
|
||||||
let is_enabled = new_config.extensions.gostc.enabled;
|
let is_enabled = new_config.extensions.easyfrp.enabled;
|
||||||
|
|
||||||
if was_enabled && !is_enabled {
|
if was_enabled && !is_enabled {
|
||||||
state.extensions.stop(ExtensionId::Gostc).await.ok();
|
state.extensions.stop(ExtensionId::Easyfrp).await.ok();
|
||||||
} else if !was_enabled && is_enabled && state.extensions.check_available(ExtensionId::Gostc) {
|
} else if !was_enabled && is_enabled && state.extensions.check_available(ExtensionId::Easyfrp) {
|
||||||
state
|
state
|
||||||
.extensions
|
.extensions
|
||||||
.start(ExtensionId::Gostc, &new_config.extensions)
|
.start(ExtensionId::Easyfrp, &new_config.extensions)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(new_config.extensions.gostc.clone()))
|
Ok(Json(new_config.extensions.easyfrp.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_easytier_config(
|
pub async fn update_easytier_config(
|
||||||
|
|||||||
@@ -259,8 +259,8 @@ pub fn create_router(state: Arc<AppState>) -> Router {
|
|||||||
patch(handlers::extensions::update_ttyd_config),
|
patch(handlers::extensions::update_ttyd_config),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/extensions/gostc/config",
|
"/extensions/easyfrp/config",
|
||||||
patch(handlers::extensions::update_gostc_config),
|
patch(handlers::extensions::update_easyfrp_config),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/extensions/easytier/config",
|
"/extensions/easytier/config",
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import type {
|
|||||||
ExtensionLogs,
|
ExtensionLogs,
|
||||||
TtydConfig,
|
TtydConfig,
|
||||||
TtydConfigUpdate,
|
TtydConfigUpdate,
|
||||||
GostcConfig,
|
EasyfrpConfig,
|
||||||
GostcConfigUpdate,
|
EasyfrpConfigUpdate,
|
||||||
EasytierConfig,
|
EasytierConfig,
|
||||||
EasytierConfigUpdate,
|
EasytierConfigUpdate,
|
||||||
FrpcConfig,
|
FrpcConfig,
|
||||||
@@ -201,8 +201,8 @@ export const extensionsApi = {
|
|||||||
body: JSON.stringify(config),
|
body: JSON.stringify(config),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateGostc: (config: GostcConfigUpdate) =>
|
updateEasyfrp: (config: EasyfrpConfigUpdate) =>
|
||||||
request<GostcConfig>('/extensions/gostc/config', {
|
request<EasyfrpConfig>('/extensions/easyfrp/config', {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify(config),
|
body: JSON.stringify(config),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1136,15 +1136,14 @@ export default {
|
|||||||
openInNewTab: 'Open in New Tab',
|
openInNewTab: 'Open in New Tab',
|
||||||
shell: 'Shell',
|
shell: 'Shell',
|
||||||
},
|
},
|
||||||
gostc: {
|
easyfrp: {
|
||||||
title: 'GOSTC NAT Traversal',
|
title: 'EasyFrp NAT Traversal',
|
||||||
desc: 'NAT traversal via GOSTC',
|
desc: 'Connect to a tunnel service with the EasyFrp client',
|
||||||
addr: 'Server Address',
|
urls: 'Client Connection URLs',
|
||||||
addrPlaceholder: 'Hostname or IP (required)',
|
urlsPlaceholder: 'wss://example.com/client-key',
|
||||||
addrRequired: 'Enter the GOSTC server address',
|
urlsHint: 'One full URL per line. Supports ws, wss, http, https and txt. Copy client URLs from the EasyFrp dashboard.',
|
||||||
key: 'Client Key',
|
urlsRequired: 'Enter an EasyFrp client connection URL',
|
||||||
keyRequired: 'Enter the GOSTC client key',
|
urlsInvalid: 'Enter valid ws/wss/http/https/txt URLs with a client key path',
|
||||||
tls: 'Enable TLS',
|
|
||||||
},
|
},
|
||||||
easytier: {
|
easytier: {
|
||||||
title: 'Easytier Network',
|
title: 'Easytier Network',
|
||||||
|
|||||||
@@ -1135,15 +1135,14 @@ export default {
|
|||||||
openInNewTab: '在新标签页打开',
|
openInNewTab: '在新标签页打开',
|
||||||
shell: 'Shell',
|
shell: 'Shell',
|
||||||
},
|
},
|
||||||
gostc: {
|
easyfrp: {
|
||||||
title: 'GOSTC 内网穿透',
|
title: 'EasyFrp 内网穿透',
|
||||||
desc: '通过 GOSTC 实现内网穿透',
|
desc: '使用 EasyFrp 客户端连接穿透服务',
|
||||||
addr: '服务器地址',
|
urls: '客户端连接 URL',
|
||||||
addrPlaceholder: '主机名或 IP(必填)',
|
urlsPlaceholder: 'wss://example.com/client-key',
|
||||||
addrRequired: '请填写 GOSTC 服务器地址',
|
urlsHint: '每行一个完整连接 URL,支持 ws、wss、http、https、txt。请从 EasyFrp 管理端复制客户端地址。',
|
||||||
key: '客户端密钥',
|
urlsRequired: '请填写 EasyFrp 客户端连接 URL',
|
||||||
keyRequired: '请填写 GOSTC 客户端密钥',
|
urlsInvalid: '请填写带客户端密钥路径的有效 ws/wss/http/https/txt URL',
|
||||||
tls: '启用 TLS',
|
|
||||||
},
|
},
|
||||||
easytier: {
|
easytier: {
|
||||||
title: 'Easytier 组网',
|
title: 'Easytier 组网',
|
||||||
|
|||||||
@@ -196,11 +196,9 @@ export interface TtydConfig {
|
|||||||
shell: string;
|
shell: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GostcConfig {
|
export interface EasyfrpConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
addr: string;
|
control_urls: string[];
|
||||||
key: string;
|
|
||||||
tls: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EasytierConfigMode {
|
export enum EasytierConfigMode {
|
||||||
@@ -252,7 +250,7 @@ export interface FrpcConfig {
|
|||||||
|
|
||||||
export interface ExtensionsConfig {
|
export interface ExtensionsConfig {
|
||||||
ttyd: TtydConfig;
|
ttyd: TtydConfig;
|
||||||
gostc: GostcConfig;
|
easyfrp: EasyfrpConfig;
|
||||||
easytier: EasytierConfig;
|
easytier: EasytierConfig;
|
||||||
frpc: FrpcConfig;
|
frpc: FrpcConfig;
|
||||||
}
|
}
|
||||||
@@ -489,7 +487,7 @@ export interface ExtensionInfo {
|
|||||||
|
|
||||||
export enum ExtensionId {
|
export enum ExtensionId {
|
||||||
Ttyd = "ttyd",
|
Ttyd = "ttyd",
|
||||||
Gostc = "gostc",
|
Easyfrp = "easyfrp",
|
||||||
Easytier = "easytier",
|
Easytier = "easytier",
|
||||||
Frpc = "frpc",
|
Frpc = "frpc",
|
||||||
}
|
}
|
||||||
@@ -505,10 +503,10 @@ export interface TtydInfo {
|
|||||||
config: TtydConfig;
|
config: TtydConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GostcInfo {
|
export interface EasyfrpInfo {
|
||||||
available: boolean;
|
available: boolean;
|
||||||
status: ExtensionStatus;
|
status: ExtensionStatus;
|
||||||
config: GostcConfig;
|
config: EasyfrpConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FrpcInfo {
|
export interface FrpcInfo {
|
||||||
@@ -519,7 +517,7 @@ export interface FrpcInfo {
|
|||||||
|
|
||||||
export interface ExtensionsStatus {
|
export interface ExtensionsStatus {
|
||||||
ttyd: TtydInfo;
|
ttyd: TtydInfo;
|
||||||
gostc: GostcInfo;
|
easyfrp: EasyfrpInfo;
|
||||||
easytier: EasytierInfo;
|
easytier: EasytierInfo;
|
||||||
frpc: FrpcInfo;
|
frpc: FrpcInfo;
|
||||||
}
|
}
|
||||||
@@ -541,11 +539,9 @@ export interface FrpcConfigUpdate {
|
|||||||
custom_toml?: string;
|
custom_toml?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GostcConfigUpdate {
|
export interface EasyfrpConfigUpdate {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
addr?: string;
|
control_urls?: string[];
|
||||||
key?: string;
|
|
||||||
tls?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** OTG USB device descriptor configuration update */
|
/** OTG USB device descriptor configuration update */
|
||||||
|
|||||||
@@ -434,13 +434,13 @@ 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: [],
|
||||||
easytier: [],
|
easytier: [],
|
||||||
frpc: [],
|
frpc: [],
|
||||||
})
|
})
|
||||||
const showLogs = ref<Record<string, boolean>>({
|
const showLogs = ref<Record<string, boolean>>({
|
||||||
ttyd: false,
|
ttyd: false,
|
||||||
gostc: false,
|
easyfrp: false,
|
||||||
easytier: false,
|
easytier: false,
|
||||||
frpc: false,
|
frpc: false,
|
||||||
})
|
})
|
||||||
@@ -449,7 +449,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[] },
|
||||||
easytier: {
|
easytier: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
config_mode: EasytierConfigMode.Quick,
|
config_mode: EasytierConfigMode.Quick,
|
||||||
@@ -477,9 +477,27 @@ const extConfig = ref({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const gostcValidationMessage = computed(() => {
|
const easyfrpUrlsText = computed({
|
||||||
if (!extConfig.value.gostc.addr?.trim()) return t('extensions.gostc.addrRequired')
|
get: () => extConfig.value.easyfrp.control_urls.join('\n'),
|
||||||
if (!extConfig.value.gostc.key) return t('extensions.gostc.keyRequired')
|
set: (value: string) => {
|
||||||
|
extConfig.value.easyfrp.control_urls = value.split(/\r?\n/).map(url => url.trim()).filter(Boolean)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const easyfrpValidationMessage = computed(() => {
|
||||||
|
const urls = extConfig.value.easyfrp.control_urls
|
||||||
|
if (urls.length === 0) return t('extensions.easyfrp.urlsRequired')
|
||||||
|
for (const value of urls) {
|
||||||
|
try {
|
||||||
|
const url = new URL(value)
|
||||||
|
if (/\s/.test(value) || !['ws:', 'wss:', 'http:', 'https:', 'txt:'].includes(url.protocol)
|
||||||
|
|| !url.hostname || !url.pathname.replace(/\//g, '') || url.username || url.password || url.hash) {
|
||||||
|
return t('extensions.easyfrp.urlsInvalid')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return t('extensions.easyfrp.urlsInvalid')
|
||||||
|
}
|
||||||
|
}
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1698,7 +1716,7 @@ async function loadExtensions() {
|
|||||||
enabled: ttyd.enabled,
|
enabled: ttyd.enabled,
|
||||||
shell: ttyd.shell,
|
shell: ttyd.shell,
|
||||||
}
|
}
|
||||||
extConfig.value.gostc = { ...extensions.value.gostc.config }
|
extConfig.value.easyfrp = { ...extensions.value.easyfrp.config }
|
||||||
const easytier = extensions.value.easytier.config
|
const easytier = extensions.value.easytier.config
|
||||||
extConfig.value.easytier = {
|
extConfig.value.easytier = {
|
||||||
enabled: easytier.enabled,
|
enabled: easytier.enabled,
|
||||||
@@ -1733,7 +1751,7 @@ async function loadExtensions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExtensionConfigId = 'ttyd' | 'gostc' | 'easytier' | 'frpc'
|
type ExtensionConfigId = 'ttyd' | 'easyfrp' | 'easytier' | 'frpc'
|
||||||
type ValidatedExtensionConfigId = Exclude<ExtensionConfigId, 'ttyd'>
|
type ValidatedExtensionConfigId = Exclude<ExtensionConfigId, 'ttyd'>
|
||||||
|
|
||||||
async function startExtension(id: ExtensionConfigId) {
|
async function startExtension(id: ExtensionConfigId) {
|
||||||
@@ -1774,8 +1792,8 @@ async function saveExtensionConfig(id: ExtensionConfigId) {
|
|||||||
try {
|
try {
|
||||||
if (id === 'ttyd') {
|
if (id === 'ttyd') {
|
||||||
await extensionsApi.updateTtyd(extConfig.value.ttyd)
|
await extensionsApi.updateTtyd(extConfig.value.ttyd)
|
||||||
} else if (id === 'gostc') {
|
} else if (id === 'easyfrp') {
|
||||||
await extensionsApi.updateGostc(extConfig.value.gostc)
|
await extensionsApi.updateEasyfrp(extConfig.value.easyfrp)
|
||||||
} else if (id === 'easytier') {
|
} else if (id === 'easytier') {
|
||||||
await extensionsApi.updateEasytier(extConfig.value.easytier)
|
await extensionsApi.updateEasytier(extConfig.value.easytier)
|
||||||
} else if (id === 'frpc') {
|
} else if (id === 'frpc') {
|
||||||
@@ -2052,8 +2070,8 @@ function showValidationError(message: string): boolean {
|
|||||||
|
|
||||||
function validateExtensionConfig(id: ValidatedExtensionConfigId): boolean {
|
function validateExtensionConfig(id: ValidatedExtensionConfigId): boolean {
|
||||||
let message = ''
|
let message = ''
|
||||||
if (id === 'gostc') {
|
if (id === 'easyfrp') {
|
||||||
message = gostcValidationMessage.value
|
message = easyfrpValidationMessage.value
|
||||||
} else if (id === 'easytier') {
|
} else if (id === 'easytier') {
|
||||||
message = easytierValidationMessage.value
|
message = easytierValidationMessage.value
|
||||||
} else {
|
} else {
|
||||||
@@ -4430,31 +4448,31 @@ watch(isWindows, () => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<CardTitle>{{ t('extensions.gostc.title') }}</CardTitle>
|
<CardTitle>{{ t('extensions.easyfrp.title') }}</CardTitle>
|
||||||
<CardDescription>{{ t('extensions.gostc.desc') }}</CardDescription>
|
<CardDescription>{{ t('extensions.easyfrp.desc') }}</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Badge :variant="extensions?.gostc?.available ? 'default' : 'destructive'">
|
<Badge :variant="extensions?.easyfrp?.available ? 'default' : 'destructive'">
|
||||||
{{ extensions?.gostc?.available ? t('extensions.available') : t('extensions.unavailable') }}
|
{{ extensions?.easyfrp?.available ? t('extensions.available') : t('extensions.unavailable') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="space-y-4">
|
<CardContent class="space-y-4">
|
||||||
<div v-if="!extensions?.gostc?.available" class="text-sm text-muted-foreground bg-muted p-3 rounded-md">
|
<div v-if="!extensions?.easyfrp?.available" class="text-sm text-muted-foreground bg-muted p-3 rounded-md">
|
||||||
{{ t('extensions.binaryNotFound', { path: isWindows ? 'gostc.exe' : '/usr/bin/gostc' }) }}
|
{{ t('extensions.binaryNotFound', { path: isWindows ? 'easyfrpc.exe' : '/usr/bin/easyfrpc' }) }}
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- Status and controls -->
|
<!-- Status and controls -->
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div :class="['size-2 rounded-full', getExtStatusClass(extensions?.gostc?.status)]" />
|
<div :class="['size-2 rounded-full', getExtStatusClass(extensions?.easyfrp?.status)]" />
|
||||||
<span class="text-sm">{{ getExtStatusText(extensions?.gostc?.status) }}</span>
|
<span class="text-sm">{{ getExtStatusText(extensions?.easyfrp?.status) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
v-if="!isExtRunning(extensions?.gostc?.status)"
|
v-if="!isExtRunning(extensions?.easyfrp?.status)"
|
||||||
size="sm"
|
size="sm"
|
||||||
@click="startExtension('gostc')"
|
@click="startExtension('easyfrp')"
|
||||||
:disabled="extensionsLoading || !!gostcValidationMessage"
|
:disabled="extensionsLoading || !!easyfrpValidationMessage"
|
||||||
>
|
>
|
||||||
<Play class="size-4 mr-1" />
|
<Play class="size-4 mr-1" />
|
||||||
{{ t('extensions.start') }}
|
{{ t('extensions.start') }}
|
||||||
@@ -4463,7 +4481,7 @@ watch(isWindows, () => {
|
|||||||
v-else
|
v-else
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@click="stopExtension('gostc')"
|
@click="stopExtension('easyfrp')"
|
||||||
:disabled="extensionsLoading"
|
:disabled="extensionsLoading"
|
||||||
>
|
>
|
||||||
<Square class="size-4 mr-1" />
|
<Square class="size-4 mr-1" />
|
||||||
@@ -4476,36 +4494,24 @@ watch(isWindows, () => {
|
|||||||
<div class="grid gap-4">
|
<div class="grid gap-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Label>{{ t('extensions.autoStart') }}</Label>
|
<Label>{{ t('extensions.autoStart') }}</Label>
|
||||||
<Switch v-model="extConfig.gostc.enabled" :disabled="isExtRunning(extensions?.gostc?.status)" />
|
<Switch v-model="extConfig.easyfrp.enabled" :disabled="isExtRunning(extensions?.easyfrp?.status)" />
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
|
||||||
<Label class="sm:text-right">{{ t('extensions.gostc.addr') }}</Label>
|
<Label class="sm:text-right">{{ t('extensions.easyfrp.urls') }}</Label>
|
||||||
<div class="sm:col-span-3 space-y-1">
|
<div class="sm:col-span-3 space-y-1">
|
||||||
<Input v-model="extConfig.gostc.addr" :placeholder="t('extensions.gostc.addrPlaceholder')" :disabled="isExtRunning(extensions?.gostc?.status)" />
|
<Textarea v-model="easyfrpUrlsText" :rows="3" autocomplete="off" :spellcheck="false" :placeholder="t('extensions.easyfrp.urlsPlaceholder')" :disabled="isExtRunning(extensions?.easyfrp?.status)" />
|
||||||
<p v-if="extConfig.gostc.enabled && !extConfig.gostc.addr?.trim()" class="text-xs text-destructive">{{ t('extensions.gostc.addrRequired') }}</p>
|
<p class="text-xs text-muted-foreground">{{ t('extensions.easyfrp.urlsHint') }}</p>
|
||||||
</div>
|
<p v-if="extConfig.easyfrp.enabled && easyfrpValidationMessage" class="text-xs text-destructive">{{ easyfrpValidationMessage }}</p>
|
||||||
</div>
|
|
||||||
<div class="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" :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="grid gap-2 sm:grid-cols-4 sm:items-center">
|
|
||||||
<Label class="sm:text-right">{{ t('extensions.gostc.tls') }}</Label>
|
|
||||||
<div class="sm:col-span-3">
|
|
||||||
<Switch v-model="extConfig.gostc.tls" :disabled="isExtRunning(extensions?.gostc?.status)" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Collapsible v-model:open="showLogs.gostc" @update:open="open => open && refreshExtensionLogs('gostc')">
|
<Collapsible v-model:open="showLogs.easyfrp" @update:open="open => open && refreshExtensionLogs('easyfrp')">
|
||||||
<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>
|
<CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['size-4 transition-transform', showLogs.easyfrp ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger>
|
||||||
<CollapsibleContent class="space-y-2 pt-2">
|
<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>
|
<pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.easyfrp || []).join('\n') || t('extensions.noLogs') }}</pre>
|
||||||
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('gostc')">
|
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('easyfrp')">
|
||||||
<RefreshCw class="size-3 mr-1" />
|
<RefreshCw class="size-3 mr-1" />
|
||||||
{{ t('common.refresh') }}
|
{{ t('common.refresh') }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -4514,8 +4520,8 @@ watch(isWindows, () => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter v-if="extensions?.gostc?.available" class="border-t pt-4 justify-end">
|
<CardFooter v-if="extensions?.easyfrp?.available" class="border-t pt-4 justify-end">
|
||||||
<Button :disabled="loading || isExtRunning(extensions?.gostc?.status)" @click="saveExtensionConfig('gostc')">
|
<Button :disabled="loading || isExtRunning(extensions?.easyfrp?.status)" @click="saveExtensionConfig('easyfrp')">
|
||||||
<Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
<Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
|||||||
Reference in New Issue
Block a user