From 5a03d9e1f1c807fc08384575f0528157fa434763 Mon Sep 17 00:00:00 2001 From: Flik Date: Tue, 17 Mar 2026 23:16:30 +0800 Subject: [PATCH] feat: remove unused plugin version comparison and types, refactor proxy server to support authentication - Deleted version comparison logic from `pkg/plugin/sign/version.go`. - Removed unused types and constants from `pkg/plugin/types.go`. - Updated `pkg/protocol/message.go` to remove plugin-related message types. - Enhanced `pkg/proxy/http.go` and `pkg/proxy/socks5.go` to include username/password authentication for HTTP and SOCKS5 proxies. - Modified `pkg/proxy/server.go` to pass authentication parameters to server constructors. - Added new API endpoint to generate installation commands with a token for clients. - Created database functions to manage installation tokens in `internal/server/db/install_token.go`. - Implemented the installation command generation logic in `internal/server/router/handler/install.go`. - Updated web frontend to support installation command generation and display in `web/src/views/ClientsView.vue`. --- CLAUDE.md | 53 +- PLUGINS.md | 645 ------------ cmd/client/main.go | 10 - cmd/server/main.go | 8 +- go.mod | 6 +- go.sum | 10 - internal/client/tunnel/client.go | 558 +---------- internal/server/app/app.go | 27 +- internal/server/config/config.go | 33 +- internal/server/db/install_token.go | 45 + internal/server/db/interface.go | 68 +- internal/server/db/sqlite.go | 193 +--- internal/server/plugin/manager.go | 34 - internal/server/router/dto/client.go | 17 - internal/server/router/dto/plugin.go | 119 --- internal/server/router/handler/client.go | 180 ---- internal/server/router/handler/config.go | 8 - internal/server/router/handler/install.go | 108 ++ internal/server/router/handler/interfaces.go | 61 -- internal/server/router/handler/js_plugin.go | 219 ----- internal/server/router/handler/plugin.go | 416 -------- internal/server/router/handler/plugin_api.go | 139 --- internal/server/router/handler/store.go | 292 ------ internal/server/router/router.go | 39 +- internal/server/tunnel/server.go | 979 ++----------------- pkg/plugin/audit/audit.go | 154 --- pkg/plugin/registry.go | 134 --- pkg/plugin/schema.go | 109 --- pkg/plugin/script/js.go | 913 ----------------- pkg/plugin/script/sandbox.go | 161 --- pkg/plugin/sign/official.go | 31 - pkg/plugin/sign/payload.go | 107 -- pkg/plugin/sign/sign.go | 92 -- pkg/plugin/sign/version.go | 47 - pkg/plugin/types.go | 110 --- pkg/protocol/message.go | 195 +--- pkg/proxy/http.go | 45 +- pkg/proxy/server.go | 6 +- pkg/proxy/socks5.go | 56 +- web/src/api/index.ts | 80 +- web/src/types/index.ts | 105 +- web/src/views/ClientsView.vue | 187 +++- 42 files changed, 638 insertions(+), 6161 deletions(-) delete mode 100644 PLUGINS.md create mode 100644 internal/server/db/install_token.go delete mode 100644 internal/server/plugin/manager.go delete mode 100644 internal/server/router/dto/plugin.go create mode 100644 internal/server/router/handler/install.go delete mode 100644 internal/server/router/handler/js_plugin.go delete mode 100644 internal/server/router/handler/plugin.go delete mode 100644 internal/server/router/handler/plugin_api.go delete mode 100644 internal/server/router/handler/store.go delete mode 100644 pkg/plugin/audit/audit.go delete mode 100644 pkg/plugin/registry.go delete mode 100644 pkg/plugin/schema.go delete mode 100644 pkg/plugin/script/js.go delete mode 100644 pkg/plugin/script/sandbox.go delete mode 100644 pkg/plugin/sign/official.go delete mode 100644 pkg/plugin/sign/payload.go delete mode 100644 pkg/plugin/sign/sign.go delete mode 100644 pkg/plugin/sign/version.go delete mode 100644 pkg/plugin/types.go 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

"; - - 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)
+ + + @@ -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; +} +