diff --git a/CLAUDE.md b/CLAUDE.md
index 7483cd9..38a1cb7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -61,13 +61,7 @@ pkg/
├── auth/ # JWT authentication
├── utils/ # Port availability checking
├── version/ # Version info and update checking (Gitea API)
- ├── update/ # Shared update logic (download, extract tar.gz/zip)
- └── plugin/ # Plugin system core
- ├── types.go # Plugin interfaces
- ├── registry.go # Plugin registry
- ├── script/ # JS plugin runtime (goja)
- ├── sign/ # Plugin signature verification
- └── store/ # Plugin persistence (SQLite)
+ └── update/ # Shared update logic (download, extract tar.gz/zip)
web/ # Vue 3 + TypeScript frontend (Vite + naive-ui)
scripts/ # Build scripts (build.sh, build.ps1)
```
@@ -75,23 +69,16 @@ scripts/ # Build scripts (build.sh, build.ps1)
### Key Interfaces
- `ClientStore` (internal/server/db/): Database abstraction for client rules storage
-- `JSPluginStore` (internal/server/db/): JS plugin persistence
- `ServerInterface` (internal/server/router/handler/): API handler interface
-- `ClientPlugin` (pkg/plugin/): Plugin interface for client-side plugins
### Proxy Types
-**内置类型** (直接在 tunnel 中处理):
1. **TCP** (default): Direct port forwarding (remote_port → local_ip:local_port)
2. **UDP**: UDP port forwarding
3. **HTTP**: HTTP proxy through client network
4. **HTTPS**: HTTPS proxy through client network
5. **SOCKS5**: SOCKS5 proxy through client network
-**JS 插件类型** (通过 goja 运行时):
-- Custom application plugins (file-server, api-server, etc.)
-- Runs on client side with sandbox restrictions
-
### Data Flow
External User → Server Port → Yamux Stream → Client → Local Service
@@ -102,44 +89,6 @@ External User → Server Port → Yamux Stream → Client → Local Service
- Client: Command-line flags only (server address, token, client ID)
- Default ports: 7000 (tunnel), 7500 (web console)
-## Plugin System
-
-GoTunnel supports a JavaScript-based plugin system using the goja runtime.
-
-### Plugin Architecture
-
-- **内置协议**: tcp, udp, http, https, socks5 直接在 tunnel 代码中处理
-- **JS Plugins**: 自定义应用插件通过 goja 运行时在客户端执行
-- **Plugin Store**: 从官方商店浏览和安装插件
-- **Signature Verification**: 插件需要签名验证才能运行
-
-### JS Plugin Lifecycle
-
-```javascript
-function metadata() {
- return {
- name: "plugin-name",
- version: "1.0.0",
- type: "app",
- description: "Plugin description",
- author: "Author"
- };
-}
-
-function start() { /* called on plugin start */ }
-function handleConn(conn) { /* handle each connection */ }
-function stop() { /* called on plugin stop */ }
-```
-
-### Plugin APIs
-
-- **Basic**: `log()`, `config()`
-- **Connection**: `conn.Read()`, `conn.Write()`, `conn.Close()`
-- **File System**: `fs.readFile()`, `fs.writeFile()`, `fs.readDir()`, `fs.stat()`, etc.
-- **HTTP**: `http.serve()`, `http.json()`, `http.sendFile()`
-
-See `PLUGINS.md` for detailed plugin development documentation.
-
## API Documentation
The server provides Swagger-documented REST APIs at `/api/`.
diff --git a/PLUGINS.md b/PLUGINS.md
deleted file mode 100644
index 7370f18..0000000
--- a/PLUGINS.md
+++ /dev/null
@@ -1,645 +0,0 @@
-# GoTunnel 插件开发指南
-
-本文档介绍如何为 GoTunnel 开发 JS 插件。JS 插件基于 [goja](https://github.com/dop251/goja) 运行时,运行在客户端上。
-
-## 目录
-
-- [快速开始](#快速开始)
-- [插件结构](#插件结构)
-- [API 参考](#api-参考)
-- [示例插件](#示例插件)
-- [插件签名](#插件签名)
-- [发布到商店](#发布到商店)
-
----
-
-## 快速开始
-
-### 最小插件示例
-
-```javascript
-// 必须:定义插件元数据
-function metadata() {
- return {
- name: "my-plugin",
- version: "1.0.0",
- type: "app",
- description: "My first plugin",
- author: "Your Name"
- };
-}
-
-// 可选:插件启动时调用
-function start() {
- log("Plugin started");
-}
-
-// 必须:处理连接
-function handleConn(conn) {
- // 处理连接逻辑
- conn.Close();
-}
-
-// 可选:插件停止时调用
-function stop() {
- log("Plugin stopped");
-}
-```
-
----
-
-## 插件结构
-
-### 生命周期函数
-
-| 函数 | 必须 | 说明 |
-|------|------|------|
-| `metadata()` | 否 | 返回插件元数据,不定义则使用默认值 |
-| `start()` | 否 | 插件启动时调用 |
-| `handleConn(conn)` | 是 | 处理每个连接 |
-| `stop()` | 否 | 插件停止时调用 |
-
-### 元数据字段
-
-```javascript
-function metadata() {
- return {
- name: "plugin-name", // 插件名称
- version: "1.0.0", // 版本号
- type: "app", // 类型: "app" (应用插件)
- description: "描述", // 插件描述
- author: "作者" // 作者名称
- };
-}
-```
-
----
-
-## API 参考
-
-### 基础 API
-
-#### `log(message)`
-
-输出日志信息。
-
-```javascript
-log("Hello, World!");
-// 输出: [JS:plugin-name] Hello, World!
-```
-
-#### `config(key)`
-
-获取插件配置值。
-
-```javascript
-var port = config("port");
-var host = config("host") || "127.0.0.1";
-```
-
----
-
-### 连接 API (conn)
-
-`handleConn` 函数接收的 `conn` 对象提供以下方法:
-
-#### `conn.Read(size)`
-
-读取数据,返回字节数组,失败返回 `null`。
-
-```javascript
-var data = conn.Read(1024);
-if (data) {
- log("Received " + data.length + " bytes");
-}
-```
-
-#### `conn.Write(data)`
-
-写入数据,返回写入的字节数。
-
-```javascript
-var written = conn.Write(data);
-log("Wrote " + written + " bytes");
-```
-
-#### `conn.Close()`
-
-关闭连接。
-
-```javascript
-conn.Close();
-```
-
----
-
-### 文件系统 API (fs)
-
-所有文件操作都在沙箱中执行,有路径和大小限制。
-
-#### `fs.readFile(path)`
-
-读取文件内容。
-
-```javascript
-var result = fs.readFile("/path/to/file.txt");
-if (result.error) {
- log("Error: " + result.error);
-} else {
- log("Content: " + result.data);
-}
-```
-
-#### `fs.writeFile(path, content)`
-
-写入文件内容。
-
-```javascript
-var result = fs.writeFile("/path/to/file.txt", "Hello");
-if (result.ok) {
- log("File written");
-} else {
- log("Error: " + result.error);
-}
-```
-
-#### `fs.readDir(path)`
-
-读取目录内容。
-
-```javascript
-var result = fs.readDir("/path/to/dir");
-if (!result.error) {
- for (var i = 0; i < result.entries.length; i++) {
- var entry = result.entries[i];
- log(entry.name + " - " + (entry.isDir ? "DIR" : entry.size + " bytes"));
- }
-}
-```
-
-#### `fs.stat(path)`
-
-获取文件信息。
-
-```javascript
-var result = fs.stat("/path/to/file");
-if (!result.error) {
- log("Name: " + result.name);
- log("Size: " + result.size);
- log("IsDir: " + result.isDir);
- log("ModTime: " + result.modTime);
-}
-```
-
-#### `fs.exists(path)`
-
-检查文件是否存在。
-
-```javascript
-var result = fs.exists("/path/to/file");
-if (result.exists) {
- log("File exists");
-}
-```
-
-#### `fs.mkdir(path)`
-
-创建目录。
-
-```javascript
-var result = fs.mkdir("/path/to/new/dir");
-if (result.ok) {
- log("Directory created");
-}
-```
-
-#### `fs.remove(path)`
-
-删除文件或目录。
-
-```javascript
-var result = fs.remove("/path/to/file");
-if (result.ok) {
- log("Removed");
-}
-```
-
----
-
-### HTTP API (http)
-
-用于构建简单的 HTTP 服务。
-
-#### `http.serve(conn, handler)`
-
-处理 HTTP 请求。
-
-```javascript
-function handleConn(conn) {
- http.serve(conn, function(req) {
- return {
- status: 200,
- contentType: "application/json",
- body: http.json({ message: "Hello", path: req.path })
- };
- });
-}
-```
-
-**请求对象 (req):**
-
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| `method` | string | HTTP 方法 (GET, POST, etc.) |
-| `path` | string | 请求路径 |
-| `body` | string | 请求体 |
-
-**响应对象:**
-
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| `status` | number | HTTP 状态码 (默认 200) |
-| `contentType` | string | Content-Type (默认 application/json) |
-| `body` | string | 响应体 |
-
-#### `http.json(data)`
-
-将对象序列化为 JSON 字符串。
-
-```javascript
-var jsonStr = http.json({ name: "test", value: 123 });
-// 返回: '{"name":"test","value":123}'
-```
-
-#### `http.sendFile(conn, filePath)`
-
-发送文件作为 HTTP 响应。
-
-```javascript
-function handleConn(conn) {
- http.sendFile(conn, "/path/to/index.html");
-}
-```
-
----
-
-### 增强 API (Enhanced APIs)
-
-GoTunnel v2.0+ 提供了更多强大的 API 能力。
-
-#### `logger` (日志)
-
-推荐使用结构化日志替代简单的 `log()`。
-
-- `logger.info(msg)`
-- `logger.warn(msg)`
-- `logger.error(msg)`
-
-```javascript
-logger.info("Server started");
-logger.error("Connection failed");
-```
-
-#### `config` (配置)
-
-增强的配置获取方式。
-
-- `config.get(key)`: 获取配置值
-- `config.getAll()`: 获取所有配置
-
-```javascript
-var all = config.getAll();
-var port = config.get("port");
-```
-
-#### `storage` (持久化存储)
-
-简单的 Key-Value 存储,数据保存在客户端本地。
-
-- `storage.get(key, default)`
-- `storage.set(key, value)`
-- `storage.delete(key)`
-- `storage.keys()`
-
-```javascript
-storage.set("last_run", Date.now());
-var last = storage.get("last_run", 0);
-```
-
-#### `event` (事件总线)
-
-插件内部或插件间的事件通信。
-
-- `event.on(name, callback)`
-- `event.emit(name, data)`
-- `event.off(name)`
-
-```javascript
-event.on("user_login", function(user) {
- logger.info("User logged in: " + user);
-});
-event.emit("user_login", "admin");
-```
-
-#### `request` (HTTP 请求)
-
-发起外部 HTTP 请求。
-
-- `request.get(url)`
-- `request.post(url, contentType, body)`
-
-```javascript
-var res = request.get("https://api.ipify.org");
-logger.info("My IP: " + res.body);
-```
-
-#### `notify` (通知)
-
-发送系统通知。
-
-- `notify.send(title, message)`
-
-```javascript
-notify.send("Download Complete", "File saved to disk");
-```
-
----
-
-## 示例插件
-
-### Echo 服务
-
-```javascript
-function metadata() {
- return {
- name: "echo",
- version: "1.0.0",
- type: "app",
- description: "Echo back received data"
- };
-}
-
-function handleConn(conn) {
- while (true) {
- var data = conn.Read(4096);
- if (!data || data.length === 0) {
- break;
- }
- conn.Write(data);
- }
- conn.Close();
-}
-```
-
-### HTTP 文件服务器
-
-```javascript
-function metadata() {
- return {
- name: "file-server",
- version: "1.0.0",
- type: "app",
- description: "Simple HTTP file server"
- };
-}
-
-var rootDir = "";
-
-function start() {
- rootDir = config("root") || "/tmp";
- log("Serving files from: " + rootDir);
-}
-
-function handleConn(conn) {
- http.serve(conn, function(req) {
- if (req.method === "GET") {
- var filePath = rootDir + req.path;
- if (req.path === "/") {
- filePath = rootDir + "/index.html";
- }
-
- var stat = fs.stat(filePath);
- if (stat.error) {
- return { status: 404, body: "Not Found" };
- }
-
- if (stat.isDir) {
- return listDirectory(filePath);
- }
-
- var file = fs.readFile(filePath);
- if (file.error) {
- return { status: 500, body: file.error };
- }
-
- return {
- status: 200,
- contentType: "text/html",
- body: file.data
- };
- }
- return { status: 405, body: "Method Not Allowed" };
- });
-}
-
-function listDirectory(path) {
- var result = fs.readDir(path);
- if (result.error) {
- return { status: 500, body: result.error };
- }
-
- var html = "
Directory Listing
";
- for (var i = 0; i < result.entries.length; i++) {
- var e = result.entries[i];
- html += "- " + e.name + "
";
- }
- html += "
";
-
- return { status: 200, contentType: "text/html", body: html };
-}
-```
-
-### JSON API 服务
-
-```javascript
-function metadata() {
- return {
- name: "api-server",
- version: "1.0.0",
- type: "app",
- description: "JSON API server"
- };
-}
-
-var counter = 0;
-
-function handleConn(conn) {
- http.serve(conn, function(req) {
- if (req.path === "/api/status") {
- return {
- status: 200,
- body: http.json({
- status: "ok",
- counter: counter++,
- timestamp: Date.now()
- })
- };
- }
-
- if (req.path === "/api/echo" && req.method === "POST") {
- return {
- status: 200,
- body: http.json({
- received: req.body
- })
- };
- }
-
- return {
- status: 404,
- body: http.json({ error: "Not Found" })
- };
- });
-}
-```
-
----
-
-## 插件签名
-
-为了安全,JS 插件需要官方签名才能运行。
-
-### 签名格式
-
-签名文件 (`.sig`) 包含 Base64 编码的签名数据:
-
-```json
-{
- "payload": {
- "name": "plugin-name",
- "version": "1.0.0",
- "checksum": "sha256-hash",
- "key_id": "official-v1"
- },
- "signature": "base64-signature"
-}
-```
-
-### 获取签名
-
-1. 提交插件到官方仓库
-2. 通过审核后获得签名
-3. 将 `.js` 和 `.sig` 文件一起分发
-
----
-
-## 发布到商店
-
-### 商店 JSON 格式
-
-插件商店使用 `store.json` 文件索引所有插件:
-
-```json
-[
- {
- "name": "echo",
- "version": "1.0.0",
- "type": "app",
- "description": "Echo service plugin",
- "author": "GoTunnel",
- "icon": "https://example.com/icon.png",
- "download_url": "https://example.com/plugins/echo.js"
- }
-]
-```
-
-### 提交流程
-
-1. Fork 官方插件仓库
-2. 添加插件文件到 `plugins/` 目录
-3. 更新 `store.json`
-4. 提交 Pull Request
-5. 等待审核和签名
-
----
-
-## 沙箱限制
-
-为了安全,JS 插件运行在沙箱环境中:
-
-| 限制项 | 默认值 |
-|--------|--------|
-| 最大读取文件大小 | 10 MB |
-| 最大写入文件大小 | 10 MB |
-| 允许读取路径 | 插件数据目录 |
-| 允许写入路径 | 插件数据目录 |
-
----
-
-## 调试技巧
-
-### 日志输出
-
-使用 `log()` 函数输出调试信息:
-
-```javascript
-log("Debug: variable = " + JSON.stringify(variable));
-```
-
-### 错误处理
-
-始终检查 API 返回的错误:
-
-```javascript
-var result = fs.readFile(path);
-if (result.error) {
- log("Error reading file: " + result.error);
- return;
-}
-```
-
-### 配置测试
-
-在 Web 控制台的插件管理页面安装并配置插件,或通过 API 安装:
-
-```bash
-# 安装 JS 插件到客户端
-POST /api/client/{id}/plugin/js/install
-Content-Type: application/json
-{
- "plugin_name": "my-plugin",
- "source": "function metadata() {...}",
- "rule_name": "my-rule",
- "remote_port": 8080,
- "config": {"debug": "true"},
- "auto_start": true
-}
-```
-
----
-
-## 常见问题
-
-**Q: 插件无法加载?**
-
-A: 检查签名文件是否存在且有效。
-
-**Q: 文件操作失败?**
-
-A: 确认路径在沙箱允许范围内。
-
-**Q: 如何获取客户端 IP?**
-
-A: 目前 API 不支持,计划在后续版本添加。
-
----
-
-## 更新日志
-
-### v1.0.0
-
-- 初始版本
-- 支持基础 API: log, config
-- 支持连接 API: Read, Write, Close
-- 支持文件系统 API: fs.*
-- 支持 HTTP API: http.*
diff --git a/cmd/client/main.go b/cmd/client/main.go
index 373c50a..413611e 100644
--- a/cmd/client/main.go
+++ b/cmd/client/main.go
@@ -7,7 +7,6 @@ import (
"github.com/gotunnel/internal/client/config"
"github.com/gotunnel/internal/client/tunnel"
"github.com/gotunnel/pkg/crypto"
- "github.com/gotunnel/pkg/plugin"
"github.com/gotunnel/pkg/version"
)
@@ -68,14 +67,5 @@ func main() {
log.Printf("[Client] TLS enabled")
}
- // 初始化插件注册表(用于 JS 插件)
- registry := plugin.NewRegistry()
- client.SetPluginRegistry(registry)
-
- // 初始化版本存储
- if err := client.InitVersionStore(); err != nil {
- log.Printf("[Client] Warning: failed to init version store: %v", err)
- }
-
client.Run()
}
diff --git a/cmd/server/main.go b/cmd/server/main.go
index 46b3243..c712525 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -26,7 +26,6 @@ import (
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/internal/server/tunnel"
"github.com/gotunnel/pkg/crypto"
- "github.com/gotunnel/pkg/plugin"
"github.com/gotunnel/pkg/version"
)
@@ -80,11 +79,8 @@ func main() {
log.Printf("[Server] TLS enabled")
}
- // 初始化插件系统(用于客户端 JS 插件管理)
- registry := plugin.NewRegistry()
- server.SetPluginRegistry(registry)
- server.SetJSPluginStore(clientStore) // 设置 JS 插件存储,用于客户端重连时恢复插件
- server.SetTrafficStore(clientStore) // 设置流量存储,用于记录流量统计
+ // 设置流量存储,用于记录流量统计
+ server.SetTrafficStore(clientStore)
// 启动 Web 控制台
if cfg.Server.Web.Enabled {
diff --git a/go.mod b/go.mod
index f8492c9..107840c 100644
--- a/go.mod
+++ b/go.mod
@@ -3,13 +3,13 @@ module github.com/gotunnel
go 1.24.0
require (
- github.com/dop251/goja v0.0.0-20251201205617-2bb4c724c0f9
github.com/gin-gonic/gin v1.11.0
github.com/go-playground/validator/v10 v10.30.1
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/yamux v0.1.1
+ github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
github.com/shirou/gopsutil/v3 v3.24.5
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
@@ -24,7 +24,6 @@ require (
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
- github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gen2brain/shm v0.1.0 // indirect
@@ -42,14 +41,11 @@ require (
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
- github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
- github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
github.com/jezek/xgb v1.1.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
diff --git a/go.sum b/go.sum
index b5250e1..0ad0adc 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,5 @@
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
-github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
-github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
@@ -14,10 +12,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
-github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/dop251/goja v0.0.0-20251201205617-2bb4c724c0f9 h1:3uSSOd6mVlwcX3k5OYOpiDqFgRmaE2dBfLvVIFWWHrw=
-github.com/dop251/goja v0.0.0-20251201205617-2bb4c724c0f9/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
@@ -67,8 +61,6 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
-github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
-github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
@@ -224,8 +216,6 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/client/tunnel/client.go b/internal/client/tunnel/client.go
index 868d01c..0ca9c89 100644
--- a/internal/client/tunnel/client.go
+++ b/internal/client/tunnel/client.go
@@ -11,13 +11,9 @@ import (
"os/exec"
"path/filepath"
"runtime"
- "strings"
"sync"
"time"
- "github.com/gotunnel/pkg/plugin"
- "github.com/gotunnel/pkg/plugin/script"
- "github.com/gotunnel/pkg/plugin/sign"
"github.com/gotunnel/pkg/protocol"
"github.com/gotunnel/pkg/relay"
"github.com/gotunnel/pkg/update"
@@ -38,21 +34,17 @@ const (
// Client 隧道客户端
type Client struct {
- ServerAddr string
- Token string
- ID string
- Name string // 客户端名称(主机名)
- TLSEnabled bool
- TLSConfig *tls.Config
- DataDir string // 数据目录
- session *yamux.Session
- rules []protocol.ProxyRule
- mu sync.RWMutex
- pluginRegistry *plugin.Registry
- runningPlugins map[string]plugin.ClientPlugin
- versionStore *PluginVersionStore
- pluginMu sync.RWMutex
- logger *Logger // 日志收集器
+ ServerAddr string
+ Token string
+ ID string
+ Name string // 客户端名称(主机名)
+ TLSEnabled bool
+ TLSConfig *tls.Config
+ DataDir string // 数据目录
+ session *yamux.Session
+ rules []protocol.ProxyRule
+ mu sync.RWMutex
+ logger *Logger // 日志收集器
}
// NewClient 创建客户端
@@ -93,30 +85,20 @@ func NewClient(serverAddr, token, id string) *Client {
}
return &Client{
- ServerAddr: serverAddr,
- Token: token,
- ID: id,
- Name: hostname,
- DataDir: dataDir,
- runningPlugins: make(map[string]plugin.ClientPlugin),
- logger: logger,
+ ServerAddr: serverAddr,
+ Token: token,
+ ID: id,
+ Name: hostname,
+ DataDir: dataDir,
+ logger: logger,
}
}
// InitVersionStore 初始化版本存储
func (c *Client) InitVersionStore() error {
- store, err := NewPluginVersionStore(c.DataDir)
- if err != nil {
- return err
- }
- c.versionStore = store
return nil
}
-// SetPluginRegistry 设置插件注册表
-func (c *Client) SetPluginRegistry(registry *plugin.Registry) {
- c.pluginRegistry = registry
-}
// logf 安全地记录日志(同时输出到标准日志和日志收集器)
func (c *Client) logf(format string, args ...interface{}) {
@@ -261,31 +243,14 @@ func (c *Client) handleStream(stream net.Conn) {
c.handleProxyConnect(stream, msg)
case protocol.MsgTypeUDPData:
c.handleUDPData(stream, msg)
- case protocol.MsgTypePluginConfig:
- defer stream.Close()
- c.handlePluginConfig(msg)
- case protocol.MsgTypeClientPluginStart:
- c.handleClientPluginStart(stream, msg)
- case protocol.MsgTypeClientPluginStop:
- c.handleClientPluginStop(stream, msg)
- case protocol.MsgTypeClientPluginConn:
- c.handleClientPluginConn(stream, msg)
- case protocol.MsgTypeJSPluginInstall:
- c.handleJSPluginInstall(stream, msg)
case protocol.MsgTypeClientRestart:
c.handleClientRestart(stream, msg)
- case protocol.MsgTypePluginConfigUpdate:
- c.handlePluginConfigUpdate(stream, msg)
case protocol.MsgTypeUpdateDownload:
c.handleUpdateDownload(stream, msg)
case protocol.MsgTypeLogRequest:
go c.handleLogRequest(stream, msg)
case protocol.MsgTypeLogStop:
c.handleLogStop(stream, msg)
- case protocol.MsgTypePluginStatusQuery:
- c.handlePluginStatusQuery(stream, msg)
- case protocol.MsgTypePluginAPIRequest:
- c.handlePluginAPIRequest(stream, msg)
case protocol.MsgTypeSystemStatsRequest:
c.handleSystemStatsRequest(stream, msg)
case protocol.MsgTypeScreenshotRequest:
@@ -451,312 +416,14 @@ func (c *Client) findRuleByPort(port int) *protocol.ProxyRule {
return nil
}
-// handlePluginConfig 处理插件配置同步
-func (c *Client) handlePluginConfig(msg *protocol.Message) {
- var cfg protocol.PluginConfigSync
- if err := msg.ParsePayload(&cfg); err != nil {
- c.logErrorf("Parse plugin config error: %v", err)
- return
- }
- c.logf("Received config for plugin: %s", cfg.PluginName)
- // 应用配置到插件
- if c.pluginRegistry != nil {
- handler, err := c.pluginRegistry.GetClient(cfg.PluginName)
- if err != nil {
- c.logWarnf("Plugin %s not found: %v", cfg.PluginName, err)
- return
- }
- if err := handler.Init(cfg.Config); err != nil {
- c.logErrorf("Plugin %s init error: %v", cfg.PluginName, err)
- return
- }
- c.logf("Plugin %s config applied", cfg.PluginName)
- }
-}
-// handleClientPluginStart 处理客户端插件启动请求
-func (c *Client) handleClientPluginStart(stream net.Conn, msg *protocol.Message) {
- defer stream.Close()
- var req protocol.ClientPluginStartRequest
- if err := msg.ParsePayload(&req); err != nil {
- c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", err.Error())
- return
- }
- c.logf("Starting plugin %s for rule %s", req.PluginName, req.RuleName)
- // 获取插件
- if c.pluginRegistry == nil {
- c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", "plugin registry not set")
- return
- }
- handler, err := c.pluginRegistry.GetClient(req.PluginName)
- if err != nil {
- c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", err.Error())
- return
- }
- // 初始化并启动
- if err := handler.Init(req.Config); err != nil {
- c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", err.Error())
- return
- }
-
- localAddr, err := handler.Start()
- if err != nil {
- c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", err.Error())
- return
- }
-
- // 保存运行中的插件
- key := req.PluginName + ":" + req.RuleName
- c.pluginMu.Lock()
- c.runningPlugins[key] = handler
- c.pluginMu.Unlock()
-
- c.logf("Plugin %s started at %s", req.PluginName, localAddr)
- c.sendPluginStatus(stream, req.PluginName, req.RuleName, true, localAddr, "")
-}
-
-// sendPluginStatus 发送插件状态响应
-func (c *Client) sendPluginStatus(stream net.Conn, pluginName, ruleName string, running bool, localAddr, errMsg string) {
- resp := protocol.ClientPluginStatusResponse{
- PluginName: pluginName,
- RuleName: ruleName,
- Running: running,
- LocalAddr: localAddr,
- Error: errMsg,
- }
- msg, _ := protocol.NewMessage(protocol.MsgTypeClientPluginStatus, resp)
- protocol.WriteMessage(stream, msg)
-}
-
-// handleClientPluginConn 处理客户端插件连接
-func (c *Client) handleClientPluginConn(stream net.Conn, msg *protocol.Message) {
- var req protocol.ClientPluginConnRequest
- if err := msg.ParsePayload(&req); err != nil {
- stream.Close()
- return
- }
-
- c.pluginMu.RLock()
- var handler plugin.ClientPlugin
- var ok bool
-
- // 优先使用 PluginID 查找
- if req.PluginID != "" {
- handler, ok = c.runningPlugins[req.PluginID]
- }
-
- // 如果没找到,回退到 pluginName:ruleName
- if !ok {
- key := req.PluginName + ":" + req.RuleName
- handler, ok = c.runningPlugins[key]
- }
- c.pluginMu.RUnlock()
-
- if !ok {
- c.logWarnf("Plugin %s (ID: %s) not running", req.PluginName, req.PluginID)
- stream.Close()
- return
- }
-
- // 让插件处理连接
- handler.HandleConn(stream)
-}
-
-// handleJSPluginInstall 处理 JS 插件安装请求
-func (c *Client) handleJSPluginInstall(stream net.Conn, msg *protocol.Message) {
- defer stream.Close()
-
- var req protocol.JSPluginInstallRequest
- if err := msg.ParsePayload(&req); err != nil {
- c.sendJSPluginResult(stream, "", false, err.Error())
- return
- }
-
- c.logf("Installing JS plugin: %s (ID: %s)", req.PluginName, req.PluginID)
-
- // 使用 PluginID 作为 key(如果有),否则回退到 pluginName:ruleName
- key := req.PluginID
- if key == "" {
- key = req.PluginName + ":" + req.RuleName
- }
-
- // 如果插件已经在运行,先停止它
- c.pluginMu.Lock()
- if existingHandler, ok := c.runningPlugins[key]; ok {
- c.logf("Stopping existing plugin %s before reinstall", key)
- if err := existingHandler.Stop(); err != nil {
- c.logErrorf("Stop existing plugin error: %v", err)
- }
- delete(c.runningPlugins, key)
- }
- c.pluginMu.Unlock()
-
- // 验证官方签名
- if err := c.verifyJSPluginSignature(req.PluginName, req.Source, req.Signature); err != nil {
- c.logErrorf("JS plugin %s signature verification failed: %v", req.PluginName, err)
- c.sendJSPluginResult(stream, req.PluginName, false, "signature verification failed: "+err.Error())
- return
- }
- c.logf("JS plugin %s signature verified", req.PluginName)
-
- // 创建 JS 插件
- jsPlugin, err := script.NewJSPlugin(req.PluginName, req.Source)
- if err != nil {
- c.sendJSPluginResult(stream, req.PluginName, false, err.Error())
- return
- }
-
- // 注册到 registry
- if c.pluginRegistry != nil {
- c.pluginRegistry.RegisterClient(jsPlugin)
- }
-
- c.logf("JS plugin %s installed", req.PluginName)
-
- // 保存版本信息(防止降级攻击)
- if c.versionStore != nil {
- signed, _ := sign.DecodeSignedPlugin(req.Signature)
- if signed != nil {
- c.versionStore.SetVersion(req.PluginName, signed.Payload.Version)
- }
- }
-
- // 先启动插件,再发送安装结果
- // 这样服务端收到结果后启动监听器时,客户端插件已经准备好了
- if req.AutoStart {
- c.startJSPlugin(jsPlugin, req)
- }
-
- c.sendJSPluginResult(stream, req.PluginName, true, "")
-}
-
-// sendJSPluginResult 发送 JS 插件安装结果
-func (c *Client) sendJSPluginResult(stream net.Conn, name string, success bool, errMsg string) {
- result := protocol.JSPluginInstallResult{
- PluginName: name,
- Success: success,
- Error: errMsg,
- }
- msg, _ := protocol.NewMessage(protocol.MsgTypeJSPluginResult, result)
- protocol.WriteMessage(stream, msg)
-}
-
-// startJSPlugin 启动 JS 插件
-func (c *Client) startJSPlugin(handler plugin.ClientPlugin, req protocol.JSPluginInstallRequest) {
- if err := handler.Init(req.Config); err != nil {
- c.logErrorf("JS plugin %s init error: %v", req.PluginName, err)
- return
- }
-
- localAddr, err := handler.Start()
- if err != nil {
- c.logErrorf("JS plugin %s start error: %v", req.PluginName, err)
- return
- }
-
- // 使用 PluginID 作为 key(如果有),否则回退到 pluginName:ruleName
- key := req.PluginID
- if key == "" {
- key = req.PluginName + ":" + req.RuleName
- }
- c.pluginMu.Lock()
- c.runningPlugins[key] = handler
- c.pluginMu.Unlock()
-
- c.logf("JS plugin %s (ID: %s) started at %s", req.PluginName, req.PluginID, localAddr)
-}
-
-// verifyJSPluginSignature 验证 JS 插件签名
-func (c *Client) verifyJSPluginSignature(pluginName, source, signature string) error {
- if signature == "" {
- return fmt.Errorf("missing signature")
- }
-
- // 解码签名
- signed, err := sign.DecodeSignedPlugin(signature)
- if err != nil {
- return fmt.Errorf("decode signature: %w", err)
- }
-
- // 根据 KeyID 获取对应公钥
- pubKey, err := sign.GetPublicKeyByID(signed.Payload.KeyID)
- if err != nil {
- return fmt.Errorf("get public key: %w", err)
- }
-
- // 验证插件名称匹配
- if signed.Payload.Name != pluginName {
- return fmt.Errorf("plugin name mismatch: expected %s, got %s",
- pluginName, signed.Payload.Name)
- }
-
- // 验证签名和源码哈希
- if err := sign.VerifyPlugin(pubKey, signed, source); err != nil {
- return err
- }
-
- // 检查版本降级攻击
- if c.versionStore != nil {
- currentVer := c.versionStore.GetVersion(pluginName)
- if currentVer != "" {
- cmp := sign.CompareVersions(signed.Payload.Version, currentVer)
- if cmp < 0 {
- return fmt.Errorf("version downgrade rejected: %s < %s",
- signed.Payload.Version, currentVer)
- }
- }
- }
-
- return nil
-}
-
-// handleClientPluginStop 处理客户端插件停止请求
-func (c *Client) handleClientPluginStop(stream net.Conn, msg *protocol.Message) {
- defer stream.Close()
-
- var req protocol.ClientPluginStopRequest
- if err := msg.ParsePayload(&req); err != nil {
- c.sendPluginStatus(stream, req.PluginName, req.RuleName, true, "", err.Error())
- return
- }
-
- c.pluginMu.Lock()
- var handler plugin.ClientPlugin
- var key string
- var ok bool
-
- // 优先使用 PluginID 查找
- if req.PluginID != "" {
- handler, ok = c.runningPlugins[req.PluginID]
- if ok {
- key = req.PluginID
- }
- }
-
- // 如果没找到,回退到 pluginName:ruleName
- if !ok {
- key = req.PluginName + ":" + req.RuleName
- handler, ok = c.runningPlugins[key]
- }
-
- if ok {
- if err := handler.Stop(); err != nil {
- c.logErrorf("Plugin %s stop error: %v", key, err)
- }
- delete(c.runningPlugins, key)
- }
- c.pluginMu.Unlock()
-
- c.logf("Plugin %s stopped", key)
- c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", "")
-}
// handleClientRestart 处理客户端重启请求
func (c *Client) handleClientRestart(stream net.Conn, msg *protocol.Message) {
@@ -776,99 +443,13 @@ func (c *Client) handleClientRestart(stream net.Conn, msg *protocol.Message) {
protocol.WriteMessage(stream, respMsg)
// 停止所有运行中的插件
- c.pluginMu.Lock()
- for key, handler := range c.runningPlugins {
- c.logf("Stopping plugin %s for restart", key)
- handler.Stop()
- }
- c.runningPlugins = make(map[string]plugin.ClientPlugin)
- c.pluginMu.Unlock()
-
// 关闭会话(会触发重连)
if c.session != nil {
c.session.Close()
}
}
-// handlePluginConfigUpdate 处理插件配置更新请求
-func (c *Client) handlePluginConfigUpdate(stream net.Conn, msg *protocol.Message) {
- defer stream.Close()
- var req protocol.PluginConfigUpdateRequest
- if err := msg.ParsePayload(&req); err != nil {
- c.sendPluginConfigUpdateResult(stream, req.PluginName, req.RuleName, false, err.Error())
- return
- }
-
- c.pluginMu.RLock()
- var handler plugin.ClientPlugin
- var key string
- var ok bool
-
- // 优先使用 PluginID 查找
- if req.PluginID != "" {
- handler, ok = c.runningPlugins[req.PluginID]
- if ok {
- key = req.PluginID
- }
- }
-
- // 如果没找到,回退到 pluginName:ruleName
- if !ok {
- key = req.PluginName + ":" + req.RuleName
- handler, ok = c.runningPlugins[key]
- }
- c.pluginMu.RUnlock()
-
- c.logf("Config update for plugin %s", key)
-
- if !ok {
- c.sendPluginConfigUpdateResult(stream, req.PluginName, req.RuleName, false, "plugin not running")
- return
- }
-
- if req.Restart {
- // 停止并重启插件
- c.pluginMu.Lock()
- if err := handler.Stop(); err != nil {
- c.logErrorf("Plugin %s stop error: %v", key, err)
- }
- delete(c.runningPlugins, key)
- c.pluginMu.Unlock()
-
- // 重新初始化和启动
- if err := handler.Init(req.Config); err != nil {
- c.sendPluginConfigUpdateResult(stream, req.PluginName, req.RuleName, false, err.Error())
- return
- }
-
- localAddr, err := handler.Start()
- if err != nil {
- c.sendPluginConfigUpdateResult(stream, req.PluginName, req.RuleName, false, err.Error())
- return
- }
-
- c.pluginMu.Lock()
- c.runningPlugins[key] = handler
- c.pluginMu.Unlock()
-
- c.logf("Plugin %s restarted at %s with new config", key, localAddr)
- }
-
- c.sendPluginConfigUpdateResult(stream, req.PluginName, req.RuleName, true, "")
-}
-
-// sendPluginConfigUpdateResult 发送插件配置更新结果
-func (c *Client) sendPluginConfigUpdateResult(stream net.Conn, pluginName, ruleName string, success bool, errMsg string) {
- result := protocol.PluginConfigUpdateResponse{
- PluginName: pluginName,
- RuleName: ruleName,
- Success: success,
- Error: errMsg,
- }
- msg, _ := protocol.NewMessage(protocol.MsgTypePluginConfigUpdate, result)
- protocol.WriteMessage(stream, msg)
-}
// handleUpdateDownload 处理更新下载请求
func (c *Client) handleUpdateDownload(stream net.Conn, msg *protocol.Message) {
@@ -957,9 +538,6 @@ func (c *Client) performSelfUpdate(downloadURL string) error {
c.logf("Will install to fallback path: %s", targetPath)
}
- // 停止所有插件
- c.stopAllPlugins()
-
if fallbackDir == "" {
// 原地替换:备份 → 复制 → 清理
backupPath := currentPath + ".bak"
@@ -1030,16 +608,6 @@ func (c *Client) checkUpdatePermissions(execPath string) error {
return nil
}
-// stopAllPlugins 停止所有运行中的插件
-func (c *Client) stopAllPlugins() {
- c.pluginMu.Lock()
- for key, handler := range c.runningPlugins {
- c.logf("Stopping plugin %s for update", key)
- handler.Stop()
- }
- c.runningPlugins = make(map[string]plugin.ClientPlugin)
- c.pluginMu.Unlock()
-}
// performWindowsClientUpdate Windows 平台更新
func performWindowsClientUpdate(newFile, currentPath, serverAddr, token, id string) error {
@@ -1092,33 +660,6 @@ func restartClientProcess(path, serverAddr, token, id string) {
os.Exit(0)
}
-// handlePluginStatusQuery 处理插件状态查询
-func (c *Client) handlePluginStatusQuery(stream net.Conn, msg *protocol.Message) {
- defer stream.Close()
-
- c.pluginMu.RLock()
- plugins := make([]protocol.PluginStatusEntry, 0, len(c.runningPlugins))
- for key, handler := range c.runningPlugins {
- // 从插件的 Metadata 获取真正的插件名称
- pluginName := handler.Metadata().Name
- // 如果 Metadata 没有名称,回退到从 key 解析
- if pluginName == "" {
- parts := strings.SplitN(key, ":", 2)
- pluginName = parts[0]
- }
- plugins = append(plugins, protocol.PluginStatusEntry{
- PluginName: pluginName,
- Running: true,
- })
- }
- c.pluginMu.RUnlock()
-
- resp := protocol.PluginStatusQueryResponse{
- Plugins: plugins,
- }
- respMsg, _ := protocol.NewMessage(protocol.MsgTypePluginStatusQueryResp, resp)
- protocol.WriteMessage(stream, respMsg)
-}
// handleLogRequest 处理日志请求
func (c *Client) handleLogRequest(stream net.Conn, msg *protocol.Message) {
@@ -1196,72 +737,7 @@ func (c *Client) handleLogStop(stream net.Conn, msg *protocol.Message) {
c.logger.Unsubscribe(req.SessionID)
}
-// handlePluginAPIRequest 处理插件 API 请求
-func (c *Client) handlePluginAPIRequest(stream net.Conn, msg *protocol.Message) {
- defer stream.Close()
- var req protocol.PluginAPIRequest
- if err := msg.ParsePayload(&req); err != nil {
- c.sendPluginAPIResponse(stream, 400, nil, "", "invalid request: "+err.Error())
- return
- }
-
- c.logf("Plugin API request: %s %s for plugin %s (ID: %s)", req.Method, req.Path, req.PluginName, req.PluginID)
-
- // 查找运行中的插件
- c.pluginMu.RLock()
- var handler plugin.ClientPlugin
-
- // 优先使用 PluginID 查找
- if req.PluginID != "" {
- handler = c.runningPlugins[req.PluginID]
- }
-
- // 如果没找到,尝试通过 PluginName 匹配(向后兼容)
- if handler == nil && req.PluginName != "" {
- for key, p := range c.runningPlugins {
- // key 可能是 PluginID 或 "pluginName:ruleName" 格式
- if strings.HasPrefix(key, req.PluginName+":") {
- handler = p
- break
- }
- }
- }
- c.pluginMu.RUnlock()
-
- if handler == nil {
- c.sendPluginAPIResponse(stream, 404, nil, "", "plugin not running: "+req.PluginName)
- return
- }
-
- // 类型断言为 JSPlugin
- jsPlugin, ok := handler.(*script.JSPlugin)
- if !ok {
- c.sendPluginAPIResponse(stream, 500, nil, "", "plugin does not support API routing")
- return
- }
-
- // 调用插件的 API 处理函数
- status, headers, body, err := jsPlugin.HandleAPIRequest(req.Method, req.Path, req.Query, req.Headers, req.Body)
- if err != nil {
- c.sendPluginAPIResponse(stream, 500, nil, "", err.Error())
- return
- }
-
- c.sendPluginAPIResponse(stream, status, headers, body, "")
-}
-
-// sendPluginAPIResponse 发送插件 API 响应
-func (c *Client) sendPluginAPIResponse(stream net.Conn, status int, headers map[string]string, body, errMsg string) {
- resp := protocol.PluginAPIResponse{
- Status: status,
- Headers: headers,
- Body: body,
- Error: errMsg,
- }
- msg, _ := protocol.NewMessage(protocol.MsgTypePluginAPIResponse, resp)
- protocol.WriteMessage(stream, msg)
-}
// handleSystemStatsRequest 处理系统状态请求
func (c *Client) handleSystemStatsRequest(stream net.Conn, msg *protocol.Message) {
diff --git a/internal/server/app/app.go b/internal/server/app/app.go
index c428a84..effaea3 100644
--- a/internal/server/app/app.go
+++ b/internal/server/app/app.go
@@ -16,23 +16,21 @@ var staticFiles embed.FS
// WebServer Web控制台服务
type WebServer struct {
- ClientStore db.ClientStore
- Server router.ServerInterface
- Config *config.ServerConfig
- ConfigPath string
- JSPluginStore db.JSPluginStore
- TrafficStore db.TrafficStore
+ ClientStore db.ClientStore
+ Server router.ServerInterface
+ Config *config.ServerConfig
+ ConfigPath string
+ TrafficStore db.TrafficStore
}
// NewWebServer 创建Web服务
func NewWebServer(cs db.ClientStore, srv router.ServerInterface, cfg *config.ServerConfig, cfgPath string, store db.Store) *WebServer {
return &WebServer{
- ClientStore: cs,
- Server: srv,
- Config: cfg,
- ConfigPath: cfgPath,
- JSPluginStore: store,
- TrafficStore: store,
+ ClientStore: cs,
+ Server: srv,
+ Config: cfg,
+ ConfigPath: cfgPath,
+ TrafficStore: store,
}
}
@@ -107,11 +105,6 @@ func (w *WebServer) SaveConfig() error {
return config.SaveServerConfig(w.ConfigPath, w.Config)
}
-// GetJSPluginStore 获取 JS 插件存储
-func (w *WebServer) GetJSPluginStore() db.JSPluginStore {
- return w.JSPluginStore
-}
-
// GetTrafficStore 获取流量存储
func (w *WebServer) GetTrafficStore() db.TrafficStore {
return w.TrafficStore
diff --git a/internal/server/config/config.go b/internal/server/config/config.go
index ff259e0..98478cd 100644
--- a/internal/server/config/config.go
+++ b/internal/server/config/config.go
@@ -13,33 +13,16 @@ type ServerConfig struct {
Server ServerSettings `yaml:"server"`
}
-// PluginStoreSettings 插件仓库设置
-type PluginStoreSettings struct {
- URL string `yaml:"url"` // 插件仓库 URL,为空则使用默认值
-}
-
-// 默认插件仓库 URL
-const DefaultPluginStoreURL = "https://git.92coco.cn/flik/GoTunnel-Plugins/raw/branch/main/store.json"
-
-// GetPluginStoreURL 获取插件仓库 URL
-func (s *PluginStoreSettings) GetPluginStoreURL() string {
- if s.URL != "" {
- return s.URL
- }
- return DefaultPluginStoreURL
-}
-
// ServerSettings 服务端设置
type ServerSettings struct {
- BindAddr string `yaml:"bind_addr"`
- BindPort int `yaml:"bind_port"`
- Token string `yaml:"token"`
- HeartbeatSec int `yaml:"heartbeat_sec"`
- HeartbeatTimeout int `yaml:"heartbeat_timeout"`
- DBPath string `yaml:"db_path"`
- TLSDisabled bool `yaml:"tls_disabled"`
- Web WebSettings `yaml:"web"`
- PluginStore PluginStoreSettings `yaml:"plugin_store"`
+ BindAddr string `yaml:"bind_addr"`
+ BindPort int `yaml:"bind_port"`
+ Token string `yaml:"token"`
+ HeartbeatSec int `yaml:"heartbeat_sec"`
+ HeartbeatTimeout int `yaml:"heartbeat_timeout"`
+ DBPath string `yaml:"db_path"`
+ TLSDisabled bool `yaml:"tls_disabled"`
+ Web WebSettings `yaml:"web"`
}
// WebSettings Web控制台设置
diff --git a/internal/server/db/install_token.go b/internal/server/db/install_token.go
new file mode 100644
index 0000000..0748ce5
--- /dev/null
+++ b/internal/server/db/install_token.go
@@ -0,0 +1,45 @@
+package db
+
+// CreateInstallToken 创建安装token
+func (s *SQLiteStore) CreateInstallToken(token *InstallToken) error {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ _, err := s.db.Exec(`INSERT INTO install_tokens (token, client_id, created_at, used) VALUES (?, ?, ?, ?)`,
+ token.Token, token.ClientID, token.CreatedAt, 0)
+ return err
+}
+
+// GetInstallToken 获取安装token
+func (s *SQLiteStore) GetInstallToken(token string) (*InstallToken, error) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ var t InstallToken
+ var used int
+ err := s.db.QueryRow(`SELECT token, client_id, created_at, used FROM install_tokens WHERE token = ?`, token).
+ Scan(&t.Token, &t.ClientID, &t.CreatedAt, &used)
+ if err != nil {
+ return nil, err
+ }
+ t.Used = used == 1
+ return &t, nil
+}
+
+// MarkTokenUsed 标记token已使用
+func (s *SQLiteStore) MarkTokenUsed(token string) error {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ _, err := s.db.Exec(`UPDATE install_tokens SET used = 1 WHERE token = ?`, token)
+ return err
+}
+
+// DeleteExpiredTokens 删除过期token
+func (s *SQLiteStore) DeleteExpiredTokens(expireTime int64) error {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ _, err := s.db.Exec(`DELETE FROM install_tokens WHERE created_at < ?`, expireTime)
+ return err
+}
diff --git a/internal/server/db/interface.go b/internal/server/db/interface.go
index 601d675..6ad72ae 100644
--- a/internal/server/db/interface.go
+++ b/internal/server/db/interface.go
@@ -2,52 +2,11 @@ package db
import "github.com/gotunnel/pkg/protocol"
-// ConfigField 配置字段定义
-type ConfigField struct {
- Key string `json:"key"`
- Label string `json:"label"`
- Type string `json:"type"`
- Default string `json:"default,omitempty"`
- Required bool `json:"required,omitempty"`
- Options []string `json:"options,omitempty"`
- Description string `json:"description,omitempty"`
-}
-
-// ClientPlugin 客户端已安装的插件
-type ClientPlugin struct {
- ID string `json:"id"` // 插件实例唯一 ID
- Name string `json:"name"`
- Version string `json:"version"`
- Enabled bool `json:"enabled"`
- Running bool `json:"running"` // 运行状态
- Config map[string]string `json:"config,omitempty"` // 插件配置
- RemotePort int `json:"remote_port,omitempty"` // 远程监听端口
- ConfigSchema []ConfigField `json:"config_schema,omitempty"` // 配置模式
- AuthEnabled bool `json:"auth_enabled,omitempty"` // 是否启用认证
- AuthUsername string `json:"auth_username,omitempty"` // 认证用户名
- AuthPassword string `json:"auth_password,omitempty"` // 认证密码
-}
-
// Client 客户端数据
type Client struct {
ID string `json:"id"`
Nickname string `json:"nickname,omitempty"`
Rules []protocol.ProxyRule `json:"rules"`
- Plugins []ClientPlugin `json:"plugins,omitempty"` // 已安装的插件
-}
-
-// JSPlugin JS 插件数据
-type JSPlugin struct {
- Name string `json:"name"`
- Source string `json:"source"`
- Signature string `json:"signature"` // 官方签名 (Base64)
- Description string `json:"description"`
- Author string `json:"author"`
- Version string `json:"version,omitempty"`
- AutoPush []string `json:"auto_push"`
- Config map[string]string `json:"config"`
- AutoStart bool `json:"auto_start"`
- Enabled bool `json:"enabled"`
}
// ClientStore 客户端存储接口
@@ -62,20 +21,9 @@ type ClientStore interface {
Close() error
}
-// JSPluginStore JS 插件存储接口
-type JSPluginStore interface {
- GetAllJSPlugins() ([]JSPlugin, error)
- GetJSPlugin(name string) (*JSPlugin, error)
- SaveJSPlugin(p *JSPlugin) error
- DeleteJSPlugin(name string) error
- SetJSPluginEnabled(name string, enabled bool) error
- UpdateJSPluginConfig(name string, config map[string]string) error
-}
-
// Store 统一存储接口
type Store interface {
ClientStore
- JSPluginStore
TrafficStore
Close() error
}
@@ -94,3 +42,19 @@ type TrafficStore interface {
Get24HourTraffic() (inbound, outbound int64, err error)
GetHourlyTraffic(hours int) ([]TrafficRecord, error)
}
+
+// InstallToken 安装token
+type InstallToken struct {
+ Token string `json:"token"`
+ ClientID string `json:"client_id"`
+ CreatedAt int64 `json:"created_at"`
+ Used bool `json:"used"`
+}
+
+// InstallTokenStore 安装token存储接口
+type InstallTokenStore interface {
+ CreateInstallToken(token *InstallToken) error
+ GetInstallToken(token string) (*InstallToken, error)
+ MarkTokenUsed(token string) error
+ DeleteExpiredTokens(expireTime int64) error
+}
diff --git a/internal/server/db/sqlite.go b/internal/server/db/sqlite.go
index b5d3e86..183670d 100644
--- a/internal/server/db/sqlite.go
+++ b/internal/server/db/sqlite.go
@@ -40,8 +40,7 @@ func (s *SQLiteStore) init() error {
CREATE TABLE IF NOT EXISTS clients (
id TEXT PRIMARY KEY,
nickname TEXT NOT NULL DEFAULT '',
- rules TEXT NOT NULL DEFAULT '[]',
- plugins TEXT NOT NULL DEFAULT '[]'
+ rules TEXT NOT NULL DEFAULT '[]'
)
`)
if err != nil {
@@ -50,36 +49,6 @@ func (s *SQLiteStore) init() error {
// 迁移:添加 nickname 列
s.db.Exec(`ALTER TABLE clients ADD COLUMN nickname TEXT NOT NULL DEFAULT ''`)
- // 迁移:添加 plugins 列
- s.db.Exec(`ALTER TABLE clients ADD COLUMN plugins TEXT NOT NULL DEFAULT '[]'`)
-
- // 创建 JS 插件表
- _, err = s.db.Exec(`
- CREATE TABLE IF NOT EXISTS js_plugins (
- name TEXT PRIMARY KEY,
- source TEXT NOT NULL,
- signature TEXT NOT NULL DEFAULT '',
- description TEXT,
- author TEXT,
- version TEXT DEFAULT '',
- auto_push TEXT NOT NULL DEFAULT '[]',
- config TEXT NOT NULL DEFAULT '{}',
- auto_start INTEGER DEFAULT 1,
- enabled INTEGER DEFAULT 1,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
- )
- `)
- if err != nil {
- return err
- }
-
- // 迁移:添加 signature 列
- s.db.Exec(`ALTER TABLE js_plugins ADD COLUMN signature TEXT NOT NULL DEFAULT ''`)
- // 迁移:添加 version 列
- s.db.Exec(`ALTER TABLE js_plugins ADD COLUMN version TEXT DEFAULT ''`)
- // 迁移:添加 updated_at 列
- s.db.Exec(`ALTER TABLE js_plugins ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP`)
// 创建流量统计表
_, err = s.db.Exec(`
@@ -108,6 +77,19 @@ func (s *SQLiteStore) init() error {
// 初始化总流量记录
s.db.Exec(`INSERT OR IGNORE INTO traffic_total (id, inbound, outbound) VALUES (1, 0, 0)`)
+ // 创建安装token表
+ _, err = s.db.Exec(`
+ CREATE TABLE IF NOT EXISTS install_tokens (
+ token TEXT PRIMARY KEY,
+ client_id TEXT NOT NULL,
+ created_at INTEGER NOT NULL,
+ used INTEGER NOT NULL DEFAULT 0
+ )
+ `)
+ if err != nil {
+ return err
+ }
+
return nil
}
@@ -121,7 +103,7 @@ func (s *SQLiteStore) GetAllClients() ([]Client, error) {
s.mu.RLock()
defer s.mu.RUnlock()
- rows, err := s.db.Query(`SELECT id, nickname, rules, plugins FROM clients`)
+ rows, err := s.db.Query(`SELECT id, nickname, rules FROM clients`)
if err != nil {
return nil, err
}
@@ -130,16 +112,13 @@ func (s *SQLiteStore) GetAllClients() ([]Client, error) {
var clients []Client
for rows.Next() {
var c Client
- var rulesJSON, pluginsJSON string
- if err := rows.Scan(&c.ID, &c.Nickname, &rulesJSON, &pluginsJSON); err != nil {
+ var rulesJSON string
+ if err := rows.Scan(&c.ID, &c.Nickname, &rulesJSON); err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(rulesJSON), &c.Rules); err != nil {
c.Rules = []protocol.ProxyRule{}
}
- if err := json.Unmarshal([]byte(pluginsJSON), &c.Plugins); err != nil {
- c.Plugins = []ClientPlugin{}
- }
clients = append(clients, c)
}
return clients, nil
@@ -151,17 +130,14 @@ func (s *SQLiteStore) GetClient(id string) (*Client, error) {
defer s.mu.RUnlock()
var c Client
- var rulesJSON, pluginsJSON string
- err := s.db.QueryRow(`SELECT id, nickname, rules, plugins FROM clients WHERE id = ?`, id).Scan(&c.ID, &c.Nickname, &rulesJSON, &pluginsJSON)
+ var rulesJSON string
+ err := s.db.QueryRow(`SELECT id, nickname, rules FROM clients WHERE id = ?`, id).Scan(&c.ID, &c.Nickname, &rulesJSON)
if err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(rulesJSON), &c.Rules); err != nil {
c.Rules = []protocol.ProxyRule{}
}
- if err := json.Unmarshal([]byte(pluginsJSON), &c.Plugins); err != nil {
- c.Plugins = []ClientPlugin{}
- }
return &c, nil
}
@@ -174,12 +150,8 @@ func (s *SQLiteStore) CreateClient(c *Client) error {
if err != nil {
return err
}
- pluginsJSON, err := json.Marshal(c.Plugins)
- if err != nil {
- return err
- }
- _, err = s.db.Exec(`INSERT INTO clients (id, nickname, rules, plugins) VALUES (?, ?, ?, ?)`,
- c.ID, c.Nickname, string(rulesJSON), string(pluginsJSON))
+ _, err = s.db.Exec(`INSERT INTO clients (id, nickname, rules) VALUES (?, ?, ?)`,
+ c.ID, c.Nickname, string(rulesJSON))
return err
}
@@ -192,12 +164,8 @@ func (s *SQLiteStore) UpdateClient(c *Client) error {
if err != nil {
return err
}
- pluginsJSON, err := json.Marshal(c.Plugins)
- if err != nil {
- return err
- }
- _, err = s.db.Exec(`UPDATE clients SET nickname = ?, rules = ?, plugins = ? WHERE id = ?`,
- c.Nickname, string(rulesJSON), string(pluginsJSON), c.ID)
+ _, err = s.db.Exec(`UPDATE clients SET nickname = ?, rules = ? WHERE id = ?`,
+ c.Nickname, string(rulesJSON), c.ID)
return err
}
@@ -229,121 +197,6 @@ func (s *SQLiteStore) GetClientRules(id string) ([]protocol.ProxyRule, error) {
return c.Rules, nil
}
-// ========== JS 插件存储方法 ==========
-
-// GetAllJSPlugins 获取所有 JS 插件
-func (s *SQLiteStore) GetAllJSPlugins() ([]JSPlugin, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- rows, err := s.db.Query(`
- SELECT name, source, signature, description, author, version, auto_push, config, auto_start, enabled
- FROM js_plugins
- `)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- var plugins []JSPlugin
- for rows.Next() {
- var p JSPlugin
- var autoPushJSON, configJSON string
- var version sql.NullString
- var autoStart, enabled int
- err := rows.Scan(&p.Name, &p.Source, &p.Signature, &p.Description, &p.Author,
- &version, &autoPushJSON, &configJSON, &autoStart, &enabled)
- if err != nil {
- return nil, err
- }
- p.Version = version.String
- json.Unmarshal([]byte(autoPushJSON), &p.AutoPush)
- json.Unmarshal([]byte(configJSON), &p.Config)
- p.AutoStart = autoStart == 1
- p.Enabled = enabled == 1
- plugins = append(plugins, p)
- }
- return plugins, nil
-}
-
-// GetJSPlugin 获取单个 JS 插件
-func (s *SQLiteStore) GetJSPlugin(name string) (*JSPlugin, error) {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
- var p JSPlugin
- var autoPushJSON, configJSON string
- var version sql.NullString
- var autoStart, enabled int
- err := s.db.QueryRow(`
- SELECT name, source, signature, description, author, version, auto_push, config, auto_start, enabled
- FROM js_plugins WHERE name = ?
- `, name).Scan(&p.Name, &p.Source, &p.Signature, &p.Description, &p.Author,
- &version, &autoPushJSON, &configJSON, &autoStart, &enabled)
- if err != nil {
- return nil, err
- }
- p.Version = version.String
- json.Unmarshal([]byte(autoPushJSON), &p.AutoPush)
- json.Unmarshal([]byte(configJSON), &p.Config)
- p.AutoStart = autoStart == 1
- p.Enabled = enabled == 1
- return &p, nil
-}
-
-// SaveJSPlugin 保存 JS 插件
-func (s *SQLiteStore) SaveJSPlugin(p *JSPlugin) error {
- s.mu.Lock()
- defer s.mu.Unlock()
-
- autoPushJSON, _ := json.Marshal(p.AutoPush)
- configJSON, _ := json.Marshal(p.Config)
- autoStart, enabled := 0, 0
- if p.AutoStart {
- autoStart = 1
- }
- if p.Enabled {
- enabled = 1
- }
-
- _, err := s.db.Exec(`
- INSERT OR REPLACE INTO js_plugins
- (name, source, signature, description, author, version, auto_push, config, auto_start, enabled, updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
- `, p.Name, p.Source, p.Signature, p.Description, p.Author, p.Version,
- string(autoPushJSON), string(configJSON), autoStart, enabled)
- return err
-}
-
-// DeleteJSPlugin 删除 JS 插件
-func (s *SQLiteStore) DeleteJSPlugin(name string) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- _, err := s.db.Exec(`DELETE FROM js_plugins WHERE name = ?`, name)
- return err
-}
-
-// SetJSPluginEnabled 设置 JS 插件启用状态
-func (s *SQLiteStore) SetJSPluginEnabled(name string, enabled bool) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- val := 0
- if enabled {
- val = 1
- }
- _, err := s.db.Exec(`UPDATE js_plugins SET enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?`, val, name)
- return err
-}
-
-// UpdateJSPluginConfig 更新 JS 插件配置
-func (s *SQLiteStore) UpdateJSPluginConfig(name string, config map[string]string) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- configJSON, _ := json.Marshal(config)
- _, err := s.db.Exec(`UPDATE js_plugins SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?`, string(configJSON), name)
- return err
-}
-
// ========== 流量统计方法 ==========
// getHourTimestamp 获取当前小时的时间戳
diff --git a/internal/server/plugin/manager.go b/internal/server/plugin/manager.go
deleted file mode 100644
index d1daf28..0000000
--- a/internal/server/plugin/manager.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package plugin
-
-import (
- "sync"
-
- "github.com/gotunnel/pkg/plugin"
-)
-
-// Manager 服务端 plugin 管理器
-type Manager struct {
- registry *plugin.Registry
- mu sync.RWMutex
-}
-
-// NewManager 创建 plugin 管理器
-func NewManager() (*Manager, error) {
- registry := plugin.NewRegistry()
-
- m := &Manager{
- registry: registry,
- }
-
- return m, nil
-}
-
-// ListPlugins 返回所有插件
-func (m *Manager) ListPlugins() []plugin.Info {
- return m.registry.List()
-}
-
-// GetRegistry 返回插件注册表
-func (m *Manager) GetRegistry() *plugin.Registry {
- return m.registry
-}
diff --git a/internal/server/router/dto/client.go b/internal/server/router/dto/client.go
index d4e576f..93ddc41 100644
--- a/internal/server/router/dto/client.go
+++ b/internal/server/router/dto/client.go
@@ -1,7 +1,6 @@
package dto
import (
- "github.com/gotunnel/internal/server/db"
"github.com/gotunnel/pkg/protocol"
)
@@ -17,7 +16,6 @@ type CreateClientRequest struct {
type UpdateClientRequest struct {
Nickname string `json:"nickname" binding:"max=128" example:"My Client"`
Rules []protocol.ProxyRule `json:"rules"`
- Plugins []db.ClientPlugin `json:"plugins"`
}
// ClientResponse 客户端详情响应
@@ -26,7 +24,6 @@ type ClientResponse struct {
ID string `json:"id" example:"client-001"`
Nickname string `json:"nickname,omitempty" example:"My Client"`
Rules []protocol.ProxyRule `json:"rules"`
- Plugins []db.ClientPlugin `json:"plugins,omitempty"`
Online bool `json:"online" example:"true"`
LastPing string `json:"last_ping,omitempty" example:"2025-01-02T10:30:00Z"`
RemoteAddr string `json:"remote_addr,omitempty" example:"192.168.1.100:54321"`
@@ -47,17 +44,3 @@ type ClientListItem struct {
OS string `json:"os,omitempty" example:"linux"`
Arch string `json:"arch,omitempty" example:"amd64"`
}
-
-// InstallPluginsRequest 安装插件到客户端请求
-// @Description 安装插件到指定客户端
-type InstallPluginsRequest struct {
- Plugins []string `json:"plugins" binding:"required,min=1,dive,required" example:"socks5,http-proxy"`
-}
-
-// ClientPluginActionRequest 客户端插件操作请求
-// @Description 对客户端插件执行操作
-type ClientPluginActionRequest struct {
- RuleName string `json:"rule_name"`
- Config map[string]string `json:"config,omitempty"`
- Restart bool `json:"restart"`
-}
diff --git a/internal/server/router/dto/plugin.go b/internal/server/router/dto/plugin.go
deleted file mode 100644
index d1b1abe..0000000
--- a/internal/server/router/dto/plugin.go
+++ /dev/null
@@ -1,119 +0,0 @@
-package dto
-
-// PluginConfigRequest 更新插件配置请求
-// @Description 更新客户端插件配置
-type PluginConfigRequest struct {
- Config map[string]string `json:"config" binding:"required"`
-}
-
-// PluginConfigResponse 插件配置响应
-// @Description 插件配置详情
-type PluginConfigResponse struct {
- PluginName string `json:"plugin_name"`
- Schema []ConfigField `json:"schema"`
- Config map[string]string `json:"config"`
-}
-
-// ConfigField 配置字段定义
-// @Description 配置表单字段
-type ConfigField struct {
- Key string `json:"key"`
- Label string `json:"label"`
- Type string `json:"type"`
- Default string `json:"default,omitempty"`
- Required bool `json:"required,omitempty"`
- Options []string `json:"options,omitempty"`
- Description string `json:"description,omitempty"`
-}
-
-// RuleSchema 规则表单模式
-// @Description 代理规则的配置模式
-type RuleSchema struct {
- NeedsLocalAddr bool `json:"needs_local_addr"`
- ExtraFields []ConfigField `json:"extra_fields,omitempty"`
-}
-
-// PluginInfo 插件信息
-// @Description 服务端插件信息
-type PluginInfo struct {
- Name string `json:"name"`
- Version string `json:"version"`
- Type string `json:"type"`
- Description string `json:"description"`
- Source string `json:"source"`
- Icon string `json:"icon,omitempty"`
- Enabled bool `json:"enabled"`
- RuleSchema *RuleSchema `json:"rule_schema,omitempty"`
-}
-
-// JSPluginCreateRequest 创建 JS 插件请求
-// @Description 创建新的 JS 插件
-type JSPluginCreateRequest struct {
- Name string `json:"name" binding:"required,min=1,max=64"`
- Source string `json:"source" binding:"required"`
- Signature string `json:"signature"`
- Description string `json:"description" binding:"max=500"`
- Author string `json:"author" binding:"max=64"`
- Config map[string]string `json:"config"`
- AutoStart bool `json:"auto_start"`
-}
-
-// JSPluginUpdateRequest 更新 JS 插件请求
-// @Description 更新 JS 插件
-type JSPluginUpdateRequest struct {
- Source string `json:"source"`
- Signature string `json:"signature"`
- Description string `json:"description" binding:"max=500"`
- Author string `json:"author" binding:"max=64"`
- Config map[string]string `json:"config"`
- AutoStart bool `json:"auto_start"`
- Enabled bool `json:"enabled"`
-}
-
-// JSPluginInstallRequest JS 插件安装请求
-// @Description 安装 JS 插件到客户端
-type JSPluginInstallRequest struct {
- PluginName string `json:"plugin_name" binding:"required"`
- Source string `json:"source" binding:"required"`
- Signature string `json:"signature"`
- RuleName string `json:"rule_name"`
- RemotePort int `json:"remote_port"`
- Config map[string]string `json:"config"`
- AutoStart bool `json:"auto_start"`
-}
-
-// StorePluginInfo 扩展商店插件信息
-// @Description 插件商店中的插件信息
-type StorePluginInfo struct {
- Name string `json:"name"`
- Version string `json:"version"`
- Type string `json:"type"`
- Description string `json:"description"`
- Author string `json:"author"`
- Icon string `json:"icon,omitempty"`
- DownloadURL string `json:"download_url,omitempty"`
- SignatureURL string `json:"signature_url,omitempty"`
- ConfigSchema []ConfigField `json:"config_schema,omitempty"`
-}
-
-// StoreInstallRequest 从商店安装插件请求
-// @Description 从插件商店安装插件到客户端
-type StoreInstallRequest struct {
- PluginName string `json:"plugin_name" binding:"required"`
- Version string `json:"version"`
- DownloadURL string `json:"download_url" binding:"required,url"`
- SignatureURL string `json:"signature_url" binding:"required,url"`
- ClientID string `json:"client_id" binding:"required"`
- RemotePort int `json:"remote_port"`
- ConfigSchema []ConfigField `json:"config_schema,omitempty"`
- // HTTP Basic Auth 配置
- AuthEnabled bool `json:"auth_enabled,omitempty"`
- AuthUsername string `json:"auth_username,omitempty"`
- AuthPassword string `json:"auth_password,omitempty"`
-}
-
-// JSPluginPushRequest 推送 JS 插件到客户端请求
-// @Description 推送 JS 插件到指定客户端
-type JSPluginPushRequest struct {
- RemotePort int `json:"remote_port"`
-}
diff --git a/internal/server/router/handler/client.go b/internal/server/router/handler/client.go
index a001eb1..39cd1b1 100644
--- a/internal/server/router/handler/client.go
+++ b/internal/server/router/handler/client.go
@@ -6,7 +6,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/internal/server/router/dto"
- "github.com/gotunnel/pkg/protocol"
)
// ClientHandler 客户端处理器
@@ -117,34 +116,6 @@ func (h *ClientHandler) Get(c *gin.Context) {
online, lastPing, remoteAddr, clientName, clientOS, clientArch, clientVersion := h.app.GetServer().GetClientStatus(clientID)
- // 复制插件列表
- plugins := make([]db.ClientPlugin, len(client.Plugins))
- copy(plugins, client.Plugins)
-
- // 如果客户端在线,获取实时插件运行状态
- if online {
- if statusList, err := h.app.GetServer().GetClientPluginStatus(clientID); err == nil {
- // 创建运行中插件的映射
- runningPlugins := make(map[string]bool)
- for _, s := range statusList {
- runningPlugins[s.PluginName] = s.Running
- }
- // 更新插件状态
- for i := range plugins {
- if running, ok := runningPlugins[plugins[i].Name]; ok {
- plugins[i].Running = running
- } else {
- plugins[i].Running = false
- }
- }
- }
- } else {
- // 客户端离线时,所有插件都标记为未运行
- for i := range plugins {
- plugins[i].Running = false
- }
- }
-
// 如果客户端在线且有名称,优先使用在线名称
nickname := client.Nickname
if online && clientName != "" && nickname == "" {
@@ -155,7 +126,6 @@ func (h *ClientHandler) Get(c *gin.Context) {
ID: client.ID,
Nickname: nickname,
Rules: client.Rules,
- Plugins: plugins,
Online: online,
LastPing: lastPing,
RemoteAddr: remoteAddr,
@@ -196,9 +166,6 @@ func (h *ClientHandler) Update(c *gin.Context) {
client.Nickname = req.Nickname
client.Rules = req.Rules
- if req.Plugins != nil {
- client.Plugins = req.Plugins
- }
if err := h.app.GetClientStore().UpdateClient(client); err != nil {
InternalError(c, err.Error())
@@ -301,159 +268,12 @@ func (h *ClientHandler) Restart(c *gin.Context) {
SuccessWithMessage(c, gin.H{"status": "ok"}, "client restart initiated")
}
-// InstallPlugins 安装插件到客户端
-// @Summary 安装插件
-// @Description 将指定插件安装到客户端
-// @Tags 客户端
-// @Accept json
-// @Produce json
-// @Security Bearer
-// @Param id path string true "客户端ID"
-// @Param request body dto.InstallPluginsRequest true "插件列表"
-// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/client/{id}/install-plugins [post]
-func (h *ClientHandler) InstallPlugins(c *gin.Context) {
- clientID := c.Param("id")
- if !h.app.GetServer().IsClientOnline(clientID) {
- ClientNotOnline(c)
- return
- }
-
- var req dto.InstallPluginsRequest
- if !BindJSON(c, &req) {
- return
- }
-
- if err := h.app.GetServer().InstallPluginsToClient(clientID, req.Plugins); err != nil {
- InternalError(c, err.Error())
- return
- }
-
- Success(c, gin.H{"status": "ok"})
-}
-
-// PluginAction 客户端插件操作
-// @Summary 插件操作
-// @Description 对客户端插件执行操作(start/stop/restart/config/delete)
-// @Tags 客户端
-// @Accept json
-// @Produce json
-// @Security Bearer
-// @Param id path string true "客户端ID"
-// @Param pluginID path string true "插件实例ID"
-// @Param action path string true "操作类型" Enums(start, stop, restart, config, delete)
-// @Param request body dto.ClientPluginActionRequest false "操作参数"
-// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/client/{id}/plugin/{pluginID}/{action} [post]
-func (h *ClientHandler) PluginAction(c *gin.Context) {
- clientID := c.Param("id")
- pluginID := c.Param("pluginID")
- action := c.Param("action")
- var req dto.ClientPluginActionRequest
- c.ShouldBindJSON(&req) // 忽略错误,使用默认值
-
- // 通过 pluginID 查找插件信息
- client, err := h.app.GetClientStore().GetClient(clientID)
- if err != nil {
- NotFound(c, "client not found")
- return
- }
-
- var pluginName string
- for _, p := range client.Plugins {
- if p.ID == pluginID {
- pluginName = p.Name
- break
- }
- }
- if pluginName == "" {
- NotFound(c, "plugin not found")
- return
- }
-
- if req.RuleName == "" {
- req.RuleName = pluginName
- }
-
- switch action {
- case "start":
- err = h.app.GetServer().StartClientPlugin(clientID, pluginID, pluginName, req.RuleName)
- case "stop":
- err = h.app.GetServer().StopClientPlugin(clientID, pluginID, pluginName, req.RuleName)
- case "restart":
- err = h.app.GetServer().RestartClientPlugin(clientID, pluginID, pluginName, req.RuleName)
- case "config":
- if req.Config == nil {
- BadRequest(c, "config required")
- return
- }
- err = h.app.GetServer().UpdateClientPluginConfig(clientID, pluginID, pluginName, req.RuleName, req.Config, req.Restart)
- case "delete":
- err = h.deleteClientPlugin(clientID, pluginID)
- default:
- BadRequest(c, "unknown action: "+action)
- return
- }
-
- if err != nil {
- InternalError(c, err.Error())
- return
- }
-
- Success(c, gin.H{
- "status": "ok",
- "action": action,
- "plugin_id": pluginID,
- "plugin": pluginName,
- })
-}
-
-func (h *ClientHandler) deleteClientPlugin(clientID, pluginID string) error {
- client, err := h.app.GetClientStore().GetClient(clientID)
- if err != nil {
- return fmt.Errorf("client not found")
- }
-
- var newPlugins []db.ClientPlugin
- var pluginName string
- var pluginPort int
- found := false
- for _, p := range client.Plugins {
- if p.ID == pluginID {
- found = true
- pluginName = p.Name
- pluginPort = p.RemotePort
- continue
- }
- newPlugins = append(newPlugins, p)
- }
-
- if !found {
- return fmt.Errorf("plugin %s not found", pluginID)
- }
-
- // 删除插件管理的代理规则
- var newRules []protocol.ProxyRule
- for _, r := range client.Rules {
- if r.PluginManaged && r.Name == pluginName {
- continue // 跳过此插件的规则
- }
- newRules = append(newRules, r)
- }
-
- // 停止端口监听器
- if pluginPort > 0 {
- h.app.GetServer().StopPluginRule(clientID, pluginPort)
- }
-
- client.Plugins = newPlugins
- client.Rules = newRules
- return h.app.GetClientStore().UpdateClient(client)
-}
// GetSystemStats 获取客户端系统状态
func (h *ClientHandler) GetSystemStats(c *gin.Context) {
diff --git a/internal/server/router/handler/config.go b/internal/server/router/handler/config.go
index ccdb515..91ceb5d 100644
--- a/internal/server/router/handler/config.go
+++ b/internal/server/router/handler/config.go
@@ -47,9 +47,6 @@ func (h *ConfigHandler) Get(c *gin.Context) {
Username: cfg.Server.Web.Username,
Password: "****",
},
- PluginStore: dto.PluginStoreConfigInfo{
- URL: cfg.Server.PluginStore.URL,
- },
}
Success(c, resp)
@@ -103,11 +100,6 @@ func (h *ConfigHandler) Update(c *gin.Context) {
cfg.Server.Web.Password = req.Web.Password
}
- // 更新 PluginStore 配置
- if req.PluginStore != nil {
- cfg.Server.PluginStore.URL = req.PluginStore.URL
- }
-
if err := h.app.SaveConfig(); err != nil {
InternalError(c, err.Error())
return
diff --git a/internal/server/router/handler/install.go b/internal/server/router/handler/install.go
new file mode 100644
index 0000000..bbb9b30
--- /dev/null
+++ b/internal/server/router/handler/install.go
@@ -0,0 +1,108 @@
+package handler
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gotunnel/internal/server/db"
+)
+
+// InstallHandler 安装处理器
+type InstallHandler struct {
+ app AppInterface
+}
+
+// NewInstallHandler 创建安装处理器
+func NewInstallHandler(app AppInterface) *InstallHandler {
+ return &InstallHandler{app: app}
+}
+
+// GenerateInstallCommandRequest 生成安装命令请求
+type GenerateInstallCommandRequest struct {
+ ClientID string `json:"client_id" binding:"required"`
+}
+
+// InstallCommandResponse 安装命令响应
+type InstallCommandResponse struct {
+ Token string `json:"token"`
+ Commands map[string]string `json:"commands"`
+ ExpiresAt int64 `json:"expires_at"`
+ ServerAddr string `json:"server_addr"`
+}
+
+// GenerateInstallCommand 生成安装命令
+// @Summary 生成客户端安装命令
+// @Tags install
+// @Accept json
+// @Produce json
+// @Param body body GenerateInstallCommandRequest true "客户端ID"
+// @Success 200 {object} InstallCommandResponse
+// @Router /api/install/generate [post]
+func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) {
+ var req GenerateInstallCommandRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ // 生成随机token
+ tokenBytes := make([]byte, 32)
+ if _, err := rand.Read(tokenBytes); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"})
+ return
+ }
+ token := hex.EncodeToString(tokenBytes)
+
+ // 保存到数据库
+ now := time.Now().Unix()
+ installToken := &db.InstallToken{
+ Token: token,
+ ClientID: req.ClientID,
+ CreatedAt: now,
+ Used: false,
+ }
+
+ store, ok := h.app.GetClientStore().(db.InstallTokenStore)
+ if !ok {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "存储不支持安装token"})
+ return
+ }
+
+ if err := store.CreateInstallToken(installToken); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "保存token失败"})
+ return
+ }
+
+ // 获取服务器地址
+ serverAddr := fmt.Sprintf("%s:%d", h.app.GetConfig().Server.BindAddr, h.app.GetServer().GetBindPort())
+ if h.app.GetConfig().Server.BindAddr == "" || h.app.GetConfig().Server.BindAddr == "0.0.0.0" {
+ serverAddr = fmt.Sprintf("your-server-ip:%d", h.app.GetServer().GetBindPort())
+ }
+
+ // 生成安装命令
+ expiresAt := now + 3600 // 1小时过期
+ tlsFlag := ""
+ if h.app.GetConfig().Server.TLSDisabled {
+ tlsFlag = " -no-tls"
+ }
+
+ commands := map[string]string{
+ "linux": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s -id %s%s",
+ serverAddr, token, req.ClientID, tlsFlag),
+ "macos": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s -id %s%s",
+ serverAddr, token, req.ClientID, tlsFlag),
+ "windows": fmt.Sprintf("powershell -c \"irm https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.ps1 | iex; Install-GoTunnel -Server '%s' -Token '%s' -ClientID '%s'%s\"",
+ serverAddr, token, req.ClientID, tlsFlag),
+ }
+
+ c.JSON(http.StatusOK, InstallCommandResponse{
+ Token: token,
+ Commands: commands,
+ ExpiresAt: expiresAt,
+ ServerAddr: serverAddr,
+ })
+}
diff --git a/internal/server/router/handler/interfaces.go b/internal/server/router/handler/interfaces.go
index e3553a2..7ba052c 100644
--- a/internal/server/router/handler/interfaces.go
+++ b/internal/server/router/handler/interfaces.go
@@ -13,7 +13,6 @@ type AppInterface interface {
GetConfig() *config.ServerConfig
GetConfigPath() string
SaveConfig() error
- GetJSPluginStore() db.JSPluginStore
GetTrafficStore() db.TrafficStore
}
@@ -35,32 +34,13 @@ type ServerInterface interface {
GetBindPort() int
PushConfigToClient(clientID string) error
DisconnectClient(clientID string) error
- GetPluginList() []PluginInfo
- EnablePlugin(name string) error
- DisablePlugin(name string) error
- InstallPluginsToClient(clientID string, plugins []string) error
- GetPluginConfigSchema(name string) ([]ConfigField, error)
- SyncPluginConfigToClient(clientID string, pluginName string, config map[string]string) error
- InstallJSPluginToClient(clientID string, req JSPluginInstallRequest) error
RestartClient(clientID string) error
- StartClientPlugin(clientID, pluginID, pluginName, ruleName string) error
- StopClientPlugin(clientID, pluginID, pluginName, ruleName string) error
- RestartClientPlugin(clientID, pluginID, pluginName, ruleName string) error
- UpdateClientPluginConfig(clientID, pluginID, pluginName, ruleName string, config map[string]string, restart bool) error
SendUpdateToClient(clientID, downloadURL string) error
// 日志流
StartClientLogStream(clientID, sessionID string, lines int, follow bool, level string) (<-chan protocol.LogEntry, error)
StopClientLogStream(sessionID string)
- // 插件状态查询
- GetClientPluginStatus(clientID string) ([]protocol.PluginStatusEntry, error)
- // 插件规则管理
- StartPluginRule(clientID string, rule protocol.ProxyRule) error
- StopPluginRule(clientID string, remotePort int) error
// 端口检查
IsPortAvailable(port int, excludeClientID string) bool
- // 插件 API 代理
- ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error)
- // 系统状态
// 系统状态
GetClientSystemStats(clientID string) (*protocol.SystemStatsResponse, error)
// 截图
@@ -68,44 +48,3 @@ type ServerInterface interface {
// Shell 执行
ExecuteClientShell(clientID, command string, timeout int) (*protocol.ShellExecuteResponse, error)
}
-
-// ConfigField 配置字段
-type ConfigField struct {
- Key string `json:"key"`
- Label string `json:"label"`
- Type string `json:"type"`
- Default string `json:"default,omitempty"`
- Required bool `json:"required,omitempty"`
- Options []string `json:"options,omitempty"`
- Description string `json:"description,omitempty"`
-}
-
-// RuleSchema 规则表单模式
-type RuleSchema struct {
- NeedsLocalAddr bool `json:"needs_local_addr"`
- ExtraFields []ConfigField `json:"extra_fields,omitempty"`
-}
-
-// PluginInfo 插件信息
-type PluginInfo struct {
- Name string `json:"name"`
- Version string `json:"version"`
- Type string `json:"type"`
- Description string `json:"description"`
- Source string `json:"source"`
- Icon string `json:"icon,omitempty"`
- Enabled bool `json:"enabled"`
- RuleSchema *RuleSchema `json:"rule_schema,omitempty"`
-}
-
-// JSPluginInstallRequest JS 插件安装请求
-type JSPluginInstallRequest struct {
- PluginID string `json:"plugin_id"`
- PluginName string `json:"plugin_name"`
- Source string `json:"source"`
- Signature string `json:"signature"`
- RuleName string `json:"rule_name"`
- RemotePort int `json:"remote_port"`
- Config map[string]string `json:"config"`
- AutoStart bool `json:"auto_start"`
-}
diff --git a/internal/server/router/handler/js_plugin.go b/internal/server/router/handler/js_plugin.go
deleted file mode 100644
index 7082740..0000000
--- a/internal/server/router/handler/js_plugin.go
+++ /dev/null
@@ -1,219 +0,0 @@
-package handler
-
-import (
- "github.com/gin-gonic/gin"
- "github.com/gotunnel/internal/server/db"
- // removed router import
- "github.com/gotunnel/internal/server/router/dto"
-)
-
-// JSPluginHandler JS 插件处理器
-type JSPluginHandler struct {
- app AppInterface
-}
-
-// NewJSPluginHandler 创建 JS 插件处理器
-func NewJSPluginHandler(app AppInterface) *JSPluginHandler {
- return &JSPluginHandler{app: app}
-}
-
-// List 获取 JS 插件列表
-// @Summary 获取所有 JS 插件
-// @Description 返回所有注册的 JS 插件
-// @Tags JS插件
-// @Produce json
-// @Security Bearer
-// @Success 200 {object} Response{data=[]db.JSPlugin}
-// @Router /api/js-plugins [get]
-func (h *JSPluginHandler) List(c *gin.Context) {
- plugins, err := h.app.GetJSPluginStore().GetAllJSPlugins()
- if err != nil {
- InternalError(c, err.Error())
- return
- }
- if plugins == nil {
- plugins = []db.JSPlugin{}
- }
- Success(c, plugins)
-}
-
-// Create 创建 JS 插件
-// @Summary 创建 JS 插件
-// @Description 创建新的 JS 插件
-// @Tags JS插件
-// @Accept json
-// @Produce json
-// @Security Bearer
-// @Param request body dto.JSPluginCreateRequest true "插件信息"
-// @Success 200 {object} Response
-// @Failure 400 {object} Response
-// @Router /api/js-plugins [post]
-func (h *JSPluginHandler) Create(c *gin.Context) {
- var req dto.JSPluginCreateRequest
- if !BindJSON(c, &req) {
- return
- }
-
- plugin := &db.JSPlugin{
- Name: req.Name,
- Source: req.Source,
- Signature: req.Signature,
- Description: req.Description,
- Author: req.Author,
- Config: req.Config,
- AutoStart: req.AutoStart,
- Enabled: true,
- }
-
- if err := h.app.GetJSPluginStore().SaveJSPlugin(plugin); err != nil {
- InternalError(c, err.Error())
- return
- }
-
- Success(c, gin.H{"status": "ok"})
-}
-
-// Get 获取单个 JS 插件
-// @Summary 获取 JS 插件详情
-// @Description 获取指定 JS 插件的详细信息
-// @Tags JS插件
-// @Produce json
-// @Security Bearer
-// @Param name path string true "插件名称"
-// @Success 200 {object} Response{data=db.JSPlugin}
-// @Failure 404 {object} Response
-// @Router /api/js-plugin/{name} [get]
-func (h *JSPluginHandler) Get(c *gin.Context) {
- name := c.Param("name")
-
- plugin, err := h.app.GetJSPluginStore().GetJSPlugin(name)
- if err != nil {
- NotFound(c, "plugin not found")
- return
- }
-
- Success(c, plugin)
-}
-
-// Update 更新 JS 插件
-// @Summary 更新 JS 插件
-// @Description 更新指定 JS 插件的信息
-// @Tags JS插件
-// @Accept json
-// @Produce json
-// @Security Bearer
-// @Param name path string true "插件名称"
-// @Param request body dto.JSPluginUpdateRequest true "更新内容"
-// @Success 200 {object} Response
-// @Failure 400 {object} Response
-// @Router /api/js-plugin/{name} [put]
-func (h *JSPluginHandler) Update(c *gin.Context) {
- name := c.Param("name")
-
- var req dto.JSPluginUpdateRequest
- if !BindJSON(c, &req) {
- return
- }
-
- plugin := &db.JSPlugin{
- Name: name,
- Source: req.Source,
- Signature: req.Signature,
- Description: req.Description,
- Author: req.Author,
- Config: req.Config,
- AutoStart: req.AutoStart,
- Enabled: req.Enabled,
- }
-
- if err := h.app.GetJSPluginStore().SaveJSPlugin(plugin); err != nil {
- InternalError(c, err.Error())
- return
- }
-
- Success(c, gin.H{"status": "ok"})
-}
-
-// Delete 删除 JS 插件
-// @Summary 删除 JS 插件
-// @Description 删除指定的 JS 插件
-// @Tags JS插件
-// @Produce json
-// @Security Bearer
-// @Param name path string true "插件名称"
-// @Success 200 {object} Response
-// @Router /api/js-plugin/{name} [delete]
-func (h *JSPluginHandler) Delete(c *gin.Context) {
- name := c.Param("name")
-
- if err := h.app.GetJSPluginStore().DeleteJSPlugin(name); err != nil {
- InternalError(c, err.Error())
- return
- }
-
- Success(c, gin.H{"status": "ok"})
-}
-
-// PushToClient 推送 JS 插件到客户端
-// @Summary 推送插件到客户端
-// @Description 将 JS 插件推送到指定客户端
-// @Tags JS插件
-// @Accept json
-// @Produce json
-// @Security Bearer
-// @Param name path string true "插件名称"
-// @Param clientID path string true "客户端ID"
-// @Param request body dto.JSPluginPushRequest false "推送配置"
-// @Success 200 {object} Response
-// @Failure 400 {object} Response
-// @Failure 404 {object} Response
-// @Router /api/js-plugin/{name}/push/{clientID} [post]
-func (h *JSPluginHandler) PushToClient(c *gin.Context) {
- pluginName := c.Param("name")
- clientID := c.Param("clientID")
-
- // 解析请求体(可选)
- var pushReq dto.JSPluginPushRequest
- c.ShouldBindJSON(&pushReq) // 忽略错误,允许空请求体
-
- // 检查客户端是否在线
- if !h.app.GetServer().IsClientOnline(clientID) {
- ClientNotOnline(c)
- return
- }
-
- // 获取插件
- plugin, err := h.app.GetJSPluginStore().GetJSPlugin(pluginName)
- if err != nil {
- NotFound(c, "plugin not found")
- return
- }
-
- if !plugin.Enabled {
- Error(c, 400, CodePluginDisabled, "plugin is disabled")
- return
- }
-
- // 推送到客户端
- req := JSPluginInstallRequest{
- PluginName: plugin.Name,
- Source: plugin.Source,
- Signature: plugin.Signature,
- RuleName: plugin.Name,
- RemotePort: pushReq.RemotePort,
- Config: plugin.Config,
- AutoStart: plugin.AutoStart,
- }
-
- if err := h.app.GetServer().InstallJSPluginToClient(clientID, req); err != nil {
- InternalError(c, err.Error())
- return
- }
-
- Success(c, gin.H{
- "status": "ok",
- "plugin": pluginName,
- "client": clientID,
- "remote_port": pushReq.RemotePort,
- })
-}
diff --git a/internal/server/router/handler/plugin.go b/internal/server/router/handler/plugin.go
deleted file mode 100644
index 4cacffd..0000000
--- a/internal/server/router/handler/plugin.go
+++ /dev/null
@@ -1,416 +0,0 @@
-package handler
-
-import (
- "fmt"
-
- "github.com/gin-gonic/gin"
- "github.com/gotunnel/internal/server/db"
- "github.com/gotunnel/internal/server/router/dto"
- "github.com/gotunnel/pkg/plugin"
-)
-
-// PluginHandler 插件处理器
-type PluginHandler struct {
- app AppInterface
-}
-
-// NewPluginHandler 创建插件处理器
-func NewPluginHandler(app AppInterface) *PluginHandler {
- return &PluginHandler{app: app}
-}
-
-// List 获取插件列表
-// @Summary 获取所有插件
-// @Description 返回服务端所有注册的插件
-// @Tags 插件
-// @Produce json
-// @Security Bearer
-// @Success 200 {object} Response{data=[]dto.PluginInfo}
-// @Router /api/plugins [get]
-func (h *PluginHandler) List(c *gin.Context) {
- plugins := h.app.GetServer().GetPluginList()
-
- result := make([]dto.PluginInfo, len(plugins))
- for i, p := range plugins {
- result[i] = dto.PluginInfo{
- Name: p.Name,
- Version: p.Version,
- Type: p.Type,
- Description: p.Description,
- Source: p.Source,
- Icon: p.Icon,
- Enabled: p.Enabled,
- }
- if p.RuleSchema != nil {
- result[i].RuleSchema = &dto.RuleSchema{
- NeedsLocalAddr: p.RuleSchema.NeedsLocalAddr,
- ExtraFields: convertRouterConfigFields(p.RuleSchema.ExtraFields),
- }
- }
- }
-
- Success(c, result)
-}
-
-// Enable 启用插件
-// @Summary 启用插件
-// @Description 启用指定插件
-// @Tags 插件
-// @Produce json
-// @Security Bearer
-// @Param name path string true "插件名称"
-// @Success 200 {object} Response
-// @Failure 400 {object} Response
-// @Router /api/plugin/{name}/enable [post]
-func (h *PluginHandler) Enable(c *gin.Context) {
- name := c.Param("name")
-
- if err := h.app.GetServer().EnablePlugin(name); err != nil {
- BadRequest(c, err.Error())
- return
- }
-
- Success(c, gin.H{"status": "ok"})
-}
-
-// Disable 禁用插件
-// @Summary 禁用插件
-// @Description 禁用指定插件
-// @Tags 插件
-// @Produce json
-// @Security Bearer
-// @Param name path string true "插件名称"
-// @Success 200 {object} Response
-// @Failure 400 {object} Response
-// @Router /api/plugin/{name}/disable [post]
-func (h *PluginHandler) Disable(c *gin.Context) {
- name := c.Param("name")
-
- if err := h.app.GetServer().DisablePlugin(name); err != nil {
- BadRequest(c, err.Error())
- return
- }
-
- Success(c, gin.H{"status": "ok"})
-}
-
-// GetRuleSchemas 获取规则配置模式
-// @Summary 获取规则模式
-// @Description 返回所有协议类型的配置模式
-// @Tags 插件
-// @Produce json
-// @Security Bearer
-// @Success 200 {object} Response{data=map[string]dto.RuleSchema}
-// @Router /api/rule-schemas [get]
-func (h *PluginHandler) GetRuleSchemas(c *gin.Context) {
- // 获取内置协议模式
- schemas := make(map[string]dto.RuleSchema)
- for name, schema := range plugin.BuiltinRuleSchemas() {
- schemas[name] = dto.RuleSchema{
- NeedsLocalAddr: schema.NeedsLocalAddr,
- ExtraFields: convertConfigFields(schema.ExtraFields),
- }
- }
-
- // 添加已注册插件的模式
- plugins := h.app.GetServer().GetPluginList()
- for _, p := range plugins {
- if p.RuleSchema != nil {
- schemas[p.Name] = dto.RuleSchema{
- NeedsLocalAddr: p.RuleSchema.NeedsLocalAddr,
- ExtraFields: convertRouterConfigFields(p.RuleSchema.ExtraFields),
- }
- }
- }
-
- Success(c, schemas)
-}
-
-// GetClientConfig 获取客户端插件配置
-// @Summary 获取客户端插件配置
-// @Description 获取客户端上指定插件的配置
-// @Tags 插件
-// @Produce json
-// @Security Bearer
-// @Param clientID path string true "客户端ID"
-// @Param pluginName path string true "插件名称"
-// @Success 200 {object} Response{data=dto.PluginConfigResponse}
-// @Failure 404 {object} Response
-// @Router /api/client-plugin/{clientID}/{pluginName}/config [get]
-func (h *PluginHandler) GetClientConfig(c *gin.Context) {
- clientID := c.Param("clientID")
- pluginName := c.Param("pluginName")
-
- client, err := h.app.GetClientStore().GetClient(clientID)
- if err != nil {
- NotFound(c, "client not found")
- return
- }
-
- // 查找客户端的插件
- var clientPlugin *db.ClientPlugin
- for i, p := range client.Plugins {
- if p.Name == pluginName {
- clientPlugin = &client.Plugins[i]
- break
- }
- }
-
- if clientPlugin == nil {
- NotFound(c, "plugin not installed on client")
- return
- }
-
- var schemaFields []dto.ConfigField
-
- // 优先使用客户端插件保存的 ConfigSchema
- if len(clientPlugin.ConfigSchema) > 0 {
- for _, f := range clientPlugin.ConfigSchema {
- schemaFields = append(schemaFields, dto.ConfigField{
- Key: f.Key,
- Label: f.Label,
- Type: f.Type,
- Default: f.Default,
- Required: f.Required,
- Options: f.Options,
- Description: f.Description,
- })
- }
- } else {
- // 尝试从内置插件获取配置模式
- schema, err := h.app.GetServer().GetPluginConfigSchema(pluginName)
- if err != nil {
- // 如果内置插件中找不到,尝试从 JS 插件获取
- jsPlugin, jsErr := h.app.GetJSPluginStore().GetJSPlugin(pluginName)
- if jsErr == nil {
- // 使用 JS 插件的 config 作为动态 schema
- for key := range jsPlugin.Config {
- schemaFields = append(schemaFields, dto.ConfigField{
- Key: key,
- Label: key,
- Type: "string",
- })
- }
- }
- } else {
- schemaFields = convertRouterConfigFields(schema)
- }
- }
-
- // 添加 remote_port 作为系统配置字段(始终显示)
- schemaFields = append([]dto.ConfigField{{
- Key: "remote_port",
- Label: "远程端口",
- Type: "number",
- Description: "服务端监听端口,修改后需重启插件生效",
- }}, schemaFields...)
-
- // 添加 Auth 配置字段
- schemaFields = append(schemaFields, dto.ConfigField{
- Key: "auth_enabled",
- Label: "启用认证",
- Type: "bool",
- Description: "启用 HTTP Basic Auth 保护",
- }, dto.ConfigField{
- Key: "auth_username",
- Label: "认证用户名",
- Type: "string",
- Description: "HTTP Basic Auth 用户名",
- }, dto.ConfigField{
- Key: "auth_password",
- Label: "认证密码",
- Type: "password",
- Description: "HTTP Basic Auth 密码",
- })
-
- // 构建配置值
- config := clientPlugin.Config
- if config == nil {
- config = make(map[string]string)
- }
- // 将 remote_port 加入配置
- if clientPlugin.RemotePort > 0 {
- config["remote_port"] = fmt.Sprintf("%d", clientPlugin.RemotePort)
- }
- // 将 Auth 配置加入
- if clientPlugin.AuthEnabled {
- config["auth_enabled"] = "true"
- } else {
- config["auth_enabled"] = "false"
- }
- config["auth_username"] = clientPlugin.AuthUsername
- config["auth_password"] = clientPlugin.AuthPassword
-
- Success(c, dto.PluginConfigResponse{
- PluginName: pluginName,
- Schema: schemaFields,
- Config: config,
- })
-}
-
-// UpdateClientConfig 更新客户端插件配置
-// @Summary 更新客户端插件配置
-// @Description 更新客户端上指定插件的配置
-// @Tags 插件
-// @Accept json
-// @Produce json
-// @Security Bearer
-// @Param clientID path string true "客户端ID"
-// @Param pluginName path string true "插件名称"
-// @Param request body dto.PluginConfigRequest true "配置内容"
-// @Success 200 {object} Response
-// @Failure 400 {object} Response
-// @Failure 404 {object} Response
-// @Router /api/client-plugin/{clientID}/{pluginName}/config [put]
-func (h *PluginHandler) UpdateClientConfig(c *gin.Context) {
- clientID := c.Param("clientID")
- pluginName := c.Param("pluginName")
-
- var req dto.PluginConfigRequest
- if !BindJSON(c, &req) {
- return
- }
-
- client, err := h.app.GetClientStore().GetClient(clientID)
- if err != nil {
- NotFound(c, "client not found")
- return
- }
-
- // 更新插件配置
- found := false
- portChanged := false
- authChanged := false
- var oldPort, newPort int
- for i, p := range client.Plugins {
- if p.Name == pluginName {
- oldPort = client.Plugins[i].RemotePort
- // 提取 remote_port 并单独处理
- if portStr, ok := req.Config["remote_port"]; ok {
- fmt.Sscanf(portStr, "%d", &newPort)
- if newPort > 0 && newPort != oldPort {
- // 检查新端口是否可用
- if !h.app.GetServer().IsPortAvailable(newPort, clientID) {
- BadRequest(c, fmt.Sprintf("port %d is already in use", newPort))
- return
- }
- client.Plugins[i].RemotePort = newPort
- portChanged = true
- }
- delete(req.Config, "remote_port") // 不保存到 Config map
- }
- // 提取 Auth 配置并单独处理
- if authEnabledStr, ok := req.Config["auth_enabled"]; ok {
- newAuthEnabled := authEnabledStr == "true"
- if newAuthEnabled != client.Plugins[i].AuthEnabled {
- client.Plugins[i].AuthEnabled = newAuthEnabled
- authChanged = true
- }
- delete(req.Config, "auth_enabled")
- }
- if authUsername, ok := req.Config["auth_username"]; ok {
- if authUsername != client.Plugins[i].AuthUsername {
- client.Plugins[i].AuthUsername = authUsername
- authChanged = true
- }
- delete(req.Config, "auth_username")
- }
- if authPassword, ok := req.Config["auth_password"]; ok {
- if authPassword != client.Plugins[i].AuthPassword {
- client.Plugins[i].AuthPassword = authPassword
- authChanged = true
- }
- delete(req.Config, "auth_password")
- }
- client.Plugins[i].Config = req.Config
- found = true
- break
- }
- }
-
- if !found {
- NotFound(c, "plugin not installed on client")
- return
- }
-
- // 如果端口变更,同步更新代理规则
- if portChanged {
- for i, r := range client.Rules {
- if r.Name == pluginName && r.PluginManaged {
- client.Rules[i].RemotePort = newPort
- break
- }
- }
- // 停止旧端口监听器
- if oldPort > 0 {
- h.app.GetServer().StopPluginRule(clientID, oldPort)
- }
- }
-
- // 如果 Auth 配置变更,同步更新代理规则
- if authChanged {
- for i, p := range client.Plugins {
- if p.Name == pluginName {
- for j, r := range client.Rules {
- if r.Name == pluginName && r.PluginManaged {
- client.Rules[j].AuthEnabled = client.Plugins[i].AuthEnabled
- client.Rules[j].AuthUsername = client.Plugins[i].AuthUsername
- client.Rules[j].AuthPassword = client.Plugins[i].AuthPassword
- break
- }
- }
- break
- }
- }
- }
-
- // 保存到数据库
- if err := h.app.GetClientStore().UpdateClient(client); err != nil {
- InternalError(c, err.Error())
- return
- }
-
- // 如果客户端在线,同步配置
- if h.app.GetServer().IsClientOnline(clientID) {
- if err := h.app.GetServer().SyncPluginConfigToClient(clientID, pluginName, req.Config); err != nil {
- PartialSuccess(c, gin.H{"status": "partial", "port_changed": portChanged}, "config saved but sync failed: "+err.Error())
- return
- }
- }
-
- Success(c, gin.H{"status": "ok", "port_changed": portChanged})
-}
-
-// convertConfigFields 转换插件配置字段到 DTO
-func convertConfigFields(fields []plugin.ConfigField) []dto.ConfigField {
- result := make([]dto.ConfigField, len(fields))
- for i, f := range fields {
- result[i] = dto.ConfigField{
- Key: f.Key,
- Label: f.Label,
- Type: string(f.Type),
- Default: f.Default,
- Required: f.Required,
- Options: f.Options,
- Description: f.Description,
- }
- }
- return result
-}
-
-// convertRouterConfigFields 转换 ConfigField 到 dto.ConfigField
-func convertRouterConfigFields(fields []ConfigField) []dto.ConfigField {
- result := make([]dto.ConfigField, len(fields))
- for i, f := range fields {
- result[i] = dto.ConfigField{
- Key: f.Key,
- Label: f.Label,
- Type: f.Type,
- Default: f.Default,
- Required: f.Required,
- Options: f.Options,
- Description: f.Description,
- }
- }
- return result
-}
diff --git a/internal/server/router/handler/plugin_api.go b/internal/server/router/handler/plugin_api.go
deleted file mode 100644
index 88447f4..0000000
--- a/internal/server/router/handler/plugin_api.go
+++ /dev/null
@@ -1,139 +0,0 @@
-package handler
-
-import (
- "io"
- "net/http"
- "strings"
- "time"
-
- "github.com/gin-gonic/gin"
- "github.com/gotunnel/pkg/protocol"
-)
-
-// PluginAPIHandler 插件 API 代理处理器
-type PluginAPIHandler struct {
- app AppInterface
-}
-
-// NewPluginAPIHandler 创建插件 API 代理处理器
-func NewPluginAPIHandler(app AppInterface) *PluginAPIHandler {
- return &PluginAPIHandler{app: app}
-}
-
-// ProxyRequest 代理请求到客户端插件
-// @Summary 代理插件 API 请求
-// @Description 将请求代理到客户端的 JS 插件处理
-// @Tags 插件 API
-// @Accept json
-// @Produce json
-// @Security Bearer
-// @Param id path string true "客户端 ID"
-// @Param pluginID path string true "插件实例 ID"
-// @Param route path string true "插件路由"
-// @Success 200 {object} object
-// @Failure 404 {object} Response
-// @Failure 502 {object} Response
-// @Router /api/client/{id}/plugin-api/{pluginID}/{route} [get]
-func (h *PluginAPIHandler) ProxyRequest(c *gin.Context) {
- clientID := c.Param("id")
- pluginID := c.Param("pluginID")
- route := c.Param("route")
-
- // 确保路由以 / 开头
- if !strings.HasPrefix(route, "/") {
- route = "/" + route
- }
-
- // 检查客户端是否在线
- if !h.app.GetServer().IsClientOnline(clientID) {
- ClientNotOnline(c)
- return
- }
-
- // 读取请求体
- var body string
- if c.Request.Body != nil {
- bodyBytes, _ := io.ReadAll(c.Request.Body)
- body = string(bodyBytes)
- }
-
- // 构建请求头
- headers := make(map[string]string)
- for key, values := range c.Request.Header {
- if len(values) > 0 {
- headers[key] = values[0]
- }
- }
-
- // 构建 API 请求
- apiReq := protocol.PluginAPIRequest{
- PluginID: pluginID,
- Method: c.Request.Method,
- Path: route,
- Query: c.Request.URL.RawQuery,
- Headers: headers,
- Body: body,
- }
-
- // 发送请求到客户端
- resp, err := h.app.GetServer().ProxyPluginAPIRequest(clientID, apiReq)
- if err != nil {
- BadGateway(c, "Plugin request failed: "+err.Error())
- return
- }
-
- // 检查错误
- if resp.Error != "" {
- c.JSON(http.StatusBadGateway, gin.H{
- "code": 502,
- "message": resp.Error,
- })
- return
- }
-
- // 设置响应头
- for key, value := range resp.Headers {
- c.Header(key, value)
- }
-
- // 返回响应
- c.String(resp.Status, resp.Body)
-}
-
-// ProxyPluginAPIRequest 接口方法声明 - 添加到 ServerInterface
-type PluginAPIProxyInterface interface {
- ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error)
-}
-
-// AuthConfig 认证配置
-type AuthConfig struct {
- Type string `json:"type"` // none, basic, token
- Username string `json:"username"` // Basic Auth 用户名
- Password string `json:"password"` // Basic Auth 密码
- Token string `json:"token"` // Token 认证
-}
-
-// BasicAuthMiddleware 创建 Basic Auth 中间件
-func BasicAuthMiddleware(username, password string) gin.HandlerFunc {
- return func(c *gin.Context) {
- user, pass, ok := c.Request.BasicAuth()
- if !ok || user != username || pass != password {
- c.Header("WWW-Authenticate", `Basic realm="Plugin"`)
- c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
- "code": 401,
- "message": "Unauthorized",
- })
- return
- }
- c.Next()
- }
-}
-
-// WithTimeout 带超时的请求处理
-func WithTimeout(timeout time.Duration, handler gin.HandlerFunc) gin.HandlerFunc {
- return func(c *gin.Context) {
- // 设置请求超时
- c.Request = c.Request.WithContext(c.Request.Context())
- handler(c)
- }
-}
diff --git a/internal/server/router/handler/store.go b/internal/server/router/handler/store.go
deleted file mode 100644
index 6247b13..0000000
--- a/internal/server/router/handler/store.go
+++ /dev/null
@@ -1,292 +0,0 @@
-package handler
-
-import (
- "fmt"
- "io"
- "net/http"
- "time"
-
- "github.com/gin-gonic/gin"
- "github.com/google/uuid"
- "github.com/gotunnel/internal/server/db"
- "github.com/gotunnel/internal/server/router/dto"
- "github.com/gotunnel/pkg/protocol"
-)
-
-// StoreHandler 插件商店处理器
-type StoreHandler struct {
- app AppInterface
-}
-
-// NewStoreHandler 创建插件商店处理器
-func NewStoreHandler(app AppInterface) *StoreHandler {
- return &StoreHandler{app: app}
-}
-
-// ListPlugins 获取商店插件列表
-// @Summary 获取商店插件
-// @Description 从远程插件商店获取可用插件列表
-// @Tags 插件商店
-// @Produce json
-// @Security Bearer
-// @Success 200 {object} Response{data=object{plugins=[]dto.StorePluginInfo}}
-// @Failure 502 {object} Response
-// @Router /api/store/plugins [get]
-func (h *StoreHandler) ListPlugins(c *gin.Context) {
- cfg := h.app.GetConfig()
- storeURL := cfg.Server.PluginStore.GetPluginStoreURL()
-
- // 从远程 URL 获取插件列表
- client := &http.Client{Timeout: 10 * time.Second}
- resp, err := client.Get(storeURL)
- if err != nil {
- BadGateway(c, "Failed to fetch store: "+err.Error())
- return
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- BadGateway(c, "Store returned error")
- return
- }
-
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- InternalError(c, "Failed to read response")
- return
- }
-
- // 直接返回原始 JSON(已经是数组格式)
- c.Header("Content-Type", "application/json")
- c.Writer.Write([]byte(`{"code":0,"data":{"plugins":`))
- c.Writer.Write(body)
- c.Writer.Write([]byte(`}}`))
-}
-
-// Install 从商店安装插件到客户端
-// @Summary 安装商店插件
-// @Description 从插件商店下载并安装插件到指定客户端
-// @Tags 插件商店
-// @Accept json
-// @Produce json
-// @Security Bearer
-// @Param request body dto.StoreInstallRequest true "安装请求"
-// @Success 200 {object} Response
-// @Failure 400 {object} Response
-// @Failure 502 {object} Response
-// @Router /api/store/install [post]
-func (h *StoreHandler) Install(c *gin.Context) {
- var req dto.StoreInstallRequest
- if !BindJSON(c, &req) {
- return
- }
-
- // 检查客户端是否在线
- if !h.app.GetServer().IsClientOnline(req.ClientID) {
- ClientNotOnline(c)
- return
- }
-
- // 下载插件
- client := &http.Client{Timeout: 30 * time.Second}
- resp, err := client.Get(req.DownloadURL)
- if err != nil {
- BadGateway(c, "Failed to download plugin: "+err.Error())
- return
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- BadGateway(c, "Plugin download failed with status: "+resp.Status)
- return
- }
-
- source, err := io.ReadAll(resp.Body)
- if err != nil {
- InternalError(c, "Failed to read plugin: "+err.Error())
- return
- }
-
- // 下载签名文件
- sigResp, err := client.Get(req.SignatureURL)
- if err != nil {
- BadGateway(c, "Failed to download signature: "+err.Error())
- return
- }
- defer sigResp.Body.Close()
-
- if sigResp.StatusCode != http.StatusOK {
- BadGateway(c, "Signature download failed with status: "+sigResp.Status)
- return
- }
-
- signature, err := io.ReadAll(sigResp.Body)
- if err != nil {
- InternalError(c, "Failed to read signature: "+err.Error())
- return
- }
-
- // 检查插件是否已存在,决定使用已有 ID 还是生成新 ID
- pluginID := ""
- dbClient, err := h.app.GetClientStore().GetClient(req.ClientID)
- if err == nil {
- for _, p := range dbClient.Plugins {
- if p.Name == req.PluginName && p.ID != "" {
- pluginID = p.ID
- break
- }
- }
- }
- if pluginID == "" {
- pluginID = uuid.New().String()
- }
-
- // 安装到客户端
- installReq := JSPluginInstallRequest{
- PluginID: pluginID,
- PluginName: req.PluginName,
- Source: string(source),
- Signature: string(signature),
- RuleName: req.PluginName,
- RemotePort: req.RemotePort,
- AutoStart: true,
- }
-
- if err := h.app.GetServer().InstallJSPluginToClient(req.ClientID, installReq); err != nil {
- InternalError(c, "Failed to install plugin: "+err.Error())
- return
- }
-
- // 将插件保存到 JSPluginStore(用于客户端重连时恢复)
- jsPlugin := &db.JSPlugin{
- Name: req.PluginName,
- Source: string(source),
- Signature: string(signature),
- AutoStart: true,
- Enabled: true,
- }
- // 尝试保存,忽略错误(可能已存在)
- h.app.GetJSPluginStore().SaveJSPlugin(jsPlugin)
-
- // 将插件信息保存到客户端记录
- // 重新获取 dbClient(可能已被修改)
- dbClient, err = h.app.GetClientStore().GetClient(req.ClientID)
- if err == nil {
- // 检查插件是否已存在(通过名称匹配)
- pluginExists := false
- for i, p := range dbClient.Plugins {
- if p.Name == req.PluginName {
- dbClient.Plugins[i].Enabled = true
- dbClient.Plugins[i].RemotePort = req.RemotePort
- dbClient.Plugins[i].AuthEnabled = req.AuthEnabled
- dbClient.Plugins[i].AuthUsername = req.AuthUsername
- dbClient.Plugins[i].AuthPassword = req.AuthPassword
- // 确保有 ID
- if dbClient.Plugins[i].ID == "" {
- dbClient.Plugins[i].ID = pluginID
- }
- pluginExists = true
- break
- }
- }
- if !pluginExists {
- version := req.Version
- if version == "" {
- version = "1.0.0"
- }
- // 转换 ConfigSchema
- var configSchema []db.ConfigField
- for _, f := range req.ConfigSchema {
- configSchema = append(configSchema, db.ConfigField{
- Key: f.Key,
- Label: f.Label,
- Type: f.Type,
- Default: f.Default,
- Required: f.Required,
- Options: f.Options,
- Description: f.Description,
- })
- }
- dbClient.Plugins = append(dbClient.Plugins, db.ClientPlugin{
- ID: pluginID,
- Name: req.PluginName,
- Version: version,
- Enabled: true,
- RemotePort: req.RemotePort,
- ConfigSchema: configSchema,
- AuthEnabled: req.AuthEnabled,
- AuthUsername: req.AuthUsername,
- AuthPassword: req.AuthPassword,
- })
- }
-
- // 自动创建代理规则(如果指定了端口)
- if req.RemotePort > 0 {
- // 检查端口是否可用
- if !h.app.GetServer().IsPortAvailable(req.RemotePort, req.ClientID) {
- InternalError(c, fmt.Sprintf("port %d is already in use", req.RemotePort))
- return
- }
- ruleExists := false
- for i, r := range dbClient.Rules {
- if r.Name == req.PluginName {
- // 更新现有规则
- dbClient.Rules[i].Type = req.PluginName
- dbClient.Rules[i].RemotePort = req.RemotePort
- dbClient.Rules[i].Enabled = boolPtr(true)
- dbClient.Rules[i].PluginID = pluginID
- dbClient.Rules[i].AuthEnabled = req.AuthEnabled
- dbClient.Rules[i].AuthUsername = req.AuthUsername
- dbClient.Rules[i].AuthPassword = req.AuthPassword
- dbClient.Rules[i].PluginManaged = true
- ruleExists = true
- break
- }
- }
- if !ruleExists {
- // 创建新规则
- dbClient.Rules = append(dbClient.Rules, protocol.ProxyRule{
- Name: req.PluginName,
- Type: req.PluginName,
- RemotePort: req.RemotePort,
- Enabled: boolPtr(true),
- PluginID: pluginID,
- AuthEnabled: req.AuthEnabled,
- AuthUsername: req.AuthUsername,
- AuthPassword: req.AuthPassword,
- PluginManaged: true,
- })
- }
- }
-
- h.app.GetClientStore().UpdateClient(dbClient)
- }
-
- // 启动服务端监听器(让外部用户可以通过 RemotePort 访问插件)
- if req.RemotePort > 0 {
- pluginRule := protocol.ProxyRule{
- Name: req.PluginName,
- Type: req.PluginName, // 使用插件名作为类型,让 isClientPlugin 识别
- RemotePort: req.RemotePort,
- Enabled: boolPtr(true),
- PluginID: pluginID,
- AuthEnabled: req.AuthEnabled,
- AuthUsername: req.AuthUsername,
- AuthPassword: req.AuthPassword,
- }
- // 启动监听器(忽略错误,可能端口已被占用)
- h.app.GetServer().StartPluginRule(req.ClientID, pluginRule)
- }
-
- Success(c, gin.H{
- "status": "ok",
- "plugin": req.PluginName,
- "plugin_id": pluginID,
- "client": req.ClientID,
- })
-}
-
-// boolPtr 返回 bool 值的指针
-func boolPtr(b bool) *bool {
- return &b
-}
diff --git a/internal/server/router/router.go b/internal/server/router/router.go
index 3a5ce9f..b3453a5 100644
--- a/internal/server/router/router.go
+++ b/internal/server/router/router.go
@@ -67,8 +67,6 @@ func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth,
api.POST("/client/:id/push", clientHandler.PushConfig)
api.POST("/client/:id/disconnect", clientHandler.Disconnect)
api.POST("/client/:id/restart", clientHandler.Restart)
- api.POST("/client/:id/install-plugins", clientHandler.InstallPlugins)
- api.POST("/client/:id/plugin/:pluginID/:action", clientHandler.PluginAction)
api.GET("/client/:id/system-stats", clientHandler.GetSystemStats)
api.GET("/client/:id/screenshot", clientHandler.GetScreenshot)
api.POST("/client/:id/shell", clientHandler.ExecuteShell)
@@ -79,29 +77,6 @@ func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth,
api.PUT("/config", configHandler.Update)
api.POST("/config/reload", configHandler.Reload)
- // 插件管理
- pluginHandler := handler.NewPluginHandler(app)
- api.GET("/plugins", pluginHandler.List)
- api.POST("/plugin/:name/enable", pluginHandler.Enable)
- api.POST("/plugin/:name/disable", pluginHandler.Disable)
- api.GET("/rule-schemas", pluginHandler.GetRuleSchemas)
- api.GET("/client-plugin/:clientID/:pluginName/config", pluginHandler.GetClientConfig)
- api.PUT("/client-plugin/:clientID/:pluginName/config", pluginHandler.UpdateClientConfig)
-
- // JS 插件管理
- jsPluginHandler := handler.NewJSPluginHandler(app)
- api.GET("/js-plugins", jsPluginHandler.List)
- api.POST("/js-plugins", jsPluginHandler.Create)
- api.GET("/js-plugin/:name", jsPluginHandler.Get)
- api.PUT("/js-plugin/:name", jsPluginHandler.Update)
- api.DELETE("/js-plugin/:name", jsPluginHandler.Delete)
- api.POST("/js-plugin/:name/push/:clientID", jsPluginHandler.PushToClient)
-
- // 插件商店
- storeHandler := handler.NewStoreHandler(app)
- api.GET("/store/plugins", storeHandler.ListPlugins)
- api.POST("/store/install", storeHandler.Install)
-
// 更新管理
updateHandler := handler.NewUpdateHandler(app)
api.GET("/update/check/server", updateHandler.CheckServer)
@@ -118,9 +93,9 @@ func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth,
api.GET("/traffic/stats", trafficHandler.GetStats)
api.GET("/traffic/hourly", trafficHandler.GetHourly)
- // 插件 API 代理 (通过 Web API 访问客户端插件)
- pluginAPIHandler := handler.NewPluginAPIHandler(app)
- api.Any("/client/:id/plugin-api/:pluginID/*route", pluginAPIHandler.ProxyRequest)
+ // 安装命令生成
+ installHandler := handler.NewInstallHandler(app)
+ api.POST("/install/generate", installHandler.GenerateInstallCommand)
}
}
@@ -200,10 +175,6 @@ func isStaticAsset(path string) bool {
// Re-export types from handler package for backward compatibility
type (
- ServerInterface = handler.ServerInterface
- AppInterface = handler.AppInterface
- ConfigField = handler.ConfigField
- RuleSchema = handler.RuleSchema
- PluginInfo = handler.PluginInfo
- JSPluginInstallRequest = handler.JSPluginInstallRequest
+ ServerInterface = handler.ServerInterface
+ AppInterface = handler.AppInterface
)
diff --git a/internal/server/tunnel/server.go b/internal/server/tunnel/server.go
index e299e9d..54b20a4 100644
--- a/internal/server/tunnel/server.go
+++ b/internal/server/tunnel/server.go
@@ -14,8 +14,6 @@ import (
"time"
"github.com/gotunnel/internal/server/db"
- "github.com/gotunnel/internal/server/router"
- "github.com/gotunnel/pkg/plugin"
"github.com/gotunnel/pkg/protocol"
"github.com/gotunnel/pkg/proxy"
"github.com/gotunnel/pkg/relay"
@@ -49,36 +47,23 @@ func generateClientID() string {
// Server 隧道服务端
type Server struct {
- clientStore db.ClientStore
- jsPluginStore db.JSPluginStore // JS 插件存储
- trafficStore db.TrafficStore // 流量存储
- bindAddr string
- bindPort int
- token string
- heartbeat int
- hbTimeout int
- portManager *utils.PortManager
- clients map[string]*ClientSession
- mu sync.RWMutex
- tlsConfig *tls.Config
- pluginRegistry *plugin.Registry
- jsPlugins []JSPluginEntry // 配置的 JS 插件
- connSem chan struct{} // 连接数信号量
- activeConns int64 // 当前活跃连接数
- listener net.Listener // 主监听器
- shutdown chan struct{} // 关闭信号
- wg sync.WaitGroup // 等待所有连接关闭
- logSessions *LogSessionManager // 日志会话管理器
-}
-
-// JSPluginEntry JS 插件条目
-type JSPluginEntry struct {
- Name string
- Source string
- Signature string
- AutoPush []string
- Config map[string]string
- AutoStart bool
+ clientStore db.ClientStore
+ trafficStore db.TrafficStore // 流量存储
+ bindAddr string
+ bindPort int
+ token string
+ heartbeat int
+ hbTimeout int
+ portManager *utils.PortManager
+ clients map[string]*ClientSession
+ mu sync.RWMutex
+ tlsConfig *tls.Config
+ connSem chan struct{} // 连接数信号量
+ activeConns int64 // 当前活跃连接数
+ listener net.Listener // 主监听器
+ shutdown chan struct{} // 关闭信号
+ wg sync.WaitGroup // 等待所有连接关闭
+ logSessions *LogSessionManager // 日志会话管理器
}
// ClientSession 客户端会话
@@ -152,27 +137,11 @@ func (s *Server) Shutdown(timeout time.Duration) error {
}
}
-// SetPluginRegistry 设置插件注册表
-func (s *Server) SetPluginRegistry(registry *plugin.Registry) {
- s.pluginRegistry = registry
-}
-
-// SetJSPluginStore 设置 JS 插件存储
-func (s *Server) SetJSPluginStore(store db.JSPluginStore) {
- s.jsPluginStore = store
-}
-
// SetTrafficStore 设置流量存储
func (s *Server) SetTrafficStore(store db.TrafficStore) {
s.trafficStore = store
}
-// LoadJSPlugins 加载 JS 插件配置
-func (s *Server) LoadJSPlugins(plugins []JSPluginEntry) {
- s.jsPlugins = plugins
- log.Printf("[Server] Loaded %d JS plugin configs", len(plugins))
-}
-
// Run 启动服务端
func (s *Server) Run() error {
addr := fmt.Sprintf("%s:%d", s.bindAddr, s.bindPort)
@@ -257,7 +226,32 @@ func (s *Server) handleConnection(conn net.Conn) {
return
}
- if authReq.Token != s.token {
+ // 验证token:支持常规token和一次性安装token
+ validToken := authReq.Token == s.token
+ var isInstallToken bool
+
+ if !validToken {
+ // 尝试验证安装token
+ if tokenStore, ok := s.clientStore.(db.InstallTokenStore); ok {
+ if installToken, err := tokenStore.GetInstallToken(authReq.Token); err == nil {
+ if !installToken.Used && time.Now().Unix()-installToken.CreatedAt < 3600 {
+ // token有效且未过期
+ validToken = true
+ isInstallToken = true
+ // 验证客户端ID匹配
+ if authReq.ClientID != "" && authReq.ClientID != installToken.ClientID {
+ security.LogInvalidClientID(clientIP, authReq.ClientID)
+ s.sendAuthResponse(conn, false, "client id mismatch", "")
+ return
+ }
+ // 使用token中的客户端ID
+ authReq.ClientID = installToken.ClientID
+ }
+ }
+ }
+ }
+
+ if !validToken {
security.LogInvalidToken(clientIP)
s.sendAuthResponse(conn, false, "invalid token", "")
return
@@ -299,6 +293,13 @@ func (s *Server) handleConnection(conn net.Conn) {
rules = []protocol.ProxyRule{}
}
+ // 如果使用安装token,标记为已使用
+ if isInstallToken {
+ if tokenStore, ok := s.clientStore.(db.InstallTokenStore); ok {
+ tokenStore.MarkTokenUsed(authReq.Token)
+ }
+ }
+
conn.SetReadDeadline(time.Time{})
if err := s.sendAuthResponse(conn, true, "ok", clientID); err != nil {
@@ -340,15 +341,15 @@ func (s *Server) setupClientSession(conn net.Conn, clientID, clientName, clientO
s.registerClient(cs)
defer s.unregisterClient(cs)
- if err := s.sendProxyConfig(session, rules); err != nil {
+ // 启动代理监听器(会更新 rules 的 PortStatus)
+ s.startProxyListeners(cs)
+
+ // 发送配置到客户端(包含端口状态)
+ if err := s.sendProxyConfig(session, cs.Rules); err != nil {
log.Printf("[Server] Send config error: %v", err)
return
}
- // 自动推送 JS 插件
- s.autoPushJSPlugins(cs)
-
- s.startProxyListeners(cs)
go s.heartbeatLoop(cs)
<-session.CloseChan()
@@ -442,7 +443,8 @@ func (s *Server) stopProxyListeners(cs *ClientSession) {
// startProxyListeners 启动代理监听
func (s *Server) startProxyListeners(cs *ClientSession) {
- for _, rule := range cs.Rules {
+ for i := range cs.Rules {
+ rule := &cs.Rules[i]
if !rule.IsEnabled() {
continue
}
@@ -458,15 +460,10 @@ func (s *Server) startProxyListeners(cs *ClientSession) {
continue
}
- // 检查是否为客户端插件
- if s.isClientPlugin(ruleType) {
- s.startClientPluginListener(cs, rule)
- continue
- }
-
// TCP 类型
if err := s.portManager.Reserve(rule.RemotePort, cs.ID); err != nil {
log.Printf("[Server] Port %d error: %v", rule.RemotePort, err)
+ rule.PortStatus = "failed: " + err.Error()
continue
}
@@ -474,9 +471,12 @@ func (s *Server) startProxyListeners(cs *ClientSession) {
if err != nil {
log.Printf("[Server] Listen %d error: %v", rule.RemotePort, err)
s.portManager.Release(rule.RemotePort)
+ rule.PortStatus = "failed: " + err.Error()
continue
}
+ rule.PortStatus = "listening"
+
cs.mu.Lock()
cs.Listeners[rule.RemotePort] = ln
cs.mu.Unlock()
@@ -484,17 +484,17 @@ func (s *Server) startProxyListeners(cs *ClientSession) {
switch ruleType {
case "socks5":
log.Printf("[Server] SOCKS5 proxy %s on :%d", rule.Name, rule.RemotePort)
- go s.acceptProxyServerConns(cs, ln, rule)
+ go s.acceptProxyServerConns(cs, ln, *rule)
case "http", "https":
log.Printf("[Server] HTTP proxy %s on :%d", rule.Name, rule.RemotePort)
- go s.acceptProxyServerConns(cs, ln, rule)
+ go s.acceptProxyServerConns(cs, ln, *rule)
case "websocket":
log.Printf("[Server] Websocket proxy %s on :%d", rule.Name, rule.RemotePort)
- go s.acceptWebsocketConns(cs, ln, rule)
+ go s.acceptWebsocketConns(cs, ln, *rule)
default:
log.Printf("[Server] TCP proxy %s: :%d -> %s:%d",
rule.Name, rule.RemotePort, rule.LocalIP, rule.LocalPort)
- go s.acceptProxyConns(cs, ln, rule)
+ go s.acceptProxyConns(cs, ln, *rule)
}
}
}
@@ -514,8 +514,14 @@ func (s *Server) acceptProxyConns(cs *ClientSession, ln net.Listener, rule proto
func (s *Server) acceptProxyServerConns(cs *ClientSession, ln net.Listener, rule protocol.ProxyRule) {
dialer := proxy.NewTunnelDialer(cs.Session)
- // 使用内置 proxy 实现 (带流量统计)
- proxyServer := proxy.NewServer(rule.Type, dialer, s.recordTraffic)
+ // 使用内置 proxy 实现 (带流量统计和认证)
+ username := ""
+ password := ""
+ if rule.AuthEnabled {
+ username = rule.AuthUsername
+ password = rule.AuthPassword
+ }
+ proxyServer := proxy.NewServer(rule.Type, dialer, s.recordTraffic, username, password)
for {
conn, err := ln.Accept()
if err != nil {
@@ -623,49 +629,6 @@ func (s *Server) IsClientOnline(clientID string) bool {
return ok
}
-// GetClientPluginStatus 获取客户端插件运行状态
-func (s *Server) GetClientPluginStatus(clientID string) ([]protocol.PluginStatusEntry, error) {
- s.mu.RLock()
- cs, ok := s.clients[clientID]
- s.mu.RUnlock()
-
- if !ok {
- return nil, fmt.Errorf("client %s not online", clientID)
- }
-
- stream, err := cs.Session.Open()
- if err != nil {
- return nil, err
- }
- defer stream.Close()
-
- // 发送查询请求
- msg, err := protocol.NewMessage(protocol.MsgTypePluginStatusQuery, nil)
- if err != nil {
- return nil, err
- }
- if err := protocol.WriteMessage(stream, msg); err != nil {
- return nil, err
- }
-
- // 读取响应
- resp, err := protocol.ReadMessage(stream)
- if err != nil {
- return nil, err
- }
-
- if resp.Type != protocol.MsgTypePluginStatusQueryResp {
- return nil, fmt.Errorf("unexpected response type: %d", resp.Type)
- }
-
- var statusResp protocol.PluginStatusQueryResponse
- if err := resp.ParsePayload(&statusResp); err != nil {
- return nil, err
- }
-
- return statusResp.Plugins, nil
-}
-
// GetAllClientStatus 获取所有客户端状态
func (s *Server) GetAllClientStatus() map[string]struct {
Online bool
@@ -760,8 +723,25 @@ func (s *Server) PushConfigToClient(clientID string) error {
// 启动新的监听器
s.startProxyListeners(cs)
+ // 检查是否有端口启动失败
+ var failedPorts []string
+ for _, rule := range cs.Rules {
+ if rule.IsEnabled() && strings.HasPrefix(rule.PortStatus, "failed:") {
+ failedPorts = append(failedPorts, fmt.Sprintf("port %d: %s", rule.RemotePort, strings.TrimPrefix(rule.PortStatus, "failed: ")))
+ }
+ }
+
// 发送配置到客户端
- return s.sendProxyConfig(cs.Session, rules)
+ if err := s.sendProxyConfig(cs.Session, cs.Rules); err != nil {
+ return err
+ }
+
+ // 如果有端口失败,返回错误
+ if len(failedPorts) > 0 {
+ return fmt.Errorf("some ports failed to start: %s", strings.Join(failedPorts, "; "))
+ }
+
+ return nil
}
// DisconnectClient 断开客户端连接
@@ -777,127 +757,16 @@ func (s *Server) DisconnectClient(clientID string) error {
return cs.Session.Close()
}
-// GetPluginList 获取插件列表
-func (s *Server) GetPluginList() []router.PluginInfo {
- var result []router.PluginInfo
- if s.pluginRegistry == nil {
- return result
- }
- for _, info := range s.pluginRegistry.List() {
- pi := router.PluginInfo{
- Name: info.Metadata.Name,
- Version: info.Metadata.Version,
- Type: string(info.Metadata.Type),
- Description: info.Metadata.Description,
- Source: string(info.Metadata.Source),
- Enabled: info.Enabled,
- }
- // 转换 RuleSchema
- if info.Metadata.RuleSchema != nil {
- rs := &router.RuleSchema{
- NeedsLocalAddr: info.Metadata.RuleSchema.NeedsLocalAddr,
- }
- for _, f := range info.Metadata.RuleSchema.ExtraFields {
- rs.ExtraFields = append(rs.ExtraFields, router.ConfigField{
- Key: f.Key,
- Label: f.Label,
- Type: string(f.Type),
- Default: f.Default,
- Required: f.Required,
- Options: f.Options,
- Description: f.Description,
- })
- }
- pi.RuleSchema = rs
- }
- result = append(result, pi)
- }
- return result
-}
-
-// EnablePlugin 启用插件
-func (s *Server) EnablePlugin(name string) error {
- if s.pluginRegistry == nil {
- return fmt.Errorf("plugin registry not initialized")
- }
- return s.pluginRegistry.Enable(name)
-}
-
-// DisablePlugin 禁用插件
-func (s *Server) DisablePlugin(name string) error {
- if s.pluginRegistry == nil {
- return fmt.Errorf("plugin registry not initialized")
- }
- return s.pluginRegistry.Disable(name)
-}
-
-// InstallPluginsToClient 安装插件到客户端
-func (s *Server) InstallPluginsToClient(clientID string, plugins []string) error {
- s.mu.RLock()
- cs, ok := s.clients[clientID]
- s.mu.RUnlock()
-
- if !ok {
- return fmt.Errorf("client %s not found", clientID)
- }
-
- // 发送安装请求到客户端
- if err := s.sendInstallPlugins(cs.Session, plugins); err != nil {
- return err
- }
-
- // 更新数据库中客户端的已安装插件列表
- client, err := s.clientStore.GetClient(clientID)
- if err != nil {
- return fmt.Errorf("failed to get client: %w", err)
- }
-
- // 获取插件版本信息并添加到客户端插件列表
- for _, pluginName := range plugins {
- // 检查是否已安装
- found := false
- for _, cp := range client.Plugins {
- if cp.Name == pluginName {
- found = true
- break
- }
- }
- if !found {
- client.Plugins = append(client.Plugins, db.ClientPlugin{
- Name: pluginName,
- Version: "1.0.0",
- Enabled: true,
- })
- }
- }
-
- return s.clientStore.UpdateClient(client)
-}
-
-// sendInstallPlugins 发送安装插件请求
-func (s *Server) sendInstallPlugins(session *yamux.Session, plugins []string) error {
- stream, err := session.Open()
- if err != nil {
- return err
- }
- defer stream.Close()
-
- req := protocol.InstallPluginsRequest{Plugins: plugins}
- msg, err := protocol.NewMessage(protocol.MsgTypeInstallPlugins, req)
- if err != nil {
- return err
- }
- return protocol.WriteMessage(stream, msg)
-}
// startUDPListener 启动 UDP 监听
-func (s *Server) startUDPListener(cs *ClientSession, rule protocol.ProxyRule) {
+func (s *Server) startUDPListener(cs *ClientSession, rule *protocol.ProxyRule) {
if err := s.portManager.Reserve(rule.RemotePort, cs.ID); err != nil {
log.Printf("[Server] UDP port %d error: %v", rule.RemotePort, err)
+ rule.PortStatus = "failed: " + err.Error()
return
}
@@ -905,6 +774,7 @@ func (s *Server) startUDPListener(cs *ClientSession, rule protocol.ProxyRule) {
if err != nil {
log.Printf("[Server] UDP resolve error: %v", err)
s.portManager.Release(rule.RemotePort)
+ rule.PortStatus = "failed: " + err.Error()
return
}
@@ -912,9 +782,12 @@ func (s *Server) startUDPListener(cs *ClientSession, rule protocol.ProxyRule) {
if err != nil {
log.Printf("[Server] UDP listen %d error: %v", rule.RemotePort, err)
s.portManager.Release(rule.RemotePort)
+ rule.PortStatus = "failed: " + err.Error()
return
}
+ rule.PortStatus = "listening"
+
cs.mu.Lock()
cs.UDPConns[rule.RemotePort] = conn
cs.mu.Unlock()
@@ -922,7 +795,7 @@ func (s *Server) startUDPListener(cs *ClientSession, rule protocol.ProxyRule) {
log.Printf("[Server] UDP proxy %s: :%d -> %s:%d",
rule.Name, rule.RemotePort, rule.LocalIP, rule.LocalPort)
- go s.handleUDPConn(cs, conn, rule)
+ go s.handleUDPConn(cs, conn, *rule)
}
// handleUDPConn 处理 UDP 连接
@@ -983,260 +856,14 @@ func (s *Server) sendUDPPacket(cs *ClientSession, conn *net.UDPConn, clientAddr
}
}
-// GetPluginConfigSchema 获取插件配置模式
-func (s *Server) GetPluginConfigSchema(name string) ([]router.ConfigField, error) {
- if s.pluginRegistry == nil {
- return nil, fmt.Errorf("plugin registry not initialized")
- }
- handler, err := s.pluginRegistry.GetClient(name)
- if err != nil {
- return nil, fmt.Errorf("plugin %s not found", name)
- }
- metadata := handler.Metadata()
- var result []router.ConfigField
- for _, f := range metadata.ConfigSchema {
- result = append(result, router.ConfigField{
- Key: f.Key,
- Label: f.Label,
- Type: string(f.Type),
- Default: f.Default,
- Required: f.Required,
- Options: f.Options,
- Description: f.Description,
- })
- }
- return result, nil
-}
-// SyncPluginConfigToClient 同步插件配置到客户端
-func (s *Server) SyncPluginConfigToClient(clientID string, pluginName string, config map[string]string) error {
- s.mu.RLock()
- cs, ok := s.clients[clientID]
- s.mu.RUnlock()
- if !ok {
- return fmt.Errorf("client %s not online", clientID)
- }
- return s.sendPluginConfig(cs.Session, pluginName, config)
-}
-// sendPluginConfig 发送插件配置到客户端
-func (s *Server) sendPluginConfig(session *yamux.Session, pluginName string, config map[string]string) error {
- stream, err := session.Open()
- if err != nil {
- return err
- }
- defer stream.Close()
- req := protocol.PluginConfigSync{
- PluginName: pluginName,
- Config: config,
- }
- msg, err := protocol.NewMessage(protocol.MsgTypePluginConfig, req)
- if err != nil {
- return err
- }
- return protocol.WriteMessage(stream, msg)
-}
-// InstallJSPluginToClient 安装 JS 插件到客户端
-func (s *Server) InstallJSPluginToClient(clientID string, req router.JSPluginInstallRequest) error {
- s.mu.RLock()
- cs, ok := s.clients[clientID]
- s.mu.RUnlock()
-
- if !ok {
- return fmt.Errorf("client %s not online", clientID)
- }
-
- stream, err := cs.Session.Open()
- if err != nil {
- return err
- }
- defer stream.Close()
-
- installReq := protocol.JSPluginInstallRequest{
- PluginID: req.PluginID,
- PluginName: req.PluginName,
- Source: req.Source,
- Signature: req.Signature,
- RuleName: req.RuleName,
- RemotePort: req.RemotePort,
- Config: req.Config,
- AutoStart: req.AutoStart,
- }
-
- msg, err := protocol.NewMessage(protocol.MsgTypeJSPluginInstall, installReq)
- if err != nil {
- return err
- }
-
- if err := protocol.WriteMessage(stream, msg); err != nil {
- return err
- }
-
- // 等待安装结果
- resp, err := protocol.ReadMessage(stream)
- if err != nil {
- return err
- }
-
- var result protocol.JSPluginInstallResult
- if err := resp.ParsePayload(&result); err != nil {
- return err
- }
-
- if !result.Success {
- return fmt.Errorf("install failed: %s", result.Error)
- }
-
- log.Printf("[Server] JS plugin %s installed on client %s", req.PluginName, clientID)
- return nil
-}
-
-// isClientPlugin 检查是否为客户端插件
-func (s *Server) isClientPlugin(pluginType string) bool {
- if s.pluginRegistry == nil {
- return false
- }
- handler, err := s.pluginRegistry.GetClient(pluginType)
- if err != nil {
- return false
- }
- return handler != nil
-}
-
-// startClientPluginListener 启动客户端插件监听
-func (s *Server) startClientPluginListener(cs *ClientSession, rule protocol.ProxyRule) {
- if err := s.portManager.Reserve(rule.RemotePort, cs.ID); err != nil {
- log.Printf("[Server] Port %d error: %v", rule.RemotePort, err)
- return
- }
-
- // 只有非 JS 插件才需要发送启动命令
- // JS 插件已经通过 JSPluginInstall 安装并启动,PluginID 不为空表示是 JS 插件
- if rule.PluginID == "" {
- if err := s.sendClientPluginStart(cs.Session, rule); err != nil {
- log.Printf("[Server] Failed to start client plugin %s: %v", rule.Type, err)
- s.portManager.Release(rule.RemotePort)
- return
- }
- }
-
- ln, err := net.Listen("tcp", fmt.Sprintf(":%d", rule.RemotePort))
- if err != nil {
- log.Printf("[Server] Listen %d error: %v", rule.RemotePort, err)
- s.portManager.Release(rule.RemotePort)
- return
- }
-
- cs.mu.Lock()
- cs.Listeners[rule.RemotePort] = ln
- cs.mu.Unlock()
-
- log.Printf("[Server] Client plugin %s on :%d", rule.Type, rule.RemotePort)
- go s.acceptClientPluginConns(cs, ln, rule)
-}
-
-// sendClientPluginStart 发送客户端插件启动命令
-func (s *Server) sendClientPluginStart(session *yamux.Session, rule protocol.ProxyRule) error {
- stream, err := session.Open()
- if err != nil {
- return err
- }
- defer stream.Close()
-
- req := protocol.ClientPluginStartRequest{
- PluginName: rule.Type,
- RuleName: rule.Name,
- RemotePort: rule.RemotePort,
- Config: rule.PluginConfig,
- }
- msg, err := protocol.NewMessage(protocol.MsgTypeClientPluginStart, req)
- if err != nil {
- return err
- }
- if err := protocol.WriteMessage(stream, msg); err != nil {
- return err
- }
-
- // 等待响应
- resp, err := protocol.ReadMessage(stream)
- if err != nil {
- return err
- }
- if resp.Type != protocol.MsgTypeClientPluginStatus {
- return fmt.Errorf("unexpected response type: %d", resp.Type)
- }
-
- var status protocol.ClientPluginStatusResponse
- if err := resp.ParsePayload(&status); err != nil {
- return err
- }
- if !status.Running {
- return fmt.Errorf("plugin failed: %s", status.Error)
- }
- return nil
-}
-
-// acceptClientPluginConns 接受客户端插件连接
-func (s *Server) acceptClientPluginConns(cs *ClientSession, ln net.Listener, rule protocol.ProxyRule) {
- for {
- conn, err := ln.Accept()
- if err != nil {
- return
- }
- go s.handleClientPluginConn(cs, conn, rule)
- }
-}
-
-// handleClientPluginConn 处理客户端插件连接
-func (s *Server) handleClientPluginConn(cs *ClientSession, conn net.Conn, rule protocol.ProxyRule) {
- defer conn.Close()
-
- log.Printf("[Server] handleClientPluginConn: plugin=%s, auth=%v", rule.Type, rule.AuthEnabled)
-
- // 如果启用了 HTTP Basic Auth,先进行认证
- var bufferedData []byte
- if rule.AuthEnabled {
- authenticated, data := s.checkHTTPBasicAuth(conn, rule.AuthUsername, rule.AuthPassword)
- if !authenticated {
- log.Printf("[Server] Auth failed for plugin %s", rule.Type)
- return
- }
- bufferedData = data
- log.Printf("[Server] Auth success, buffered %d bytes", len(bufferedData))
- }
-
- stream, err := cs.Session.Open()
- if err != nil {
- log.Printf("[Server] Open stream error: %v", err)
- return
- }
- defer stream.Close()
-
- req := protocol.ClientPluginConnRequest{
- PluginID: rule.PluginID,
- PluginName: rule.Type,
- RuleName: rule.Name,
- }
- msg, _ := protocol.NewMessage(protocol.MsgTypeClientPluginConn, req)
- if err := protocol.WriteMessage(stream, msg); err != nil {
- return
- }
-
- // 如果有缓冲的数据(已读取的 HTTP 请求头),先发送给客户端
- if len(bufferedData) > 0 {
- if _, err := stream.Write(bufferedData); err != nil {
- return
- }
- }
-
- relay.RelayWithStats(conn, stream, s.recordTraffic)
-}
// checkHTTPBasicAuth 检查 HTTP Basic Auth
// 返回 (认证成功, 已读取的数据)
@@ -1306,116 +933,7 @@ func (s *Server) sendHTTPUnauthorized(conn net.Conn) {
conn.Write([]byte(response))
}
-// autoPushJSPlugins 自动推送 JS 插件到客户端
-func (s *Server) autoPushJSPlugins(cs *ClientSession) {
- // 记录已推送的插件,避免重复推送
- pushedPlugins := make(map[string]bool)
- // 1. 推送配置文件中的 JS 插件
- for _, jp := range s.jsPlugins {
- if !s.shouldPushToClient(jp.AutoPush, cs.ID) {
- continue
- }
-
- log.Printf("[Server] Auto-pushing JS plugin %s to client %s", jp.Name, cs.ID)
-
- req := router.JSPluginInstallRequest{
- PluginName: jp.Name,
- Source: jp.Source,
- Signature: jp.Signature,
- RuleName: jp.Name,
- Config: jp.Config,
- AutoStart: jp.AutoStart,
- }
-
- if err := s.InstallJSPluginToClient(cs.ID, req); err != nil {
- log.Printf("[Server] Failed to push JS plugin %s: %v", jp.Name, err)
- } else {
- pushedPlugins[jp.Name] = true
- }
- }
-
- // 2. 推送客户端已安装的插件(从数据库)
- s.pushClientInstalledPlugins(cs, pushedPlugins)
-}
-
-// pushClientInstalledPlugins 推送客户端已安装的插件
-func (s *Server) pushClientInstalledPlugins(cs *ClientSession, alreadyPushed map[string]bool) {
- if s.jsPluginStore == nil {
- return
- }
-
- // 获取客户端信息
- client, err := s.clientStore.GetClient(cs.ID)
- if err != nil {
- return
- }
-
- // 遍历客户端已安装的插件
- for _, cp := range client.Plugins {
- if !cp.Enabled {
- continue
- }
-
- // 跳过已推送的
- if alreadyPushed[cp.Name] {
- continue
- }
-
- // 从 JSPluginStore 获取插件完整信息
- jsPlugin, err := s.jsPluginStore.GetJSPlugin(cp.Name)
- if err != nil {
- log.Printf("[Server] JS plugin %s not found in store: %v", cp.Name, err)
- continue
- }
-
- log.Printf("[Server] Restoring installed plugin %s to client %s", cp.Name, cs.ID)
-
- // 合并配置(客户端配置优先)
- config := jsPlugin.Config
- if config == nil {
- config = make(map[string]string)
- }
- for k, v := range cp.Config {
- config[k] = v
- }
-
- req := router.JSPluginInstallRequest{
- PluginID: cp.ID,
- PluginName: cp.Name,
- Source: jsPlugin.Source,
- Signature: jsPlugin.Signature,
- RuleName: cp.Name,
- Config: config,
- AutoStart: jsPlugin.AutoStart,
- }
-
- if err := s.InstallJSPluginToClient(cs.ID, req); err != nil {
- log.Printf("[Server] Failed to restore plugin %s: %v", cp.Name, err)
- } else if cp.RemotePort > 0 {
- // 检查端口是否已在监听(避免重复启动)
- cs.mu.Lock()
- _, exists := cs.Listeners[cp.RemotePort]
- cs.mu.Unlock()
- if exists {
- continue
- }
-
- // 安装成功后启动服务端监听器
- pluginRule := protocol.ProxyRule{
- Name: cp.Name,
- Type: cp.Name,
- RemotePort: cp.RemotePort,
- Enabled: boolPtr(true),
- PluginID: cp.ID,
- AuthEnabled: cp.AuthEnabled,
- AuthUsername: cp.AuthUsername,
- AuthPassword: cp.AuthPassword,
- }
- s.startClientPluginListener(cs, pluginRule)
- }
- }
-}
// shouldPushToClient 检查是否应推送到指定客户端
func (s *Server) shouldPushToClient(autoPush []string, clientID string) bool {
@@ -1462,117 +980,10 @@ func (s *Server) RestartClient(clientID string) error {
return nil
}
-// StartClientPlugin 启动客户端插件
-func (s *Server) StartClientPlugin(clientID, pluginID, pluginName, ruleName string) error {
- s.mu.RLock()
- _, ok := s.clients[clientID]
- s.mu.RUnlock()
- if !ok {
- return fmt.Errorf("client %s not found or not online", clientID)
- }
- // 重新发送安装请求来启动插件
- return s.reinstallJSPlugin(clientID, pluginName, ruleName)
-}
-// StopClientPlugin 停止客户端插件
-func (s *Server) StopClientPlugin(clientID, pluginID, pluginName, ruleName string) error {
- s.mu.RLock()
- cs, ok := s.clients[clientID]
- s.mu.RUnlock()
- if !ok {
- return fmt.Errorf("client %s not found or not online", clientID)
- }
-
- return s.sendClientPluginStop(cs.Session, pluginID, pluginName, ruleName)
-}
-
-// sendClientPluginStop 发送客户端插件停止命令
-func (s *Server) sendClientPluginStop(session *yamux.Session, pluginID, pluginName, ruleName string) error {
- stream, err := session.Open()
- if err != nil {
- return err
- }
- defer stream.Close()
-
- req := protocol.ClientPluginStopRequest{
- PluginID: pluginID,
- PluginName: pluginName,
- RuleName: ruleName,
- }
- msg, err := protocol.NewMessage(protocol.MsgTypeClientPluginStop, req)
- if err != nil {
- return err
- }
- if err := protocol.WriteMessage(stream, msg); err != nil {
- return err
- }
-
- // 等待响应
- resp, err := protocol.ReadMessage(stream)
- if err != nil {
- return err
- }
- if resp.Type != protocol.MsgTypeClientPluginStatus {
- return fmt.Errorf("unexpected response type: %d", resp.Type)
- }
-
- var status protocol.ClientPluginStatusResponse
- if err := resp.ParsePayload(&status); err != nil {
- return err
- }
- if status.Running {
- return fmt.Errorf("plugin still running: %s", status.Error)
- }
- return nil
-}
-
-// StartPluginRule 为客户端插件启动服务端监听器
-func (s *Server) StartPluginRule(clientID string, rule protocol.ProxyRule) error {
- s.mu.RLock()
- cs, ok := s.clients[clientID]
- s.mu.RUnlock()
-
- if !ok {
- return fmt.Errorf("client %s not found or not online", clientID)
- }
-
- // 检查端口是否已被占用
- cs.mu.Lock()
- _, exists := cs.Listeners[rule.RemotePort]
- cs.mu.Unlock()
- if exists {
- // 端口已在监听,无需重复启动
- return nil
- }
-
- // 启动插件监听器
- s.startClientPluginListener(cs, rule)
- return nil
-}
-
-// StopPluginRule 停止客户端插件的服务端监听器
-func (s *Server) StopPluginRule(clientID string, remotePort int) error {
- s.mu.RLock()
- cs, ok := s.clients[clientID]
- s.mu.RUnlock()
-
- if !ok {
- return nil // 客户端不在线,无需停止
- }
-
- cs.mu.Lock()
- if ln, exists := cs.Listeners[remotePort]; exists {
- ln.Close()
- delete(cs.Listeners, remotePort)
- }
- cs.mu.Unlock()
-
- s.portManager.Release(remotePort)
- return nil
-}
// IsPortAvailable 检查端口是否可用
func (s *Server) IsPortAvailable(port int, excludeClientID string) bool {
@@ -1597,204 +1008,10 @@ func (s *Server) IsPortAvailable(port int, excludeClientID string) bool {
return true
}
-// ProxyPluginAPIRequest 代理插件 API 请求到客户端
-func (s *Server) ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error) {
- s.mu.RLock()
- cs, ok := s.clients[clientID]
- s.mu.RUnlock()
- if !ok {
- return nil, fmt.Errorf("client %s not found or not online", clientID)
- }
- stream, err := cs.Session.Open()
- if err != nil {
- return nil, fmt.Errorf("open stream: %w", err)
- }
- defer stream.Close()
- // 设置超时(30秒)
- stream.SetDeadline(time.Now().Add(30 * time.Second))
- // 发送 API 请求
- msg, err := protocol.NewMessage(protocol.MsgTypePluginAPIRequest, req)
- if err != nil {
- return nil, err
- }
- if err := protocol.WriteMessage(stream, msg); err != nil {
- return nil, err
- }
-
- // 读取响应
- resp, err := protocol.ReadMessage(stream)
- if err != nil {
- return nil, fmt.Errorf("read response: %w", err)
- }
-
- if resp.Type != protocol.MsgTypePluginAPIResponse {
- return nil, fmt.Errorf("unexpected response type: %d", resp.Type)
- }
-
- var apiResp protocol.PluginAPIResponse
- if err := resp.ParsePayload(&apiResp); err != nil {
- return nil, err
- }
-
- return &apiResp, nil
-}
-
-// RestartClientPlugin 重启客户端 JS 插件
-func (s *Server) RestartClientPlugin(clientID, pluginID, pluginName, ruleName string) error {
- s.mu.RLock()
- _, ok := s.clients[clientID]
- s.mu.RUnlock()
-
- if !ok {
- return fmt.Errorf("client %s not found or not online", clientID)
- }
-
- // 重新发送完整的安装请求来重启 JS 插件
- return s.reinstallJSPlugin(clientID, pluginName, ruleName)
-}
-
-// reinstallJSPlugin 重新安装 JS 插件(用于重启)
-func (s *Server) reinstallJSPlugin(clientID, pluginName, ruleName string) error {
- // 从数据库获取插件信息
- if s.jsPluginStore == nil {
- return fmt.Errorf("JS plugin store not configured")
- }
-
- jsPlugin, err := s.jsPluginStore.GetJSPlugin(pluginName)
- if err != nil {
- return fmt.Errorf("plugin %s not found: %w", pluginName, err)
- }
-
- // 获取客户端的插件配置
- client, err := s.clientStore.GetClient(clientID)
- if err != nil {
- return fmt.Errorf("client not found: %w", err)
- }
-
- // 合并配置并获取 PluginID
- config := jsPlugin.Config
- if config == nil {
- config = make(map[string]string)
- }
- var pluginID string
- for _, cp := range client.Plugins {
- if cp.Name == pluginName {
- pluginID = cp.ID
- for k, v := range cp.Config {
- config[k] = v
- }
- break
- }
- }
-
- log.Printf("[Server] Reinstalling JS plugin %s (ID: %s) to client %s", pluginName, pluginID, clientID)
-
- req := router.JSPluginInstallRequest{
- PluginID: pluginID,
- PluginName: pluginName,
- Source: jsPlugin.Source,
- Signature: jsPlugin.Signature,
- RuleName: ruleName,
- Config: config,
- AutoStart: true, // 重启时总是自动启动
- }
-
- return s.InstallJSPluginToClient(clientID, req)
-}
-
-// sendJSPluginRestart 发送 JS 插件重启命令
-func (s *Server) sendJSPluginRestart(session *yamux.Session, pluginName, ruleName string) error {
- stream, err := session.Open()
- if err != nil {
- return err
- }
- defer stream.Close()
-
- // 使用 PluginConfigUpdate 消息触发重启
- req := protocol.PluginConfigUpdateRequest{
- PluginName: pluginName,
- RuleName: ruleName,
- Config: nil,
- Restart: true,
- }
- msg, err := protocol.NewMessage(protocol.MsgTypePluginConfigUpdate, req)
- if err != nil {
- return err
- }
- if err := protocol.WriteMessage(stream, msg); err != nil {
- return err
- }
-
- // 等待响应
- resp, err := protocol.ReadMessage(stream)
- if err != nil {
- return err
- }
-
- var result struct {
- Success bool `json:"success"`
- Error string `json:"error,omitempty"`
- }
- if err := resp.ParsePayload(&result); err != nil {
- return err
- }
- if !result.Success {
- return fmt.Errorf("restart failed: %s", result.Error)
- }
-
- log.Printf("[Server] JS plugin %s restarted on client", pluginName)
- return nil
-}
-
-// UpdateClientPluginConfig 更新客户端插件配置
-func (s *Server) UpdateClientPluginConfig(clientID, pluginID, pluginName, ruleName string, config map[string]string, restart bool) error {
- s.mu.RLock()
- cs, ok := s.clients[clientID]
- s.mu.RUnlock()
-
- if !ok {
- return fmt.Errorf("client %s not found or not online", clientID)
- }
-
- // 发送配置更新消息
- stream, err := cs.Session.Open()
- if err != nil {
- return err
- }
- defer stream.Close()
-
- req := protocol.PluginConfigUpdateRequest{
- PluginID: pluginID,
- PluginName: pluginName,
- RuleName: ruleName,
- Config: config,
- Restart: restart,
- }
- msg, _ := protocol.NewMessage(protocol.MsgTypePluginConfigUpdate, req)
- if err := protocol.WriteMessage(stream, msg); err != nil {
- return err
- }
-
- // 等待响应
- resp, err := protocol.ReadMessage(stream)
- if err != nil {
- return err
- }
-
- var result protocol.PluginConfigUpdateResponse
- if err := resp.ParsePayload(&result); err != nil {
- return err
- }
- if !result.Success {
- return fmt.Errorf("config update failed: %s", result.Error)
- }
-
- return nil
-}
// SendUpdateToClient 发送更新命令到客户端
func (s *Server) SendUpdateToClient(clientID, downloadURL string) error {
diff --git a/pkg/plugin/audit/audit.go b/pkg/plugin/audit/audit.go
deleted file mode 100644
index 88db7a8..0000000
--- a/pkg/plugin/audit/audit.go
+++ /dev/null
@@ -1,154 +0,0 @@
-package audit
-
-import (
- "encoding/json"
- "log"
- "os"
- "path/filepath"
- "sync"
- "time"
-)
-
-// EventType 审计事件类型
-type EventType string
-
-const (
- EventPluginInstall EventType = "plugin_install"
- EventPluginUninstall EventType = "plugin_uninstall"
- EventPluginStart EventType = "plugin_start"
- EventPluginStop EventType = "plugin_stop"
- EventPluginVerify EventType = "plugin_verify"
- EventPluginReject EventType = "plugin_reject"
- EventConfigChange EventType = "config_change"
-)
-
-// Event 审计事件
-type Event struct {
- Timestamp time.Time `json:"timestamp"`
- Type EventType `json:"type"`
- PluginName string `json:"plugin_name,omitempty"`
- Version string `json:"version,omitempty"`
- ClientID string `json:"client_id,omitempty"`
- Success bool `json:"success"`
- Message string `json:"message,omitempty"`
- Details map[string]string `json:"details,omitempty"`
-}
-
-// Logger 审计日志记录器
-type Logger struct {
- path string
- file *os.File
- mu sync.Mutex
- enabled bool
-}
-
-var (
- defaultLogger *Logger
- loggerOnce sync.Once
-)
-
-// NewLogger 创建审计日志记录器
-func NewLogger(dataDir string) (*Logger, error) {
- path := filepath.Join(dataDir, "audit.log")
- dir := filepath.Dir(path)
- if err := os.MkdirAll(dir, 0755); err != nil {
- return nil, err
- }
-
- file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
- if err != nil {
- return nil, err
- }
-
- return &Logger{path: path, file: file, enabled: true}, nil
-}
-
-// InitDefault 初始化默认日志记录器
-func InitDefault(dataDir string) error {
- var err error
- loggerOnce.Do(func() {
- defaultLogger, err = NewLogger(dataDir)
- })
- return err
-}
-
-// Log 记录审计事件
-func (l *Logger) Log(event Event) {
- if l == nil || !l.enabled {
- return
- }
-
- event.Timestamp = time.Now()
- l.mu.Lock()
- defer l.mu.Unlock()
-
- data, err := json.Marshal(event)
- if err != nil {
- log.Printf("[Audit] Marshal error: %v", err)
- return
- }
-
- if _, err := l.file.Write(append(data, '\n')); err != nil {
- log.Printf("[Audit] Write error: %v", err)
- }
-}
-
-// Close 关闭日志文件
-func (l *Logger) Close() error {
- if l == nil || l.file == nil {
- return nil
- }
- return l.file.Close()
-}
-
-// LogEvent 使用默认记录器记录事件
-func LogEvent(event Event) {
- if defaultLogger != nil {
- defaultLogger.Log(event)
- }
-}
-
-// LogPluginInstall 记录插件安装事件
-func LogPluginInstall(pluginName, version, clientID string, success bool, msg string) {
- LogEvent(Event{
- Type: EventPluginInstall,
- PluginName: pluginName,
- Version: version,
- ClientID: clientID,
- Success: success,
- Message: msg,
- })
-}
-
-// LogPluginVerify 记录插件验证事件
-func LogPluginVerify(pluginName, version string, success bool, msg string) {
- LogEvent(Event{
- Type: EventPluginVerify,
- PluginName: pluginName,
- Version: version,
- Success: success,
- Message: msg,
- })
-}
-
-// LogPluginReject 记录插件拒绝事件
-func LogPluginReject(pluginName, version, reason string) {
- LogEvent(Event{
- Type: EventPluginReject,
- PluginName: pluginName,
- Version: version,
- Success: false,
- Message: reason,
- })
-}
-
-// LogWithDetails 记录带详情的事件
-func LogWithDetails(eventType EventType, pluginName string, success bool, msg string, details map[string]string) {
- LogEvent(Event{
- Type: eventType,
- PluginName: pluginName,
- Success: success,
- Message: msg,
- Details: details,
- })
-}
diff --git a/pkg/plugin/registry.go b/pkg/plugin/registry.go
deleted file mode 100644
index bbe5fea..0000000
--- a/pkg/plugin/registry.go
+++ /dev/null
@@ -1,134 +0,0 @@
-package plugin
-
-import (
- "context"
- "fmt"
- "sync"
-)
-
-// Registry 管理可用的 plugins (仅客户端插件)
-type Registry struct {
- clientPlugins map[string]ClientPlugin // 客户端插件
- enabled map[string]bool // 启用状态
- mu sync.RWMutex
-}
-
-// NewRegistry 创建 plugin 注册表
-func NewRegistry() *Registry {
- return &Registry{
- clientPlugins: make(map[string]ClientPlugin),
- enabled: make(map[string]bool),
- }
-}
-
-// RegisterClient 注册客户端插件
-func (r *Registry) RegisterClient(handler ClientPlugin) error {
- r.mu.Lock()
- defer r.mu.Unlock()
-
- meta := handler.Metadata()
- if meta.Name == "" {
- return fmt.Errorf("plugin name cannot be empty")
- }
-
- if _, exists := r.clientPlugins[meta.Name]; exists {
- return fmt.Errorf("client plugin %s already registered", meta.Name)
- }
-
- r.clientPlugins[meta.Name] = handler
- r.enabled[meta.Name] = true
- return nil
-}
-
-// GetClient 返回客户端插件
-func (r *Registry) GetClient(name string) (ClientPlugin, error) {
- r.mu.RLock()
- defer r.mu.RUnlock()
-
- if handler, ok := r.clientPlugins[name]; ok {
- if !r.enabled[name] {
- return nil, fmt.Errorf("client plugin %s is disabled", name)
- }
- return handler, nil
- }
- return nil, fmt.Errorf("client plugin %s not found", name)
-}
-
-// List 返回所有可用的 plugins
-func (r *Registry) List() []Info {
- r.mu.RLock()
- defer r.mu.RUnlock()
-
- var plugins []Info
-
- for name, handler := range r.clientPlugins {
- plugins = append(plugins, Info{
- Metadata: handler.Metadata(),
- Loaded: true,
- Enabled: r.enabled[name],
- })
- }
-
- return plugins
-}
-
-// Has 检查 plugin 是否存在
-func (r *Registry) Has(name string) bool {
- r.mu.RLock()
- defer r.mu.RUnlock()
-
- _, ok := r.clientPlugins[name]
- return ok
-}
-
-// Close 关闭所有 plugins
-func (r *Registry) Close(ctx context.Context) error {
- r.mu.Lock()
- defer r.mu.Unlock()
-
- var lastErr error
- for name, handler := range r.clientPlugins {
- if err := handler.Stop(); err != nil {
- lastErr = fmt.Errorf("failed to stop client plugin %s: %w", name, err)
- }
- }
-
- return lastErr
-}
-
-// Enable 启用插件
-func (r *Registry) Enable(name string) error {
- r.mu.Lock()
- defer r.mu.Unlock()
-
- if !r.has(name) {
- return fmt.Errorf("plugin %s not found", name)
- }
- r.enabled[name] = true
- return nil
-}
-
-// Disable 禁用插件
-func (r *Registry) Disable(name string) error {
- r.mu.Lock()
- defer r.mu.Unlock()
-
- if !r.has(name) {
- return fmt.Errorf("plugin %s not found", name)
- }
- r.enabled[name] = false
- return nil
-}
-
-// has 内部检查(无锁)
-func (r *Registry) has(name string) bool {
- _, ok := r.clientPlugins[name]
- return ok
-}
-
-// IsEnabled 检查插件是否启用
-func (r *Registry) IsEnabled(name string) bool {
- r.mu.RLock()
- defer r.mu.RUnlock()
- return r.enabled[name]
-}
diff --git a/pkg/plugin/schema.go b/pkg/plugin/schema.go
deleted file mode 100644
index 57b37e1..0000000
--- a/pkg/plugin/schema.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package plugin
-
-// 内置协议类型配置模式
-
-// BuiltinRuleSchemas 返回所有内置协议类型的配置模式
-func BuiltinRuleSchemas() map[string]RuleSchema {
- return map[string]RuleSchema{
- "tcp": {
- NeedsLocalAddr: true,
- ExtraFields: nil,
- },
- "udp": {
- NeedsLocalAddr: true,
- ExtraFields: nil,
- },
- "http": {
- NeedsLocalAddr: false,
- ExtraFields: []ConfigField{
- {
- Key: "auth_enabled",
- Label: "启用认证",
- Type: ConfigFieldBool,
- Default: "false",
- Description: "是否启用 HTTP Basic 认证",
- },
- {
- Key: "username",
- Label: "用户名",
- Type: ConfigFieldString,
- Description: "HTTP 代理认证用户名",
- },
- {
- Key: "password",
- Label: "密码",
- Type: ConfigFieldPassword,
- Description: "HTTP 代理认证密码",
- },
- },
- },
- "https": {
- NeedsLocalAddr: false,
- ExtraFields: []ConfigField{
- {
- Key: "auth_enabled",
- Label: "启用认证",
- Type: ConfigFieldBool,
- Default: "false",
- Description: "是否启用 HTTPS 代理认证",
- },
- {
- Key: "username",
- Label: "用户名",
- Type: ConfigFieldString,
- Description: "HTTPS 代理认证用户名",
- },
- {
- Key: "password",
- Label: "密码",
- Type: ConfigFieldPassword,
- Description: "HTTPS 代理认证密码",
- },
- },
- },
- "socks5": {
- NeedsLocalAddr: false,
- ExtraFields: []ConfigField{
- {
- Key: "auth_enabled",
- Label: "启用认证",
- Type: ConfigFieldBool,
- Default: "false",
- Description: "是否启用 SOCKS5 用户名/密码认证",
- },
- {
- Key: "username",
- Label: "用户名",
- Type: ConfigFieldString,
- Description: "SOCKS5 认证用户名",
- },
- {
- Key: "password",
- Label: "密码",
- Type: ConfigFieldPassword,
- Description: "SOCKS5 认证密码",
- },
- },
- },
- }
-}
-
-// GetRuleSchema 获取指定协议类型的配置模式
-func GetRuleSchema(proxyType string) *RuleSchema {
- schemas := BuiltinRuleSchemas()
- if schema, ok := schemas[proxyType]; ok {
- return &schema
- }
- return nil
-}
-
-// IsBuiltinType 检查是否为内置协议类型
-func IsBuiltinType(proxyType string) bool {
- builtinTypes := []string{"tcp", "udp", "http", "https"}
- for _, t := range builtinTypes {
- if t == proxyType {
- return true
- }
- }
- return false
-}
diff --git a/pkg/plugin/script/js.go b/pkg/plugin/script/js.go
deleted file mode 100644
index dc8d555..0000000
--- a/pkg/plugin/script/js.go
+++ /dev/null
@@ -1,913 +0,0 @@
-package script
-
-import (
- "bufio"
- "encoding/json"
- "fmt"
- "io"
- "net"
- "net/http"
- "os"
- "path/filepath"
- "strconv"
- "strings"
- "sync"
-
- "github.com/dop251/goja"
- "github.com/gotunnel/pkg/plugin"
-)
-
-// JSPlugin JavaScript 脚本插件
-type JSPlugin struct {
- name string
- source string
- vm *goja.Runtime
- metadata plugin.Metadata
- config map[string]string
- sandbox *Sandbox
- running bool
- mu sync.Mutex
- eventListeners map[string][]func(goja.Value)
- storagePath string
- apiHandlers map[string]map[string]goja.Callable // method -> path -> handler
-}
-
-// NewJSPlugin 从 JS 源码创建插件
-func NewJSPlugin(name, source string) (*JSPlugin, error) {
- p := &JSPlugin{
- name: name,
- source: source,
- vm: goja.New(),
- sandbox: DefaultSandbox(),
- eventListeners: make(map[string][]func(goja.Value)),
- storagePath: filepath.Join("plugin_data", name+".json"),
- apiHandlers: make(map[string]map[string]goja.Callable),
- }
-
- // 确保存储目录存在
- os.MkdirAll("plugin_data", 0755)
-
- if err := p.init(); err != nil {
- return nil, err
- }
-
- return p, nil
-}
-
-// SetSandbox 设置沙箱配置
-func (p *JSPlugin) SetSandbox(sandbox *Sandbox) {
- p.sandbox = sandbox
-}
-
-// init 初始化 JS 运行时
-func (p *JSPlugin) init() error {
- // 设置栈深度限制(防止递归攻击)
- if p.sandbox.MaxStackDepth > 0 {
- p.vm.SetMaxCallStackSize(p.sandbox.MaxStackDepth)
- }
-
- // 注入基础 API
- p.vm.Set("log", p.jsLog)
-
- // Config API (兼容旧的 config() 调用,同时支持 config.get/getAll)
- p.vm.Set("config", p.jsGetConfig)
- if configObj := p.vm.Get("config"); configObj != nil {
- obj := configObj.ToObject(p.vm)
- obj.Set("get", p.jsGetConfig)
- obj.Set("getAll", p.jsGetAllConfig)
- }
-
- // 注入增强 API
- p.vm.Set("logger", p.createLoggerAPI())
- p.vm.Set("storage", p.createStorageAPI())
- p.vm.Set("event", p.createEventAPI())
- p.vm.Set("request", p.createRequestAPI())
- p.vm.Set("notify", p.createNotifyAPI())
-
- // 注入文件 API
- p.vm.Set("fs", p.createFsAPI())
-
- // 注入 HTTP API
- p.vm.Set("http", p.createHttpAPI())
-
- // 注入路由 API
- p.vm.Set("api", p.createRouteAPI())
-
- // 执行脚本
- _, err := p.vm.RunString(p.source)
- if err != nil {
- return fmt.Errorf("run script: %w", err)
- }
-
- // 获取元数据
- if err := p.loadMetadata(); err != nil {
- return err
- }
-
- return nil
-}
-
-// loadMetadata 从 JS 获取元数据
-func (p *JSPlugin) loadMetadata() error {
- fn, ok := goja.AssertFunction(p.vm.Get("metadata"))
- if !ok {
- // 使用默认元数据
- p.metadata = plugin.Metadata{
- Name: p.name,
- Type: plugin.PluginTypeApp,
- Source: plugin.PluginSourceScript,
- RunAt: plugin.SideClient,
- }
- return nil
- }
-
- result, err := fn(goja.Undefined())
- if err != nil {
- return err
- }
-
- obj := result.ToObject(p.vm)
- p.metadata = plugin.Metadata{
- Name: getString(obj, "name", p.name),
- Version: getString(obj, "version", "1.0.0"),
- Type: plugin.PluginType(getString(obj, "type", "app")),
- Source: plugin.PluginSourceScript,
- RunAt: plugin.Side(getString(obj, "run_at", "client")),
- Description: getString(obj, "description", ""),
- Author: getString(obj, "author", ""),
- }
- return nil
-}
-
-// Metadata 返回插件元数据
-func (p *JSPlugin) Metadata() plugin.Metadata {
- return p.metadata
-}
-
-// Init 初始化插件配置
-func (p *JSPlugin) Init(config map[string]string) error {
- p.config = config
-
- // 根据 root_path 配置设置沙箱允许的路径
- if rootPath := config["root_path"]; rootPath != "" {
- absPath, err := filepath.Abs(rootPath)
- if err == nil {
- p.sandbox.AllowedPaths = append(p.sandbox.AllowedPaths, absPath)
- p.sandbox.WritablePaths = append(p.sandbox.WritablePaths, absPath)
- }
- } else {
- // 如果没有配置 root_path,默认允许访问当前目录
- cwd, err := os.Getwd()
- if err == nil {
- p.sandbox.AllowedPaths = append(p.sandbox.AllowedPaths, cwd)
- p.sandbox.WritablePaths = append(p.sandbox.WritablePaths, cwd)
- }
- }
-
- return nil
-}
-
-// Start 启动插件
-func (p *JSPlugin) Start() (string, error) {
- p.mu.Lock()
- defer p.mu.Unlock()
-
- if p.running {
- return "", nil
- }
-
- fn, ok := goja.AssertFunction(p.vm.Get("start"))
- if ok {
- _, err := fn(goja.Undefined())
- if err != nil {
- return "", err
- }
- }
-
- p.running = true
- return "script-plugin", nil
-}
-
-// HandleConn 处理连接
-func (p *JSPlugin) HandleConn(conn net.Conn) error {
- defer conn.Close()
-
- // goja Runtime 不是线程安全的,需要加锁
- p.mu.Lock()
- defer p.mu.Unlock()
-
- // 创建连接包装器
- jsConn := newJSConn(conn)
-
- fn, ok := goja.AssertFunction(p.vm.Get("handleConn"))
- if !ok {
- return fmt.Errorf("handleConn not defined")
- }
-
- _, err := fn(goja.Undefined(), p.vm.ToValue(jsConn))
- return err
-}
-
-// Stop 停止插件
-func (p *JSPlugin) Stop() error {
- p.mu.Lock()
- defer p.mu.Unlock()
-
- if !p.running {
- return nil
- }
-
- fn, ok := goja.AssertFunction(p.vm.Get("stop"))
- if ok {
- fn(goja.Undefined())
- }
-
- p.running = false
- return nil
-}
-
-// jsLog JS 日志函数
-func (p *JSPlugin) jsLog(msg string) {
- fmt.Printf("[JS:%s] %s\n", p.name, msg)
-}
-
-// jsGetConfig 获取配置
-func (p *JSPlugin) jsGetConfig(key string) string {
- if p.config == nil {
- return ""
- }
- return p.config[key]
-}
-
-// getString 从 JS 对象获取字符串
-func getString(obj *goja.Object, key, def string) string {
- v := obj.Get(key)
- if v == nil || goja.IsUndefined(v) {
- return def
- }
- return v.String()
-}
-
-// jsConn JS 连接包装器
-type jsConn struct {
- conn net.Conn
-}
-
-func newJSConn(conn net.Conn) *jsConn {
- return &jsConn{conn: conn}
-}
-
-func (c *jsConn) Read(size int) []byte {
- buf := make([]byte, size)
- n, err := c.conn.Read(buf)
- if err != nil {
- return nil
- }
- return buf[:n]
-}
-
-func (c *jsConn) Write(data []byte) int {
- n, _ := c.conn.Write(data)
- return n
-}
-
-func (c *jsConn) Close() {
- c.conn.Close()
-}
-
-// =============================================================================
-// 文件系统 API
-// =============================================================================
-
-// createFsAPI 创建文件系统 API
-func (p *JSPlugin) createFsAPI() map[string]interface{} {
- return map[string]interface{}{
- "readFile": p.fsReadFile,
- "writeFile": p.fsWriteFile,
- "readDir": p.fsReadDir,
- "stat": p.fsStat,
- "exists": p.fsExists,
- "mkdir": p.fsMkdir,
- "remove": p.fsRemove,
- }
-}
-
-func (p *JSPlugin) fsReadFile(path string) map[string]interface{} {
- if err := p.sandbox.ValidateReadPath(path); err != nil {
- return map[string]interface{}{"error": err.Error(), "data": ""}
- }
-
- info, err := os.Stat(path)
- if err != nil {
- return map[string]interface{}{"error": err.Error(), "data": ""}
- }
- if info.Size() > p.sandbox.MaxReadSize {
- return map[string]interface{}{"error": "file too large", "data": ""}
- }
-
- data, err := os.ReadFile(path)
- if err != nil {
- return map[string]interface{}{"error": err.Error(), "data": ""}
- }
- return map[string]interface{}{"error": "", "data": string(data)}
-}
-
-func (p *JSPlugin) fsWriteFile(path, content string) map[string]interface{} {
- if err := p.sandbox.ValidateWritePath(path); err != nil {
- return map[string]interface{}{"error": err.Error(), "ok": false}
- }
-
- if int64(len(content)) > p.sandbox.MaxWriteSize {
- return map[string]interface{}{"error": "content too large", "ok": false}
- }
-
- err := os.WriteFile(path, []byte(content), 0644)
- if err != nil {
- return map[string]interface{}{"error": err.Error(), "ok": false}
- }
- return map[string]interface{}{"error": "", "ok": true}
-}
-
-func (p *JSPlugin) fsReadDir(path string) map[string]interface{} {
- if err := p.sandbox.ValidateReadPath(path); err != nil {
- return map[string]interface{}{"error": err.Error(), "entries": nil}
- }
-
- entries, err := os.ReadDir(path)
- if err != nil {
- return map[string]interface{}{"error": err.Error(), "entries": nil}
- }
- var result []map[string]interface{}
- for _, e := range entries {
- info, _ := e.Info()
- result = append(result, map[string]interface{}{
- "name": e.Name(),
- "isDir": e.IsDir(),
- "size": info.Size(),
- })
- }
- return map[string]interface{}{"error": "", "entries": result}
-}
-
-func (p *JSPlugin) fsStat(path string) map[string]interface{} {
- if err := p.sandbox.ValidateReadPath(path); err != nil {
- return map[string]interface{}{"error": err.Error()}
- }
-
- info, err := os.Stat(path)
- if err != nil {
- return map[string]interface{}{"error": err.Error()}
- }
- return map[string]interface{}{
- "error": "",
- "name": info.Name(),
- "size": info.Size(),
- "isDir": info.IsDir(),
- "modTime": info.ModTime().Unix(),
- }
-}
-
-func (p *JSPlugin) fsExists(path string) map[string]interface{} {
- if err := p.sandbox.ValidateReadPath(path); err != nil {
- return map[string]interface{}{"error": err.Error(), "exists": false}
- }
- _, err := os.Stat(path)
- return map[string]interface{}{"error": "", "exists": err == nil}
-}
-
-func (p *JSPlugin) fsMkdir(path string) map[string]interface{} {
- if err := p.sandbox.ValidateWritePath(path); err != nil {
- return map[string]interface{}{"error": err.Error(), "ok": false}
- }
- err := os.MkdirAll(path, 0755)
- if err != nil {
- return map[string]interface{}{"error": err.Error(), "ok": false}
- }
- return map[string]interface{}{"error": "", "ok": true}
-}
-
-func (p *JSPlugin) fsRemove(path string) map[string]interface{} {
- if err := p.sandbox.ValidateWritePath(path); err != nil {
- return map[string]interface{}{"error": err.Error(), "ok": false}
- }
- err := os.RemoveAll(path)
- if err != nil {
- return map[string]interface{}{"error": err.Error(), "ok": false}
- }
- return map[string]interface{}{"error": "", "ok": true}
-}
-
-// =============================================================================
-// HTTP 服务 API
-// =============================================================================
-
-// createHttpAPI 创建 HTTP API
-func (p *JSPlugin) createHttpAPI() map[string]interface{} {
- return map[string]interface{}{
- "serve": p.httpServe,
- "json": p.httpJSON,
- "sendFile": p.httpSendFile,
- }
-}
-
-// httpServe 启动 HTTP 服务处理连接
-func (p *JSPlugin) httpServe(connObj interface{}, handler goja.Callable) {
- // 从 jsConn 包装器中提取原始 net.Conn
- var conn net.Conn
- if jc, ok := connObj.(*jsConn); ok {
- conn = jc.conn
- } else if nc, ok := connObj.(net.Conn); ok {
- conn = nc
- } else {
- fmt.Printf("[JS:%s] httpServe: invalid conn type: %T\n", p.name, connObj)
- return
- }
-
- // 注意:不要在这里关闭连接,HandleConn 会负责关闭
-
- // Use bufio to read the request properly
- reader := bufio.NewReader(conn)
-
- for {
- // 1. Read Request Line
- line, err := reader.ReadString('\n')
- if err != nil {
- if err != io.EOF {
- fmt.Printf("[JS:%s] httpServe read error: %v\n", p.name, err)
- }
- return
- }
- line = strings.TrimSpace(line)
- if line == "" {
- continue
- }
-
- parts := strings.Split(line, " ")
- if len(parts) < 2 {
- fmt.Printf("[JS:%s] Invalid request line: %s\n", p.name, line)
- return
- }
- method := parts[0]
- path := parts[1]
-
- fmt.Printf("[JS:%s] Request: %s %s\n", p.name, method, path)
-
- // 2. Read Headers
- headers := make(map[string]string)
- contentLength := 0
- for {
- hLine, err := reader.ReadString('\n')
- if err != nil {
- break
- }
- hLine = strings.TrimSpace(hLine)
- if hLine == "" {
- break
- }
- if idx := strings.Index(hLine, ":"); idx > 0 {
- key := strings.TrimSpace(hLine[:idx])
- val := strings.TrimSpace(hLine[idx+1:])
- headers[strings.ToLower(key)] = val
- if strings.ToLower(key) == "content-length" {
- contentLength, _ = strconv.Atoi(val)
- }
- }
- }
-
- // 3. Read Body
- body := ""
- if contentLength > 0 {
- bodyBuf := make([]byte, contentLength)
- if _, err := io.ReadFull(reader, bodyBuf); err == nil {
- body = string(bodyBuf)
- }
- }
-
- req := map[string]interface{}{
- "method": method,
- "path": path,
- "headers": headers,
- "body": body,
- }
-
- // 调用 JS handler 函数
- result, err := handler(goja.Undefined(), p.vm.ToValue(req))
- if err != nil {
- fmt.Printf("[JS:%s] HTTP handler error: %v\n", p.name, err)
- conn.Write([]byte("HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n"))
- return
- }
-
- // 将结果转换为 map
- if result == nil || goja.IsUndefined(result) || goja.IsNull(result) {
- conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"))
- continue
- }
-
- resp := make(map[string]interface{})
- respObj := result.ToObject(p.vm)
- for _, key := range respObj.Keys() {
- val := respObj.Get(key)
- resp[key] = val.Export()
- }
-
- writeHTTPResponse(conn, resp)
- }
-}
-
-func (p *JSPlugin) httpJSON(data interface{}) string {
- b, _ := json.Marshal(data)
- return string(b)
-}
-
-func (p *JSPlugin) httpSendFile(connObj interface{}, filePath string) {
- // 从 jsConn 包装器中提取原始 net.Conn
- var conn net.Conn
- if jc, ok := connObj.(*jsConn); ok {
- conn = jc.conn
- } else if nc, ok := connObj.(net.Conn); ok {
- conn = nc
- } else {
- fmt.Printf("[JS:%s] httpSendFile: invalid conn type: %T\n", p.name, connObj)
- return
- }
-
- f, err := os.Open(filePath)
- if err != nil {
- conn.Write([]byte("HTTP/1.1 404 Not Found\r\n\r\n"))
- return
- }
- defer f.Close()
-
- info, _ := f.Stat()
- contentType := getContentType(filePath)
-
- header := fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n",
- contentType, info.Size())
- conn.Write([]byte(header))
- io.Copy(conn, f)
-}
-
-// parseHTTPRequest is deprecated, logic moved to httpServe
-func parseHTTPRequest(data []byte) map[string]interface{} {
- return nil
-}
-
-// writeHTTPResponse 写入 HTTP 响应
-func writeHTTPResponse(conn net.Conn, resp map[string]interface{}) {
- status := 200
- if s, ok := resp["status"].(int); ok {
- status = s
- }
-
- body := ""
- if b, ok := resp["body"].(string); ok {
- body = b
- }
-
- contentType := "application/json"
- if ct, ok := resp["contentType"].(string); ok {
- contentType = ct
- }
-
- header := fmt.Sprintf("HTTP/1.1 %d OK\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n",
- status, contentType, len(body))
- conn.Write([]byte(header + body))
-}
-
-func indexOf(s, substr string) int {
- for i := 0; i <= len(s)-len(substr); i++ {
- if s[i:i+len(substr)] == substr {
- return i
- }
- }
- return -1
-}
-
-func getContentType(path string) string {
- ext := filepath.Ext(path)
- types := map[string]string{
- ".html": "text/html",
- ".css": "text/css",
- ".js": "application/javascript",
- ".json": "application/json",
- ".png": "image/png",
- ".jpg": "image/jpeg",
- ".gif": "image/gif",
- ".txt": "text/plain",
- }
- if ct, ok := types[ext]; ok {
- return ct
- }
- return "application/octet-stream"
-}
-
-// =============================================================================
-// Logger API
-// =============================================================================
-
-func (p *JSPlugin) createLoggerAPI() map[string]interface{} {
- return map[string]interface{}{
- "info": func(msg string) { fmt.Printf("[JS:%s][INFO] %s\n", p.name, msg) },
- "warn": func(msg string) { fmt.Printf("[JS:%s][WARN] %s\n", p.name, msg) },
- "error": func(msg string) { fmt.Printf("[JS:%s][ERROR] %s\n", p.name, msg) },
- }
-}
-
-// =============================================================================
-// Config API Enhancements
-// =============================================================================
-
-func (p *JSPlugin) jsGetAllConfig() map[string]string {
- if p.config == nil {
- return map[string]string{}
- }
- return p.config
-}
-
-// =============================================================================
-// Storage API
-// =============================================================================
-
-func (p *JSPlugin) createStorageAPI() map[string]interface{} {
- return map[string]interface{}{
- "get": p.storageGet,
- "set": p.storageSet,
- "delete": p.storageDelete,
- "keys": p.storageKeys,
- }
-}
-
-func (p *JSPlugin) loadStorage() map[string]interface{} {
- data := make(map[string]interface{})
- if _, err := os.Stat(p.storagePath); err == nil {
- content, _ := os.ReadFile(p.storagePath)
- json.Unmarshal(content, &data)
- }
- return data
-}
-
-func (p *JSPlugin) saveStorage(data map[string]interface{}) {
- content, _ := json.MarshalIndent(data, "", " ")
- os.WriteFile(p.storagePath, content, 0644)
-}
-
-func (p *JSPlugin) storageGet(key string, def interface{}) interface{} {
- p.mu.Lock()
- defer p.mu.Unlock()
- data := p.loadStorage()
- if v, ok := data[key]; ok {
- return v
- }
- return def
-}
-
-func (p *JSPlugin) storageSet(key string, value interface{}) {
- p.mu.Lock()
- defer p.mu.Unlock()
- data := p.loadStorage()
- data[key] = value
- p.saveStorage(data)
-}
-
-func (p *JSPlugin) storageDelete(key string) {
- p.mu.Lock()
- defer p.mu.Unlock()
- data := p.loadStorage()
- delete(data, key)
- p.saveStorage(data)
-}
-
-func (p *JSPlugin) storageKeys() []string {
- p.mu.Lock()
- defer p.mu.Unlock()
- data := p.loadStorage()
- keys := make([]string, 0, len(data))
- for k := range data {
- keys = append(keys, k)
- }
- return keys
-}
-
-// =============================================================================
-// Event API
-// =============================================================================
-
-func (p *JSPlugin) createEventAPI() map[string]interface{} {
- return map[string]interface{}{
- "on": p.eventOn,
- "emit": p.eventEmit,
- "off": p.eventOff,
- }
-}
-
-func (p *JSPlugin) eventOn(event string, callback func(goja.Value)) {
- p.mu.Lock()
- defer p.mu.Unlock()
- p.eventListeners[event] = append(p.eventListeners[event], callback)
-}
-
-func (p *JSPlugin) eventEmit(event string, data interface{}) {
- p.mu.Lock()
- listeners := p.eventListeners[event]
- p.mu.Unlock() // 释放锁以允许回调中操作
-
- val := p.vm.ToValue(data)
- for _, cb := range listeners {
- cb(val)
- }
-}
-
-func (p *JSPlugin) eventOff(event string) {
- p.mu.Lock()
- defer p.mu.Unlock()
- delete(p.eventListeners, event)
-}
-
-// =============================================================================
-// Request API (HTTP Client)
-// =============================================================================
-
-func (p *JSPlugin) createRequestAPI() map[string]interface{} {
- return map[string]interface{}{
- "get": p.requestGet,
- "post": p.requestPost,
- }
-}
-
-func (p *JSPlugin) requestGet(url string) map[string]interface{} {
- resp, err := http.Get(url)
- if err != nil {
- return map[string]interface{}{"error": err.Error(), "status": 0}
- }
- defer resp.Body.Close()
- body, _ := io.ReadAll(resp.Body)
- return map[string]interface{}{
- "status": resp.StatusCode,
- "body": string(body),
- "error": "",
- }
-}
-
-func (p *JSPlugin) requestPost(url string, contentType, data string) map[string]interface{} {
- resp, err := http.Post(url, contentType, strings.NewReader(data))
- if err != nil {
- return map[string]interface{}{"error": err.Error(), "status": 0}
- }
- defer resp.Body.Close()
- body, _ := io.ReadAll(resp.Body)
- return map[string]interface{}{
- "status": resp.StatusCode,
- "body": string(body),
- "error": "",
- }
-}
-
-// =============================================================================
-// Notify API
-// =============================================================================
-
-func (p *JSPlugin) createNotifyAPI() map[string]interface{} {
- return map[string]interface{}{
- "send": func(title, msg string) {
- // 目前仅打印到日志,后续对接系统通知
- fmt.Printf("[NOTIFY][%s] %s: %s\n", p.name, title, msg)
- },
- }
-}
-
-// =============================================================================
-// Route API (用于 Web API 代理)
-// =============================================================================
-
-func (p *JSPlugin) createRouteAPI() map[string]interface{} {
- return map[string]interface{}{
- "handle": p.apiHandle,
- "get": func(path string, handler goja.Callable) { p.apiRegister("GET", path, handler) },
- "post": func(path string, handler goja.Callable) { p.apiRegister("POST", path, handler) },
- "put": func(path string, handler goja.Callable) { p.apiRegister("PUT", path, handler) },
- "delete": func(path string, handler goja.Callable) { p.apiRegister("DELETE", path, handler) },
- }
-}
-
-// apiHandle 注册 API 路由处理函数
-func (p *JSPlugin) apiHandle(method, path string, handler goja.Callable) {
- p.apiRegister(method, path, handler)
-}
-
-// apiRegister 注册 API 路由
-func (p *JSPlugin) apiRegister(method, path string, handler goja.Callable) {
- p.mu.Lock()
- defer p.mu.Unlock()
-
- if p.apiHandlers[method] == nil {
- p.apiHandlers[method] = make(map[string]goja.Callable)
- }
- p.apiHandlers[method][path] = handler
- fmt.Printf("[JS:%s] Registered API: %s %s\n", p.name, method, path)
-}
-
-// HandleAPIRequest 处理 API 请求
-func (p *JSPlugin) HandleAPIRequest(method, path, query string, headers map[string]string, body string) (int, map[string]string, string, error) {
- p.mu.Lock()
- handlers := p.apiHandlers[method]
- p.mu.Unlock()
-
- if handlers == nil {
- return 404, nil, `{"error":"method not allowed"}`, nil
- }
-
- // 查找匹配的路由
- var handler goja.Callable
- var matchedPath string
-
- for registeredPath, h := range handlers {
- if matchRoute(registeredPath, path) {
- handler = h
- matchedPath = registeredPath
- break
- }
- }
-
- if handler == nil {
- return 404, nil, `{"error":"route not found"}`, nil
- }
-
- // 构建请求对象
- reqObj := map[string]interface{}{
- "method": method,
- "path": path,
- "pattern": matchedPath,
- "query": query,
- "headers": headers,
- "body": body,
- "params": extractParams(matchedPath, path),
- }
-
- // 调用处理函数
- result, err := handler(goja.Undefined(), p.vm.ToValue(reqObj))
- if err != nil {
- return 500, nil, fmt.Sprintf(`{"error":"%s"}`, err.Error()), nil
- }
-
- // 解析响应
- if result == nil || goja.IsUndefined(result) || goja.IsNull(result) {
- return 200, nil, "", nil
- }
-
- respObj := result.ToObject(p.vm)
- status := 200
- if s := respObj.Get("status"); s != nil && !goja.IsUndefined(s) {
- status = int(s.ToInteger())
- }
-
- respHeaders := make(map[string]string)
- if h := respObj.Get("headers"); h != nil && !goja.IsUndefined(h) {
- hObj := h.ToObject(p.vm)
- for _, key := range hObj.Keys() {
- respHeaders[key] = hObj.Get(key).String()
- }
- }
-
- respBody := ""
- if b := respObj.Get("body"); b != nil && !goja.IsUndefined(b) {
- respBody = b.String()
- }
-
- return status, respHeaders, respBody, nil
-}
-
-// matchRoute 匹配路由 (支持简单的路径参数)
-func matchRoute(pattern, path string) bool {
- patternParts := strings.Split(strings.Trim(pattern, "/"), "/")
- pathParts := strings.Split(strings.Trim(path, "/"), "/")
-
- if len(patternParts) != len(pathParts) {
- return false
- }
-
- for i, part := range patternParts {
- if strings.HasPrefix(part, ":") {
- continue // 路径参数,匹配任意值
- }
- if part != pathParts[i] {
- return false
- }
- }
- return true
-}
-
-// extractParams 提取路径参数
-func extractParams(pattern, path string) map[string]string {
- params := make(map[string]string)
- patternParts := strings.Split(strings.Trim(pattern, "/"), "/")
- pathParts := strings.Split(strings.Trim(path, "/"), "/")
-
- for i, part := range patternParts {
- if strings.HasPrefix(part, ":") && i < len(pathParts) {
- paramName := strings.TrimPrefix(part, ":")
- params[paramName] = pathParts[i]
- }
- }
- return params
-}
diff --git a/pkg/plugin/script/sandbox.go b/pkg/plugin/script/sandbox.go
deleted file mode 100644
index d65178b..0000000
--- a/pkg/plugin/script/sandbox.go
+++ /dev/null
@@ -1,161 +0,0 @@
-package script
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
-)
-
-// Sandbox 插件沙箱配置
-type Sandbox struct {
- // 允许访问的路径列表(绝对路径)
- AllowedPaths []string
- // 允许写入的路径列表(必须是 AllowedPaths 的子集)
- WritablePaths []string
- // 禁止访问的路径(黑名单,优先级高于白名单)
- DeniedPaths []string
- // 是否允许网络访问
- AllowNetwork bool
- // 最大文件读取大小 (bytes)
- MaxReadSize int64
- // 最大文件写入大小 (bytes)
- MaxWriteSize int64
- // 最大内存使用量 (bytes),0 表示不限制
- MaxMemory int64
- // 最大调用栈深度
- MaxStackDepth int
-}
-
-// DefaultSandbox 返回默认沙箱配置(最小权限)
-func DefaultSandbox() *Sandbox {
- return &Sandbox{
- AllowedPaths: []string{},
- WritablePaths: []string{},
- DeniedPaths: defaultDeniedPaths(),
- AllowNetwork: false,
- MaxReadSize: 10 * 1024 * 1024, // 10MB
- MaxWriteSize: 1 * 1024 * 1024, // 1MB
- MaxMemory: 64 * 1024 * 1024, // 64MB
- MaxStackDepth: 1000, // 最大调用栈深度
- }
-}
-
-// defaultDeniedPaths 返回默认禁止访问的路径
-func defaultDeniedPaths() []string {
- home, _ := os.UserHomeDir()
- denied := []string{
- "/etc/passwd",
- "/etc/shadow",
- "/etc/sudoers",
- "/root",
- "/.ssh",
- "/.gnupg",
- "/.aws",
- "/.kube",
- "/proc",
- "/sys",
- }
- if home != "" {
- denied = append(denied,
- filepath.Join(home, ".ssh"),
- filepath.Join(home, ".gnupg"),
- filepath.Join(home, ".aws"),
- filepath.Join(home, ".kube"),
- filepath.Join(home, ".config"),
- filepath.Join(home, ".local"),
- )
- }
- return denied
-}
-
-// ValidateReadPath 验证读取路径是否允许
-func (s *Sandbox) ValidateReadPath(path string) error {
- return s.validatePath(path, false)
-}
-
-// ValidateWritePath 验证写入路径是否允许
-func (s *Sandbox) ValidateWritePath(path string) error {
- return s.validatePath(path, true)
-}
-
-func (s *Sandbox) validatePath(path string, write bool) error {
- // 清理路径,防止路径遍历攻击
- cleanPath, err := s.cleanPath(path)
- if err != nil {
- return err
- }
-
- // 检查黑名单(优先级最高)
- if s.isDenied(cleanPath) {
- return fmt.Errorf("access denied: path is in denied list")
- }
-
- // 检查白名单
- allowedList := s.AllowedPaths
- if write {
- allowedList = s.WritablePaths
- }
-
- if len(allowedList) == 0 {
- return fmt.Errorf("access denied: no paths allowed")
- }
-
- if !s.isAllowed(cleanPath, allowedList) {
- if write {
- return fmt.Errorf("access denied: path not in writable list")
- }
- return fmt.Errorf("access denied: path not in allowed list")
- }
-
- return nil
-}
-
-// cleanPath 清理并验证路径
-func (s *Sandbox) cleanPath(path string) (string, error) {
- // 转换为绝对路径
- absPath, err := filepath.Abs(path)
- if err != nil {
- return "", fmt.Errorf("invalid path: %w", err)
- }
-
- // 清理路径(解析 .. 和 .)
- cleanPath := filepath.Clean(absPath)
-
- // 检查符号链接(防止通过符号链接绕过限制)
- realPath, err := filepath.EvalSymlinks(cleanPath)
- if err != nil {
- // 文件可能不存在,使用清理后的路径
- if !os.IsNotExist(err) {
- return "", fmt.Errorf("invalid path: %w", err)
- }
- realPath = cleanPath
- }
-
- // 再次检查路径遍历
- if strings.Contains(realPath, "..") {
- return "", fmt.Errorf("path traversal detected")
- }
-
- return realPath, nil
-}
-
-// isDenied 检查路径是否在黑名单中
-func (s *Sandbox) isDenied(path string) bool {
- for _, denied := range s.DeniedPaths {
- if strings.HasPrefix(path, denied) || path == denied {
- return true
- }
- }
- return false
-}
-
-// isAllowed 检查路径是否在白名单中
-func (s *Sandbox) isAllowed(path string, allowedList []string) bool {
- for _, allowed := range allowedList {
- if strings.HasPrefix(path, allowed) || path == allowed {
- return true
- }
- }
- return false
-}
diff --git a/pkg/plugin/sign/official.go b/pkg/plugin/sign/official.go
deleted file mode 100644
index 60f0aa2..0000000
--- a/pkg/plugin/sign/official.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package sign
-
-import (
- "crypto/ed25519"
- "sync"
-)
-
-// 官方固定公钥(客户端内置)
-const OfficialPublicKeyBase64 = "0A0xRthj0wgPg8X8GJZ6/EnNpAUw5v7O//XLty+P5Yw="
-
-var (
- officialPubKey ed25519.PublicKey
- officialPubKeyOnce sync.Once
- officialPubKeyErr error
-)
-
-// initOfficialKey 初始化官方公钥
-func initOfficialKey() {
- officialPubKey, officialPubKeyErr = DecodePublicKey(OfficialPublicKeyBase64)
-}
-
-// GetOfficialPublicKey 获取官方公钥
-func GetOfficialPublicKey() (ed25519.PublicKey, error) {
- officialPubKeyOnce.Do(initOfficialKey)
- return officialPubKey, officialPubKeyErr
-}
-
-// GetPublicKeyByID 根据 ID 获取公钥(兼容旧接口,忽略 keyID)
-func GetPublicKeyByID(keyID string) (ed25519.PublicKey, error) {
- return GetOfficialPublicKey()
-}
diff --git a/pkg/plugin/sign/payload.go b/pkg/plugin/sign/payload.go
deleted file mode 100644
index 01c1855..0000000
--- a/pkg/plugin/sign/payload.go
+++ /dev/null
@@ -1,107 +0,0 @@
-package sign
-
-import (
- "crypto/ed25519"
- "crypto/sha256"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "strings"
- "time"
-)
-
-// PluginPayload 插件签名载荷
-type PluginPayload struct {
- Name string `json:"name"` // 插件名称
- Version string `json:"version"` // 版本号
- SourceHash string `json:"source_hash"` // 源码 SHA256
- KeyID string `json:"key_id"` // 签名密钥 ID
- Timestamp int64 `json:"timestamp"` // 签名时间戳
-}
-
-// SignedPlugin 已签名的插件
-type SignedPlugin struct {
- Payload PluginPayload `json:"payload"`
- Signature string `json:"signature"` // Base64 签名
-}
-
-// NormalizeSource 规范化源码(统一换行符)
-func NormalizeSource(source string) string {
- // 统一换行符为 LF
- normalized := strings.ReplaceAll(source, "\r\n", "\n")
- normalized = strings.ReplaceAll(normalized, "\r", "\n")
- // 去除尾部空白
- normalized = strings.TrimRight(normalized, " \t\n")
- return normalized
-}
-
-// HashSource 计算源码哈希
-func HashSource(source string) string {
- normalized := NormalizeSource(source)
- hash := sha256.Sum256([]byte(normalized))
- return hex.EncodeToString(hash[:])
-}
-
-// CreatePayload 创建签名载荷
-func CreatePayload(name, version, source, keyID string) *PluginPayload {
- return &PluginPayload{
- Name: name,
- Version: version,
- SourceHash: HashSource(source),
- KeyID: keyID,
- Timestamp: time.Now().Unix(),
- }
-}
-
-// SignPlugin 签名插件
-func SignPlugin(priv ed25519.PrivateKey, payload *PluginPayload) (*SignedPlugin, error) {
- // 序列化载荷
- data, err := json.Marshal(payload)
- if err != nil {
- return nil, fmt.Errorf("marshal payload: %w", err)
- }
-
- // 签名
- sig := SignBase64(priv, data)
-
- return &SignedPlugin{
- Payload: *payload,
- Signature: sig,
- }, nil
-}
-
-// VerifyPlugin 验证插件签名
-func VerifyPlugin(pub ed25519.PublicKey, signed *SignedPlugin, source string) error {
- // 验证源码哈希
- expectedHash := HashSource(source)
- if signed.Payload.SourceHash != expectedHash {
- return fmt.Errorf("source hash mismatch")
- }
-
- // 序列化载荷
- data, err := json.Marshal(signed.Payload)
- if err != nil {
- return fmt.Errorf("marshal payload: %w", err)
- }
-
- // 验证签名
- return VerifyBase64(pub, data, signed.Signature)
-}
-
-// EncodeSignedPlugin 编码已签名插件为 JSON
-func EncodeSignedPlugin(sp *SignedPlugin) (string, error) {
- data, err := json.Marshal(sp)
- if err != nil {
- return "", err
- }
- return string(data), nil
-}
-
-// DecodeSignedPlugin 从 JSON 解码已签名插件
-func DecodeSignedPlugin(data string) (*SignedPlugin, error) {
- var sp SignedPlugin
- if err := json.Unmarshal([]byte(data), &sp); err != nil {
- return nil, err
- }
- return &sp, nil
-}
diff --git a/pkg/plugin/sign/sign.go b/pkg/plugin/sign/sign.go
deleted file mode 100644
index 33a885f..0000000
--- a/pkg/plugin/sign/sign.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package sign
-
-import (
- "crypto/ed25519"
- "crypto/rand"
- "encoding/base64"
- "errors"
- "fmt"
-)
-
-var (
- ErrInvalidSignature = errors.New("invalid signature")
- ErrInvalidPublicKey = errors.New("invalid public key")
- ErrInvalidPrivateKey = errors.New("invalid private key")
-)
-
-// KeyPair Ed25519 密钥对
-type KeyPair struct {
- PublicKey ed25519.PublicKey
- PrivateKey ed25519.PrivateKey
-}
-
-// GenerateKeyPair 生成新的密钥对
-func GenerateKeyPair() (*KeyPair, error) {
- pub, priv, err := ed25519.GenerateKey(rand.Reader)
- if err != nil {
- return nil, fmt.Errorf("generate key: %w", err)
- }
- return &KeyPair{PublicKey: pub, PrivateKey: priv}, nil
-}
-
-// Sign 使用私钥签名数据
-func Sign(privateKey ed25519.PrivateKey, data []byte) []byte {
- return ed25519.Sign(privateKey, data)
-}
-
-// Verify 使用公钥验证签名
-func Verify(publicKey ed25519.PublicKey, data, signature []byte) bool {
- return ed25519.Verify(publicKey, data, signature)
-}
-
-// SignBase64 签名并返回 Base64 编码
-func SignBase64(privateKey ed25519.PrivateKey, data []byte) string {
- sig := Sign(privateKey, data)
- return base64.StdEncoding.EncodeToString(sig)
-}
-
-// VerifyBase64 验证 Base64 编码的签名
-func VerifyBase64(publicKey ed25519.PublicKey, data []byte, sigB64 string) error {
- sig, err := base64.StdEncoding.DecodeString(sigB64)
- if err != nil {
- return fmt.Errorf("decode signature: %w", err)
- }
- if !Verify(publicKey, data, sig) {
- return ErrInvalidSignature
- }
- return nil
-}
-
-// EncodePublicKey 编码公钥为 Base64
-func EncodePublicKey(pub ed25519.PublicKey) string {
- return base64.StdEncoding.EncodeToString(pub)
-}
-
-// DecodePublicKey 从 Base64 解码公钥
-func DecodePublicKey(s string) (ed25519.PublicKey, error) {
- data, err := base64.StdEncoding.DecodeString(s)
- if err != nil {
- return nil, err
- }
- if len(data) != ed25519.PublicKeySize {
- return nil, ErrInvalidPublicKey
- }
- return ed25519.PublicKey(data), nil
-}
-
-// EncodePrivateKey 编码私钥为 Base64
-func EncodePrivateKey(priv ed25519.PrivateKey) string {
- return base64.StdEncoding.EncodeToString(priv)
-}
-
-// DecodePrivateKey 从 Base64 解码私钥
-func DecodePrivateKey(s string) (ed25519.PrivateKey, error) {
- data, err := base64.StdEncoding.DecodeString(s)
- if err != nil {
- return nil, err
- }
- if len(data) != ed25519.PrivateKeySize {
- return nil, ErrInvalidPrivateKey
- }
- return ed25519.PrivateKey(data), nil
-}
diff --git a/pkg/plugin/sign/version.go b/pkg/plugin/sign/version.go
deleted file mode 100644
index ef308d4..0000000
--- a/pkg/plugin/sign/version.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package sign
-
-import (
- "strconv"
- "strings"
-)
-
-// CompareVersions 比较两个版本号
-// 返回: -1 (v1 < v2), 0 (v1 == v2), 1 (v1 > v2)
-func CompareVersions(v1, v2 string) int {
- parts1 := parseVersion(v1)
- parts2 := parseVersion(v2)
-
- maxLen := len(parts1)
- if len(parts2) > maxLen {
- maxLen = len(parts2)
- }
-
- for i := 0; i < maxLen; i++ {
- var p1, p2 int
- if i < len(parts1) {
- p1 = parts1[i]
- }
- if i < len(parts2) {
- p2 = parts2[i]
- }
-
- if p1 < p2 {
- return -1
- }
- if p1 > p2 {
- return 1
- }
- }
- return 0
-}
-
-func parseVersion(v string) []int {
- v = strings.TrimPrefix(v, "v")
- parts := strings.Split(v, ".")
- result := make([]int, len(parts))
- for i, p := range parts {
- n, _ := strconv.Atoi(p)
- result[i] = n
- }
- return result
-}
diff --git a/pkg/plugin/types.go b/pkg/plugin/types.go
deleted file mode 100644
index e55aff9..0000000
--- a/pkg/plugin/types.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package plugin
-
-import (
- "net"
- "time"
-)
-
-// =============================================================================
-// 基础类型
-// =============================================================================
-
-// Side 运行侧
-type Side string
-
-const (
- SideClient Side = "client"
-)
-
-// PluginType 插件类别
-type PluginType string
-
-const (
- PluginTypeProxy PluginType = "proxy" // 代理协议 (SOCKS5 等)
- PluginTypeApp PluginType = "app" // 应用服务 (VNC, Echo 等)
-)
-
-// PluginSource 插件来源
-type PluginSource string
-
-const (
- PluginSourceBuiltin PluginSource = "builtin" // 内置编译
- PluginSourceScript PluginSource = "script" // 脚本插件
-)
-
-// =============================================================================
-// 配置相关
-// =============================================================================
-
-// ConfigFieldType 配置字段类型
-type ConfigFieldType string
-
-const (
- ConfigFieldString ConfigFieldType = "string"
- ConfigFieldNumber ConfigFieldType = "number"
- ConfigFieldBool ConfigFieldType = "bool"
- ConfigFieldSelect ConfigFieldType = "select"
- ConfigFieldPassword ConfigFieldType = "password"
-)
-
-// ConfigField 配置字段定义
-type ConfigField struct {
- Key string `json:"key"`
- Label string `json:"label"`
- Type ConfigFieldType `json:"type"`
- Default string `json:"default,omitempty"`
- Required bool `json:"required,omitempty"`
- Options []string `json:"options,omitempty"`
- Description string `json:"description,omitempty"`
-}
-
-// RuleSchema 规则表单模式
-type RuleSchema struct {
- NeedsLocalAddr bool `json:"needs_local_addr"`
- ExtraFields []ConfigField `json:"extra_fields,omitempty"`
-}
-
-// =============================================================================
-// 元数据
-// =============================================================================
-
-// Metadata 插件元数据
-type Metadata struct {
- Name string `json:"name"`
- Version string `json:"version"`
- Type PluginType `json:"type"`
- Source PluginSource `json:"source"`
- RunAt Side `json:"run_at"`
- Description string `json:"description"`
- Author string `json:"author,omitempty"`
- ConfigSchema []ConfigField `json:"config_schema,omitempty"`
- RuleSchema *RuleSchema `json:"rule_schema,omitempty"`
-}
-
-// Info 插件运行时信息
-type Info struct {
- Metadata Metadata `json:"metadata"`
- Loaded bool `json:"loaded"`
- Enabled bool `json:"enabled"`
- LoadedAt time.Time `json:"loaded_at,omitempty"`
- Error string `json:"error,omitempty"`
-}
-
-// =============================================================================
-// 核心接口
-// =============================================================================
-
-// Dialer 网络拨号接口
-type Dialer interface {
- Dial(network, address string) (net.Conn, error)
-}
-
-// ClientPlugin 客户端插件接口
-// 运行在客户端,提供本地服务
-type ClientPlugin interface {
- Metadata() Metadata
- Init(config map[string]string) error
- Start() (localAddr string, err error)
- HandleConn(conn net.Conn) error
- Stop() error
-}
diff --git a/pkg/protocol/message.go b/pkg/protocol/message.go
index a73f684..ad0315c 100644
--- a/pkg/protocol/message.go
+++ b/pkg/protocol/message.go
@@ -26,34 +26,11 @@ const (
MsgTypeProxyConnect uint8 = 9 // 代理连接请求 (SOCKS5/HTTP)
MsgTypeProxyResult uint8 = 10 // 代理连接结果
- // Plugin 相关消息
- MsgTypePluginList uint8 = 20 // 请求/响应可用 plugins
- MsgTypePluginDownload uint8 = 21 // 请求下载 plugin
- MsgTypePluginData uint8 = 22 // Plugin 二进制数据(分块)
- MsgTypePluginReady uint8 = 23 // Plugin 加载确认
-
// UDP 相关消息
MsgTypeUDPData uint8 = 30 // UDP 数据包
- // 插件安装消息
- MsgTypeInstallPlugins uint8 = 24 // 服务端推送安装插件列表
- MsgTypePluginConfig uint8 = 25 // 插件配置同步
-
- // 客户端插件消息
- MsgTypeClientPluginStart uint8 = 40 // 启动客户端插件
- MsgTypeClientPluginStop uint8 = 41 // 停止客户端插件
- MsgTypeClientPluginStatus uint8 = 42 // 客户端插件状态
- MsgTypeClientPluginConn uint8 = 43 // 客户端插件连接请求
- MsgTypePluginStatusQuery uint8 = 44 // 查询所有插件状态
- MsgTypePluginStatusQueryResp uint8 = 45 // 插件状态查询响应
-
- // JS 插件动态安装
- MsgTypeJSPluginInstall uint8 = 50 // 安装 JS 插件
- MsgTypeJSPluginResult uint8 = 51 // 安装结果
-
// 客户端控制消息
- MsgTypeClientRestart uint8 = 60 // 重启客户端
- MsgTypePluginConfigUpdate uint8 = 61 // 更新插件配置
+ MsgTypeClientRestart uint8 = 60 // 重启客户端
// 更新相关消息
MsgTypeUpdateCheck uint8 = 70 // 检查更新请求
@@ -68,10 +45,6 @@ const (
MsgTypeLogData uint8 = 81 // 日志数据
MsgTypeLogStop uint8 = 82 // 停止日志流
- // 插件 API 路由消息
- MsgTypePluginAPIRequest uint8 = 90 // 插件 API 请求
- MsgTypePluginAPIResponse uint8 = 91 // 插件 API 响应
-
// 系统状态消息
MsgTypeSystemStatsRequest uint8 = 100 // 请求系统状态
MsgTypeSystemStatsResponse uint8 = 101 // 系统状态响应
@@ -111,22 +84,17 @@ type AuthResponse struct {
// ProxyRule 代理规则
type ProxyRule struct {
Name string `json:"name" yaml:"name"`
- Type string `json:"type" yaml:"type"` // 内置: tcp, udp, http, https, websocket; 插件: socks5 等
+ Type string `json:"type" yaml:"type"` // tcp, udp, http, https, socks5
LocalIP string `json:"local_ip" yaml:"local_ip"` // tcp/udp 模式使用
LocalPort int `json:"local_port" yaml:"local_port"` // tcp/udp 模式使用
RemotePort int `json:"remote_port" yaml:"remote_port"` // 服务端监听端口
Enabled *bool `json:"enabled,omitempty" yaml:"enabled"` // 是否启用,默认为 true
- // Plugin 支持字段
- PluginID string `json:"plugin_id,omitempty" yaml:"plugin_id"` // 插件实例ID
- PluginName string `json:"plugin_name,omitempty" yaml:"plugin_name"`
- PluginVersion string `json:"plugin_version,omitempty" yaml:"plugin_version"`
- PluginConfig map[string]string `json:"plugin_config,omitempty" yaml:"plugin_config"`
- // HTTP Basic Auth 字段 (用于独立端口模式)
+ // HTTP Basic Auth 字段
AuthEnabled bool `json:"auth_enabled,omitempty" yaml:"auth_enabled"`
AuthUsername string `json:"auth_username,omitempty" yaml:"auth_username"`
AuthPassword string `json:"auth_password,omitempty" yaml:"auth_password"`
- // 插件管理标记 - 由插件自动创建的规则,不允许手动编辑/删除
- PluginManaged bool `json:"plugin_managed,omitempty" yaml:"plugin_managed"`
+ // 端口状态: "listening", "failed: ", ""
+ PortStatus string `json:"port_status,omitempty" yaml:"-"`
}
// IsEnabled 检查规则是否启用,默认为 true
@@ -164,60 +132,6 @@ type ProxyConnectResult struct {
Message string `json:"message,omitempty"`
}
-// PluginMetadata Plugin 元数据(协议层)
-type PluginMetadata struct {
- Name string `json:"name"`
- Version string `json:"version"`
- Checksum string `json:"checksum"`
- Size int64 `json:"size"`
- Description string `json:"description,omitempty"`
-}
-
-// PluginListRequest 请求可用 plugins
-type PluginListRequest struct {
- ClientVersion string `json:"client_version"`
-}
-
-// PluginListResponse 返回可用 plugins
-type PluginListResponse struct {
- Plugins []PluginMetadata `json:"plugins"`
-}
-
-// PluginDownloadRequest 请求下载 plugin
-type PluginDownloadRequest struct {
- Name string `json:"name"`
- Version string `json:"version"`
-}
-
-// PluginDataChunk Plugin 二进制数据块
-type PluginDataChunk struct {
- Name string `json:"name"`
- Version string `json:"version"`
- ChunkIndex int `json:"chunk_index"`
- TotalChunks int `json:"total_chunks"`
- Data []byte `json:"data"`
- Checksum string `json:"checksum,omitempty"`
-}
-
-// PluginReadyNotification Plugin 加载确认
-type PluginReadyNotification struct {
- Name string `json:"name"`
- Version string `json:"version"`
- Success bool `json:"success"`
- Error string `json:"error,omitempty"`
-}
-
-// InstallPluginsRequest 安装插件请求
-type InstallPluginsRequest struct {
- Plugins []string `json:"plugins"` // 要安装的插件名称列表
-}
-
-// PluginConfigSync 插件配置同步
-type PluginConfigSync struct {
- PluginName string `json:"plugin_name"` // 插件名称
- Config map[string]string `json:"config"` // 配置内容
-}
-
// UDPPacket UDP 数据包
type UDPPacket struct {
RemotePort int `json:"remote_port"` // 服务端监听端口
@@ -225,67 +139,6 @@ type UDPPacket struct {
Data []byte `json:"data"` // UDP 数据
}
-// ClientPluginStartRequest 启动客户端插件请求
-type ClientPluginStartRequest struct {
- PluginName string `json:"plugin_name"` // 插件名称
- RuleName string `json:"rule_name"` // 规则名称
- RemotePort int `json:"remote_port"` // 服务端监听端口
- Config map[string]string `json:"config"` // 插件配置
-}
-
-// ClientPluginStopRequest 停止客户端插件请求
-type ClientPluginStopRequest struct {
- PluginID string `json:"plugin_id,omitempty"` // 插件ID(优先使用)
- PluginName string `json:"plugin_name"` // 插件名称
- RuleName string `json:"rule_name"` // 规则名称
-}
-
-// ClientPluginStatusResponse 客户端插件状态响应
-type ClientPluginStatusResponse struct {
- PluginName string `json:"plugin_name"` // 插件名称
- RuleName string `json:"rule_name"` // 规则名称
- Running bool `json:"running"` // 是否运行中
- LocalAddr string `json:"local_addr"` // 本地监听地址
- Error string `json:"error"` // 错误信息
-}
-
-// ClientPluginConnRequest 客户端插件连接请求
-type ClientPluginConnRequest struct {
- PluginID string `json:"plugin_id,omitempty"` // 插件ID(优先使用)
- PluginName string `json:"plugin_name"` // 插件名称
- RuleName string `json:"rule_name"` // 规则名称
-}
-
-// PluginStatusEntry 单个插件状态
-type PluginStatusEntry struct {
- PluginName string `json:"plugin_name"` // 插件名称
- Running bool `json:"running"` // 是否运行中
-}
-
-// PluginStatusQueryResponse 插件状态查询响应
-type PluginStatusQueryResponse struct {
- Plugins []PluginStatusEntry `json:"plugins"` // 所有插件状态
-}
-
-// JSPluginInstallRequest JS 插件安装请求
-type JSPluginInstallRequest struct {
- PluginID string `json:"plugin_id"` // 插件实例唯一 ID
- PluginName string `json:"plugin_name"` // 插件名称
- Source string `json:"source"` // JS 源码
- Signature string `json:"signature"` // 官方签名 (Base64)
- RuleName string `json:"rule_name"` // 规则名称
- RemotePort int `json:"remote_port"` // 服务端监听端口
- Config map[string]string `json:"config"` // 插件配置
- AutoStart bool `json:"auto_start"` // 是否自动启动
-}
-
-// JSPluginInstallResult JS 插件安装结果
-type JSPluginInstallResult struct {
- PluginName string `json:"plugin_name"`
- Success bool `json:"success"`
- Error string `json:"error,omitempty"`
-}
-
// ClientRestartRequest 客户端重启请求
type ClientRestartRequest struct {
Reason string `json:"reason,omitempty"` // 重启原因
@@ -297,23 +150,6 @@ type ClientRestartResponse struct {
Message string `json:"message,omitempty"`
}
-// PluginConfigUpdateRequest 插件配置更新请求
-type PluginConfigUpdateRequest struct {
- PluginID string `json:"plugin_id,omitempty"` // 插件ID(优先使用)
- PluginName string `json:"plugin_name"` // 插件名称
- RuleName string `json:"rule_name"` // 规则名称
- Config map[string]string `json:"config"` // 新配置
- Restart bool `json:"restart"` // 是否重启插件
-}
-
-// PluginConfigUpdateResponse 插件配置更新响应
-type PluginConfigUpdateResponse struct {
- PluginName string `json:"plugin_name"`
- RuleName string `json:"rule_name"`
- Success bool `json:"success"`
- Error string `json:"error,omitempty"`
-}
-
// UpdateCheckRequest 更新检查请求
type UpdateCheckRequest struct {
Component string `json:"component"` // "server" 或 "client"
@@ -367,7 +203,7 @@ type LogEntry struct {
Timestamp int64 `json:"ts"` // Unix 时间戳 (毫秒)
Level string `json:"level"` // 日志级别: debug, info, warn, error
Message string `json:"msg"` // 日志消息
- Source string `json:"src"` // 来源: client, plugin:
+ Source string `json:"src"` // 来源: client
}
// LogData 日志数据
@@ -382,25 +218,6 @@ type LogStopRequest struct {
SessionID string `json:"session_id"` // 会话 ID
}
-// PluginAPIRequest 插件 API 请求
-type PluginAPIRequest struct {
- PluginID string `json:"plugin_id"` // 插件实例唯一 ID
- PluginName string `json:"plugin_name"` // 插件名称 (向后兼容)
- Method string `json:"method"` // HTTP 方法: GET, POST, PUT, DELETE
- Path string `json:"path"` // 路由路径
- Query string `json:"query"` // 查询参数
- Headers map[string]string `json:"headers"` // 请求头
- Body string `json:"body"` // 请求体
-}
-
-// PluginAPIResponse 插件 API 响应
-type PluginAPIResponse struct {
- Status int `json:"status"` // HTTP 状态码
- Headers map[string]string `json:"headers"` // 响应头
- Body string `json:"body"` // 响应体
- Error string `json:"error"` // 错误信息
-}
-
// WriteMessage 写入消息到 writer
func WriteMessage(w io.Writer, msg *Message) error {
header := make([]byte, HeaderSize)
diff --git a/pkg/proxy/http.go b/pkg/proxy/http.go
index 4817626..2c4ac92 100644
--- a/pkg/proxy/http.go
+++ b/pkg/proxy/http.go
@@ -2,6 +2,8 @@ package proxy
import (
"bufio"
+ "encoding/base64"
+ "errors"
"io"
"net"
"net/http"
@@ -12,13 +14,15 @@ import (
// HTTPServer HTTP 代理服务
type HTTPServer struct {
- dialer Dialer
- onStats func(in, out int64) // 流量统计回调
+ dialer Dialer
+ onStats func(in, out int64) // 流量统计回调
+ username string
+ password string
}
// NewHTTPServer 创建 HTTP 代理服务
-func NewHTTPServer(dialer Dialer, onStats func(in, out int64)) *HTTPServer {
- return &HTTPServer{dialer: dialer, onStats: onStats}
+func NewHTTPServer(dialer Dialer, onStats func(in, out int64), username, password string) *HTTPServer {
+ return &HTTPServer{dialer: dialer, onStats: onStats, username: username, password: password}
}
// HandleConn 处理 HTTP 代理连接
@@ -31,12 +35,45 @@ func (h *HTTPServer) HandleConn(conn net.Conn) error {
return err
}
+ // 检查认证
+ if h.username != "" && h.password != "" {
+ if !h.checkAuth(req) {
+ conn.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\n\r\n"))
+ return errors.New("authentication required")
+ }
+ }
+
if req.Method == http.MethodConnect {
return h.handleConnect(conn, req)
}
return h.handleHTTP(conn, req, reader)
}
+// checkAuth 检查 Proxy-Authorization 头
+func (h *HTTPServer) checkAuth(req *http.Request) bool {
+ auth := req.Header.Get("Proxy-Authorization")
+ if auth == "" {
+ return false
+ }
+
+ const prefix = "Basic "
+ if !strings.HasPrefix(auth, prefix) {
+ return false
+ }
+
+ decoded, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
+ if err != nil {
+ return false
+ }
+
+ credentials := strings.SplitN(string(decoded), ":", 2)
+ if len(credentials) != 2 {
+ return false
+ }
+
+ return credentials[0] == h.username && credentials[1] == h.password
+}
+
// handleConnect 处理 CONNECT 方法 (HTTPS)
func (h *HTTPServer) handleConnect(conn net.Conn, req *http.Request) error {
target := req.Host
diff --git a/pkg/proxy/server.go b/pkg/proxy/server.go
index 611bc8d..904af00 100644
--- a/pkg/proxy/server.go
+++ b/pkg/proxy/server.go
@@ -14,10 +14,10 @@ type Server struct {
}
// NewServer 创建代理服务器
-func NewServer(typ string, dialer Dialer, onStats func(in, out int64)) *Server {
+func NewServer(typ string, dialer Dialer, onStats func(in, out int64), username, password string) *Server {
return &Server{
- socks5: NewSOCKS5Server(dialer, onStats),
- http: NewHTTPServer(dialer, onStats),
+ socks5: NewSOCKS5Server(dialer, onStats, username, password),
+ http: NewHTTPServer(dialer, onStats, username, password),
typ: typ,
}
}
diff --git a/pkg/proxy/socks5.go b/pkg/proxy/socks5.go
index cc4fa01..f4b45eb 100644
--- a/pkg/proxy/socks5.go
+++ b/pkg/proxy/socks5.go
@@ -13,6 +13,7 @@ import (
const (
socks5Version = 0x05
noAuth = 0x00
+ userPassAuth = 0x02
cmdConnect = 0x01
atypIPv4 = 0x01
atypDomain = 0x03
@@ -21,8 +22,10 @@ const (
// SOCKS5Server SOCKS5 代理服务
type SOCKS5Server struct {
- dialer Dialer
- onStats func(in, out int64) // 流量统计回调
+ dialer Dialer
+ onStats func(in, out int64) // 流量统计回调
+ username string
+ password string
}
// Dialer 连接拨号器接口
@@ -31,8 +34,8 @@ type Dialer interface {
}
// NewSOCKS5Server 创建 SOCKS5 服务
-func NewSOCKS5Server(dialer Dialer, onStats func(in, out int64)) *SOCKS5Server {
- return &SOCKS5Server{dialer: dialer, onStats: onStats}
+func NewSOCKS5Server(dialer Dialer, onStats func(in, out int64), username, password string) *SOCKS5Server {
+ return &SOCKS5Server{dialer: dialer, onStats: onStats, username: username, password: password}
}
// HandleConn 处理 SOCKS5 连接
@@ -85,11 +88,54 @@ func (s *SOCKS5Server) handshake(conn net.Conn) error {
return err
}
- // 响应:使用无认证
+ // 如果配置了用户名密码,要求认证
+ if s.username != "" && s.password != "" {
+ _, err := conn.Write([]byte{socks5Version, userPassAuth})
+ if err != nil {
+ return err
+ }
+ return s.authenticate(conn)
+ }
+
+ // 无认证
_, err := conn.Write([]byte{socks5Version, noAuth})
return err
}
+// authenticate 处理用户名密码认证
+func (s *SOCKS5Server) authenticate(conn net.Conn) error {
+ buf := make([]byte, 2)
+ if _, err := io.ReadFull(conn, buf); err != nil {
+ return err
+ }
+ if buf[0] != 0x01 {
+ return errors.New("unsupported auth version")
+ }
+
+ ulen := int(buf[1])
+ username := make([]byte, ulen)
+ if _, err := io.ReadFull(conn, username); err != nil {
+ return err
+ }
+
+ plen := make([]byte, 1)
+ if _, err := io.ReadFull(conn, plen); err != nil {
+ return err
+ }
+ password := make([]byte, plen[0])
+ if _, err := io.ReadFull(conn, password); err != nil {
+ return err
+ }
+
+ if string(username) == s.username && string(password) == s.password {
+ conn.Write([]byte{0x01, 0x00}) // 认证成功
+ return nil
+ }
+
+ conn.Write([]byte{0x01, 0x01}) // 认证失败
+ return errors.New("authentication failed")
+}
+
// readRequest 读取请求
func (s *SOCKS5Server) readRequest(conn net.Conn) (string, error) {
buf := make([]byte, 4)
diff --git a/web/src/api/index.ts b/web/src/api/index.ts
index 1707ad7..b1bdf0a 100644
--- a/web/src/api/index.ts
+++ b/web/src/api/index.ts
@@ -1,5 +1,5 @@
import { get, post, put, del, getToken } from '../config/axios'
-import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, PluginInfo, StorePluginInfo, PluginConfigResponse, JSPlugin, RuleSchemasMap, LogEntry, LogStreamOptions, ConfigField } from '../types'
+import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, LogEntry, LogStreamOptions, InstallCommandResponse } from '../types'
// 重新导出 token 管理方法
export { getToken, setToken, removeToken } from '../config/axios'
@@ -24,80 +24,8 @@ export const reloadConfig = () => post('/config/reload')
export const pushConfigToClient = (id: string) => post(`/client/${id}/push`)
export const disconnectClient = (id: string) => post(`/client/${id}/disconnect`)
export const restartClient = (id: string) => post(`/client/${id}/restart`)
-export const installPluginsToClient = (id: string, plugins: string[]) =>
- post(`/client/${id}/install-plugins`, { plugins })
-// 规则配置模式
-export const getRuleSchemas = () => get('/rule-schemas')
-
-// 客户端插件控制(使用 pluginID)
-export const startClientPlugin = (clientId: string, pluginId: string, ruleName: string) =>
- post(`/client/${clientId}/plugin/${pluginId}/start`, { rule_name: ruleName })
-export const stopClientPlugin = (clientId: string, pluginId: string, ruleName: string) =>
- post(`/client/${clientId}/plugin/${pluginId}/stop`, { rule_name: ruleName })
-export const restartClientPlugin = (clientId: string, pluginId: string, ruleName: string) =>
- post(`/client/${clientId}/plugin/${pluginId}/restart`, { rule_name: ruleName })
-export const deleteClientPlugin = (clientId: string, pluginId: string) =>
- post(`/client/${clientId}/plugin/${pluginId}/delete`)
-export const updateClientPluginConfigWithRestart = (clientId: string, pluginId: string, ruleName: string, config: Record, restart: boolean) =>
- post(`/client/${clientId}/plugin/${pluginId}/config`, { rule_name: ruleName, config, restart })
-
-// 插件管理
-export const getPlugins = () => get('/plugins')
-export const enablePlugin = (name: string) => post(`/plugin/${name}/enable`)
-export const disablePlugin = (name: string) => post(`/plugin/${name}/disable`)
-
-// 扩展商店
-export const getStorePlugins = () => get<{ plugins: StorePluginInfo[] }>('/store/plugins')
-export const installStorePlugin = (
- pluginName: string,
- downloadUrl: string,
- signatureUrl: string,
- clientId: string,
- remotePort?: number,
- version?: string,
- configSchema?: ConfigField[],
- authEnabled?: boolean,
- authUsername?: string,
- authPassword?: string
-) =>
- post('/store/install', {
- plugin_name: pluginName,
- version: version || '',
- download_url: downloadUrl,
- signature_url: signatureUrl,
- client_id: clientId,
- remote_port: remotePort || 0,
- config_schema: configSchema || [],
- auth_enabled: authEnabled || false,
- auth_username: authUsername || '',
- auth_password: authPassword || ''
- })
-
-// 客户端插件配置
-export const getClientPluginConfig = (clientId: string, pluginName: string) =>
- get(`/client-plugin/${clientId}/${pluginName}/config`)
-export const updateClientPluginConfig = (clientId: string, pluginName: string, config: Record) =>
- put(`/client-plugin/${clientId}/${pluginName}/config`, { config })
-
-// JS 插件管理
-export const getJSPlugins = () => get('/js-plugins')
-export const createJSPlugin = (plugin: JSPlugin) => post('/js-plugins', plugin)
-export const getJSPlugin = (name: string) => get(`/js-plugin/${name}`)
-export const updateJSPlugin = (name: string, plugin: JSPlugin) => put(`/js-plugin/${name}`, plugin)
-export const deleteJSPlugin = (name: string) => del(`/js-plugin/${name}`)
-export const pushJSPluginToClient = (pluginName: string, clientId: string, remotePort?: number) =>
- post(`/js-plugin/${pluginName}/push/${clientId}`, { remote_port: remotePort || 0 })
-export const updateJSPluginConfig = (name: string, config: Record) =>
- put(`/js-plugin/${name}/config`, { config })
-export const setJSPluginEnabled = (name: string, enabled: boolean) =>
- post(`/js-plugin/${name}/${enabled ? 'enable' : 'disable'}`)
-
-// 插件 API 代理(通过 pluginID 调用插件自定义 API)
-export const callPluginAPI = (clientId: string, pluginId: string, method: string, route: string, body?: any) => {
- const path = `/client/${clientId}/plugin-api/${pluginId}${route.startsWith('/') ? route : '/' + route}`
- switch (method.toUpperCase()) {
- case 'GET':
+// 更新管理
return get(path)
case 'POST':
return post(path, body)
@@ -265,3 +193,7 @@ export interface UpdateServerConfigRequest {
export const getServerConfig = () => get('/config')
export const updateServerConfig = (config: UpdateServerConfigRequest) => put('/config', config)
+
+// 安装命令生成
+export const generateInstallCommand = (clientId: string) =>
+ post('/install/generate', { client_id: clientId })
diff --git a/web/src/types/index.ts b/web/src/types/index.ts
index c7bbd3d..750dfde 100644
--- a/web/src/types/index.ts
+++ b/web/src/types/index.ts
@@ -6,43 +6,6 @@ export interface ProxyRule {
remote_port: number
type?: string
enabled?: boolean
- plugin_config?: Record
- plugin_managed?: boolean // 插件管理标记 - 由插件自动创建的规则
-}
-
-// 客户端已安装的插件
-export interface ClientPlugin {
- id: string // 插件实例唯一 ID
- name: string
- version: string
- enabled: boolean
- running: boolean
- config?: Record
- remote_port?: number // 远程监听端口
-}
-
-// 插件配置字段
-export interface ConfigField {
- key: string
- label: string
- type: 'string' | 'number' | 'bool' | 'select' | 'password'
- default?: string
- required?: boolean
- options?: string[]
- description?: string
-}
-
-// 规则表单模式
-export interface RuleSchema {
- needs_local_addr: boolean
- extra_fields?: ConfigField[]
-}
-
-// 插件配置响应
-export interface PluginConfigResponse {
- plugin_name: string
- schema: ConfigField[]
- config: Record
}
// 客户端配置
@@ -50,7 +13,6 @@ export interface ClientConfig {
id: string
nickname?: string
rules: ProxyRule[]
- plugins?: ClientPlugin[]
}
// 客户端状态
@@ -70,7 +32,6 @@ export interface ClientDetail {
id: string
nickname?: string
rules: ProxyRule[]
- plugins?: ClientPlugin[]
online: boolean
last_ping?: string
remote_addr?: string
@@ -88,64 +49,12 @@ export interface ServerStatus {
client_count: number
}
-// 插件类型
-export const PluginType = {
- Proxy: 'proxy',
- App: 'app',
- Service: 'service',
- Tool: 'tool'
-} as const
-
-export type PluginTypeValue = typeof PluginType[keyof typeof PluginType]
-
-// 插件信息
-export interface PluginInfo {
- name: string
- version: string
- type: string
- description: string
- source: string
- icon?: string
- enabled: boolean
- rule_schema?: RuleSchema
-}
-
-// 扩展商店插件信息
-export interface StorePluginInfo {
- name: string
- version: string
- type: string
- description: string
- author: string
- icon?: string
- download_url?: string
- signature_url?: string
- config_schema?: ConfigField[]
-}
-
-// JS 插件信息
-export interface JSPlugin {
- name: string
- source: string
- signature?: string
- description: string
- author: string
- version?: string
- auto_push: string[]
- config: Record
- auto_start: boolean
- enabled: boolean
-}
-
-// 规则配置模式集合
-export type RuleSchemasMap = Record
-
// 日志条目
export interface LogEntry {
ts: number // Unix 时间戳 (毫秒)
level: string // 日志级别: debug, info, warn, error
msg: string // 日志消息
- src: string // 来源: client, plugin:
+ src: string // 来源: client
}
// 日志流选项
@@ -154,3 +63,15 @@ export interface LogStreamOptions {
follow?: boolean // 是否持续推送
level?: string // 日志级别过滤
}
+
+// 安装命令响应
+export interface InstallCommandResponse {
+ token: string
+ commands: {
+ linux: string
+ macos: string
+ windows: string
+ }
+ expires_at: number
+ server_addr: string
+}
diff --git a/web/src/views/ClientsView.vue b/web/src/views/ClientsView.vue
index 9f1cbc9..c629d94 100644
--- a/web/src/views/ClientsView.vue
+++ b/web/src/views/ClientsView.vue
@@ -1,12 +1,16 @@
@@ -65,7 +92,12 @@ onMounted(loadClients)
加载中...
@@ -101,6 +133,42 @@ onMounted(loadClients)
+
+
+
+
+
+
+
选择您的操作系统,复制命令并在目标机器上执行:
+
+
Linux
+
+ {{ installData.commands.linux }}
+
+
+
+
+
macOS
+
+ {{ installData.commands.macos }}
+
+
+
+
+
Windows
+
+ {{ installData.commands.windows }}
+
+
+
+
客户端ID: {{ installClientId }}
+
⚠️ 此命令包含一次性token,使用后需重新生成
+
+
+
@@ -427,4 +495,117 @@ onMounted(loadClients)
transform: scale(1.1);
}
}
+
+/* Install Modal */
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.modal-content {
+ background: var(--glass-bg);
+ backdrop-filter: blur(20px);
+ border: 1px solid var(--glass-border);
+ border-radius: 16px;
+ max-width: 800px;
+ width: 90%;
+ max-height: 80vh;
+ overflow-y: auto;
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px 24px;
+ border-bottom: 1px solid var(--glass-border);
+}
+
+.modal-header h3 {
+ margin: 0;
+ font-size: 18px;
+}
+
+.close-btn {
+ background: none;
+ border: none;
+ font-size: 28px;
+ cursor: pointer;
+ color: var(--color-text-secondary);
+ line-height: 1;
+}
+
+.modal-body {
+ padding: 24px;
+}
+
+.install-hint {
+ margin-bottom: 20px;
+ color: var(--color-text-secondary);
+}
+
+.install-section {
+ margin-bottom: 20px;
+}
+
+.install-section h4 {
+ margin: 0 0 8px 0;
+ font-size: 14px;
+ color: var(--color-text-primary);
+}
+
+.command-box {
+ display: flex;
+ gap: 8px;
+ background: rgba(0, 0, 0, 0.3);
+ padding: 12px;
+ border-radius: 8px;
+ border: 1px solid var(--glass-border);
+}
+
+.command-box code {
+ flex: 1;
+ font-family: monospace;
+ font-size: 12px;
+ word-break: break-all;
+ color: var(--color-text-primary);
+}
+
+.copy-btn {
+ background: var(--glass-bg);
+ border: 1px solid var(--glass-border);
+ border-radius: 6px;
+ padding: 6px 12px;
+ font-size: 12px;
+ cursor: pointer;
+ color: var(--color-text-primary);
+ white-space: nowrap;
+}
+
+.copy-btn:hover {
+ background: var(--glass-bg-hover);
+}
+
+.token-info {
+ margin-top: 20px;
+ padding: 12px;
+ background: rgba(59, 130, 246, 0.1);
+ border-radius: 8px;
+ font-size: 13px;
+}
+
+.token-warning {
+ margin-top: 12px;
+ padding: 12px;
+ background: rgba(245, 158, 11, 0.1);
+ border-radius: 8px;
+ font-size: 13px;
+ color: #f59e0b;
+}
+