From f46741a84b03fb502ffef2daf7780d63beff5708 Mon Sep 17 00:00:00 2001 From: Flik Date: Fri, 2 Jan 2026 01:59:44 +0800 Subject: [PATCH] updete --- .claude/settings.local.json | 10 + cmd/server/main.go | 12 + docs/docs.go | 2398 +++++++++++++++++ docs/swagger.json | 2374 ++++++++++++++++ docs/swagger.yaml | 1500 +++++++++++ go.mod | 62 +- go.sum | 195 ++ internal/client/tunnel/client.go | 184 ++ internal/server/app/app.go | 74 +- internal/server/router/api.go | 1204 --------- internal/server/router/auth.go | 108 - internal/server/router/dto/client.go | 58 + internal/server/router/dto/config.go | 53 + internal/server/router/dto/plugin.go | 105 + internal/server/router/dto/update.go | 76 + internal/server/router/errors.go | 106 + internal/server/router/handler/auth.go | 100 + internal/server/router/handler/client.go | 393 +++ internal/server/router/handler/config.go | 131 + internal/server/router/handler/helpers.go | 230 ++ internal/server/router/handler/interfaces.go | 83 + internal/server/router/handler/js_plugin.go | 212 ++ internal/server/router/handler/plugin.go | 285 ++ internal/server/router/handler/response.go | 159 ++ internal/server/router/handler/status.go | 51 + internal/server/router/handler/store.go | 169 ++ internal/server/router/handler/update.go | 132 + internal/server/router/middleware/cors.go | 28 + internal/server/router/middleware/jwt.go | 49 + internal/server/router/middleware/logger.go | 31 + internal/server/router/middleware/recovery.go | 26 + internal/server/router/response.go | 116 + internal/server/router/router.go | 241 +- internal/server/router/update_helpers.go | 256 ++ internal/server/tunnel/server.go | 29 + pkg/protocol/message.go | 48 + pkg/version/version.go | 207 ++ web/package.json | 4 +- web/src/api/index.ts | 36 + web/src/config/axios/index.ts | 72 +- web/src/router/index.ts | 5 + web/src/views/ClientView.vue | 25 +- web/src/views/HomeView.vue | 23 +- web/src/views/UpdateView.vue | 328 +++ 44 files changed, 10502 insertions(+), 1486 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml delete mode 100644 internal/server/router/api.go delete mode 100644 internal/server/router/auth.go create mode 100644 internal/server/router/dto/client.go create mode 100644 internal/server/router/dto/config.go create mode 100644 internal/server/router/dto/plugin.go create mode 100644 internal/server/router/dto/update.go create mode 100644 internal/server/router/errors.go create mode 100644 internal/server/router/handler/auth.go create mode 100644 internal/server/router/handler/client.go create mode 100644 internal/server/router/handler/config.go create mode 100644 internal/server/router/handler/helpers.go create mode 100644 internal/server/router/handler/interfaces.go create mode 100644 internal/server/router/handler/js_plugin.go create mode 100644 internal/server/router/handler/plugin.go create mode 100644 internal/server/router/handler/response.go create mode 100644 internal/server/router/handler/status.go create mode 100644 internal/server/router/handler/store.go create mode 100644 internal/server/router/handler/update.go create mode 100644 internal/server/router/middleware/cors.go create mode 100644 internal/server/router/middleware/jwt.go create mode 100644 internal/server/router/middleware/logger.go create mode 100644 internal/server/router/middleware/recovery.go create mode 100644 internal/server/router/response.go create mode 100644 internal/server/router/update_helpers.go create mode 100644 pkg/version/version.go create mode 100644 web/src/views/UpdateView.vue diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..13efbbf --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(go build:*)", + "Bash(grep:*)", + "Bash(go list:*)", + "Bash(go get:*)" + ] + } +} diff --git a/cmd/server/main.go b/cmd/server/main.go index 839452f..a48ec45 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,5 +1,15 @@ package main +// @title GoTunnel API +// @version 1.0 +// @description GoTunnel 内网穿透服务器 API +// @host localhost:7500 +// @BasePath / +// @securityDefinitions.apikey Bearer +// @in header +// @name Authorization +// @description JWT Bearer token + import ( "flag" "fmt" @@ -9,6 +19,8 @@ import ( "syscall" "time" + _ "github.com/gotunnel/docs" // Swagger docs + "github.com/gotunnel/internal/server/app" "github.com/gotunnel/internal/server/config" "github.com/gotunnel/internal/server/db" diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..85cdef0 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,2398 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/auth/check": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "验证 JWT token 是否有效", + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "检查 Token", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.TokenCheckResponse" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/auth/login": { + "post": { + "description": "使用用户名密码登录,返回 JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "登录信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.LoginResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client-plugin/{clientID}/{pluginName}/config": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取客户端上指定插件的配置", + "produces": [ + "application/json" + ], + "tags": [ + "插件" + ], + "summary": "获取客户端插件配置", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "clientID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "插件名称", + "name": "pluginName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.PluginConfigResponse" + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "更新客户端上指定插件的配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "插件" + ], + "summary": "更新客户端插件配置", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "clientID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "插件名称", + "name": "pluginName", + "in": "path", + "required": true + }, + { + "description": "配置内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.PluginConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取指定客户端的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "获取客户端详情", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ClientResponse" + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "更新指定客户端的配置信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "更新客户端配置", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.UpdateClientRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "删除指定的客户端配置", + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "删除客户端", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client/{id}/disconnect": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "强制断开客户端连接", + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "断开连接", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client/{id}/install-plugins": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "将指定插件安装到客户端", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "安装插件", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "插件列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.InstallPluginsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client/{id}/plugin/{pluginName}/{action}": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "对客户端插件执行操作(stop/restart/config/delete)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "插件操作", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "插件名称", + "name": "pluginName", + "in": "path", + "required": true + }, + { + "enum": [ + "stop", + "restart", + "config", + "delete" + ], + "type": "string", + "description": "操作类型", + "name": "action", + "in": "path", + "required": true + }, + { + "description": "操作参数", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ClientPluginActionRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client/{id}/push": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "将配置推送到在线客户端", + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "推送配置", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client/{id}/restart": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "发送重启命令到客户端", + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "重启客户端", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/clients": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回所有注册客户端的列表及其在线状态", + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "获取所有客户端", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ClientListItem" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "创建一个新的客户端配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "创建新客户端", + "parameters": [ + { + "description": "客户端信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.CreateClientRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/config": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回服务器配置(敏感信息脱敏)", + "produces": [ + "application/json" + ], + "tags": [ + "配置" + ], + "summary": "获取配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ServerConfigResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "更新服务器配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "配置" + ], + "summary": "更新配置", + "parameters": [ + { + "description": "配置内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.UpdateServerConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/config/reload": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "重新加载服务器配置", + "produces": [ + "application/json" + ], + "tags": [ + "配置" + ], + "summary": "重新加载配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/js-plugin/{name}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取指定 JS 插件的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "JS插件" + ], + "summary": "获取 JS 插件详情", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_db.JSPlugin" + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "更新指定 JS 插件的信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "JS插件" + ], + "summary": "更新 JS 插件", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.JSPluginUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "删除指定的 JS 插件", + "produces": [ + "application/json" + ], + "tags": [ + "JS插件" + ], + "summary": "删除 JS 插件", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/js-plugin/{name}/push/{clientID}": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "将 JS 插件推送到指定客户端", + "produces": [ + "application/json" + ], + "tags": [ + "JS插件" + ], + "summary": "推送插件到客户端", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "客户端ID", + "name": "clientID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/js-plugins": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回所有注册的 JS 插件", + "produces": [ + "application/json" + ], + "tags": [ + "JS插件" + ], + "summary": "获取所有 JS 插件", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_db.JSPlugin" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "创建新的 JS 插件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "JS插件" + ], + "summary": "创建 JS 插件", + "parameters": [ + { + "description": "插件信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.JSPluginCreateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/plugin/{name}/disable": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "禁用指定插件", + "produces": [ + "application/json" + ], + "tags": [ + "插件" + ], + "summary": "禁用插件", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/plugin/{name}/enable": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "启用指定插件", + "produces": [ + "application/json" + ], + "tags": [ + "插件" + ], + "summary": "启用插件", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/plugins": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回服务端所有注册的插件", + "produces": [ + "application/json" + ], + "tags": [ + "插件" + ], + "summary": "获取所有插件", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.PluginInfo" + } + } + } + } + ] + } + } + } + } + }, + "/api/rule-schemas": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回所有协议类型的配置模式", + "produces": [ + "application/json" + ], + "tags": [ + "插件" + ], + "summary": "获取规则模式", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.RuleSchema" + } + } + } + } + ] + } + } + } + } + }, + "/api/status": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回服务器运行状态和客户端数量", + "produces": [ + "application/json" + ], + "tags": [ + "状态" + ], + "summary": "获取服务器状态", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.StatusResponse" + } + } + } + ] + } + } + } + } + }, + "/api/store/install": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "从插件商店下载并安装插件到指定客户端", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "插件商店" + ], + "summary": "安装商店插件", + "parameters": [ + { + "description": "安装请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.StoreInstallRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/store/plugins": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "从远程插件商店获取可用插件列表", + "produces": [ + "application/json" + ], + "tags": [ + "插件商店" + ], + "summary": "获取商店插件", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "plugins": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.StorePluginInfo" + } + } + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/update/apply/client": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "向指定客户端推送更新命令", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "更新" + ], + "summary": "推送客户端更新", + "parameters": [ + { + "description": "更新请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ApplyClientUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/update/apply/server": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "下载并应用服务端更新,服务器将自动重启", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "更新" + ], + "summary": "应用服务端更新", + "parameters": [ + { + "description": "更新请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ApplyServerUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/update/check/client": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "检查是否有新的客户端版本可用", + "produces": [ + "application/json" + ], + "tags": [ + "更新" + ], + "summary": "检查客户端更新", + "parameters": [ + { + "enum": [ + "linux", + "darwin", + "windows" + ], + "type": "string", + "description": "操作系统", + "name": "os", + "in": "query" + }, + { + "enum": [ + "amd64", + "arm64", + "386", + "arm" + ], + "type": "string", + "description": "架构", + "name": "arch", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.CheckUpdateResponse" + } + } + } + ] + } + } + } + } + }, + "/api/update/check/server": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "检查是否有新的服务端版本可用", + "produces": [ + "application/json" + ], + "tags": [ + "更新" + ], + "summary": "检查服务端更新", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.CheckUpdateResponse" + } + } + } + ] + } + } + } + } + }, + "/api/update/version": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回服务器版本信息", + "produces": [ + "application/json" + ], + "tags": [ + "状态" + ], + "summary": "获取版本信息", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.VersionInfo" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "github_com_gotunnel_internal_server_db.ClientPlugin": { + "type": "object", + "properties": { + "config": { + "description": "插件配置", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_db.JSPlugin": { + "type": "object", + "properties": { + "author": { + "type": "string" + }, + "auto_push": { + "type": "array", + "items": { + "type": "string" + } + }, + "auto_start": { + "type": "boolean" + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "signature": { + "description": "官方签名 (Base64)", + "type": "string" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ApplyClientUpdateRequest": { + "description": "推送更新到客户端", + "type": "object", + "required": [ + "client_id", + "download_url" + ], + "properties": { + "client_id": { + "type": "string" + }, + "download_url": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ApplyServerUpdateRequest": { + "description": "应用服务端更新", + "type": "object", + "required": [ + "download_url" + ], + "properties": { + "download_url": { + "type": "string" + }, + "restart": { + "type": "boolean" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.CheckUpdateResponse": { + "description": "更新检查结果", + "type": "object", + "properties": { + "current_version": { + "type": "string" + }, + "download_url": { + "type": "string" + }, + "has_update": { + "type": "boolean" + }, + "latest_version": { + "type": "string" + }, + "published_at": { + "type": "string" + }, + "release_notes": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ClientListItem": { + "description": "客户端列表中的单个项目", + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "client-001" + }, + "last_ping": { + "type": "string" + }, + "nickname": { + "type": "string", + "example": "My Client" + }, + "online": { + "type": "boolean", + "example": true + }, + "remote_addr": { + "type": "string" + }, + "rule_count": { + "type": "integer", + "example": 3 + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ClientPluginActionRequest": { + "description": "对客户端插件执行操作", + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "restart": { + "type": "boolean" + }, + "rule_name": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ClientResponse": { + "description": "客户端详细信息", + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "client-001" + }, + "last_ping": { + "type": "string", + "example": "2025-01-02T10:30:00Z" + }, + "nickname": { + "type": "string", + "example": "My Client" + }, + "online": { + "type": "boolean", + "example": true + }, + "plugins": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_db.ClientPlugin" + } + }, + "remote_addr": { + "type": "string", + "example": "192.168.1.100:54321" + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_pkg_protocol.ProxyRule" + } + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ConfigField": { + "description": "配置表单字段", + "type": "object", + "properties": { + "default": { + "type": "string" + }, + "description": { + "type": "string" + }, + "key": { + "type": "string" + }, + "label": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + }, + "required": { + "type": "boolean" + }, + "type": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.CreateClientRequest": { + "description": "创建新客户端的请求体", + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "example": "client-001" + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_pkg_protocol.ProxyRule" + } + } + } + }, + "github_com_gotunnel_internal_server_router_dto.InstallPluginsRequest": { + "description": "安装插件到指定客户端", + "type": "object", + "required": [ + "plugins" + ], + "properties": { + "plugins": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + }, + "example": [ + "socks5", + "http-proxy" + ] + } + } + }, + "github_com_gotunnel_internal_server_router_dto.JSPluginCreateRequest": { + "description": "创建新的 JS 插件", + "type": "object", + "required": [ + "name", + "source" + ], + "properties": { + "author": { + "type": "string", + "maxLength": 64 + }, + "auto_start": { + "type": "boolean" + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string", + "maxLength": 500 + }, + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1 + }, + "signature": { + "type": "string" + }, + "source": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.JSPluginUpdateRequest": { + "description": "更新 JS 插件", + "type": "object", + "properties": { + "author": { + "type": "string", + "maxLength": 64 + }, + "auto_start": { + "type": "boolean" + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string", + "maxLength": 500 + }, + "enabled": { + "type": "boolean" + }, + "signature": { + "type": "string" + }, + "source": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.LoginRequest": { + "description": "用户登录", + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.LoginResponse": { + "description": "登录成功返回", + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.PluginConfigRequest": { + "description": "更新客户端插件配置", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "github_com_gotunnel_internal_server_router_dto.PluginConfigResponse": { + "description": "插件配置详情", + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "plugin_name": { + "type": "string" + }, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ConfigField" + } + } + } + }, + "github_com_gotunnel_internal_server_router_dto.PluginInfo": { + "description": "服务端插件信息", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rule_schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.RuleSchema" + }, + "source": { + "type": "string" + }, + "type": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.RuleSchema": { + "description": "代理规则的配置模式", + "type": "object", + "properties": { + "extra_fields": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ConfigField" + } + }, + "needs_local_addr": { + "type": "boolean" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ServerConfigInfo": { + "type": "object", + "properties": { + "bind_addr": { + "type": "string" + }, + "bind_port": { + "type": "integer" + }, + "heartbeat_sec": { + "type": "integer" + }, + "heartbeat_timeout": { + "type": "integer" + }, + "token": { + "description": "脱敏后的 token", + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ServerConfigPart": { + "description": "隧道服务器配置", + "type": "object", + "properties": { + "bind_addr": { + "type": "string" + }, + "bind_port": { + "type": "integer", + "maximum": 65535, + "minimum": 1 + }, + "heartbeat_sec": { + "type": "integer", + "maximum": 300, + "minimum": 1 + }, + "heartbeat_timeout": { + "type": "integer", + "maximum": 600, + "minimum": 1 + }, + "token": { + "type": "string", + "minLength": 8 + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ServerConfigResponse": { + "description": "服务器配置信息", + "type": "object", + "properties": { + "server": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ServerConfigInfo" + }, + "web": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.WebConfigInfo" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ServerStatus": { + "type": "object", + "properties": { + "bind_addr": { + "type": "string" + }, + "bind_port": { + "type": "integer" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.StatusResponse": { + "description": "服务器状态信息", + "type": "object", + "properties": { + "client_count": { + "type": "integer" + }, + "server": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ServerStatus" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.StoreInstallRequest": { + "description": "从插件商店安装插件到客户端", + "type": "object", + "required": [ + "client_id", + "download_url", + "plugin_name", + "signature_url" + ], + "properties": { + "client_id": { + "type": "string" + }, + "download_url": { + "type": "string" + }, + "plugin_name": { + "type": "string" + }, + "signature_url": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.StorePluginInfo": { + "description": "插件商店中的插件信息", + "type": "object", + "properties": { + "author": { + "type": "string" + }, + "description": { + "type": "string" + }, + "download_url": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "signature_url": { + "type": "string" + }, + "type": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.TokenCheckResponse": { + "description": "Token 验证结果", + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "valid": { + "type": "boolean" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.UpdateClientRequest": { + "description": "更新客户端配置的请求体", + "type": "object", + "properties": { + "nickname": { + "type": "string", + "maxLength": 128, + "example": "My Client" + }, + "plugins": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_db.ClientPlugin" + } + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_pkg_protocol.ProxyRule" + } + } + } + }, + "github_com_gotunnel_internal_server_router_dto.UpdateServerConfigRequest": { + "description": "更新服务器配置", + "type": "object", + "properties": { + "server": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ServerConfigPart" + }, + "web": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.WebConfigPart" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.VersionInfo": { + "description": "当前版本信息", + "type": "object", + "properties": { + "build_time": { + "type": "string" + }, + "git_commit": { + "type": "string" + }, + "go_version": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.WebConfigInfo": { + "type": "object", + "properties": { + "bind_addr": { + "type": "string" + }, + "bind_port": { + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "password": { + "description": "显示为 ****", + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.WebConfigPart": { + "description": "Web 控制台配置", + "type": "object", + "properties": { + "bind_addr": { + "type": "string" + }, + "bind_port": { + "type": "integer", + "maximum": 65535, + "minimum": 1 + }, + "enabled": { + "type": "boolean" + }, + "password": { + "type": "string", + "maxLength": 64, + "minLength": 6 + }, + "username": { + "type": "string", + "maxLength": 32, + "minLength": 3 + } + } + }, + "github_com_gotunnel_pkg_protocol.ProxyRule": { + "type": "object", + "properties": { + "enabled": { + "description": "是否启用,默认为 true", + "type": "boolean" + }, + "local_ip": { + "description": "tcp/udp 模式使用", + "type": "string" + }, + "local_port": { + "description": "tcp/udp 模式使用", + "type": "integer" + }, + "name": { + "type": "string" + }, + "plugin_config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "plugin_name": { + "description": "Plugin 支持字段", + "type": "string" + }, + "plugin_version": { + "type": "string" + }, + "remote_port": { + "description": "服务端监听端口", + "type": "integer" + }, + "type": { + "description": "内置: tcp, udp, http, https; 插件: socks5 等", + "type": "string" + } + } + }, + "internal_server_router_handler.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "Bearer": { + "description": "JWT Bearer token", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:7500", + BasePath: "/", + Schemes: []string{}, + Title: "GoTunnel API", + Description: "GoTunnel 内网穿透服务器 API", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..509306e --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,2374 @@ +{ + "swagger": "2.0", + "info": { + "description": "GoTunnel 内网穿透服务器 API", + "title": "GoTunnel API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:7500", + "basePath": "/", + "paths": { + "/api/auth/check": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "验证 JWT token 是否有效", + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "检查 Token", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.TokenCheckResponse" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/auth/login": { + "post": { + "description": "使用用户名密码登录,返回 JWT token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "登录信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.LoginResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client-plugin/{clientID}/{pluginName}/config": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取客户端上指定插件的配置", + "produces": [ + "application/json" + ], + "tags": [ + "插件" + ], + "summary": "获取客户端插件配置", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "clientID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "插件名称", + "name": "pluginName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.PluginConfigResponse" + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "更新客户端上指定插件的配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "插件" + ], + "summary": "更新客户端插件配置", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "clientID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "插件名称", + "name": "pluginName", + "in": "path", + "required": true + }, + { + "description": "配置内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.PluginConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client/{id}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取指定客户端的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "获取客户端详情", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ClientResponse" + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "更新指定客户端的配置信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "更新客户端配置", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.UpdateClientRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "删除指定的客户端配置", + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "删除客户端", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client/{id}/disconnect": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "强制断开客户端连接", + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "断开连接", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client/{id}/install-plugins": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "将指定插件安装到客户端", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "安装插件", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "插件列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.InstallPluginsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client/{id}/plugin/{pluginName}/{action}": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "对客户端插件执行操作(stop/restart/config/delete)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "插件操作", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "插件名称", + "name": "pluginName", + "in": "path", + "required": true + }, + { + "enum": [ + "stop", + "restart", + "config", + "delete" + ], + "type": "string", + "description": "操作类型", + "name": "action", + "in": "path", + "required": true + }, + { + "description": "操作参数", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ClientPluginActionRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client/{id}/push": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "将配置推送到在线客户端", + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "推送配置", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/client/{id}/restart": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "发送重启命令到客户端", + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "重启客户端", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/clients": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回所有注册客户端的列表及其在线状态", + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "获取所有客户端", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ClientListItem" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "创建一个新的客户端配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "客户端" + ], + "summary": "创建新客户端", + "parameters": [ + { + "description": "客户端信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.CreateClientRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/config": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回服务器配置(敏感信息脱敏)", + "produces": [ + "application/json" + ], + "tags": [ + "配置" + ], + "summary": "获取配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ServerConfigResponse" + } + } + } + ] + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "更新服务器配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "配置" + ], + "summary": "更新配置", + "parameters": [ + { + "description": "配置内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.UpdateServerConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/config/reload": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "重新加载服务器配置", + "produces": [ + "application/json" + ], + "tags": [ + "配置" + ], + "summary": "重新加载配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/js-plugin/{name}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取指定 JS 插件的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "JS插件" + ], + "summary": "获取 JS 插件详情", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_db.JSPlugin" + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "更新指定 JS 插件的信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "JS插件" + ], + "summary": "更新 JS 插件", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.JSPluginUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "删除指定的 JS 插件", + "produces": [ + "application/json" + ], + "tags": [ + "JS插件" + ], + "summary": "删除 JS 插件", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/js-plugin/{name}/push/{clientID}": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "将 JS 插件推送到指定客户端", + "produces": [ + "application/json" + ], + "tags": [ + "JS插件" + ], + "summary": "推送插件到客户端", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "客户端ID", + "name": "clientID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/js-plugins": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回所有注册的 JS 插件", + "produces": [ + "application/json" + ], + "tags": [ + "JS插件" + ], + "summary": "获取所有 JS 插件", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_db.JSPlugin" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "创建新的 JS 插件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "JS插件" + ], + "summary": "创建 JS 插件", + "parameters": [ + { + "description": "插件信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.JSPluginCreateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/plugin/{name}/disable": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "禁用指定插件", + "produces": [ + "application/json" + ], + "tags": [ + "插件" + ], + "summary": "禁用插件", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/plugin/{name}/enable": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "启用指定插件", + "produces": [ + "application/json" + ], + "tags": [ + "插件" + ], + "summary": "启用插件", + "parameters": [ + { + "type": "string", + "description": "插件名称", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/plugins": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回服务端所有注册的插件", + "produces": [ + "application/json" + ], + "tags": [ + "插件" + ], + "summary": "获取所有插件", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.PluginInfo" + } + } + } + } + ] + } + } + } + } + }, + "/api/rule-schemas": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回所有协议类型的配置模式", + "produces": [ + "application/json" + ], + "tags": [ + "插件" + ], + "summary": "获取规则模式", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.RuleSchema" + } + } + } + } + ] + } + } + } + } + }, + "/api/status": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回服务器运行状态和客户端数量", + "produces": [ + "application/json" + ], + "tags": [ + "状态" + ], + "summary": "获取服务器状态", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.StatusResponse" + } + } + } + ] + } + } + } + } + }, + "/api/store/install": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "从插件商店下载并安装插件到指定客户端", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "插件商店" + ], + "summary": "安装商店插件", + "parameters": [ + { + "description": "安装请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.StoreInstallRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/store/plugins": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "从远程插件商店获取可用插件列表", + "produces": [ + "application/json" + ], + "tags": [ + "插件商店" + ], + "summary": "获取商店插件", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "plugins": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.StorePluginInfo" + } + } + } + } + } + } + ] + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/update/apply/client": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "向指定客户端推送更新命令", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "更新" + ], + "summary": "推送客户端更新", + "parameters": [ + { + "description": "更新请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ApplyClientUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/update/apply/server": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "下载并应用服务端更新,服务器将自动重启", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "更新" + ], + "summary": "应用服务端更新", + "parameters": [ + { + "description": "更新请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ApplyServerUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/internal_server_router_handler.Response" + } + } + } + } + }, + "/api/update/check/client": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "检查是否有新的客户端版本可用", + "produces": [ + "application/json" + ], + "tags": [ + "更新" + ], + "summary": "检查客户端更新", + "parameters": [ + { + "enum": [ + "linux", + "darwin", + "windows" + ], + "type": "string", + "description": "操作系统", + "name": "os", + "in": "query" + }, + { + "enum": [ + "amd64", + "arm64", + "386", + "arm" + ], + "type": "string", + "description": "架构", + "name": "arch", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.CheckUpdateResponse" + } + } + } + ] + } + } + } + } + }, + "/api/update/check/server": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "检查是否有新的服务端版本可用", + "produces": [ + "application/json" + ], + "tags": [ + "更新" + ], + "summary": "检查服务端更新", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.CheckUpdateResponse" + } + } + } + ] + } + } + } + } + }, + "/api/update/version": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "返回服务器版本信息", + "produces": [ + "application/json" + ], + "tags": [ + "状态" + ], + "summary": "获取版本信息", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/internal_server_router_handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.VersionInfo" + } + } + } + ] + } + } + } + } + } + }, + "definitions": { + "github_com_gotunnel_internal_server_db.ClientPlugin": { + "type": "object", + "properties": { + "config": { + "description": "插件配置", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_db.JSPlugin": { + "type": "object", + "properties": { + "author": { + "type": "string" + }, + "auto_push": { + "type": "array", + "items": { + "type": "string" + } + }, + "auto_start": { + "type": "boolean" + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "signature": { + "description": "官方签名 (Base64)", + "type": "string" + }, + "source": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ApplyClientUpdateRequest": { + "description": "推送更新到客户端", + "type": "object", + "required": [ + "client_id", + "download_url" + ], + "properties": { + "client_id": { + "type": "string" + }, + "download_url": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ApplyServerUpdateRequest": { + "description": "应用服务端更新", + "type": "object", + "required": [ + "download_url" + ], + "properties": { + "download_url": { + "type": "string" + }, + "restart": { + "type": "boolean" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.CheckUpdateResponse": { + "description": "更新检查结果", + "type": "object", + "properties": { + "current_version": { + "type": "string" + }, + "download_url": { + "type": "string" + }, + "has_update": { + "type": "boolean" + }, + "latest_version": { + "type": "string" + }, + "published_at": { + "type": "string" + }, + "release_notes": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ClientListItem": { + "description": "客户端列表中的单个项目", + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "client-001" + }, + "last_ping": { + "type": "string" + }, + "nickname": { + "type": "string", + "example": "My Client" + }, + "online": { + "type": "boolean", + "example": true + }, + "remote_addr": { + "type": "string" + }, + "rule_count": { + "type": "integer", + "example": 3 + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ClientPluginActionRequest": { + "description": "对客户端插件执行操作", + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "restart": { + "type": "boolean" + }, + "rule_name": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ClientResponse": { + "description": "客户端详细信息", + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "client-001" + }, + "last_ping": { + "type": "string", + "example": "2025-01-02T10:30:00Z" + }, + "nickname": { + "type": "string", + "example": "My Client" + }, + "online": { + "type": "boolean", + "example": true + }, + "plugins": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_db.ClientPlugin" + } + }, + "remote_addr": { + "type": "string", + "example": "192.168.1.100:54321" + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_pkg_protocol.ProxyRule" + } + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ConfigField": { + "description": "配置表单字段", + "type": "object", + "properties": { + "default": { + "type": "string" + }, + "description": { + "type": "string" + }, + "key": { + "type": "string" + }, + "label": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + }, + "required": { + "type": "boolean" + }, + "type": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.CreateClientRequest": { + "description": "创建新客户端的请求体", + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "example": "client-001" + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_pkg_protocol.ProxyRule" + } + } + } + }, + "github_com_gotunnel_internal_server_router_dto.InstallPluginsRequest": { + "description": "安装插件到指定客户端", + "type": "object", + "required": [ + "plugins" + ], + "properties": { + "plugins": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + }, + "example": [ + "socks5", + "http-proxy" + ] + } + } + }, + "github_com_gotunnel_internal_server_router_dto.JSPluginCreateRequest": { + "description": "创建新的 JS 插件", + "type": "object", + "required": [ + "name", + "source" + ], + "properties": { + "author": { + "type": "string", + "maxLength": 64 + }, + "auto_start": { + "type": "boolean" + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string", + "maxLength": 500 + }, + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1 + }, + "signature": { + "type": "string" + }, + "source": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.JSPluginUpdateRequest": { + "description": "更新 JS 插件", + "type": "object", + "properties": { + "author": { + "type": "string", + "maxLength": 64 + }, + "auto_start": { + "type": "boolean" + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string", + "maxLength": 500 + }, + "enabled": { + "type": "boolean" + }, + "signature": { + "type": "string" + }, + "source": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.LoginRequest": { + "description": "用户登录", + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.LoginResponse": { + "description": "登录成功返回", + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.PluginConfigRequest": { + "description": "更新客户端插件配置", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "github_com_gotunnel_internal_server_router_dto.PluginConfigResponse": { + "description": "插件配置详情", + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "plugin_name": { + "type": "string" + }, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ConfigField" + } + } + } + }, + "github_com_gotunnel_internal_server_router_dto.PluginInfo": { + "description": "服务端插件信息", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rule_schema": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.RuleSchema" + }, + "source": { + "type": "string" + }, + "type": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.RuleSchema": { + "description": "代理规则的配置模式", + "type": "object", + "properties": { + "extra_fields": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ConfigField" + } + }, + "needs_local_addr": { + "type": "boolean" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ServerConfigInfo": { + "type": "object", + "properties": { + "bind_addr": { + "type": "string" + }, + "bind_port": { + "type": "integer" + }, + "heartbeat_sec": { + "type": "integer" + }, + "heartbeat_timeout": { + "type": "integer" + }, + "token": { + "description": "脱敏后的 token", + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ServerConfigPart": { + "description": "隧道服务器配置", + "type": "object", + "properties": { + "bind_addr": { + "type": "string" + }, + "bind_port": { + "type": "integer", + "maximum": 65535, + "minimum": 1 + }, + "heartbeat_sec": { + "type": "integer", + "maximum": 300, + "minimum": 1 + }, + "heartbeat_timeout": { + "type": "integer", + "maximum": 600, + "minimum": 1 + }, + "token": { + "type": "string", + "minLength": 8 + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ServerConfigResponse": { + "description": "服务器配置信息", + "type": "object", + "properties": { + "server": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ServerConfigInfo" + }, + "web": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.WebConfigInfo" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.ServerStatus": { + "type": "object", + "properties": { + "bind_addr": { + "type": "string" + }, + "bind_port": { + "type": "integer" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.StatusResponse": { + "description": "服务器状态信息", + "type": "object", + "properties": { + "client_count": { + "type": "integer" + }, + "server": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ServerStatus" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.StoreInstallRequest": { + "description": "从插件商店安装插件到客户端", + "type": "object", + "required": [ + "client_id", + "download_url", + "plugin_name", + "signature_url" + ], + "properties": { + "client_id": { + "type": "string" + }, + "download_url": { + "type": "string" + }, + "plugin_name": { + "type": "string" + }, + "signature_url": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.StorePluginInfo": { + "description": "插件商店中的插件信息", + "type": "object", + "properties": { + "author": { + "type": "string" + }, + "description": { + "type": "string" + }, + "download_url": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "signature_url": { + "type": "string" + }, + "type": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.TokenCheckResponse": { + "description": "Token 验证结果", + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "valid": { + "type": "boolean" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.UpdateClientRequest": { + "description": "更新客户端配置的请求体", + "type": "object", + "properties": { + "nickname": { + "type": "string", + "maxLength": 128, + "example": "My Client" + }, + "plugins": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_db.ClientPlugin" + } + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_gotunnel_pkg_protocol.ProxyRule" + } + } + } + }, + "github_com_gotunnel_internal_server_router_dto.UpdateServerConfigRequest": { + "description": "更新服务器配置", + "type": "object", + "properties": { + "server": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.ServerConfigPart" + }, + "web": { + "$ref": "#/definitions/github_com_gotunnel_internal_server_router_dto.WebConfigPart" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.VersionInfo": { + "description": "当前版本信息", + "type": "object", + "properties": { + "build_time": { + "type": "string" + }, + "git_commit": { + "type": "string" + }, + "go_version": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.WebConfigInfo": { + "type": "object", + "properties": { + "bind_addr": { + "type": "string" + }, + "bind_port": { + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "password": { + "description": "显示为 ****", + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_gotunnel_internal_server_router_dto.WebConfigPart": { + "description": "Web 控制台配置", + "type": "object", + "properties": { + "bind_addr": { + "type": "string" + }, + "bind_port": { + "type": "integer", + "maximum": 65535, + "minimum": 1 + }, + "enabled": { + "type": "boolean" + }, + "password": { + "type": "string", + "maxLength": 64, + "minLength": 6 + }, + "username": { + "type": "string", + "maxLength": 32, + "minLength": 3 + } + } + }, + "github_com_gotunnel_pkg_protocol.ProxyRule": { + "type": "object", + "properties": { + "enabled": { + "description": "是否启用,默认为 true", + "type": "boolean" + }, + "local_ip": { + "description": "tcp/udp 模式使用", + "type": "string" + }, + "local_port": { + "description": "tcp/udp 模式使用", + "type": "integer" + }, + "name": { + "type": "string" + }, + "plugin_config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "plugin_name": { + "description": "Plugin 支持字段", + "type": "string" + }, + "plugin_version": { + "type": "string" + }, + "remote_port": { + "description": "服务端监听端口", + "type": "integer" + }, + "type": { + "description": "内置: tcp, udp, http, https; 插件: socks5 等", + "type": "string" + } + } + }, + "internal_server_router_handler.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "Bearer": { + "description": "JWT Bearer token", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..3d6e9e8 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,1500 @@ +basePath: / +definitions: + github_com_gotunnel_internal_server_db.ClientPlugin: + properties: + config: + additionalProperties: + type: string + description: 插件配置 + type: object + enabled: + type: boolean + name: + type: string + version: + type: string + type: object + github_com_gotunnel_internal_server_db.JSPlugin: + properties: + author: + type: string + auto_push: + items: + type: string + type: array + auto_start: + type: boolean + config: + additionalProperties: + type: string + type: object + description: + type: string + enabled: + type: boolean + name: + type: string + signature: + description: 官方签名 (Base64) + type: string + source: + type: string + version: + type: string + type: object + github_com_gotunnel_internal_server_router_dto.ApplyClientUpdateRequest: + description: 推送更新到客户端 + properties: + client_id: + type: string + download_url: + type: string + required: + - client_id + - download_url + type: object + github_com_gotunnel_internal_server_router_dto.ApplyServerUpdateRequest: + description: 应用服务端更新 + properties: + download_url: + type: string + restart: + type: boolean + required: + - download_url + type: object + github_com_gotunnel_internal_server_router_dto.CheckUpdateResponse: + description: 更新检查结果 + properties: + current_version: + type: string + download_url: + type: string + has_update: + type: boolean + latest_version: + type: string + published_at: + type: string + release_notes: + type: string + type: object + github_com_gotunnel_internal_server_router_dto.ClientListItem: + description: 客户端列表中的单个项目 + properties: + id: + example: client-001 + type: string + last_ping: + type: string + nickname: + example: My Client + type: string + online: + example: true + type: boolean + remote_addr: + type: string + rule_count: + example: 3 + type: integer + type: object + github_com_gotunnel_internal_server_router_dto.ClientPluginActionRequest: + description: 对客户端插件执行操作 + properties: + config: + additionalProperties: + type: string + type: object + restart: + type: boolean + rule_name: + type: string + type: object + github_com_gotunnel_internal_server_router_dto.ClientResponse: + description: 客户端详细信息 + properties: + id: + example: client-001 + type: string + last_ping: + example: "2025-01-02T10:30:00Z" + type: string + nickname: + example: My Client + type: string + online: + example: true + type: boolean + plugins: + items: + $ref: '#/definitions/github_com_gotunnel_internal_server_db.ClientPlugin' + type: array + remote_addr: + example: 192.168.1.100:54321 + type: string + rules: + items: + $ref: '#/definitions/github_com_gotunnel_pkg_protocol.ProxyRule' + type: array + type: object + github_com_gotunnel_internal_server_router_dto.ConfigField: + description: 配置表单字段 + properties: + default: + type: string + description: + type: string + key: + type: string + label: + type: string + options: + items: + type: string + type: array + required: + type: boolean + type: + type: string + type: object + github_com_gotunnel_internal_server_router_dto.CreateClientRequest: + description: 创建新客户端的请求体 + properties: + id: + example: client-001 + maxLength: 64 + minLength: 1 + type: string + rules: + items: + $ref: '#/definitions/github_com_gotunnel_pkg_protocol.ProxyRule' + type: array + required: + - id + type: object + github_com_gotunnel_internal_server_router_dto.InstallPluginsRequest: + description: 安装插件到指定客户端 + properties: + plugins: + example: + - socks5 + - http-proxy + items: + type: string + minItems: 1 + type: array + required: + - plugins + type: object + github_com_gotunnel_internal_server_router_dto.JSPluginCreateRequest: + description: 创建新的 JS 插件 + properties: + author: + maxLength: 64 + type: string + auto_start: + type: boolean + config: + additionalProperties: + type: string + type: object + description: + maxLength: 500 + type: string + name: + maxLength: 64 + minLength: 1 + type: string + signature: + type: string + source: + type: string + required: + - name + - source + type: object + github_com_gotunnel_internal_server_router_dto.JSPluginUpdateRequest: + description: 更新 JS 插件 + properties: + author: + maxLength: 64 + type: string + auto_start: + type: boolean + config: + additionalProperties: + type: string + type: object + description: + maxLength: 500 + type: string + enabled: + type: boolean + signature: + type: string + source: + type: string + type: object + github_com_gotunnel_internal_server_router_dto.LoginRequest: + description: 用户登录 + properties: + password: + type: string + username: + type: string + required: + - password + - username + type: object + github_com_gotunnel_internal_server_router_dto.LoginResponse: + description: 登录成功返回 + properties: + token: + type: string + type: object + github_com_gotunnel_internal_server_router_dto.PluginConfigRequest: + description: 更新客户端插件配置 + properties: + config: + additionalProperties: + type: string + type: object + required: + - config + type: object + github_com_gotunnel_internal_server_router_dto.PluginConfigResponse: + description: 插件配置详情 + properties: + config: + additionalProperties: + type: string + type: object + plugin_name: + type: string + schema: + items: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.ConfigField' + type: array + type: object + github_com_gotunnel_internal_server_router_dto.PluginInfo: + description: 服务端插件信息 + properties: + description: + type: string + enabled: + type: boolean + icon: + type: string + name: + type: string + rule_schema: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.RuleSchema' + source: + type: string + type: + type: string + version: + type: string + type: object + github_com_gotunnel_internal_server_router_dto.RuleSchema: + description: 代理规则的配置模式 + properties: + extra_fields: + items: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.ConfigField' + type: array + needs_local_addr: + type: boolean + type: object + github_com_gotunnel_internal_server_router_dto.ServerConfigInfo: + properties: + bind_addr: + type: string + bind_port: + type: integer + heartbeat_sec: + type: integer + heartbeat_timeout: + type: integer + token: + description: 脱敏后的 token + type: string + type: object + github_com_gotunnel_internal_server_router_dto.ServerConfigPart: + description: 隧道服务器配置 + properties: + bind_addr: + type: string + bind_port: + maximum: 65535 + minimum: 1 + type: integer + heartbeat_sec: + maximum: 300 + minimum: 1 + type: integer + heartbeat_timeout: + maximum: 600 + minimum: 1 + type: integer + token: + minLength: 8 + type: string + type: object + github_com_gotunnel_internal_server_router_dto.ServerConfigResponse: + description: 服务器配置信息 + properties: + server: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.ServerConfigInfo' + web: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.WebConfigInfo' + type: object + github_com_gotunnel_internal_server_router_dto.ServerStatus: + properties: + bind_addr: + type: string + bind_port: + type: integer + type: object + github_com_gotunnel_internal_server_router_dto.StatusResponse: + description: 服务器状态信息 + properties: + client_count: + type: integer + server: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.ServerStatus' + type: object + github_com_gotunnel_internal_server_router_dto.StoreInstallRequest: + description: 从插件商店安装插件到客户端 + properties: + client_id: + type: string + download_url: + type: string + plugin_name: + type: string + signature_url: + type: string + required: + - client_id + - download_url + - plugin_name + - signature_url + type: object + github_com_gotunnel_internal_server_router_dto.StorePluginInfo: + description: 插件商店中的插件信息 + properties: + author: + type: string + description: + type: string + download_url: + type: string + icon: + type: string + name: + type: string + signature_url: + type: string + type: + type: string + version: + type: string + type: object + github_com_gotunnel_internal_server_router_dto.TokenCheckResponse: + description: Token 验证结果 + properties: + username: + type: string + valid: + type: boolean + type: object + github_com_gotunnel_internal_server_router_dto.UpdateClientRequest: + description: 更新客户端配置的请求体 + properties: + nickname: + example: My Client + maxLength: 128 + type: string + plugins: + items: + $ref: '#/definitions/github_com_gotunnel_internal_server_db.ClientPlugin' + type: array + rules: + items: + $ref: '#/definitions/github_com_gotunnel_pkg_protocol.ProxyRule' + type: array + type: object + github_com_gotunnel_internal_server_router_dto.UpdateServerConfigRequest: + description: 更新服务器配置 + properties: + server: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.ServerConfigPart' + web: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.WebConfigPart' + type: object + github_com_gotunnel_internal_server_router_dto.VersionInfo: + description: 当前版本信息 + properties: + build_time: + type: string + git_commit: + type: string + go_version: + type: string + platform: + type: string + version: + type: string + type: object + github_com_gotunnel_internal_server_router_dto.WebConfigInfo: + properties: + bind_addr: + type: string + bind_port: + type: integer + enabled: + type: boolean + password: + description: 显示为 **** + type: string + username: + type: string + type: object + github_com_gotunnel_internal_server_router_dto.WebConfigPart: + description: Web 控制台配置 + properties: + bind_addr: + type: string + bind_port: + maximum: 65535 + minimum: 1 + type: integer + enabled: + type: boolean + password: + maxLength: 64 + minLength: 6 + type: string + username: + maxLength: 32 + minLength: 3 + type: string + type: object + github_com_gotunnel_pkg_protocol.ProxyRule: + properties: + enabled: + description: 是否启用,默认为 true + type: boolean + local_ip: + description: tcp/udp 模式使用 + type: string + local_port: + description: tcp/udp 模式使用 + type: integer + name: + type: string + plugin_config: + additionalProperties: + type: string + type: object + plugin_name: + description: Plugin 支持字段 + type: string + plugin_version: + type: string + remote_port: + description: 服务端监听端口 + type: integer + type: + description: '内置: tcp, udp, http, https; 插件: socks5 等' + type: string + type: object + internal_server_router_handler.Response: + properties: + code: + type: integer + data: {} + message: + type: string + type: object +host: localhost:7500 +info: + contact: {} + description: GoTunnel 内网穿透服务器 API + title: GoTunnel API + version: "1.0" +paths: + /api/auth/check: + get: + description: 验证 JWT token 是否有效 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.TokenCheckResponse' + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 检查 Token + tags: + - 认证 + /api/auth/login: + post: + consumes: + - application/json + description: 使用用户名密码登录,返回 JWT token + parameters: + - description: 登录信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.LoginResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + summary: 用户登录 + tags: + - 认证 + /api/client-plugin/{clientID}/{pluginName}/config: + get: + description: 获取客户端上指定插件的配置 + parameters: + - description: 客户端ID + in: path + name: clientID + required: true + type: string + - description: 插件名称 + in: path + name: pluginName + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.PluginConfigResponse' + type: object + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 获取客户端插件配置 + tags: + - 插件 + put: + consumes: + - application/json + description: 更新客户端上指定插件的配置 + parameters: + - description: 客户端ID + in: path + name: clientID + required: true + type: string + - description: 插件名称 + in: path + name: pluginName + required: true + type: string + - description: 配置内容 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.PluginConfigRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 更新客户端插件配置 + tags: + - 插件 + /api/client/{id}: + delete: + description: 删除指定的客户端配置 + parameters: + - description: 客户端ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 删除客户端 + tags: + - 客户端 + get: + description: 获取指定客户端的详细信息 + parameters: + - description: 客户端ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.ClientResponse' + type: object + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 获取客户端详情 + tags: + - 客户端 + put: + consumes: + - application/json + description: 更新指定客户端的配置信息 + parameters: + - description: 客户端ID + in: path + name: id + required: true + type: string + - description: 更新内容 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.UpdateClientRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 更新客户端配置 + tags: + - 客户端 + /api/client/{id}/disconnect: + post: + description: 强制断开客户端连接 + parameters: + - description: 客户端ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 断开连接 + tags: + - 客户端 + /api/client/{id}/install-plugins: + post: + consumes: + - application/json + description: 将指定插件安装到客户端 + parameters: + - description: 客户端ID + in: path + name: id + required: true + type: string + - description: 插件列表 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.InstallPluginsRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 安装插件 + tags: + - 客户端 + /api/client/{id}/plugin/{pluginName}/{action}: + post: + consumes: + - application/json + description: 对客户端插件执行操作(stop/restart/config/delete) + parameters: + - description: 客户端ID + in: path + name: id + required: true + type: string + - description: 插件名称 + in: path + name: pluginName + required: true + type: string + - description: 操作类型 + enum: + - stop + - restart + - config + - delete + in: path + name: action + required: true + type: string + - description: 操作参数 + in: body + name: request + schema: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.ClientPluginActionRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 插件操作 + tags: + - 客户端 + /api/client/{id}/push: + post: + description: 将配置推送到在线客户端 + parameters: + - description: 客户端ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 推送配置 + tags: + - 客户端 + /api/client/{id}/restart: + post: + description: 发送重启命令到客户端 + parameters: + - description: 客户端ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 重启客户端 + tags: + - 客户端 + /api/clients: + get: + description: 返回所有注册客户端的列表及其在线状态 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + items: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.ClientListItem' + type: array + type: object + security: + - Bearer: [] + summary: 获取所有客户端 + tags: + - 客户端 + post: + consumes: + - application/json + description: 创建一个新的客户端配置 + parameters: + - description: 客户端信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.CreateClientRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "409": + description: Conflict + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 创建新客户端 + tags: + - 客户端 + /api/config: + get: + description: 返回服务器配置(敏感信息脱敏) + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.ServerConfigResponse' + type: object + security: + - Bearer: [] + summary: 获取配置 + tags: + - 配置 + put: + consumes: + - application/json + description: 更新服务器配置 + parameters: + - description: 配置内容 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.UpdateServerConfigRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 更新配置 + tags: + - 配置 + /api/config/reload: + post: + description: 重新加载服务器配置 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 重新加载配置 + tags: + - 配置 + /api/js-plugin/{name}: + delete: + description: 删除指定的 JS 插件 + parameters: + - description: 插件名称 + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 删除 JS 插件 + tags: + - JS插件 + get: + description: 获取指定 JS 插件的详细信息 + parameters: + - description: 插件名称 + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + $ref: '#/definitions/github_com_gotunnel_internal_server_db.JSPlugin' + type: object + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 获取 JS 插件详情 + tags: + - JS插件 + put: + consumes: + - application/json + description: 更新指定 JS 插件的信息 + parameters: + - description: 插件名称 + in: path + name: name + required: true + type: string + - description: 更新内容 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.JSPluginUpdateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 更新 JS 插件 + tags: + - JS插件 + /api/js-plugin/{name}/push/{clientID}: + post: + description: 将 JS 插件推送到指定客户端 + parameters: + - description: 插件名称 + in: path + name: name + required: true + type: string + - description: 客户端ID + in: path + name: clientID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "404": + description: Not Found + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 推送插件到客户端 + tags: + - JS插件 + /api/js-plugins: + get: + description: 返回所有注册的 JS 插件 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + items: + $ref: '#/definitions/github_com_gotunnel_internal_server_db.JSPlugin' + type: array + type: object + security: + - Bearer: [] + summary: 获取所有 JS 插件 + tags: + - JS插件 + post: + consumes: + - application/json + description: 创建新的 JS 插件 + parameters: + - description: 插件信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.JSPluginCreateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 创建 JS 插件 + tags: + - JS插件 + /api/plugin/{name}/disable: + post: + description: 禁用指定插件 + parameters: + - description: 插件名称 + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 禁用插件 + tags: + - 插件 + /api/plugin/{name}/enable: + post: + description: 启用指定插件 + parameters: + - description: 插件名称 + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 启用插件 + tags: + - 插件 + /api/plugins: + get: + description: 返回服务端所有注册的插件 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + items: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.PluginInfo' + type: array + type: object + security: + - Bearer: [] + summary: 获取所有插件 + tags: + - 插件 + /api/rule-schemas: + get: + description: 返回所有协议类型的配置模式 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + additionalProperties: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.RuleSchema' + type: object + type: object + security: + - Bearer: [] + summary: 获取规则模式 + tags: + - 插件 + /api/status: + get: + description: 返回服务器运行状态和客户端数量 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.StatusResponse' + type: object + security: + - Bearer: [] + summary: 获取服务器状态 + tags: + - 状态 + /api/store/install: + post: + consumes: + - application/json + description: 从插件商店下载并安装插件到指定客户端 + parameters: + - description: 安装请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.StoreInstallRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 安装商店插件 + tags: + - 插件商店 + /api/store/plugins: + get: + description: 从远程插件商店获取可用插件列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + properties: + plugins: + items: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.StorePluginInfo' + type: array + type: object + type: object + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 获取商店插件 + tags: + - 插件商店 + /api/update/apply/client: + post: + consumes: + - application/json + description: 向指定客户端推送更新命令 + parameters: + - description: 更新请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.ApplyClientUpdateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 推送客户端更新 + tags: + - 更新 + /api/update/apply/server: + post: + consumes: + - application/json + description: 下载并应用服务端更新,服务器将自动重启 + parameters: + - description: 更新请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.ApplyServerUpdateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/internal_server_router_handler.Response' + security: + - Bearer: [] + summary: 应用服务端更新 + tags: + - 更新 + /api/update/check/client: + get: + description: 检查是否有新的客户端版本可用 + parameters: + - description: 操作系统 + enum: + - linux + - darwin + - windows + in: query + name: os + type: string + - description: 架构 + enum: + - amd64 + - arm64 + - "386" + - arm + in: query + name: arch + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.CheckUpdateResponse' + type: object + security: + - Bearer: [] + summary: 检查客户端更新 + tags: + - 更新 + /api/update/check/server: + get: + description: 检查是否有新的服务端版本可用 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.CheckUpdateResponse' + type: object + security: + - Bearer: [] + summary: 检查服务端更新 + tags: + - 更新 + /api/update/version: + get: + description: 返回服务器版本信息 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/internal_server_router_handler.Response' + - properties: + data: + $ref: '#/definitions/github_com_gotunnel_internal_server_router_dto.VersionInfo' + type: object + security: + - Bearer: [] + summary: 获取版本信息 + tags: + - 状态 +securityDefinitions: + Bearer: + description: JWT Bearer token + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/go.mod b/go.mod index 5b65c0d..2b6e070 100644 --- a/go.mod +++ b/go.mod @@ -10,19 +10,77 @@ require ( ) require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.2.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + 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/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dop251/goja v0.0.0-20251201205617-2bb4c724c0f9 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-gonic/gin v1.11.0 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/spec v0.22.3 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + 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-playground/validator/v10 v10.30.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/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.58.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/swaggo/files v1.0.1 // indirect + github.com/swaggo/gin-swagger v1.6.1 // indirect + github.com/swaggo/swag v1.16.6 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + github.com/urfave/cli/v2 v2.27.7 // indirect + github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect + go.uber.org/mock v0.6.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.23.0 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.3.8 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/tools v0.40.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index f5645c0..bb3011e 100644 --- a/go.sum +++ b/go.sum @@ -1,40 +1,233 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= +github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +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.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= +github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +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.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +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.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE= +github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug= +github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= +golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/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= modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= @@ -63,3 +256,5 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/client/tunnel/client.go b/internal/client/tunnel/client.go index 7d75c60..7030cb7 100644 --- a/internal/client/tunnel/client.go +++ b/internal/client/tunnel/client.go @@ -3,10 +3,14 @@ package tunnel import ( "crypto/tls" "fmt" + "io" "log" "net" + "net/http" "os" + "os/exec" "path/filepath" + "runtime" "sync" "time" @@ -231,6 +235,8 @@ func (c *Client) handleStream(stream net.Conn) { c.handleClientRestart(stream, msg) case protocol.MsgTypePluginConfigUpdate: c.handlePluginConfigUpdate(stream, msg) + case protocol.MsgTypeUpdateDownload: + c.handleUpdateDownload(stream, msg) } } @@ -738,3 +744,181 @@ func (c *Client) sendPluginConfigUpdateResult(stream net.Conn, pluginName, ruleN msg, _ := protocol.NewMessage(protocol.MsgTypePluginConfigUpdate, result) protocol.WriteMessage(stream, msg) } + +// handleUpdateDownload 处理更新下载请求 +func (c *Client) handleUpdateDownload(stream net.Conn, msg *protocol.Message) { + defer stream.Close() + + var req protocol.UpdateDownloadRequest + if err := msg.ParsePayload(&req); err != nil { + log.Printf("[Client] Parse update request error: %v", err) + c.sendUpdateResult(stream, false, "invalid request") + return + } + + log.Printf("[Client] Update download requested: %s", req.DownloadURL) + + // 异步执行更新 + go func() { + if err := c.performSelfUpdate(req.DownloadURL); err != nil { + log.Printf("[Client] Update failed: %v", err) + } + }() + + c.sendUpdateResult(stream, true, "update started") +} + +// sendUpdateResult 发送更新结果 +func (c *Client) sendUpdateResult(stream net.Conn, success bool, message string) { + result := protocol.UpdateResultResponse{ + Success: success, + Message: message, + } + msg, _ := protocol.NewMessage(protocol.MsgTypeUpdateResult, result) + protocol.WriteMessage(stream, msg) +} + +// performSelfUpdate 执行自更新 +func (c *Client) performSelfUpdate(downloadURL string) error { + log.Printf("[Client] Starting self-update from: %s", downloadURL) + + // 创建临时文件 + tempDir := os.TempDir() + tempFile := filepath.Join(tempDir, "gotunnel_client_update") + + if runtime.GOOS == "windows" { + tempFile += ".exe" + } + + // 下载新版本 + if err := downloadUpdateFile(downloadURL, tempFile); err != nil { + return fmt.Errorf("download update: %w", err) + } + + // 设置执行权限 + if runtime.GOOS != "windows" { + if err := os.Chmod(tempFile, 0755); err != nil { + os.Remove(tempFile) + return fmt.Errorf("chmod: %w", err) + } + } + + // 获取当前可执行文件路径 + currentPath, err := os.Executable() + if err != nil { + os.Remove(tempFile) + return fmt.Errorf("get executable: %w", err) + } + currentPath, _ = filepath.EvalSymlinks(currentPath) + + // Windows 需要特殊处理 + if runtime.GOOS == "windows" { + return performWindowsClientUpdate(tempFile, currentPath, c.ServerAddr, c.Token, c.ID) + } + + // Linux/Mac: 直接替换 + backupPath := currentPath + ".bak" + + // 停止所有插件 + c.stopAllPlugins() + + // 备份当前文件 + if err := os.Rename(currentPath, backupPath); err != nil { + os.Remove(tempFile) + return fmt.Errorf("backup current: %w", err) + } + + // 移动新文件 + if err := os.Rename(tempFile, currentPath); err != nil { + os.Rename(backupPath, currentPath) + return fmt.Errorf("replace binary: %w", err) + } + + // 删除备份 + os.Remove(backupPath) + + log.Printf("[Client] Update completed, restarting...") + + // 重启进程 + restartClientProcess(currentPath, c.ServerAddr, c.Token, c.ID) + return nil +} + +// stopAllPlugins 停止所有运行中的插件 +func (c *Client) stopAllPlugins() { + c.pluginMu.Lock() + for key, handler := range c.runningPlugins { + log.Printf("[Client] Stopping plugin %s for update", key) + handler.Stop() + } + c.runningPlugins = make(map[string]plugin.ClientPlugin) + c.pluginMu.Unlock() +} + +// downloadUpdateFile 下载更新文件 +func downloadUpdateFile(url, dest string) error { + client := &http.Client{Timeout: 10 * time.Minute} + resp, err := client.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("download failed: %s", resp.Status) + } + + out, err := os.Create(dest) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} + +// performWindowsClientUpdate Windows 平台更新 +func performWindowsClientUpdate(newFile, currentPath, serverAddr, token, id string) error { + // 创建批处理脚本 + args := fmt.Sprintf(`-s "%s" -t "%s"`, serverAddr, token) + if id != "" { + args += fmt.Sprintf(` -id "%s"`, id) + } + + batchScript := fmt.Sprintf(`@echo off +ping 127.0.0.1 -n 2 > nul +del "%s" +move "%s" "%s" +start "" "%s" %s +del "%%~f0" +`, currentPath, newFile, currentPath, currentPath, args) + + batchPath := filepath.Join(os.TempDir(), "gotunnel_client_update.bat") + if err := os.WriteFile(batchPath, []byte(batchScript), 0755); err != nil { + return fmt.Errorf("write batch: %w", err) + } + + cmd := exec.Command("cmd", "/C", "start", "/MIN", batchPath) + if err := cmd.Start(); err != nil { + return fmt.Errorf("start batch: %w", err) + } + + // 退出当前进程 + os.Exit(0) + return nil +} + +// restartClientProcess 重启客户端进程 +func restartClientProcess(path, serverAddr, token, id string) { + args := []string{"-s", serverAddr, "-t", token} + if id != "" { + args = append(args, "-id", id) + } + + cmd := exec.Command(path, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Start() + os.Exit(0) +} diff --git a/internal/server/app/app.go b/internal/server/app/app.go index d7415de..5d1320f 100644 --- a/internal/server/app/app.go +++ b/internal/server/app/app.go @@ -2,10 +2,8 @@ package app import ( "embed" - "io" "io/fs" "log" - "net/http" "github.com/gotunnel/internal/server/config" "github.com/gotunnel/internal/server/db" @@ -16,33 +14,6 @@ import ( //go:embed dist/* var staticFiles embed.FS -// spaHandler SPA路由处理器 -type spaHandler struct { - fs http.FileSystem -} - -func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - f, err := h.fs.Open(path) - if err != nil { - f, err = h.fs.Open("index.html") - if err != nil { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - } - defer f.Close() - - stat, _ := f.Stat() - if stat.IsDir() { - f, err = h.fs.Open(path + "/index.html") - if err != nil { - f, _ = h.fs.Open("index.html") - } - } - http.ServeContent(w, r, path, stat.ModTime(), f.(io.ReadSeeker)) -} - // WebServer Web控制台服务 type WebServer struct { ClientStore db.ClientStore @@ -63,36 +34,29 @@ func NewWebServer(cs db.ClientStore, srv router.ServerInterface, cfg *config.Ser } } -// Run 启动Web服务 +// Run 启动Web服务 (无认证,仅用于开发) func (w *WebServer) Run(addr string) error { r := router.New() - router.RegisterRoutes(r, w) + // 使用默认凭据和 JWT + jwtAuth := auth.NewJWTAuth("dev-secret", 24) + r.SetupRoutes(w, jwtAuth, "admin", "admin") + + // 静态文件 staticFS, err := fs.Sub(staticFiles, "dist") if err != nil { return err } - r.Handle("/", spaHandler{fs: http.FS(staticFS)}) + r.SetupStaticFiles(staticFS) log.Printf("[Web] Console listening on %s", addr) - return http.ListenAndServe(addr, r.Handler()) + return r.Engine.Run(addr) } -// RunWithAuth 启动带认证的Web服务 +// RunWithAuth 启动带 Basic Auth 的 Web 服务 (已废弃,使用 RunWithJWT) func (w *WebServer) RunWithAuth(addr, username, password string) error { - r := router.New() - router.RegisterRoutes(r, w) - - staticFS, err := fs.Sub(staticFiles, "dist") - if err != nil { - return err - } - r.Handle("/", spaHandler{fs: http.FS(staticFS)}) - - auth := &router.AuthConfig{Username: username, Password: password} - handler := router.BasicAuthMiddleware(auth, r.Handler()) - log.Printf("[Web] Console listening on %s (auth enabled)", addr) - return http.ListenAndServe(addr, handler) + // 转发到 JWT 认证 + return w.RunWithJWT(addr, username, password, "auto-generated-secret") } // RunWithJWT 启动带 JWT 认证的 Web 服务 @@ -102,26 +66,18 @@ func (w *WebServer) RunWithJWT(addr, username, password, jwtSecret string) error // JWT 认证器 jwtAuth := auth.NewJWTAuth(jwtSecret, 24) // 24小时过期 - // 注册认证路由(不需要认证) - authHandler := router.NewAuthHandler(username, password, jwtAuth) - router.RegisterAuthRoutes(r, authHandler) - - // 注册业务路由 - router.RegisterRoutes(r, w) + // 设置所有路由 + r.SetupRoutes(w, jwtAuth, username, password) // 静态文件 staticFS, err := fs.Sub(staticFiles, "dist") if err != nil { return err } - r.Handle("/", spaHandler{fs: http.FS(staticFS)}) - - // JWT 中间件,只对 /api/ 路径进行认证(排除 /api/auth/) - skipPaths := []string{"/api/auth/"} - handler := router.JWTMiddleware(jwtAuth, skipPaths, r.Handler()) + r.SetupStaticFiles(staticFS) log.Printf("[Web] Console listening on %s (JWT auth enabled)", addr) - return http.ListenAndServe(addr, handler) + return r.Engine.Run(addr) } // GetClientStore 获取客户端存储 diff --git a/internal/server/router/api.go b/internal/server/router/api.go deleted file mode 100644 index 07995f0..0000000 --- a/internal/server/router/api.go +++ /dev/null @@ -1,1204 +0,0 @@ -package router - -import ( - "encoding/json" - "io" - "net/http" - "regexp" - "time" - - "github.com/gotunnel/internal/server/config" - "github.com/gotunnel/internal/server/db" - "github.com/gotunnel/pkg/plugin" - "github.com/gotunnel/pkg/protocol" -) - -// 客户端 ID 验证规则 -var clientIDRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,64}$`) - -// validateClientID 验证客户端 ID 格式 -func validateClientID(id string) bool { - return clientIDRegex.MatchString(id) -} - -// ClientStatus 客户端状态 -type ClientStatus struct { - ID string `json:"id"` - Nickname string `json:"nickname,omitempty"` - Online bool `json:"online"` - LastPing string `json:"last_ping,omitempty"` - RemoteAddr string `json:"remote_addr,omitempty"` - RuleCount int `json:"rule_count"` -} - -// ServerInterface 服务端接口 -type ServerInterface interface { - GetClientStatus(clientID string) (online bool, lastPing string, remoteAddr string) - GetAllClientStatus() map[string]struct { - Online bool - LastPing string - RemoteAddr string - } - ReloadConfig() error - GetBindAddr() string - 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 - // JS 插件 - InstallJSPluginToClient(clientID string, req JSPluginInstallRequest) error - // 客户端/插件重启 - RestartClient(clientID string) error - StopClientPlugin(clientID, pluginName, ruleName string) error - RestartClientPlugin(clientID, pluginName, ruleName string) error - UpdateClientPluginConfig(clientID, pluginName, ruleName string, config map[string]string, restart bool) error -} - -// JSPluginInstallRequest JS 插件安装请求 -type JSPluginInstallRequest struct { - 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"` -} - -// ConfigField 配置字段(从 plugin 包导出) -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"` -} - -// AppInterface 应用接口 -type AppInterface interface { - GetClientStore() db.ClientStore - GetServer() ServerInterface - GetConfig() *config.ServerConfig - GetConfigPath() string - SaveConfig() error - GetJSPluginStore() db.JSPluginStore -} - -// APIHandler API处理器 -type APIHandler struct { - clientStore db.ClientStore - server ServerInterface - app AppInterface - jsPluginStore db.JSPluginStore -} - -// RegisterRoutes 注册所有 API 路由 -func RegisterRoutes(r *Router, app AppInterface) { - h := &APIHandler{ - clientStore: app.GetClientStore(), - server: app.GetServer(), - app: app, - jsPluginStore: app.GetJSPluginStore(), - } - - api := r.Group("/api") - api.HandleFunc("/status", h.handleStatus) - api.HandleFunc("/clients", h.handleClients) - api.HandleFunc("/client/", h.handleClient) - api.HandleFunc("/config", h.handleConfig) - api.HandleFunc("/config/reload", h.handleReload) - api.HandleFunc("/plugins", h.handlePlugins) - api.HandleFunc("/plugin/", h.handlePlugin) - api.HandleFunc("/store/plugins", h.handleStorePlugins) - api.HandleFunc("/store/install", h.handleStoreInstall) - api.HandleFunc("/client-plugin/", h.handleClientPlugin) - api.HandleFunc("/js-plugin/", h.handleJSPlugin) - api.HandleFunc("/js-plugins", h.handleJSPlugins) - api.HandleFunc("/rule-schemas", h.handleRuleSchemas) -} - -func (h *APIHandler) handleStatus(rw http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - clients, _ := h.clientStore.GetAllClients() - status := map[string]interface{}{ - "server": map[string]interface{}{ - "bind_addr": h.server.GetBindAddr(), - "bind_port": h.server.GetBindPort(), - }, - "client_count": len(clients), - } - h.jsonResponse(rw, status) -} - -func (h *APIHandler) handleClients(rw http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - h.getClients(rw) - case http.MethodPost: - h.addClient(rw, r) - default: - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - } -} - -func (h *APIHandler) getClients(rw http.ResponseWriter) { - clients, err := h.clientStore.GetAllClients() - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - statusMap := h.server.GetAllClientStatus() - var result []ClientStatus - for _, c := range clients { - cs := ClientStatus{ID: c.ID, Nickname: c.Nickname, RuleCount: len(c.Rules)} - if s, ok := statusMap[c.ID]; ok { - cs.Online = s.Online - cs.LastPing = s.LastPing - cs.RemoteAddr = s.RemoteAddr - } - result = append(result, cs) - } - h.jsonResponse(rw, result) -} - -func (h *APIHandler) addClient(rw http.ResponseWriter, r *http.Request) { - var req struct { - ID string `json:"id"` - Rules []protocol.ProxyRule `json:"rules"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - if req.ID == "" { - http.Error(rw, "client id required", http.StatusBadRequest) - return - } - if !validateClientID(req.ID) { - http.Error(rw, "invalid client id: must be 1-64 alphanumeric characters, underscore or hyphen", http.StatusBadRequest) - return - } - - exists, _ := h.clientStore.ClientExists(req.ID) - if exists { - http.Error(rw, "client already exists", http.StatusConflict) - return - } - - client := &db.Client{ID: req.ID, Rules: req.Rules} - if err := h.clientStore.CreateClient(client); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -func (h *APIHandler) handleClient(rw http.ResponseWriter, r *http.Request) { - clientID := r.URL.Path[len("/api/client/"):] - if clientID == "" { - http.Error(rw, "client id required", http.StatusBadRequest) - return - } - - // 处理子路径操作 - if idx := len(clientID) - 1; idx > 0 { - if clientID[idx] == '/' { - clientID = clientID[:idx] - } - } - - // 检查是否是特殊操作 - parts := splitPath(clientID) - if len(parts) == 2 { - clientID = parts[0] - action := parts[1] - switch action { - case "push": - h.pushConfigToClient(rw, r, clientID) - return - case "disconnect": - h.disconnectClient(rw, r, clientID) - return - case "install-plugins": - h.installPluginsToClient(rw, r, clientID) - return - case "restart": - h.restartClient(rw, r, clientID) - return - } - } - - // 检查是否是插件操作: /api/client/{id}/plugin/{name}/{action} - if len(parts) >= 2 && parts[1] == "plugin" { - // 重新解析路径 - remaining := clientID[len(parts[0])+1:] // "plugin/xxx/action" - pluginParts := splitPath(remaining[7:]) // 跳过 "plugin/" - if len(pluginParts) >= 2 { - pluginName := pluginParts[0] - pluginAction := pluginParts[1] - h.handleClientPluginAction(rw, r, parts[0], pluginName, pluginAction) - return - } - } - - switch r.Method { - case http.MethodGet: - h.getClient(rw, clientID) - case http.MethodPut: - h.updateClient(rw, r, clientID) - case http.MethodDelete: - h.deleteClient(rw, clientID) - default: - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - } -} - -// splitPath 分割路径 -func splitPath(path string) []string { - for i, c := range path { - if c == '/' { - return []string{path[:i], path[i+1:]} - } - } - return []string{path} -} - -func (h *APIHandler) getClient(rw http.ResponseWriter, clientID string) { - client, err := h.clientStore.GetClient(clientID) - if err != nil { - http.Error(rw, "client not found", http.StatusNotFound) - return - } - online, lastPing, remoteAddr := h.server.GetClientStatus(clientID) - h.jsonResponse(rw, map[string]interface{}{ - "id": client.ID, "nickname": client.Nickname, "rules": client.Rules, - "plugins": client.Plugins, "online": online, "last_ping": lastPing, - "remote_addr": remoteAddr, - }) -} - -func (h *APIHandler) updateClient(rw http.ResponseWriter, r *http.Request, clientID string) { - var req struct { - Nickname string `json:"nickname"` - Rules []protocol.ProxyRule `json:"rules"` - Plugins []db.ClientPlugin `json:"plugins"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - - client, err := h.clientStore.GetClient(clientID) - if err != nil { - http.Error(rw, "client not found", http.StatusNotFound) - return - } - - client.Nickname = req.Nickname - client.Rules = req.Rules - if req.Plugins != nil { - client.Plugins = req.Plugins - } - if err := h.clientStore.UpdateClient(client); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -func (h *APIHandler) deleteClient(rw http.ResponseWriter, clientID string) { - exists, _ := h.clientStore.ClientExists(clientID) - if !exists { - http.Error(rw, "client not found", http.StatusNotFound) - return - } - - if err := h.clientStore.DeleteClient(clientID); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -func (h *APIHandler) handleConfig(rw http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - h.getConfig(rw) - case http.MethodPut: - h.updateConfig(rw, r) - default: - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - } -} - -func (h *APIHandler) getConfig(rw http.ResponseWriter) { - cfg := h.app.GetConfig() - // Token 脱敏处理,只显示前4位 - maskedToken := cfg.Server.Token - if len(maskedToken) > 4 { - maskedToken = maskedToken[:4] + "****" - } - h.jsonResponse(rw, map[string]interface{}{ - "server": map[string]interface{}{ - "bind_addr": cfg.Server.BindAddr, - "bind_port": cfg.Server.BindPort, - "token": maskedToken, - "heartbeat_sec": cfg.Server.HeartbeatSec, - "heartbeat_timeout": cfg.Server.HeartbeatTimeout, - }, - "web": map[string]interface{}{ - "enabled": cfg.Web.Enabled, - "bind_addr": cfg.Web.BindAddr, - "bind_port": cfg.Web.BindPort, - "username": cfg.Web.Username, - "password": "****", - }, - }) -} - -func (h *APIHandler) updateConfig(rw http.ResponseWriter, r *http.Request) { - var req struct { - Server *struct { - BindAddr string `json:"bind_addr"` - BindPort int `json:"bind_port"` - Token string `json:"token"` - HeartbeatSec int `json:"heartbeat_sec"` - HeartbeatTimeout int `json:"heartbeat_timeout"` - } `json:"server"` - Web *struct { - Enabled bool `json:"enabled"` - BindAddr string `json:"bind_addr"` - BindPort int `json:"bind_port"` - Username string `json:"username"` - Password string `json:"password"` - } `json:"web"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - - cfg := h.app.GetConfig() - - // 更新 Server 配置 - if req.Server != nil { - if req.Server.BindAddr != "" { - cfg.Server.BindAddr = req.Server.BindAddr - } - if req.Server.BindPort > 0 { - cfg.Server.BindPort = req.Server.BindPort - } - if req.Server.Token != "" { - cfg.Server.Token = req.Server.Token - } - if req.Server.HeartbeatSec > 0 { - cfg.Server.HeartbeatSec = req.Server.HeartbeatSec - } - if req.Server.HeartbeatTimeout > 0 { - cfg.Server.HeartbeatTimeout = req.Server.HeartbeatTimeout - } - } - - // 更新 Web 配置 - if req.Web != nil { - cfg.Web.Enabled = req.Web.Enabled - if req.Web.BindAddr != "" { - cfg.Web.BindAddr = req.Web.BindAddr - } - if req.Web.BindPort > 0 { - cfg.Web.BindPort = req.Web.BindPort - } - cfg.Web.Username = req.Web.Username - cfg.Web.Password = req.Web.Password - } - - if err := h.app.SaveConfig(); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -func (h *APIHandler) handleReload(rw http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - if err := h.server.ReloadConfig(); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -func (h *APIHandler) jsonResponse(rw http.ResponseWriter, data interface{}) { - rw.Header().Set("Content-Type", "application/json") - json.NewEncoder(rw).Encode(data) -} - -// pushConfigToClient 推送配置到客户端 -func (h *APIHandler) pushConfigToClient(rw http.ResponseWriter, r *http.Request, clientID string) { - if r.Method != http.MethodPost { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - online, _, _ := h.server.GetClientStatus(clientID) - if !online { - http.Error(rw, "client not online", http.StatusBadRequest) - return - } - - if err := h.server.PushConfigToClient(clientID); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -// disconnectClient 断开客户端连接 -func (h *APIHandler) disconnectClient(rw http.ResponseWriter, r *http.Request, clientID string) { - if r.Method != http.MethodPost { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - if err := h.server.DisconnectClient(clientID); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -// handlePlugins 处理插件列表 -func (h *APIHandler) handlePlugins(rw http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - plugins := h.server.GetPluginList() - h.jsonResponse(rw, plugins) -} - -// handlePlugin 处理单个插件操作 -func (h *APIHandler) handlePlugin(rw http.ResponseWriter, r *http.Request) { - path := r.URL.Path[len("/api/plugin/"):] - if path == "" { - http.Error(rw, "plugin name required", http.StatusBadRequest) - return - } - - parts := splitPath(path) - pluginName := parts[0] - - if len(parts) == 2 { - action := parts[1] - switch action { - case "enable": - h.enablePlugin(rw, r, pluginName) - return - case "disable": - h.disablePlugin(rw, r, pluginName) - return - } - } - - http.Error(rw, "invalid action", http.StatusBadRequest) -} - -func (h *APIHandler) enablePlugin(rw http.ResponseWriter, r *http.Request, name string) { - if r.Method != http.MethodPost { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - if err := h.server.EnablePlugin(name); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -func (h *APIHandler) disablePlugin(rw http.ResponseWriter, r *http.Request, name string) { - if r.Method != http.MethodPost { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - if err := h.server.DisablePlugin(name); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -// installPluginsToClient 安装插件到客户端 -func (h *APIHandler) installPluginsToClient(rw http.ResponseWriter, r *http.Request, clientID string) { - if r.Method != http.MethodPost { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - online, _, _ := h.server.GetClientStatus(clientID) - if !online { - http.Error(rw, "client not online", http.StatusBadRequest) - return - } - - var req struct { - Plugins []string `json:"plugins"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - - if len(req.Plugins) == 0 { - http.Error(rw, "no plugins specified", http.StatusBadRequest) - return - } - - if err := h.server.InstallPluginsToClient(clientID, req.Plugins); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -// StorePluginInfo 扩展商店插件信息 -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"` -} - -// StorePluginInstallRequest 从商店安装插件的请求 -type StorePluginInstallRequest struct { - PluginName string `json:"plugin_name"` - DownloadURL string `json:"download_url"` - SignatureURL string `json:"signature_url"` - ClientID string `json:"client_id"` -} - -// handleStorePlugins 处理扩展商店插件列表 -func (h *APIHandler) handleStorePlugins(rw http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - cfg := h.app.GetConfig() - storeURL := cfg.PluginStore.GetPluginStoreURL() - - // 从远程URL获取插件列表 - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Get(storeURL) - if err != nil { - http.Error(rw, "Failed to fetch store: "+err.Error(), http.StatusBadGateway) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - http.Error(rw, "Store returned error", http.StatusBadGateway) - return - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - http.Error(rw, "Failed to read response", http.StatusInternalServerError) - return - } - - var plugins []StorePluginInfo - if err := json.Unmarshal(body, &plugins); err != nil { - http.Error(rw, "Invalid store format", http.StatusBadGateway) - return - } - - h.jsonResponse(rw, map[string]interface{}{ - "plugins": plugins, - }) -} - -// handleStoreInstall 从商店安装插件到客户端 -func (h *APIHandler) handleStoreInstall(rw http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - var req StorePluginInstallRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - - if req.PluginName == "" || req.DownloadURL == "" || req.ClientID == "" || req.SignatureURL == "" { - http.Error(rw, "plugin_name, download_url, signature_url and client_id required", http.StatusBadRequest) - return - } - - // 检查客户端是否在线 - online, _, _ := h.server.GetClientStatus(req.ClientID) - if !online { - http.Error(rw, "client not online", http.StatusBadRequest) - return - } - - // 下载插件 - client := &http.Client{Timeout: 30 * time.Second} - resp, err := client.Get(req.DownloadURL) - if err != nil { - http.Error(rw, "Failed to download plugin: "+err.Error(), http.StatusBadGateway) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - http.Error(rw, "Plugin download failed with status: "+resp.Status, http.StatusBadGateway) - return - } - - source, err := io.ReadAll(resp.Body) - if err != nil { - http.Error(rw, "Failed to read plugin: "+err.Error(), http.StatusInternalServerError) - return - } - - // 下载签名文件 - sigResp, err := client.Get(req.SignatureURL) - if err != nil { - http.Error(rw, "Failed to download signature: "+err.Error(), http.StatusBadGateway) - return - } - defer sigResp.Body.Close() - - if sigResp.StatusCode != http.StatusOK { - http.Error(rw, "Signature download failed with status: "+sigResp.Status, http.StatusBadGateway) - return - } - - signature, err := io.ReadAll(sigResp.Body) - if err != nil { - http.Error(rw, "Failed to read signature: "+err.Error(), http.StatusInternalServerError) - return - } - - // 安装到客户端 - installReq := JSPluginInstallRequest{ - PluginName: req.PluginName, - Source: string(source), - Signature: string(signature), - RuleName: req.PluginName, - AutoStart: true, - } - - if err := h.server.InstallJSPluginToClient(req.ClientID, installReq); err != nil { - http.Error(rw, "Failed to install plugin: "+err.Error(), http.StatusInternalServerError) - return - } - - // 将插件信息保存到数据库 - dbClient, err := h.clientStore.GetClient(req.ClientID) - if err == nil { - // 检查插件是否已存在 - exists := false - for i, p := range dbClient.Plugins { - if p.Name == req.PluginName { - // 更新已存在的插件 - dbClient.Plugins[i].Enabled = true - exists = true - break - } - } - if !exists { - // 添加新插件 - dbClient.Plugins = append(dbClient.Plugins, db.ClientPlugin{ - Name: req.PluginName, - Version: "1.0.0", - Enabled: true, - }) - } - _ = h.clientStore.UpdateClient(dbClient) - } - - h.jsonResponse(rw, map[string]interface{}{ - "status": "ok", - "plugin": req.PluginName, - "client": req.ClientID, - }) -} - -// handleClientPlugin 处理客户端插件配置 -// 路由: /api/client-plugin/{clientID}/{pluginName}/config -func (h *APIHandler) handleClientPlugin(rw http.ResponseWriter, r *http.Request) { - path := r.URL.Path[len("/api/client-plugin/"):] - if path == "" { - http.Error(rw, "client id required", http.StatusBadRequest) - return - } - - // 解析路径: clientID/pluginName/action - parts := splitPathMulti(path) - if len(parts) < 3 { - http.Error(rw, "invalid path, expected: /api/client-plugin/{clientID}/{pluginName}/config", http.StatusBadRequest) - return - } - - clientID := parts[0] - pluginName := parts[1] - action := parts[2] - - if action != "config" { - http.Error(rw, "invalid action", http.StatusBadRequest) - return - } - - switch r.Method { - case http.MethodGet: - h.getClientPluginConfig(rw, clientID, pluginName) - case http.MethodPut: - h.updateClientPluginConfig(rw, r, clientID, pluginName) - default: - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - } -} - -// splitPathMulti 分割路径为多个部分 -func splitPathMulti(path string) []string { - var parts []string - start := 0 - for i, c := range path { - if c == '/' { - if i > start { - parts = append(parts, path[start:i]) - } - start = i + 1 - } - } - if start < len(path) { - parts = append(parts, path[start:]) - } - return parts -} - -// getClientPluginConfig 获取客户端插件配置 -func (h *APIHandler) getClientPluginConfig(rw http.ResponseWriter, clientID, pluginName string) { - client, err := h.clientStore.GetClient(clientID) - if err != nil { - http.Error(rw, "client not found", http.StatusNotFound) - return - } - - // 尝试从内置插件获取配置模式 - schema, err := h.server.GetPluginConfigSchema(pluginName) - if err != nil { - // 如果内置插件中找不到,尝试从 JS 插件获取 - jsPlugin, jsErr := h.jsPluginStore.GetJSPlugin(pluginName) - if jsErr != nil { - // 两者都找不到,返回空 schema(允许配置但没有预定义的 schema) - schema = []ConfigField{} - } else { - // 使用 JS 插件的 config 作为动态 schema - for key := range jsPlugin.Config { - schema = append(schema, ConfigField{ - Key: key, - Label: key, - Type: "string", - }) - } - } - } - - // 查找客户端的插件配置 - var config map[string]string - for _, p := range client.Plugins { - if p.Name == pluginName { - config = p.Config - break - } - } - if config == nil { - config = make(map[string]string) - } - - h.jsonResponse(rw, map[string]interface{}{ - "plugin_name": pluginName, - "schema": schema, - "config": config, - }) -} - -// updateClientPluginConfig 更新客户端插件配置 -func (h *APIHandler) updateClientPluginConfig(rw http.ResponseWriter, r *http.Request, clientID, pluginName string) { - var req struct { - Config map[string]string `json:"config"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - - client, err := h.clientStore.GetClient(clientID) - if err != nil { - http.Error(rw, "client not found", http.StatusNotFound) - return - } - - // 更新插件配置 - found := false - for i, p := range client.Plugins { - if p.Name == pluginName { - client.Plugins[i].Config = req.Config - found = true - break - } - } - - if !found { - http.Error(rw, "plugin not installed on client", http.StatusNotFound) - return - } - - // 保存到数据库 - if err := h.clientStore.UpdateClient(client); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - // 如果客户端在线,同步配置 - online, _, _ := h.server.GetClientStatus(clientID) - if online { - if err := h.server.SyncPluginConfigToClient(clientID, pluginName, req.Config); err != nil { - // 配置已保存,但同步失败,返回警告 - h.jsonResponse(rw, map[string]interface{}{ - "status": "partial", - "message": "config saved but sync failed: " + err.Error(), - }) - return - } - } - - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -// handleJSPlugin 处理单个 JS 插件操作 -// GET/PUT/DELETE /api/js-plugin/{name} -// POST /api/js-plugin/{name}/push/{clientID} -func (h *APIHandler) handleJSPlugin(rw http.ResponseWriter, r *http.Request) { - path := r.URL.Path[len("/api/js-plugin/"):] - if path == "" { - http.Error(rw, "plugin name required", http.StatusBadRequest) - return - } - - parts := splitPathMulti(path) - - // POST /api/js-plugin/{name}/push/{clientID} - if len(parts) == 3 && parts[1] == "push" { - if r.Method != http.MethodPost { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - h.pushJSPluginToClient(rw, parts[0], parts[2]) - return - } - - // GET/PUT/DELETE /api/js-plugin/{name} - pluginName := parts[0] - switch r.Method { - case http.MethodGet: - h.getJSPlugin(rw, pluginName) - case http.MethodPut: - h.updateJSPlugin(rw, r, pluginName) - case http.MethodDelete: - h.deleteJSPlugin(rw, pluginName) - default: - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - } -} - -// installJSPluginToClient 安装 JS 插件到客户端 -func (h *APIHandler) installJSPluginToClient(rw http.ResponseWriter, r *http.Request, clientID string) { - online, _, _ := h.server.GetClientStatus(clientID) - if !online { - http.Error(rw, "client not online", http.StatusBadRequest) - return - } - - var req JSPluginInstallRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - - if req.PluginName == "" || req.Source == "" { - http.Error(rw, "plugin_name and source required", http.StatusBadRequest) - return - } - - if err := h.server.InstallJSPluginToClient(clientID, req); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - h.jsonResponse(rw, map[string]interface{}{ - "status": "ok", - "plugin": req.PluginName, - }) -} - -// handleJSPlugins 处理 JS 插件列表和创建 -// GET /api/js-plugins - 获取所有 JS 插件 -// POST /api/js-plugins - 创建新 JS 插件 -func (h *APIHandler) handleJSPlugins(rw http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - h.getJSPlugins(rw) - case http.MethodPost: - h.createJSPlugin(rw, r) - default: - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - } -} - -func (h *APIHandler) getJSPlugins(rw http.ResponseWriter) { - plugins, err := h.jsPluginStore.GetAllJSPlugins() - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - if plugins == nil { - plugins = []db.JSPlugin{} - } - h.jsonResponse(rw, plugins) -} - -func (h *APIHandler) createJSPlugin(rw http.ResponseWriter, r *http.Request) { - var req db.JSPlugin - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - - if req.Name == "" || req.Source == "" { - http.Error(rw, "name and source required", http.StatusBadRequest) - return - } - - req.Enabled = true - if err := h.jsPluginStore.SaveJSPlugin(&req); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -func (h *APIHandler) getJSPlugin(rw http.ResponseWriter, name string) { - p, err := h.jsPluginStore.GetJSPlugin(name) - if err != nil { - http.Error(rw, "plugin not found", http.StatusNotFound) - return - } - h.jsonResponse(rw, p) -} - -func (h *APIHandler) updateJSPlugin(rw http.ResponseWriter, r *http.Request, name string) { - var req db.JSPlugin - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(rw, err.Error(), http.StatusBadRequest) - return - } - req.Name = name - if err := h.jsPluginStore.SaveJSPlugin(&req); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -func (h *APIHandler) deleteJSPlugin(rw http.ResponseWriter, name string) { - if err := h.jsPluginStore.DeleteJSPlugin(name); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - h.jsonResponse(rw, map[string]string{"status": "ok"}) -} - -// pushJSPluginToClient 推送 JS 插件到指定客户端 -func (h *APIHandler) pushJSPluginToClient(rw http.ResponseWriter, pluginName, clientID string) { - // 检查客户端是否在线 - online, _, _ := h.server.GetClientStatus(clientID) - if !online { - http.Error(rw, "client not online", http.StatusBadRequest) - return - } - - // 获取插件 - p, err := h.jsPluginStore.GetJSPlugin(pluginName) - if err != nil { - http.Error(rw, "plugin not found", http.StatusNotFound) - return - } - - if !p.Enabled { - http.Error(rw, "plugin is disabled", http.StatusBadRequest) - return - } - - // 推送到客户端 - req := JSPluginInstallRequest{ - PluginName: p.Name, - Source: p.Source, - Signature: p.Signature, - RuleName: p.Name, - Config: p.Config, - AutoStart: p.AutoStart, - } - - if err := h.server.InstallJSPluginToClient(clientID, req); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - h.jsonResponse(rw, map[string]string{"status": "ok", "plugin": pluginName, "client": clientID}) -} - -// handleRuleSchemas 返回所有协议类型的配置模式 -func (h *APIHandler) handleRuleSchemas(rw http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // 获取内置协议模式 - schemas := make(map[string]RuleSchema) - for name, schema := range plugin.BuiltinRuleSchemas() { - schemas[name] = RuleSchema{ - NeedsLocalAddr: schema.NeedsLocalAddr, - ExtraFields: convertConfigFields(schema.ExtraFields), - } - } - - // 添加已注册插件的模式 - plugins := h.server.GetPluginList() - for _, p := range plugins { - if p.RuleSchema != nil { - schemas[p.Name] = *p.RuleSchema - } - } - - h.jsonResponse(rw, schemas) -} - -// convertConfigFields 将 plugin.ConfigField 转换为 router.ConfigField -func convertConfigFields(fields []plugin.ConfigField) []ConfigField { - result := make([]ConfigField, len(fields)) - for i, f := range fields { - result[i] = 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 -} - -// restartClient 重启客户端 -func (h *APIHandler) restartClient(rw http.ResponseWriter, r *http.Request, clientID string) { - if r.Method != http.MethodPost { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - if err := h.server.RestartClient(clientID); err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - h.jsonResponse(rw, map[string]string{"status": "ok", "message": "client restart initiated"}) -} - -// handleClientPluginAction 处理客户端插件操作 -func (h *APIHandler) handleClientPluginAction(rw http.ResponseWriter, r *http.Request, clientID, pluginName, action string) { - if r.Method != http.MethodPost && r.Method != http.MethodPut { - http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // 获取规则名称(从请求体或使用插件名作为默认值) - var req struct { - RuleName string `json:"rule_name"` - Config map[string]string `json:"config"` - Restart bool `json:"restart"` - } - json.NewDecoder(r.Body).Decode(&req) - if req.RuleName == "" { - req.RuleName = pluginName - } - - var err error - switch action { - case "stop": - err = h.server.StopClientPlugin(clientID, pluginName, req.RuleName) - case "restart": - err = h.server.RestartClientPlugin(clientID, pluginName, req.RuleName) - case "config": - if req.Config == nil { - http.Error(rw, "config required", http.StatusBadRequest) - return - } - err = h.server.UpdateClientPluginConfig(clientID, pluginName, req.RuleName, req.Config, req.Restart) - default: - http.Error(rw, "unknown action", http.StatusBadRequest) - return - } - - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - h.jsonResponse(rw, map[string]string{"status": "ok", "action": action, "plugin": pluginName}) -} diff --git a/internal/server/router/auth.go b/internal/server/router/auth.go deleted file mode 100644 index 1a89cf6..0000000 --- a/internal/server/router/auth.go +++ /dev/null @@ -1,108 +0,0 @@ -package router - -import ( - "crypto/subtle" - "encoding/json" - "net/http" - - "github.com/gotunnel/pkg/auth" - "github.com/gotunnel/pkg/security" -) - -// AuthHandler 认证处理器 -type AuthHandler struct { - username string - password string - jwtAuth *auth.JWTAuth -} - -// NewAuthHandler 创建认证处理器 -func NewAuthHandler(username, password string, jwtAuth *auth.JWTAuth) *AuthHandler { - return &AuthHandler{ - username: username, - password: password, - jwtAuth: jwtAuth, - } -} - -// RegisterAuthRoutes 注册认证路由 -func RegisterAuthRoutes(r *Router, h *AuthHandler) { - r.HandleFunc("/api/auth/login", h.handleLogin) - r.HandleFunc("/api/auth/check", h.handleCheck) -} - -// handleLogin 处理登录请求 -func (h *AuthHandler) handleLogin(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) - return - } - - var req struct { - Username string `json:"username"` - Password string `json:"password"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) - return - } - - // 验证用户名密码 - userMatch := subtle.ConstantTimeCompare([]byte(req.Username), []byte(h.username)) == 1 - passMatch := subtle.ConstantTimeCompare([]byte(req.Password), []byte(h.password)) == 1 - - if !userMatch || !passMatch { - security.LogWebLogin(r.RemoteAddr, req.Username, false) - http.Error(w, `{"error":"invalid credentials"}`, http.StatusUnauthorized) - return - } - - // 生成 token - token, err := h.jwtAuth.GenerateToken(req.Username) - if err != nil { - http.Error(w, `{"error":"failed to generate token"}`, http.StatusInternalServerError) - return - } - - security.LogWebLogin(r.RemoteAddr, req.Username, true) - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{ - "token": token, - }) -} - -// handleCheck 检查 token 是否有效 -func (h *AuthHandler) handleCheck(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) - return - } - - // 从 Authorization header 获取 token - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - http.Error(w, `{"error":"missing authorization header"}`, http.StatusUnauthorized) - return - } - - // 解析 Bearer token - const prefix = "Bearer " - if len(authHeader) < len(prefix) || authHeader[:len(prefix)] != prefix { - http.Error(w, `{"error":"invalid authorization format"}`, http.StatusUnauthorized) - return - } - tokenStr := authHeader[len(prefix):] - - // 验证 token - claims, err := h.jwtAuth.ValidateToken(tokenStr) - if err != nil { - http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "valid": true, - "username": claims.Username, - }) -} diff --git a/internal/server/router/dto/client.go b/internal/server/router/dto/client.go new file mode 100644 index 0000000..09dad12 --- /dev/null +++ b/internal/server/router/dto/client.go @@ -0,0 +1,58 @@ +package dto + +import ( + "github.com/gotunnel/internal/server/db" + "github.com/gotunnel/pkg/protocol" +) + +// CreateClientRequest 创建客户端请求 +// @Description 创建新客户端的请求体 +type CreateClientRequest struct { + ID string `json:"id" binding:"required,min=1,max=64" example:"client-001"` + Rules []protocol.ProxyRule `json:"rules"` +} + +// UpdateClientRequest 更新客户端请求 +// @Description 更新客户端配置的请求体 +type UpdateClientRequest struct { + Nickname string `json:"nickname" binding:"max=128" example:"My Client"` + Rules []protocol.ProxyRule `json:"rules"` + Plugins []db.ClientPlugin `json:"plugins"` +} + +// ClientResponse 客户端详情响应 +// @Description 客户端详细信息 +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"` +} + +// ClientListItem 客户端列表项 +// @Description 客户端列表中的单个项目 +type ClientListItem struct { + ID string `json:"id" example:"client-001"` + Nickname string `json:"nickname,omitempty" example:"My Client"` + Online bool `json:"online" example:"true"` + LastPing string `json:"last_ping,omitempty"` + RemoteAddr string `json:"remote_addr,omitempty"` + RuleCount int `json:"rule_count" example:"3"` +} + +// 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/config.go b/internal/server/router/dto/config.go new file mode 100644 index 0000000..96956c6 --- /dev/null +++ b/internal/server/router/dto/config.go @@ -0,0 +1,53 @@ +package dto + +// UpdateServerConfigRequest 更新服务器配置请求 +// @Description 更新服务器配置 +type UpdateServerConfigRequest struct { + Server *ServerConfigPart `json:"server"` + Web *WebConfigPart `json:"web"` +} + +// ServerConfigPart 服务器配置部分 +// @Description 隧道服务器配置 +type ServerConfigPart struct { + BindAddr string `json:"bind_addr" binding:"omitempty"` + BindPort int `json:"bind_port" binding:"omitempty,min=1,max=65535"` + Token string `json:"token" binding:"omitempty,min=8"` + HeartbeatSec int `json:"heartbeat_sec" binding:"omitempty,min=1,max=300"` + HeartbeatTimeout int `json:"heartbeat_timeout" binding:"omitempty,min=1,max=600"` +} + +// WebConfigPart Web 配置部分 +// @Description Web 控制台配置 +type WebConfigPart struct { + Enabled bool `json:"enabled"` + BindAddr string `json:"bind_addr" binding:"omitempty"` + BindPort int `json:"bind_port" binding:"omitempty,min=1,max=65535"` + Username string `json:"username" binding:"omitempty,min=3,max=32"` + Password string `json:"password" binding:"omitempty,min=6,max=64"` +} + +// ServerConfigResponse 服务器配置响应 +// @Description 服务器配置信息 +type ServerConfigResponse struct { + Server ServerConfigInfo `json:"server"` + Web WebConfigInfo `json:"web"` +} + +// ServerConfigInfo 服务器配置信息 +type ServerConfigInfo struct { + BindAddr string `json:"bind_addr"` + BindPort int `json:"bind_port"` + Token string `json:"token"` // 脱敏后的 token + HeartbeatSec int `json:"heartbeat_sec"` + HeartbeatTimeout int `json:"heartbeat_timeout"` +} + +// WebConfigInfo Web 配置信息 +type WebConfigInfo struct { + Enabled bool `json:"enabled"` + BindAddr string `json:"bind_addr"` + BindPort int `json:"bind_port"` + Username string `json:"username"` + Password string `json:"password"` // 显示为 **** +} diff --git a/internal/server/router/dto/plugin.go b/internal/server/router/dto/plugin.go new file mode 100644 index 0000000..9864415 --- /dev/null +++ b/internal/server/router/dto/plugin.go @@ -0,0 +1,105 @@ +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"` +} + +// StoreInstallRequest 从商店安装插件请求 +// @Description 从插件商店安装插件到客户端 +type StoreInstallRequest struct { + PluginName string `json:"plugin_name" binding:"required"` + DownloadURL string `json:"download_url" binding:"required,url"` + SignatureURL string `json:"signature_url" binding:"required,url"` + ClientID string `json:"client_id" binding:"required"` +} diff --git a/internal/server/router/dto/update.go b/internal/server/router/dto/update.go new file mode 100644 index 0000000..3df3b41 --- /dev/null +++ b/internal/server/router/dto/update.go @@ -0,0 +1,76 @@ +package dto + +// CheckUpdateResponse 检查更新响应 +// @Description 更新检查结果 +type CheckUpdateResponse struct { + HasUpdate bool `json:"has_update"` + CurrentVersion string `json:"current_version"` + LatestVersion string `json:"latest_version,omitempty"` + DownloadURL string `json:"download_url,omitempty"` + ReleaseNotes string `json:"release_notes,omitempty"` + PublishedAt string `json:"published_at,omitempty"` +} + +// CheckClientUpdateQuery 检查客户端更新查询参数 +// @Description 检查客户端更新的查询参数 +type CheckClientUpdateQuery struct { + OS string `form:"os" binding:"omitempty,oneof=linux darwin windows"` + Arch string `form:"arch" binding:"omitempty,oneof=amd64 arm64 386 arm"` +} + +// ApplyServerUpdateRequest 应用服务端更新请求 +// @Description 应用服务端更新 +type ApplyServerUpdateRequest struct { + DownloadURL string `json:"download_url" binding:"required,url"` + Restart bool `json:"restart"` +} + +// ApplyClientUpdateRequest 应用客户端更新请求 +// @Description 推送更新到客户端 +type ApplyClientUpdateRequest struct { + ClientID string `json:"client_id" binding:"required"` + DownloadURL string `json:"download_url" binding:"required,url"` +} + +// VersionInfo 版本信息 +// @Description 当前版本信息 +type VersionInfo struct { + Version string `json:"version"` + GitCommit string `json:"git_commit,omitempty"` + BuildTime string `json:"build_time,omitempty"` + GoVersion string `json:"go_version,omitempty"` + Platform string `json:"platform,omitempty"` +} + +// StatusResponse 服务器状态响应 +// @Description 服务器状态信息 +type StatusResponse struct { + Server ServerStatus `json:"server"` + ClientCount int `json:"client_count"` +} + +// ServerStatus 服务器状态 +type ServerStatus struct { + BindAddr string `json:"bind_addr"` + BindPort int `json:"bind_port"` +} + +// LoginRequest 登录请求 +// @Description 用户登录 +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// LoginResponse 登录响应 +// @Description 登录成功返回 +type LoginResponse struct { + Token string `json:"token"` +} + +// TokenCheckResponse Token 检查响应 +// @Description Token 验证结果 +type TokenCheckResponse struct { + Valid bool `json:"valid"` + Username string `json:"username"` +} diff --git a/internal/server/router/errors.go b/internal/server/router/errors.go new file mode 100644 index 0000000..32c5cfa --- /dev/null +++ b/internal/server/router/errors.go @@ -0,0 +1,106 @@ +package router + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" +) + +// ValidationError 验证错误详情 +type ValidationError struct { + Field string `json:"field"` // 字段名 + Message string `json:"message"` // 错误消息 +} + +// HandleValidationError 处理验证错误并返回统一格式 +func HandleValidationError(c *gin.Context, err error) { + var ve validator.ValidationErrors + if errors.As(err, &ve) { + errs := make([]ValidationError, len(ve)) + for i, fe := range ve { + errs[i] = ValidationError{ + Field: fe.Field(), + Message: getValidationMessage(fe), + } + } + c.JSON(http.StatusBadRequest, Response{ + Code: CodeBadRequest, + Message: "validation failed", + Data: errs, + }) + return + } + + // 非验证错误,返回通用错误消息 + BadRequest(c, err.Error()) +} + +// getValidationMessage 根据验证标签返回友好的错误消息 +func getValidationMessage(fe validator.FieldError) string { + switch fe.Tag() { + case "required": + return "this field is required" + case "min": + return "value is too short or too small" + case "max": + return "value is too long or too large" + case "email": + return "invalid email format" + case "url": + return "invalid URL format" + case "oneof": + return "value must be one of: " + fe.Param() + case "alphanum": + return "must contain only letters and numbers" + case "alphanumunicode": + return "must contain only letters, numbers and unicode characters" + case "ip": + return "invalid IP address" + case "hostname": + return "invalid hostname" + case "clientid": + return "must be 1-64 alphanumeric characters, underscore or hyphen" + case "gte": + return "value must be greater than or equal to " + fe.Param() + case "lte": + return "value must be less than or equal to " + fe.Param() + case "gt": + return "value must be greater than " + fe.Param() + case "lt": + return "value must be less than " + fe.Param() + default: + return "validation failed on " + fe.Tag() + } +} + +// BindJSON 绑定 JSON 并自动处理验证错误 +// 返回 true 表示绑定成功,false 表示已处理错误响应 +func BindJSON(c *gin.Context, obj interface{}) bool { + if err := c.ShouldBindJSON(obj); err != nil { + HandleValidationError(c, err) + return false + } + return true +} + +// BindQuery 绑定查询参数并自动处理验证错误 +// 返回 true 表示绑定成功,false 表示已处理错误响应 +func BindQuery(c *gin.Context, obj interface{}) bool { + if err := c.ShouldBindQuery(obj); err != nil { + HandleValidationError(c, err) + return false + } + return true +} + +// BindURI 绑定 URI 参数并自动处理验证错误 +// 返回 true 表示绑定成功,false 表示已处理错误响应 +func BindURI(c *gin.Context, obj interface{}) bool { + if err := c.ShouldBindUri(obj); err != nil { + HandleValidationError(c, err) + return false + } + return true +} diff --git a/internal/server/router/handler/auth.go b/internal/server/router/handler/auth.go new file mode 100644 index 0000000..5a1fbd7 --- /dev/null +++ b/internal/server/router/handler/auth.go @@ -0,0 +1,100 @@ +package handler + +import ( + "crypto/subtle" + + "github.com/gin-gonic/gin" + // removed router import + "github.com/gotunnel/internal/server/router/dto" + "github.com/gotunnel/pkg/auth" + "github.com/gotunnel/pkg/security" +) + +// AuthHandler 认证处理器 +type AuthHandler struct { + username string + password string + jwtAuth *auth.JWTAuth +} + +// NewAuthHandler 创建认证处理器 +func NewAuthHandler(username, password string, jwtAuth *auth.JWTAuth) *AuthHandler { + return &AuthHandler{ + username: username, + password: password, + jwtAuth: jwtAuth, + } +} + +// Login 用户登录 +// @Summary 用户登录 +// @Description 使用用户名密码登录,返回 JWT token +// @Tags 认证 +// @Accept json +// @Produce json +// @Param request body dto.LoginRequest true "登录信息" +// @Success 200 {object} Response{data=dto.LoginResponse} +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Router /api/auth/login [post] +func (h *AuthHandler) Login(c *gin.Context) { + var req dto.LoginRequest + if !BindJSON(c, &req) { + return + } + + // 验证用户名密码 (使用常量时间比较防止时序攻击) + userMatch := subtle.ConstantTimeCompare([]byte(req.Username), []byte(h.username)) == 1 + passMatch := subtle.ConstantTimeCompare([]byte(req.Password), []byte(h.password)) == 1 + + if !userMatch || !passMatch { + security.LogWebLogin(c.ClientIP(), req.Username, false) + Unauthorized(c, "invalid credentials") + return + } + + // 生成 token + token, err := h.jwtAuth.GenerateToken(req.Username) + if err != nil { + InternalError(c, "failed to generate token") + return + } + + security.LogWebLogin(c.ClientIP(), req.Username, true) + Success(c, dto.LoginResponse{Token: token}) +} + +// Check 检查 token 是否有效 +// @Summary 检查 Token +// @Description 验证 JWT token 是否有效 +// @Tags 认证 +// @Produce json +// @Security Bearer +// @Success 200 {object} Response{data=dto.TokenCheckResponse} +// @Failure 401 {object} Response +// @Router /api/auth/check [get] +func (h *AuthHandler) Check(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + Unauthorized(c, "missing authorization header") + return + } + + const prefix = "Bearer " + if len(authHeader) < len(prefix) || authHeader[:len(prefix)] != prefix { + Unauthorized(c, "invalid authorization format") + return + } + tokenStr := authHeader[len(prefix):] + + claims, err := h.jwtAuth.ValidateToken(tokenStr) + if err != nil { + Unauthorized(c, "invalid token") + return + } + + Success(c, dto.TokenCheckResponse{ + Valid: true, + Username: claims.Username, + }) +} diff --git a/internal/server/router/handler/client.go b/internal/server/router/handler/client.go new file mode 100644 index 0000000..285e834 --- /dev/null +++ b/internal/server/router/handler/client.go @@ -0,0 +1,393 @@ +package handler + +import ( + "fmt" + + "github.com/gin-gonic/gin" + "github.com/gotunnel/internal/server/db" + // removed router import + "github.com/gotunnel/internal/server/router/dto" +) + +// ClientHandler 客户端处理器 +type ClientHandler struct { + app AppInterface +} + +// NewClientHandler 创建客户端处理器 +func NewClientHandler(app AppInterface) *ClientHandler { + return &ClientHandler{app: app} +} + +// List 获取客户端列表 +// @Summary 获取所有客户端 +// @Description 返回所有注册客户端的列表及其在线状态 +// @Tags 客户端 +// @Produce json +// @Security Bearer +// @Success 200 {object} Response{data=[]dto.ClientListItem} +// @Router /api/clients [get] +func (h *ClientHandler) List(c *gin.Context) { + clients, err := h.app.GetClientStore().GetAllClients() + if err != nil { + InternalError(c, err.Error()) + return + } + + statusMap := h.app.GetServer().GetAllClientStatus() + result := make([]dto.ClientListItem, 0, len(clients)) + + for _, client := range clients { + item := dto.ClientListItem{ + ID: client.ID, + Nickname: client.Nickname, + RuleCount: len(client.Rules), + } + if status, ok := statusMap[client.ID]; ok { + item.Online = status.Online + item.LastPing = status.LastPing + item.RemoteAddr = status.RemoteAddr + } + result = append(result, item) + } + + Success(c, result) +} + +// Create 创建客户端 +// @Summary 创建新客户端 +// @Description 创建一个新的客户端配置 +// @Tags 客户端 +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body dto.CreateClientRequest true "客户端信息" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 409 {object} Response +// @Router /api/clients [post] +func (h *ClientHandler) Create(c *gin.Context) { + var req dto.CreateClientRequest + if !BindJSON(c, &req) { + return + } + + // 验证客户端 ID 格式 + if !validateClientID(req.ID) { + BadRequest(c, "invalid client id: must be 1-64 alphanumeric characters, underscore or hyphen") + return + } + + // 检查客户端是否已存在 + exists, _ := h.app.GetClientStore().ClientExists(req.ID) + if exists { + Conflict(c, "client already exists") + return + } + + client := &db.Client{ID: req.ID, Rules: req.Rules} + if err := h.app.GetClientStore().CreateClient(client); err != nil { + InternalError(c, err.Error()) + return + } + + Success(c, gin.H{"status": "ok"}) +} + +// Get 获取单个客户端 +// @Summary 获取客户端详情 +// @Description 获取指定客户端的详细信息 +// @Tags 客户端 +// @Produce json +// @Security Bearer +// @Param id path string true "客户端ID" +// @Success 200 {object} Response{data=dto.ClientResponse} +// @Failure 404 {object} Response +// @Router /api/client/{id} [get] +func (h *ClientHandler) Get(c *gin.Context) { + clientID := c.Param("id") + + client, err := h.app.GetClientStore().GetClient(clientID) + if err != nil { + NotFound(c, "client not found") + return + } + + online, lastPing, remoteAddr := h.app.GetServer().GetClientStatus(clientID) + + resp := dto.ClientResponse{ + ID: client.ID, + Nickname: client.Nickname, + Rules: client.Rules, + Plugins: client.Plugins, + Online: online, + LastPing: lastPing, + RemoteAddr: remoteAddr, + } + + Success(c, resp) +} + +// Update 更新客户端 +// @Summary 更新客户端配置 +// @Description 更新指定客户端的配置信息 +// @Tags 客户端 +// @Accept json +// @Produce json +// @Security Bearer +// @Param id path string true "客户端ID" +// @Param request body dto.UpdateClientRequest true "更新内容" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Failure 404 {object} Response +// @Router /api/client/{id} [put] +func (h *ClientHandler) Update(c *gin.Context) { + clientID := c.Param("id") + + var req dto.UpdateClientRequest + if !BindJSON(c, &req) { + return + } + + client, err := h.app.GetClientStore().GetClient(clientID) + if err != nil { + NotFound(c, "client not found") + return + } + + 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()) + return + } + + Success(c, gin.H{"status": "ok"}) +} + +// Delete 删除客户端 +// @Summary 删除客户端 +// @Description 删除指定的客户端配置 +// @Tags 客户端 +// @Produce json +// @Security Bearer +// @Param id path string true "客户端ID" +// @Success 200 {object} Response +// @Failure 404 {object} Response +// @Router /api/client/{id} [delete] +func (h *ClientHandler) Delete(c *gin.Context) { + clientID := c.Param("id") + + exists, _ := h.app.GetClientStore().ClientExists(clientID) + if !exists { + NotFound(c, "client not found") + return + } + + if err := h.app.GetClientStore().DeleteClient(clientID); err != nil { + InternalError(c, err.Error()) + return + } + + Success(c, gin.H{"status": "ok"}) +} + +// PushConfig 推送配置到客户端 +// @Summary 推送配置 +// @Description 将配置推送到在线客户端 +// @Tags 客户端 +// @Produce json +// @Security Bearer +// @Param id path string true "客户端ID" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Router /api/client/{id}/push [post] +func (h *ClientHandler) PushConfig(c *gin.Context) { + clientID := c.Param("id") + + online, _, _ := h.app.GetServer().GetClientStatus(clientID) + if !online { + ClientNotOnline(c) + return + } + + if err := h.app.GetServer().PushConfigToClient(clientID); err != nil { + InternalError(c, err.Error()) + return + } + + Success(c, gin.H{"status": "ok"}) +} + +// Disconnect 断开客户端连接 +// @Summary 断开连接 +// @Description 强制断开客户端连接 +// @Tags 客户端 +// @Produce json +// @Security Bearer +// @Param id path string true "客户端ID" +// @Success 200 {object} Response +// @Router /api/client/{id}/disconnect [post] +func (h *ClientHandler) Disconnect(c *gin.Context) { + clientID := c.Param("id") + + if err := h.app.GetServer().DisconnectClient(clientID); err != nil { + InternalError(c, err.Error()) + return + } + + Success(c, gin.H{"status": "ok"}) +} + +// Restart 重启客户端 +// @Summary 重启客户端 +// @Description 发送重启命令到客户端 +// @Tags 客户端 +// @Produce json +// @Security Bearer +// @Param id path string true "客户端ID" +// @Success 200 {object} Response +// @Router /api/client/{id}/restart [post] +func (h *ClientHandler) Restart(c *gin.Context) { + clientID := c.Param("id") + + if err := h.app.GetServer().RestartClient(clientID); err != nil { + InternalError(c, err.Error()) + return + } + + 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") + + online, _, _ := h.app.GetServer().GetClientStatus(clientID) + if !online { + 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 对客户端插件执行操作(stop/restart/config/delete) +// @Tags 客户端 +// @Accept json +// @Produce json +// @Security Bearer +// @Param id path string true "客户端ID" +// @Param pluginName path string true "插件名称" +// @Param action path string true "操作类型" Enums(stop, restart, config, delete) +// @Param request body dto.ClientPluginActionRequest false "操作参数" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Router /api/client/{id}/plugin/{pluginName}/{action} [post] +func (h *ClientHandler) PluginAction(c *gin.Context) { + clientID := c.Param("id") + pluginName := c.Param("pluginName") + action := c.Param("action") + + var req dto.ClientPluginActionRequest + c.ShouldBindJSON(&req) // 忽略错误,使用默认值 + + if req.RuleName == "" { + req.RuleName = pluginName + } + + var err error + switch action { + case "stop": + err = h.app.GetServer().StopClientPlugin(clientID, pluginName, req.RuleName) + case "restart": + err = h.app.GetServer().RestartClientPlugin(clientID, pluginName, req.RuleName) + case "config": + if req.Config == nil { + BadRequest(c, "config required") + return + } + err = h.app.GetServer().UpdateClientPluginConfig(clientID, pluginName, req.RuleName, req.Config, req.Restart) + case "delete": + err = h.deleteClientPlugin(clientID, pluginName) + default: + BadRequest(c, "unknown action: "+action) + return + } + + if err != nil { + InternalError(c, err.Error()) + return + } + + Success(c, gin.H{ + "status": "ok", + "action": action, + "plugin": pluginName, + }) +} + +func (h *ClientHandler) deleteClientPlugin(clientID, pluginName string) error { + client, err := h.app.GetClientStore().GetClient(clientID) + if err != nil { + return fmt.Errorf("client not found") + } + + var newPlugins []db.ClientPlugin + found := false + for _, p := range client.Plugins { + if p.Name == pluginName { + found = true + continue + } + newPlugins = append(newPlugins, p) + } + + if !found { + return fmt.Errorf("plugin %s not found", pluginName) + } + + client.Plugins = newPlugins + return h.app.GetClientStore().UpdateClient(client) +} + +// validateClientID 验证客户端 ID 格式 +func validateClientID(id string) bool { + if len(id) < 1 || len(id) > 64 { + return false + } + for _, c := range id { + if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-') { + return false + } + } + return true +} diff --git a/internal/server/router/handler/config.go b/internal/server/router/handler/config.go new file mode 100644 index 0000000..a203fc2 --- /dev/null +++ b/internal/server/router/handler/config.go @@ -0,0 +1,131 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + // removed router import + "github.com/gotunnel/internal/server/router/dto" +) + +// ConfigHandler 配置处理器 +type ConfigHandler struct { + app AppInterface +} + +// NewConfigHandler 创建配置处理器 +func NewConfigHandler(app AppInterface) *ConfigHandler { + return &ConfigHandler{app: app} +} + +// Get 获取服务器配置 +// @Summary 获取配置 +// @Description 返回服务器配置(敏感信息脱敏) +// @Tags 配置 +// @Produce json +// @Security Bearer +// @Success 200 {object} Response{data=dto.ServerConfigResponse} +// @Router /api/config [get] +func (h *ConfigHandler) Get(c *gin.Context) { + cfg := h.app.GetConfig() + + // Token 脱敏处理,只显示前4位 + maskedToken := cfg.Server.Token + if len(maskedToken) > 4 { + maskedToken = maskedToken[:4] + "****" + } + + resp := dto.ServerConfigResponse{ + Server: dto.ServerConfigInfo{ + BindAddr: cfg.Server.BindAddr, + BindPort: cfg.Server.BindPort, + Token: maskedToken, + HeartbeatSec: cfg.Server.HeartbeatSec, + HeartbeatTimeout: cfg.Server.HeartbeatTimeout, + }, + Web: dto.WebConfigInfo{ + Enabled: cfg.Web.Enabled, + BindAddr: cfg.Web.BindAddr, + BindPort: cfg.Web.BindPort, + Username: cfg.Web.Username, + Password: "****", + }, + } + + Success(c, resp) +} + +// Update 更新服务器配置 +// @Summary 更新配置 +// @Description 更新服务器配置 +// @Tags 配置 +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body dto.UpdateServerConfigRequest true "配置内容" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Router /api/config [put] +func (h *ConfigHandler) Update(c *gin.Context) { + var req dto.UpdateServerConfigRequest + if !BindJSON(c, &req) { + return + } + + cfg := h.app.GetConfig() + + // 更新 Server 配置 + if req.Server != nil { + if req.Server.BindAddr != "" { + cfg.Server.BindAddr = req.Server.BindAddr + } + if req.Server.BindPort > 0 { + cfg.Server.BindPort = req.Server.BindPort + } + if req.Server.Token != "" { + cfg.Server.Token = req.Server.Token + } + if req.Server.HeartbeatSec > 0 { + cfg.Server.HeartbeatSec = req.Server.HeartbeatSec + } + if req.Server.HeartbeatTimeout > 0 { + cfg.Server.HeartbeatTimeout = req.Server.HeartbeatTimeout + } + } + + // 更新 Web 配置 + if req.Web != nil { + cfg.Web.Enabled = req.Web.Enabled + if req.Web.BindAddr != "" { + cfg.Web.BindAddr = req.Web.BindAddr + } + if req.Web.BindPort > 0 { + cfg.Web.BindPort = req.Web.BindPort + } + cfg.Web.Username = req.Web.Username + cfg.Web.Password = req.Web.Password + } + + if err := h.app.SaveConfig(); err != nil { + InternalError(c, err.Error()) + return + } + + Success(c, gin.H{"status": "ok"}) +} + +// Reload 重新加载配置 +// @Summary 重新加载配置 +// @Description 重新加载服务器配置 +// @Tags 配置 +// @Produce json +// @Security Bearer +// @Success 200 {object} Response +// @Failure 500 {object} Response +// @Router /api/config/reload [post] +func (h *ConfigHandler) Reload(c *gin.Context) { + if err := h.app.GetServer().ReloadConfig(); err != nil { + InternalError(c, err.Error()) + return + } + + Success(c, gin.H{"status": "ok"}) +} diff --git a/internal/server/router/handler/helpers.go b/internal/server/router/handler/helpers.go new file mode 100644 index 0000000..5188800 --- /dev/null +++ b/internal/server/router/handler/helpers.go @@ -0,0 +1,230 @@ +package handler + +import ( + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "time" + + "github.com/gotunnel/pkg/version" +) + +// UpdateInfo 更新信息 +type UpdateInfo struct { + Available bool `json:"available"` + Current string `json:"current"` + Latest string `json:"latest"` + ReleaseNote string `json:"release_note"` + DownloadURL string `json:"download_url"` + AssetName string `json:"asset_name"` + AssetSize int64 `json:"asset_size"` +} + +// checkUpdateForComponent 检查组件更新 +func checkUpdateForComponent(component string) (*UpdateInfo, error) { + release, err := version.GetLatestRelease() + if err != nil { + return nil, fmt.Errorf("get latest release: %w", err) + } + + latestVersion := release.TagName + currentVersion := version.Version + available := version.CompareVersions(currentVersion, latestVersion) < 0 + + // 查找对应平台的资产 + assetName := getAssetNameForPlatform(component, runtime.GOOS, runtime.GOARCH) + var downloadURL string + var assetSize int64 + + for _, asset := range release.Assets { + if asset.Name == assetName { + downloadURL = asset.BrowserDownloadURL + assetSize = asset.Size + break + } + } + + return &UpdateInfo{ + Available: available, + Current: currentVersion, + Latest: latestVersion, + ReleaseNote: release.Body, + DownloadURL: downloadURL, + AssetName: assetName, + AssetSize: assetSize, + }, nil +} + +// checkClientUpdateForPlatform 检查指定平台的客户端更新 +func checkClientUpdateForPlatform(osName, arch string) (*UpdateInfo, error) { + if osName == "" { + osName = runtime.GOOS + } + if arch == "" { + arch = runtime.GOARCH + } + + release, err := version.GetLatestRelease() + if err != nil { + return nil, fmt.Errorf("get latest release: %w", err) + } + + latestVersion := release.TagName + + // 查找对应平台的资产 + assetName := getAssetNameForPlatform("client", osName, arch) + var downloadURL string + var assetSize int64 + + for _, asset := range release.Assets { + if asset.Name == assetName { + downloadURL = asset.BrowserDownloadURL + assetSize = asset.Size + break + } + } + + return &UpdateInfo{ + Available: true, + Current: "", + Latest: latestVersion, + ReleaseNote: release.Body, + DownloadURL: downloadURL, + AssetName: assetName, + AssetSize: assetSize, + }, nil +} + +// getAssetNameForPlatform 获取指定平台的资产名称 +func getAssetNameForPlatform(component, osName, arch string) string { + ext := "" + if osName == "windows" { + ext = ".exe" + } + return fmt.Sprintf("%s_%s_%s%s", component, osName, arch, ext) +} + +// performSelfUpdate 执行自更新 +func performSelfUpdate(downloadURL string, restart bool) error { + // 下载新版本 + tempDir := os.TempDir() + tempFile := filepath.Join(tempDir, "gotunnel_update_"+time.Now().Format("20060102150405")) + + if runtime.GOOS == "windows" { + tempFile += ".exe" + } + + if err := downloadFile(downloadURL, tempFile); err != nil { + return fmt.Errorf("download update: %w", err) + } + + // 设置执行权限 + if runtime.GOOS != "windows" { + if err := os.Chmod(tempFile, 0755); err != nil { + os.Remove(tempFile) + return fmt.Errorf("chmod: %w", err) + } + } + + // 获取当前可执行文件路径 + currentPath, err := os.Executable() + if err != nil { + os.Remove(tempFile) + return fmt.Errorf("get executable: %w", err) + } + currentPath, _ = filepath.EvalSymlinks(currentPath) + + // Windows 需要特殊处理(运行中的文件无法直接替换) + if runtime.GOOS == "windows" { + return performWindowsUpdate(tempFile, currentPath, restart) + } + + // Linux/Mac: 直接替换 + backupPath := currentPath + ".bak" + + // 备份当前文件 + if err := os.Rename(currentPath, backupPath); err != nil { + os.Remove(tempFile) + return fmt.Errorf("backup current: %w", err) + } + + // 移动新文件 + if err := os.Rename(tempFile, currentPath); err != nil { + os.Rename(backupPath, currentPath) + return fmt.Errorf("replace binary: %w", err) + } + + // 删除备份 + os.Remove(backupPath) + + if restart { + restartProcess(currentPath) + } + + return nil +} + +// performWindowsUpdate Windows 平台更新 +func performWindowsUpdate(newFile, currentPath string, restart bool) error { + batchScript := fmt.Sprintf(`@echo off +ping 127.0.0.1 -n 2 > nul +del "%s" +move "%s" "%s" +`, currentPath, newFile, currentPath) + + if restart { + batchScript += fmt.Sprintf(`start "" "%s" +`, currentPath) + } + + batchScript += "del \"%~f0\"\n" + + batchPath := filepath.Join(os.TempDir(), "gotunnel_update.bat") + if err := os.WriteFile(batchPath, []byte(batchScript), 0755); err != nil { + return fmt.Errorf("write batch: %w", err) + } + + cmd := exec.Command("cmd", "/C", "start", "/MIN", batchPath) + if err := cmd.Start(); err != nil { + return fmt.Errorf("start batch: %w", err) + } + + os.Exit(0) + return nil +} + +// restartProcess 重启进程 +func restartProcess(path string) { + cmd := exec.Command(path, os.Args[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Start() + os.Exit(0) +} + +// downloadFile 下载文件 +func downloadFile(url, dest string) error { + client := &http.Client{Timeout: 10 * time.Minute} + resp, err := client.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("download failed: %s", resp.Status) + } + + out, err := os.Create(dest) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} diff --git a/internal/server/router/handler/interfaces.go b/internal/server/router/handler/interfaces.go new file mode 100644 index 0000000..d53d102 --- /dev/null +++ b/internal/server/router/handler/interfaces.go @@ -0,0 +1,83 @@ +package handler + +import ( + "github.com/gotunnel/internal/server/config" + "github.com/gotunnel/internal/server/db" +) + +// AppInterface 应用接口 +type AppInterface interface { + GetClientStore() db.ClientStore + GetServer() ServerInterface + GetConfig() *config.ServerConfig + GetConfigPath() string + SaveConfig() error + GetJSPluginStore() db.JSPluginStore +} + +// ServerInterface 服务端接口 +type ServerInterface interface { + GetClientStatus(clientID string) (online bool, lastPing string, remoteAddr string) + GetAllClientStatus() map[string]struct { + Online bool + LastPing string + RemoteAddr string + } + ReloadConfig() error + GetBindAddr() string + 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 + StopClientPlugin(clientID, pluginName, ruleName string) error + RestartClientPlugin(clientID, pluginName, ruleName string) error + UpdateClientPluginConfig(clientID, pluginName, ruleName string, config map[string]string, restart bool) error + SendUpdateToClient(clientID, downloadURL string) 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 { + 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 new file mode 100644 index 0000000..59ed43d --- /dev/null +++ b/internal/server/router/handler/js_plugin.go @@ -0,0 +1,212 @@ +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插件 +// @Produce json +// @Security Bearer +// @Param name path string true "插件名称" +// @Param clientID path string true "客户端ID" +// @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") + + // 检查客户端是否在线 + online, _, _ := h.app.GetServer().GetClientStatus(clientID) + if !online { + 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, + 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, + }) +} diff --git a/internal/server/router/handler/plugin.go b/internal/server/router/handler/plugin.go new file mode 100644 index 0000000..a191606 --- /dev/null +++ b/internal/server/router/handler/plugin.go @@ -0,0 +1,285 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + // removed router import + "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 + } + + // 尝试从内置插件获取配置模式 + schema, err := h.app.GetServer().GetPluginConfigSchema(pluginName) + var schemaFields []dto.ConfigField + if err != nil { + // 如果内置插件中找不到,尝试从 JS 插件获取 + jsPlugin, jsErr := h.app.GetJSPluginStore().GetJSPlugin(pluginName) + if jsErr != nil { + // 两者都找不到,返回空 schema + schemaFields = []dto.ConfigField{} + } else { + // 使用 JS 插件的 config 作为动态 schema + for key := range jsPlugin.Config { + schemaFields = append(schemaFields, dto.ConfigField{ + Key: key, + Label: key, + Type: "string", + }) + } + } + } else { + schemaFields = convertRouterConfigFields(schema) + } + + // 查找客户端的插件配置 + var config map[string]string + for _, p := range client.Plugins { + if p.Name == pluginName { + config = p.Config + break + } + } + if config == nil { + config = make(map[string]string) + } + + 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 + for i, p := range client.Plugins { + if p.Name == pluginName { + client.Plugins[i].Config = req.Config + found = true + break + } + } + + if !found { + NotFound(c, "plugin not installed on client") + return + } + + // 保存到数据库 + if err := h.app.GetClientStore().UpdateClient(client); err != nil { + InternalError(c, err.Error()) + return + } + + // 如果客户端在线,同步配置 + online, _, _ := h.app.GetServer().GetClientStatus(clientID) + if online { + if err := h.app.GetServer().SyncPluginConfigToClient(clientID, pluginName, req.Config); err != nil { + // 配置已保存,但同步失败,返回警告 + PartialSuccess(c, gin.H{"status": "partial"}, "config saved but sync failed: "+err.Error()) + return + } + } + + Success(c, gin.H{"status": "ok"}) +} + +// 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/response.go b/internal/server/router/handler/response.go new file mode 100644 index 0000000..0b88ccf --- /dev/null +++ b/internal/server/router/handler/response.go @@ -0,0 +1,159 @@ +package handler + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" +) + +// Response 统一 API 响应结构 +type Response struct { + Code int `json:"code"` + Data interface{} `json:"data,omitempty"` + Message string `json:"message,omitempty"` +} + +// 业务错误码定义 +const ( + CodeSuccess = 0 + CodeBadRequest = 400 + CodeUnauthorized = 401 + CodeForbidden = 403 + CodeNotFound = 404 + CodeConflict = 409 + CodeInternalError = 500 + CodeBadGateway = 502 + + CodeClientNotOnline = 1001 + CodePluginNotFound = 1002 + CodeInvalidClientID = 1003 + CodePluginDisabled = 1004 + CodeConfigSyncFailed = 1005 +) + +// Success 成功响应 +func Success(c *gin.Context, data interface{}) { + c.JSON(http.StatusOK, Response{ + Code: CodeSuccess, + Data: data, + }) +} + +// SuccessWithMessage 成功响应带消息 +func SuccessWithMessage(c *gin.Context, data interface{}, message string) { + c.JSON(http.StatusOK, Response{ + Code: CodeSuccess, + Data: data, + Message: message, + }) +} + +// Error 错误响应 +func Error(c *gin.Context, httpCode int, bizCode int, message string) { + c.JSON(httpCode, Response{ + Code: bizCode, + Message: message, + }) +} + +// BadRequest 400 错误 +func BadRequest(c *gin.Context, message string) { + Error(c, http.StatusBadRequest, CodeBadRequest, message) +} + +// Unauthorized 401 错误 +func Unauthorized(c *gin.Context, message string) { + Error(c, http.StatusUnauthorized, CodeUnauthorized, message) +} + +// NotFound 404 错误 +func NotFound(c *gin.Context, message string) { + Error(c, http.StatusNotFound, CodeNotFound, message) +} + +// Conflict 409 错误 +func Conflict(c *gin.Context, message string) { + Error(c, http.StatusConflict, CodeConflict, message) +} + +// InternalError 500 错误 +func InternalError(c *gin.Context, message string) { + Error(c, http.StatusInternalServerError, CodeInternalError, message) +} + +// BadGateway 502 错误 +func BadGateway(c *gin.Context, message string) { + Error(c, http.StatusBadGateway, CodeBadGateway, message) +} + +// ClientNotOnline 客户端不在线错误 +func ClientNotOnline(c *gin.Context) { + Error(c, http.StatusBadRequest, CodeClientNotOnline, "client not online") +} + +// PartialSuccess 部分成功响应 +func PartialSuccess(c *gin.Context, data interface{}, message string) { + c.JSON(http.StatusOK, Response{ + Code: CodeConfigSyncFailed, + Data: data, + Message: message, + }) +} + +// BindJSON 绑定 JSON 并自动处理验证错误 +func BindJSON(c *gin.Context, obj interface{}) bool { + if err := c.ShouldBindJSON(obj); err != nil { + handleValidationError(c, err) + return false + } + return true +} + +// BindQuery 绑定查询参数并自动处理验证错误 +func BindQuery(c *gin.Context, obj interface{}) bool { + if err := c.ShouldBindQuery(obj); err != nil { + handleValidationError(c, err) + return false + } + return true +} + +// handleValidationError 处理验证错误 +func handleValidationError(c *gin.Context, err error) { + var ve validator.ValidationErrors + if errors.As(err, &ve) { + errs := make([]map[string]string, len(ve)) + for i, fe := range ve { + errs[i] = map[string]string{ + "field": fe.Field(), + "message": getValidationMessage(fe), + } + } + c.JSON(http.StatusBadRequest, Response{ + Code: CodeBadRequest, + Message: "validation failed", + Data: errs, + }) + return + } + BadRequest(c, err.Error()) +} + +func getValidationMessage(fe validator.FieldError) string { + switch fe.Tag() { + case "required": + return "this field is required" + case "min": + return "value is too short or too small" + case "max": + return "value is too long or too large" + case "url": + return "invalid URL format" + case "oneof": + return "value must be one of: " + fe.Param() + default: + return "validation failed on " + fe.Tag() + } +} diff --git a/internal/server/router/handler/status.go b/internal/server/router/handler/status.go new file mode 100644 index 0000000..a1a7e6d --- /dev/null +++ b/internal/server/router/handler/status.go @@ -0,0 +1,51 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + // removed router import + "github.com/gotunnel/internal/server/router/dto" +) + +// StatusHandler 状态处理器 +type StatusHandler struct { + app AppInterface +} + +// NewStatusHandler 创建状态处理器 +func NewStatusHandler(app AppInterface) *StatusHandler { + return &StatusHandler{app: app} +} + +// GetStatus 获取服务器状态 +// @Summary 获取服务器状态 +// @Description 返回服务器运行状态和客户端数量 +// @Tags 状态 +// @Produce json +// @Security Bearer +// @Success 200 {object} Response{data=dto.StatusResponse} +// @Router /api/status [get] +func (h *StatusHandler) GetStatus(c *gin.Context) { + clients, _ := h.app.GetClientStore().GetAllClients() + + status := dto.StatusResponse{ + Server: dto.ServerStatus{ + BindAddr: h.app.GetServer().GetBindAddr(), + BindPort: h.app.GetServer().GetBindPort(), + }, + ClientCount: len(clients), + } + + Success(c, status) +} + +// GetVersion 获取版本信息 +// @Summary 获取版本信息 +// @Description 返回服务器版本信息 +// @Tags 状态 +// @Produce json +// @Security Bearer +// @Success 200 {object} Response{data=dto.VersionInfo} +// @Router /api/update/version [get] +func (h *StatusHandler) GetVersion(c *gin.Context) { + Success(c, getVersionInfo()) +} diff --git a/internal/server/router/handler/store.go b/internal/server/router/handler/store.go new file mode 100644 index 0000000..c8b96b4 --- /dev/null +++ b/internal/server/router/handler/store.go @@ -0,0 +1,169 @@ +package handler + +import ( + "io" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/gotunnel/internal/server/db" + // removed router import + "github.com/gotunnel/internal/server/router/dto" +) + +// 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.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 + } + + // 检查客户端是否在线 + online, _, _ := h.app.GetServer().GetClientStatus(req.ClientID) + if !online { + 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 + } + + // 安装到客户端 + installReq := JSPluginInstallRequest{ + PluginName: req.PluginName, + Source: string(source), + Signature: string(signature), + RuleName: req.PluginName, + AutoStart: true, + } + + if err := h.app.GetServer().InstallJSPluginToClient(req.ClientID, installReq); err != nil { + InternalError(c, "Failed to install plugin: "+err.Error()) + return + } + + // 将插件信息保存到数据库 + dbClient, err := h.app.GetClientStore().GetClient(req.ClientID) + if err == nil { + // 检查插件是否已存在 + exists := false + for i, p := range dbClient.Plugins { + if p.Name == req.PluginName { + dbClient.Plugins[i].Enabled = true + exists = true + break + } + } + if !exists { + dbClient.Plugins = append(dbClient.Plugins, db.ClientPlugin{ + Name: req.PluginName, + Version: "1.0.0", + Enabled: true, + }) + } + h.app.GetClientStore().UpdateClient(dbClient) + } + + Success(c, gin.H{ + "status": "ok", + "plugin": req.PluginName, + "client": req.ClientID, + }) +} diff --git a/internal/server/router/handler/update.go b/internal/server/router/handler/update.go new file mode 100644 index 0000000..8191dbb --- /dev/null +++ b/internal/server/router/handler/update.go @@ -0,0 +1,132 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + // removed router import + "github.com/gotunnel/internal/server/router/dto" + "github.com/gotunnel/pkg/version" +) + +// UpdateHandler 更新处理器 +type UpdateHandler struct { + app AppInterface +} + +// NewUpdateHandler 创建更新处理器 +func NewUpdateHandler(app AppInterface) *UpdateHandler { + return &UpdateHandler{app: app} +} + +// CheckServer 检查服务端更新 +// @Summary 检查服务端更新 +// @Description 检查是否有新的服务端版本可用 +// @Tags 更新 +// @Produce json +// @Security Bearer +// @Success 200 {object} Response{data=dto.CheckUpdateResponse} +// @Router /api/update/check/server [get] +func (h *UpdateHandler) CheckServer(c *gin.Context) { + updateInfo, err := checkUpdateForComponent("server") + if err != nil { + InternalError(c, err.Error()) + return + } + + Success(c, updateInfo) +} + +// CheckClient 检查客户端更新 +// @Summary 检查客户端更新 +// @Description 检查是否有新的客户端版本可用 +// @Tags 更新 +// @Produce json +// @Security Bearer +// @Param os query string false "操作系统" Enums(linux, darwin, windows) +// @Param arch query string false "架构" Enums(amd64, arm64, 386, arm) +// @Success 200 {object} Response{data=dto.CheckUpdateResponse} +// @Router /api/update/check/client [get] +func (h *UpdateHandler) CheckClient(c *gin.Context) { + var query dto.CheckClientUpdateQuery + if !BindQuery(c, &query) { + return + } + + updateInfo, err := checkClientUpdateForPlatform(query.OS, query.Arch) + if err != nil { + InternalError(c, err.Error()) + return + } + + Success(c, updateInfo) +} + +// ApplyServer 应用服务端更新 +// @Summary 应用服务端更新 +// @Description 下载并应用服务端更新,服务器将自动重启 +// @Tags 更新 +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body dto.ApplyServerUpdateRequest true "更新请求" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Router /api/update/apply/server [post] +func (h *UpdateHandler) ApplyServer(c *gin.Context) { + var req dto.ApplyServerUpdateRequest + if !BindJSON(c, &req) { + return + } + + // 异步执行更新 + go func() { + if err := performSelfUpdate(req.DownloadURL, req.Restart); err != nil { + println("[Update] Server update failed:", err.Error()) + } + }() + + Success(c, gin.H{ + "success": true, + "message": "Update started, server will restart shortly", + }) +} + +// ApplyClient 应用客户端更新 +// @Summary 推送客户端更新 +// @Description 向指定客户端推送更新命令 +// @Tags 更新 +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body dto.ApplyClientUpdateRequest true "更新请求" +// @Success 200 {object} Response +// @Failure 400 {object} Response +// @Router /api/update/apply/client [post] +func (h *UpdateHandler) ApplyClient(c *gin.Context) { + var req dto.ApplyClientUpdateRequest + if !BindJSON(c, &req) { + return + } + + // 发送更新命令到客户端 + if err := h.app.GetServer().SendUpdateToClient(req.ClientID, req.DownloadURL); err != nil { + InternalError(c, err.Error()) + return + } + + Success(c, gin.H{ + "success": true, + "message": "Update command sent to client", + }) +} + +// getVersionInfo 获取版本信息 +func getVersionInfo() dto.VersionInfo { + info := version.GetInfo() + return dto.VersionInfo{ + Version: info.Version, + GitCommit: info.GitCommit, + BuildTime: info.BuildTime, + GoVersion: info.GoVersion, + Platform: info.OS + "/" + info.Arch, + } +} diff --git a/internal/server/router/middleware/cors.go b/internal/server/router/middleware/cors.go new file mode 100644 index 0000000..e7de9a0 --- /dev/null +++ b/internal/server/router/middleware/cors.go @@ -0,0 +1,28 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +// CORS 跨域中间件 +func CORS() gin.HandlerFunc { + return func(c *gin.Context) { + origin := c.GetHeader("Origin") + if origin == "" { + origin = "*" + } + + c.Header("Access-Control-Allow-Origin", origin) + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization, X-Request-ID") + c.Header("Access-Control-Allow-Credentials", "true") + c.Header("Access-Control-Max-Age", "86400") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + } +} diff --git a/internal/server/router/middleware/jwt.go b/internal/server/router/middleware/jwt.go new file mode 100644 index 0000000..50ee0f3 --- /dev/null +++ b/internal/server/router/middleware/jwt.go @@ -0,0 +1,49 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/gotunnel/pkg/auth" +) + +// JWTAuth JWT 认证中间件 +func JWTAuth(jwtAuth *auth.JWTAuth) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": 401, + "message": "missing authorization header", + }) + c.Abort() + return + } + + if !strings.HasPrefix(authHeader, "Bearer ") { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": 401, + "message": "invalid authorization format", + }) + c.Abort() + return + } + + token := strings.TrimPrefix(authHeader, "Bearer ") + claims, err := jwtAuth.ValidateToken(token) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{ + "code": 401, + "message": "invalid or expired token", + }) + c.Abort() + return + } + + // 将用户信息存入上下文 + c.Set("username", claims.Username) + c.Set("claims", claims) + c.Next() + } +} diff --git a/internal/server/router/middleware/logger.go b/internal/server/router/middleware/logger.go new file mode 100644 index 0000000..9326640 --- /dev/null +++ b/internal/server/router/middleware/logger.go @@ -0,0 +1,31 @@ +package middleware + +import ( + "log" + "time" + + "github.com/gin-gonic/gin" +) + +// Logger 请求日志中间件 +func Logger() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + query := c.Request.URL.RawQuery + method := c.Request.Method + + c.Next() + + latency := time.Since(start) + status := c.Writer.Status() + clientIP := c.ClientIP() + + if query != "" { + path = path + "?" + query + } + + log.Printf("[API] %s %s %d %v %s", + method, path, status, latency, clientIP) + } +} diff --git a/internal/server/router/middleware/recovery.go b/internal/server/router/middleware/recovery.go new file mode 100644 index 0000000..876eab0 --- /dev/null +++ b/internal/server/router/middleware/recovery.go @@ -0,0 +1,26 @@ +package middleware + +import ( + "log" + "net/http" + "runtime/debug" + + "github.com/gin-gonic/gin" +) + +// Recovery 自定义恢复中间件(返回统一格式) +func Recovery() gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + log.Printf("[PANIC] %v\n%s", err, debug.Stack()) + c.JSON(http.StatusInternalServerError, gin.H{ + "code": 500, + "message": "internal server error", + }) + c.Abort() + } + }() + c.Next() + } +} diff --git a/internal/server/router/response.go b/internal/server/router/response.go new file mode 100644 index 0000000..b8a4d21 --- /dev/null +++ b/internal/server/router/response.go @@ -0,0 +1,116 @@ +package router + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// Response 统一 API 响应结构 +type Response struct { + Code int `json:"code"` // 业务状态码: 0=成功, 非0=错误 + Data interface{} `json:"data,omitempty"` // 响应数据 + Message string `json:"message,omitempty"` // 提示信息 +} + +// 业务错误码定义 +const ( + CodeSuccess = 0 // 成功 + CodeBadRequest = 400 // 请求参数错误 + CodeUnauthorized = 401 // 未授权 + CodeForbidden = 403 // 禁止访问 + CodeNotFound = 404 // 资源不存在 + CodeConflict = 409 // 资源冲突 + CodeInternalError = 500 // 服务器内部错误 + CodeBadGateway = 502 // 网关错误 + + // 业务错误码 (1000+) + CodeClientNotOnline = 1001 // 客户端不在线 + CodePluginNotFound = 1002 // 插件不存在 + CodeInvalidClientID = 1003 // 无效的客户端ID + CodePluginDisabled = 1004 // 插件已禁用 + CodeConfigSyncFailed = 1005 // 配置同步失败 +) + +// Success 成功响应 +func Success(c *gin.Context, data interface{}) { + c.JSON(http.StatusOK, Response{ + Code: CodeSuccess, + Data: data, + }) +} + +// SuccessWithMessage 成功响应带消息 +func SuccessWithMessage(c *gin.Context, data interface{}, message string) { + c.JSON(http.StatusOK, Response{ + Code: CodeSuccess, + Data: data, + Message: message, + }) +} + +// Error 错误响应 +func Error(c *gin.Context, httpCode int, bizCode int, message string) { + c.JSON(httpCode, Response{ + Code: bizCode, + Message: message, + }) +} + +// ErrorWithData 错误响应带数据 +func ErrorWithData(c *gin.Context, httpCode int, bizCode int, message string, data interface{}) { + c.JSON(httpCode, Response{ + Code: bizCode, + Message: message, + Data: data, + }) +} + +// BadRequest 400 错误 +func BadRequest(c *gin.Context, message string) { + Error(c, http.StatusBadRequest, CodeBadRequest, message) +} + +// Unauthorized 401 错误 +func Unauthorized(c *gin.Context, message string) { + Error(c, http.StatusUnauthorized, CodeUnauthorized, message) +} + +// Forbidden 403 错误 +func Forbidden(c *gin.Context, message string) { + Error(c, http.StatusForbidden, CodeForbidden, message) +} + +// NotFound 404 错误 +func NotFound(c *gin.Context, message string) { + Error(c, http.StatusNotFound, CodeNotFound, message) +} + +// Conflict 409 错误 +func Conflict(c *gin.Context, message string) { + Error(c, http.StatusConflict, CodeConflict, message) +} + +// InternalError 500 错误 +func InternalError(c *gin.Context, message string) { + Error(c, http.StatusInternalServerError, CodeInternalError, message) +} + +// BadGateway 502 错误 +func BadGateway(c *gin.Context, message string) { + Error(c, http.StatusBadGateway, CodeBadGateway, message) +} + +// ClientNotOnline 客户端不在线错误 +func ClientNotOnline(c *gin.Context) { + Error(c, http.StatusBadRequest, CodeClientNotOnline, "client not online") +} + +// PartialSuccess 部分成功响应 +func PartialSuccess(c *gin.Context, data interface{}, message string) { + c.JSON(http.StatusOK, Response{ + Code: CodeConfigSyncFailed, + Data: data, + Message: message, + }) +} diff --git a/internal/server/router/router.go b/internal/server/router/router.go index 0a09da3..2c2731b 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -1,123 +1,162 @@ package router import ( - "crypto/subtle" + "io" + "io/fs" "net/http" - "strings" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + + "github.com/gotunnel/internal/server/router/handler" + "github.com/gotunnel/internal/server/router/middleware" "github.com/gotunnel/pkg/auth" ) -// Router 路由管理器 -type Router struct { - mux *http.ServeMux +// GinRouter Gin 路由管理器 +type GinRouter struct { + Engine *gin.Engine } -// AuthConfig 认证配置 -type AuthConfig struct { - Username string - Password string -} - -// New 创建路由管理器 -func New() *Router { - return &Router{ - mux: http.NewServeMux(), - } -} - -// Handle 注册路由处理器 -func (r *Router) Handle(pattern string, handler http.Handler) { - r.mux.Handle(pattern, handler) -} - -// HandleFunc 注册路由处理函数 -func (r *Router) HandleFunc(pattern string, handler http.HandlerFunc) { - r.mux.HandleFunc(pattern, handler) -} - -// Group 创建路由组 -func (r *Router) Group(prefix string) *RouteGroup { - return &RouteGroup{ - router: r, - prefix: prefix, - } -} - -// RouteGroup 路由组 -type RouteGroup struct { - router *Router - prefix string -} - -// HandleFunc 注册路由组处理函数 -func (g *RouteGroup) HandleFunc(pattern string, handler http.HandlerFunc) { - g.router.mux.HandleFunc(g.prefix+pattern, handler) +// New 创建 Gin 路由管理器 +func New() *GinRouter { + gin.SetMode(gin.ReleaseMode) + engine := gin.New() + return &GinRouter{Engine: engine} } // Handler 返回 http.Handler -func (r *Router) Handler() http.Handler { - return r.mux +func (r *GinRouter) Handler() http.Handler { + return r.Engine } -// BasicAuthMiddleware 基础认证中间件 -func BasicAuthMiddleware(auth *AuthConfig, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if auth == nil || (auth.Username == "" && auth.Password == "") { - next.ServeHTTP(w, r) - return - } +// SetupRoutes 配置所有路由 +func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth, username, password string) { + engine := r.Engine - user, pass, ok := r.BasicAuth() - if !ok { - w.Header().Set("WWW-Authenticate", `Basic realm="GoTunnel"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } + // 全局中间件 + engine.Use(middleware.Recovery()) + engine.Use(middleware.Logger()) + engine.Use(middleware.CORS()) - userMatch := subtle.ConstantTimeCompare([]byte(user), []byte(auth.Username)) == 1 - passMatch := subtle.ConstantTimeCompare([]byte(pass), []byte(auth.Password)) == 1 + // Swagger 文档 + engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) - if !userMatch || !passMatch { - w.Header().Set("WWW-Authenticate", `Basic realm="GoTunnel"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } + // 认证路由 (无需 JWT) + authHandler := handler.NewAuthHandler(username, password, jwtAuth) + engine.POST("/api/auth/login", authHandler.Login) + engine.GET("/api/auth/check", authHandler.Check) - next.ServeHTTP(w, r) - }) + // API 路由 (需要 JWT) + api := engine.Group("/api") + api.Use(middleware.JWTAuth(jwtAuth)) + { + // 状态 + statusHandler := handler.NewStatusHandler(app) + api.GET("/status", statusHandler.GetStatus) + api.GET("/update/version", statusHandler.GetVersion) + + // 客户端管理 + clientHandler := handler.NewClientHandler(app) + api.GET("/clients", clientHandler.List) + api.POST("/clients", clientHandler.Create) + api.GET("/client/:id", clientHandler.Get) + api.PUT("/client/:id", clientHandler.Update) + api.DELETE("/client/:id", clientHandler.Delete) + 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/:pluginName/:action", clientHandler.PluginAction) + + // 配置管理 + configHandler := handler.NewConfigHandler(app) + api.GET("/config", configHandler.Get) + 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) + api.GET("/update/check/client", updateHandler.CheckClient) + api.POST("/update/apply/server", updateHandler.ApplyServer) + api.POST("/update/apply/client", updateHandler.ApplyClient) + } } -// JWTMiddleware JWT 认证中间件 -func JWTMiddleware(jwtAuth *auth.JWTAuth, skipPaths []string, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // 只对 /api/ 路径进行认证 - if !strings.HasPrefix(r.URL.Path, "/api/") { - next.ServeHTTP(w, r) - return - } - - // 检查是否跳过认证 - for _, path := range skipPaths { - if strings.HasPrefix(r.URL.Path, path) { - next.ServeHTTP(w, r) - return - } - } - - // 从 Header 获取 token - authHeader := r.Header.Get("Authorization") - if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { - http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) - return - } - - token := strings.TrimPrefix(authHeader, "Bearer ") - if _, err := jwtAuth.ValidateToken(token); err != nil { - http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized) - return - } - - next.ServeHTTP(w, r) - }) +// SetupStaticFiles 配置静态文件处理 +func (r *GinRouter) SetupStaticFiles(staticFS fs.FS) { + // 使用 NoRoute 处理 SPA 路由 + r.Engine.NoRoute(gin.WrapH(&spaHandler{fs: http.FS(staticFS)})) } + +// spaHandler SPA 路由处理器 +type spaHandler struct { + fs http.FileSystem +} + +func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + f, err := h.fs.Open(path) + if err != nil { + f, err = h.fs.Open("index.html") + if err != nil { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + } + defer f.Close() + + stat, err := f.Stat() + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + if stat.IsDir() { + f.Close() + f, err = h.fs.Open(path + "/index.html") + if err != nil { + f, _ = h.fs.Open("index.html") + } + stat, _ = f.Stat() + } + + if seeker, ok := f.(io.ReadSeeker); ok { + http.ServeContent(w, r, path, stat.ModTime(), seeker) + } +} + +// 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 +) diff --git a/internal/server/router/update_helpers.go b/internal/server/router/update_helpers.go new file mode 100644 index 0000000..7775303 --- /dev/null +++ b/internal/server/router/update_helpers.go @@ -0,0 +1,256 @@ +package router + +import ( + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "time" + + "github.com/gotunnel/pkg/version" +) + +// UpdateInfo 更新信息 +type UpdateInfo struct { + Available bool `json:"available"` + Current string `json:"current"` + Latest string `json:"latest"` + ReleaseNote string `json:"release_note"` + DownloadURL string `json:"download_url"` + AssetName string `json:"asset_name"` + AssetSize int64 `json:"asset_size"` +} + +// checkUpdateForComponent 检查组件更新 +func checkUpdateForComponent(component string) (*UpdateInfo, error) { + release, err := version.GetLatestRelease() + if err != nil { + return nil, fmt.Errorf("get latest release: %w", err) + } + + latestVersion := release.TagName + currentVersion := version.Version + available := version.CompareVersions(currentVersion, latestVersion) < 0 + + // 查找对应平台的资产 + assetName := getAssetNameForPlatform(component, runtime.GOOS, runtime.GOARCH) + var downloadURL string + var assetSize int64 + + for _, asset := range release.Assets { + if asset.Name == assetName { + downloadURL = asset.BrowserDownloadURL + assetSize = asset.Size + break + } + } + + return &UpdateInfo{ + Available: available, + Current: currentVersion, + Latest: latestVersion, + ReleaseNote: release.Body, + DownloadURL: downloadURL, + AssetName: assetName, + AssetSize: assetSize, + }, nil +} + +// checkClientUpdateForPlatform 检查指定平台的客户端更新 +func checkClientUpdateForPlatform(osName, arch string) (*UpdateInfo, error) { + if osName == "" { + osName = runtime.GOOS + } + if arch == "" { + arch = runtime.GOARCH + } + + release, err := version.GetLatestRelease() + if err != nil { + return nil, fmt.Errorf("get latest release: %w", err) + } + + latestVersion := release.TagName + + // 查找对应平台的资产 + assetName := getAssetNameForPlatform("client", osName, arch) + var downloadURL string + var assetSize int64 + + for _, asset := range release.Assets { + if asset.Name == assetName { + downloadURL = asset.BrowserDownloadURL + assetSize = asset.Size + break + } + } + + return &UpdateInfo{ + Available: true, // 客户端版本由服务端判断 + Current: "", // 客户端版本需要单独获取 + Latest: latestVersion, + ReleaseNote: release.Body, + DownloadURL: downloadURL, + AssetName: assetName, + AssetSize: assetSize, + }, nil +} + +// getAssetNameForPlatform 获取指定平台的资产名称 +func getAssetNameForPlatform(component, osName, arch string) string { + ext := "" + if osName == "windows" { + ext = ".exe" + } + return fmt.Sprintf("%s_%s_%s%s", component, osName, arch, ext) +} + +// performSelfUpdate 执行自更新 +func performSelfUpdate(downloadURL string, restart bool) error { + // 下载新版本 + tempDir := os.TempDir() + tempFile := filepath.Join(tempDir, "gotunnel_update_"+time.Now().Format("20060102150405")) + + if runtime.GOOS == "windows" { + tempFile += ".exe" + } + + if err := downloadFile(downloadURL, tempFile); err != nil { + return fmt.Errorf("download update: %w", err) + } + + // 设置执行权限 + if runtime.GOOS != "windows" { + if err := os.Chmod(tempFile, 0755); err != nil { + os.Remove(tempFile) + return fmt.Errorf("chmod: %w", err) + } + } + + // 获取当前可执行文件路径 + currentPath, err := os.Executable() + if err != nil { + os.Remove(tempFile) + return fmt.Errorf("get executable: %w", err) + } + currentPath, _ = filepath.EvalSymlinks(currentPath) + + // Windows 需要特殊处理(运行中的文件无法直接替换) + if runtime.GOOS == "windows" { + return performWindowsUpdate(tempFile, currentPath, restart) + } + + // Linux/Mac: 直接替换 + backupPath := currentPath + ".bak" + + // 备份当前文件 + if err := os.Rename(currentPath, backupPath); err != nil { + os.Remove(tempFile) + return fmt.Errorf("backup current: %w", err) + } + + // 移动新文件 + if err := os.Rename(tempFile, currentPath); err != nil { + os.Rename(backupPath, currentPath) + return fmt.Errorf("replace binary: %w", err) + } + + // 删除备份 + os.Remove(backupPath) + + if restart { + // 重启进程 + restartProcess(currentPath) + } + + return nil +} + +// performWindowsUpdate Windows 平台更新 +func performWindowsUpdate(newFile, currentPath string, restart bool) error { + // 创建批处理脚本来替换文件并重启 + batchScript := fmt.Sprintf(`@echo off +ping 127.0.0.1 -n 2 > nul +del "%s" +move "%s" "%s" +`, currentPath, newFile, currentPath) + + if restart { + batchScript += fmt.Sprintf(`start "" "%s" +`, currentPath) + } + + batchScript += "del \"%~f0\"\n" + + batchPath := filepath.Join(os.TempDir(), "gotunnel_update.bat") + if err := os.WriteFile(batchPath, []byte(batchScript), 0755); err != nil { + return fmt.Errorf("write batch: %w", err) + } + + cmd := exec.Command("cmd", "/C", "start", "/MIN", batchPath) + if err := cmd.Start(); err != nil { + return fmt.Errorf("start batch: %w", err) + } + + // 退出当前进程 + os.Exit(0) + return nil +} + +// restartProcess 重启进程 +func restartProcess(path string) { + cmd := exec.Command(path, os.Args[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Start() + os.Exit(0) +} + +// downloadFile 下载文件 +func downloadFile(url, dest string) error { + client := &http.Client{Timeout: 10 * time.Minute} + resp, err := client.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("download failed: %s", resp.Status) + } + + out, err := os.Create(dest) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} + +// VersionInfo 版本信息 +type VersionInfo struct { + Version string `json:"version"` + GitCommit string `json:"git_commit"` + BuildTime string `json:"build_time"` + GoVersion string `json:"go_version"` + OS string `json:"os"` + Arch string `json:"arch"` +} + +// getVersionInfo 获取版本信息 +func getVersionInfo() VersionInfo { + info := version.GetInfo() + return VersionInfo{ + Version: info.Version, + GitCommit: info.GitCommit, + BuildTime: info.BuildTime, + GoVersion: info.GoVersion, + OS: info.OS, + Arch: info.Arch, + } +} diff --git a/internal/server/tunnel/server.go b/internal/server/tunnel/server.go index d159b17..56bfd46 100644 --- a/internal/server/tunnel/server.go +++ b/internal/server/tunnel/server.go @@ -1306,3 +1306,32 @@ func (s *Server) UpdateClientPluginConfig(clientID, pluginName, ruleName string, return nil } + +// SendUpdateToClient 发送更新命令到客户端 +func (s *Server) SendUpdateToClient(clientID, downloadURL 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) + } + + // 发送更新消息 + stream, err := cs.Session.Open() + if err != nil { + return err + } + defer stream.Close() + + req := protocol.UpdateDownloadRequest{ + DownloadURL: downloadURL, + } + msg, _ := protocol.NewMessage(protocol.MsgTypeUpdateDownload, req) + if err := protocol.WriteMessage(stream, msg); err != nil { + return err + } + + log.Printf("[Server] Update command sent to client %s: %s", clientID, downloadURL) + return nil +} diff --git a/pkg/protocol/message.go b/pkg/protocol/message.go index 2560ecf..26d177c 100644 --- a/pkg/protocol/message.go +++ b/pkg/protocol/message.go @@ -52,6 +52,14 @@ const ( // 客户端控制消息 MsgTypeClientRestart uint8 = 60 // 重启客户端 MsgTypePluginConfigUpdate uint8 = 61 // 更新插件配置 + + // 更新相关消息 + MsgTypeUpdateCheck uint8 = 70 // 检查更新请求 + MsgTypeUpdateInfo uint8 = 71 // 更新信息响应 + MsgTypeUpdateDownload uint8 = 72 // 下载更新请求 + MsgTypeUpdateApply uint8 = 73 // 应用更新请求 + MsgTypeUpdateProgress uint8 = 74 // 更新进度 + MsgTypeUpdateResult uint8 = 75 // 更新结果 ) // Message 基础消息结构 @@ -257,6 +265,46 @@ type PluginConfigUpdateResponse struct { Error string `json:"error,omitempty"` } +// UpdateCheckRequest 更新检查请求 +type UpdateCheckRequest struct { + Component string `json:"component"` // "server" 或 "client" +} + +// UpdateInfoResponse 更新信息响应 +type UpdateInfoResponse struct { + Available bool `json:"available"` + Current string `json:"current"` + Latest string `json:"latest"` + ReleaseNote string `json:"release_note"` + DownloadURL string `json:"download_url"` + AssetName string `json:"asset_name"` + AssetSize int64 `json:"asset_size"` +} + +// UpdateDownloadRequest 下载更新请求 +type UpdateDownloadRequest struct { + DownloadURL string `json:"download_url"` +} + +// UpdateApplyRequest 应用更新请求 +type UpdateApplyRequest struct { + Restart bool `json:"restart"` // 是否自动重启 +} + +// UpdateProgressResponse 更新进度响应 +type UpdateProgressResponse struct { + Downloaded int64 `json:"downloaded"` + Total int64 `json:"total"` + Percent int `json:"percent"` + Status string `json:"status"` // downloading, applying, completed, failed +} + +// UpdateResultResponse 更新结果响应 +type UpdateResultResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + // WriteMessage 写入消息到 writer func WriteMessage(w io.Writer, msg *Message) error { header := make([]byte, HeaderSize) diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 0000000..c636d88 --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,207 @@ +package version + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "runtime" + "strconv" + "strings" + "time" +) + +// 版本信息 +const Version = "1.0.0" + +// 仓库信息 +const ( + RepoURL = "https://git.92coco.cn:8443/flik/GoTunnel" + APIBaseURL = "https://git.92coco.cn:8443/api/v1" + RepoOwner = "flik" + RepoName = "GoTunnel" +) + +// Info 版本详细信息 +type Info struct { + Version string `json:"version"` + GitCommit string `json:"git_commit"` + BuildTime string `json:"build_time"` + GoVersion string `json:"go_version"` + OS string `json:"os"` + Arch string `json:"arch"` +} + +// GetInfo 获取版本信息 +func GetInfo() Info { + return Info{ + Version: Version, + GitCommit: "", + BuildTime: "", + GoVersion: runtime.Version(), + OS: runtime.GOOS, + Arch: runtime.GOARCH, + } +} + +// ReleaseInfo Release 信息 +type ReleaseInfo struct { + TagName string `json:"tag_name"` + Name string `json:"name"` + Body string `json:"body"` + PublishedAt string `json:"published_at"` + Assets []ReleaseAsset `json:"assets"` +} + +// ReleaseAsset Release 资产 +type ReleaseAsset struct { + Name string `json:"name"` + Size int64 `json:"size"` + BrowserDownloadURL string `json:"browser_download_url"` +} + +// UpdateInfo 更新信息 +type UpdateInfo struct { + Latest string `json:"latest"` + ReleaseNote string `json:"release_note"` + DownloadURL string `json:"download_url"` + AssetName string `json:"asset_name"` + AssetSize int64 `json:"asset_size"` +} + +// GetLatestRelease 获取最新 Release +func GetLatestRelease() (*ReleaseInfo, error) { + url := fmt.Sprintf("%s/repos/%s/%s/releases/latest", APIBaseURL, RepoOwner, RepoName) + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("API error: %s - %s", resp.Status, string(body)) + } + + var release ReleaseInfo + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return nil, err + } + + return &release, nil +} + +// CheckUpdate 检查更新(返回最新版本信息) +func CheckUpdate(component string) (*UpdateInfo, error) { + release, err := GetLatestRelease() + if err != nil { + return nil, fmt.Errorf("get latest release: %w", err) + } + + // 查找对应平台的资产 + assetName := getAssetName(component) + var downloadURL string + var assetSize int64 + + for _, asset := range release.Assets { + if asset.Name == assetName { + downloadURL = asset.BrowserDownloadURL + assetSize = asset.Size + break + } + } + + return &UpdateInfo{ + Latest: release.TagName, + ReleaseNote: release.Body, + DownloadURL: downloadURL, + AssetName: assetName, + AssetSize: assetSize, + }, nil +} + +// CheckUpdateForPlatform 检查指定平台的更新 +func CheckUpdateForPlatform(component, osName, arch string) (*UpdateInfo, error) { + release, err := GetLatestRelease() + if err != nil { + return nil, fmt.Errorf("get latest release: %w", err) + } + + // 查找对应平台的资产 + assetName := getAssetNameForPlatform(component, osName, arch) + var downloadURL string + var assetSize int64 + + for _, asset := range release.Assets { + if asset.Name == assetName { + downloadURL = asset.BrowserDownloadURL + assetSize = asset.Size + break + } + } + + return &UpdateInfo{ + Latest: release.TagName, + ReleaseNote: release.Body, + DownloadURL: downloadURL, + AssetName: assetName, + AssetSize: assetSize, + }, nil +} + +// getAssetName 获取当前平台的资产文件名 +func getAssetName(component string) string { + return getAssetNameForPlatform(component, runtime.GOOS, runtime.GOARCH) +} + +// getAssetNameForPlatform 获取指定平台的资产文件名 +func getAssetNameForPlatform(component, osName, arch string) string { + ext := "" + if osName == "windows" { + ext = ".exe" + } + return fmt.Sprintf("%s_%s_%s%s", component, osName, arch, ext) +} + +// CompareVersions 比较版本号 +// 返回: -1 (v1 < v2), 0 (v1 == v2), 1 (v1 > v2) +func CompareVersions(v1, v2 string) int { + parts1 := parseVersionParts(v1) + parts2 := parseVersionParts(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 parseVersionParts(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/web/package.json b/web/package.json index 66ffbac..062ea91 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { - "name": "webui", + "name": "GoTunnel", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 6112410..cd02924 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -35,6 +35,8 @@ export const stopClientPlugin = (clientId: string, pluginName: string, ruleName: post(`/client/${clientId}/plugin/${pluginName}/stop`, { rule_name: ruleName }) export const restartClientPlugin = (clientId: string, pluginName: string, ruleName: string) => post(`/client/${clientId}/plugin/${pluginName}/restart`, { rule_name: ruleName }) +export const deleteClientPlugin = (clientId: string, pluginName: string) => + del(`/client/${clientId}/plugin/${pluginName}/delete`) export const updateClientPluginConfigWithRestart = (clientId: string, pluginName: string, ruleName: string, config: Record, restart: boolean) => post(`/client/${clientId}/plugin/${pluginName}/config`, { rule_name: ruleName, config, restart }) @@ -66,3 +68,37 @@ export const updateJSPluginConfig = (name: string, config: Record post(`/js-plugin/${name}/${enabled ? 'enable' : 'disable'}`) + +// 更新管理 +export interface UpdateInfo { + available: boolean + current: string + latest: string + release_note: string + download_url: string + asset_name: string + asset_size: number +} + +export interface VersionInfo { + version: string + git_commit: string + build_time: string + go_version: string + os: string + arch: string +} + +export const getVersionInfo = () => get('/update/version') +export const checkServerUpdate = () => get('/update/check/server') +export const checkClientUpdate = (os?: string, arch?: string) => { + const params = new URLSearchParams() + if (os) params.append('os', os) + if (arch) params.append('arch', arch) + const query = params.toString() + return get(`/update/check/client${query ? '?' + query : ''}`) +} +export const applyServerUpdate = (downloadUrl: string, restart: boolean = true) => + post('/update/apply/server', { download_url: downloadUrl, restart }) +export const applyClientUpdate = (clientId: string, downloadUrl: string) => + post('/update/apply/client', { client_id: clientId, download_url: downloadUrl }) diff --git a/web/src/config/axios/index.ts b/web/src/config/axios/index.ts index d43cffd..13f3dfe 100644 --- a/web/src/config/axios/index.ts +++ b/web/src/config/axios/index.ts @@ -1,4 +1,4 @@ -import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios' +import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse, type AxiosError } from 'axios' // Token 管理 const TOKEN_KEY = 'gotunnel_token' @@ -7,6 +7,30 @@ export const getToken = (): string | null => localStorage.getItem(TOKEN_KEY) export const setToken = (token: string): void => localStorage.setItem(TOKEN_KEY, token) export const removeToken = (): void => localStorage.removeItem(TOKEN_KEY) +// 统一 API 响应结构 +export interface ApiResponse { + code: number + data?: T + message?: string +} + +// 业务错误码 +export const ErrorCodes = { + Success: 0, + BadRequest: 400, + Unauthorized: 401, + Forbidden: 403, + NotFound: 404, + Conflict: 409, + InternalError: 500, + BadGateway: 502, + ClientNotOnline: 1001, + PluginNotFound: 1002, + InvalidClientID: 1003, + PluginDisabled: 1004, + ConfigSyncFailed: 1005, +} + // 创建 axios 实例 const instance: AxiosInstance = axios.create({ baseURL: '/api', @@ -30,10 +54,39 @@ instance.interceptors.request.use( } ) -// 响应拦截器 +// 响应拦截器 - 处理统一响应格式 instance.interceptors.response.use( - (response) => response, - (error) => { + (response: AxiosResponse) => { + const apiResponse = response.data + + // 检查业务错误码 + if (apiResponse.code !== undefined && apiResponse.code !== ErrorCodes.Success) { + // 处理认证错误 + if (apiResponse.code === ErrorCodes.Unauthorized && !isRedirecting) { + isRedirecting = true + removeToken() + setTimeout(() => { + window.location.replace('/login') + isRedirecting = false + }, 0) + } + + // 返回包含业务错误信息的 rejected promise + return Promise.reject({ + code: apiResponse.code, + message: apiResponse.message || 'Unknown error', + response: response + }) + } + + // 成功时返回 data 字段 + return { + ...response, + data: apiResponse.data !== undefined ? apiResponse.data : apiResponse + } as AxiosResponse + }, + (error: AxiosError) => { + // 处理 HTTP 错误 if (error.response?.status === 401 && !isRedirecting) { isRedirecting = true removeToken() @@ -42,6 +95,17 @@ instance.interceptors.response.use( isRedirecting = false }, 0) } + + // 尝试从响应中提取业务错误信息 + const apiResponse = error.response?.data + if (apiResponse?.message) { + return Promise.reject({ + code: apiResponse.code || error.response?.status, + message: apiResponse.message, + response: error.response + }) + } + return Promise.reject(error) } ) diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 462feee..93eda53 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -25,6 +25,11 @@ const router = createRouter({ name: 'plugins', component: () => import('../views/PluginsView.vue'), }, + { + path: '/update', + name: 'update', + component: () => import('../views/UpdateView.vue'), + }, ], }) diff --git a/web/src/views/ClientView.vue b/web/src/views/ClientView.vue index 20f2cc3..2211578 100644 --- a/web/src/views/ClientView.vue +++ b/web/src/views/ClientView.vue @@ -14,7 +14,7 @@ import { import { getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, restartClient, getClientPluginConfig, updateClientPluginConfig, - getStorePlugins, installStorePlugin, getRuleSchemas, restartClientPlugin, stopClientPlugin + getStorePlugins, installStorePlugin, getRuleSchemas, restartClientPlugin, stopClientPlugin, deleteClientPlugin } from '../api' import type { ProxyRule, ClientPlugin, ConfigField, StorePluginInfo, RuleSchemasMap } from '../types' @@ -351,6 +351,25 @@ const savePluginConfig = async () => { message.error(e.response?.data || '保存失败') } } + +// 删除客户端插件 +const handleDeletePlugin = (plugin: ClientPlugin) => { + dialog.warning({ + title: '确认删除', + content: `确定要删除插件 ${plugin.name} 吗?`, + positiveText: '删除', + negativeText: '取消', + onPositiveClick: async () => { + try { + await deleteClientPlugin(clientId, plugin.name) + message.success(`已删除 ${plugin.name}`) + await loadClient() + } catch (e: any) { + message.error(e.response?.data || '删除失败') + } + } + }) +}