updete
Some checks failed
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m4s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m29s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m27s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 50s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m42s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Has been cancelled
Some checks failed
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m4s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m29s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m27s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 50s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m42s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Has been cancelled
This commit is contained in:
100
internal/server/router/handler/auth.go
Normal file
100
internal/server/router/handler/auth.go
Normal file
@@ -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,
|
||||
})
|
||||
}
|
||||
393
internal/server/router/handler/client.go
Normal file
393
internal/server/router/handler/client.go
Normal file
@@ -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
|
||||
}
|
||||
131
internal/server/router/handler/config.go
Normal file
131
internal/server/router/handler/config.go
Normal file
@@ -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"})
|
||||
}
|
||||
230
internal/server/router/handler/helpers.go
Normal file
230
internal/server/router/handler/helpers.go
Normal file
@@ -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
|
||||
}
|
||||
83
internal/server/router/handler/interfaces.go
Normal file
83
internal/server/router/handler/interfaces.go
Normal file
@@ -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"`
|
||||
}
|
||||
212
internal/server/router/handler/js_plugin.go
Normal file
212
internal/server/router/handler/js_plugin.go
Normal file
@@ -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,
|
||||
})
|
||||
}
|
||||
285
internal/server/router/handler/plugin.go
Normal file
285
internal/server/router/handler/plugin.go
Normal file
@@ -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
|
||||
}
|
||||
159
internal/server/router/handler/response.go
Normal file
159
internal/server/router/handler/response.go
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
51
internal/server/router/handler/status.go
Normal file
51
internal/server/router/handler/status.go
Normal file
@@ -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())
|
||||
}
|
||||
169
internal/server/router/handler/store.go
Normal file
169
internal/server/router/handler/store.go
Normal file
@@ -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,
|
||||
})
|
||||
}
|
||||
132
internal/server/router/handler/update.go
Normal file
132
internal/server/router/handler/update.go
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user