feat: remove unused plugin version comparison and types, refactor proxy server to support authentication

- Deleted version comparison logic from `pkg/plugin/sign/version.go`.
- Removed unused types and constants from `pkg/plugin/types.go`.
- Updated `pkg/protocol/message.go` to remove plugin-related message types.
- Enhanced `pkg/proxy/http.go` and `pkg/proxy/socks5.go` to include username/password authentication for HTTP and SOCKS5 proxies.
- Modified `pkg/proxy/server.go` to pass authentication parameters to server constructors.
- Added new API endpoint to generate installation commands with a token for clients.
- Created database functions to manage installation tokens in `internal/server/db/install_token.go`.
- Implemented the installation command generation logic in `internal/server/router/handler/install.go`.
- Updated web frontend to support installation command generation and display in `web/src/views/ClientsView.vue`.
This commit is contained in:
2026-03-17 23:16:30 +08:00
parent dcfd2f4466
commit 5a03d9e1f1
42 changed files with 638 additions and 6161 deletions

View File

@@ -1,7 +1,6 @@
package dto
import (
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/pkg/protocol"
)
@@ -17,7 +16,6 @@ type CreateClientRequest struct {
type UpdateClientRequest struct {
Nickname string `json:"nickname" binding:"max=128" example:"My Client"`
Rules []protocol.ProxyRule `json:"rules"`
Plugins []db.ClientPlugin `json:"plugins"`
}
// ClientResponse 客户端详情响应
@@ -26,7 +24,6 @@ type ClientResponse struct {
ID string `json:"id" example:"client-001"`
Nickname string `json:"nickname,omitempty" example:"My Client"`
Rules []protocol.ProxyRule `json:"rules"`
Plugins []db.ClientPlugin `json:"plugins,omitempty"`
Online bool `json:"online" example:"true"`
LastPing string `json:"last_ping,omitempty" example:"2025-01-02T10:30:00Z"`
RemoteAddr string `json:"remote_addr,omitempty" example:"192.168.1.100:54321"`
@@ -47,17 +44,3 @@ type ClientListItem struct {
OS string `json:"os,omitempty" example:"linux"`
Arch string `json:"arch,omitempty" example:"amd64"`
}
// InstallPluginsRequest 安装插件到客户端请求
// @Description 安装插件到指定客户端
type InstallPluginsRequest struct {
Plugins []string `json:"plugins" binding:"required,min=1,dive,required" example:"socks5,http-proxy"`
}
// ClientPluginActionRequest 客户端插件操作请求
// @Description 对客户端插件执行操作
type ClientPluginActionRequest struct {
RuleName string `json:"rule_name"`
Config map[string]string `json:"config,omitempty"`
Restart bool `json:"restart"`
}

View File

@@ -1,119 +0,0 @@
package dto
// PluginConfigRequest 更新插件配置请求
// @Description 更新客户端插件配置
type PluginConfigRequest struct {
Config map[string]string `json:"config" binding:"required"`
}
// PluginConfigResponse 插件配置响应
// @Description 插件配置详情
type PluginConfigResponse struct {
PluginName string `json:"plugin_name"`
Schema []ConfigField `json:"schema"`
Config map[string]string `json:"config"`
}
// ConfigField 配置字段定义
// @Description 配置表单字段
type ConfigField struct {
Key string `json:"key"`
Label string `json:"label"`
Type string `json:"type"`
Default string `json:"default,omitempty"`
Required bool `json:"required,omitempty"`
Options []string `json:"options,omitempty"`
Description string `json:"description,omitempty"`
}
// RuleSchema 规则表单模式
// @Description 代理规则的配置模式
type RuleSchema struct {
NeedsLocalAddr bool `json:"needs_local_addr"`
ExtraFields []ConfigField `json:"extra_fields,omitempty"`
}
// PluginInfo 插件信息
// @Description 服务端插件信息
type PluginInfo struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Description string `json:"description"`
Source string `json:"source"`
Icon string `json:"icon,omitempty"`
Enabled bool `json:"enabled"`
RuleSchema *RuleSchema `json:"rule_schema,omitempty"`
}
// JSPluginCreateRequest 创建 JS 插件请求
// @Description 创建新的 JS 插件
type JSPluginCreateRequest struct {
Name string `json:"name" binding:"required,min=1,max=64"`
Source string `json:"source" binding:"required"`
Signature string `json:"signature"`
Description string `json:"description" binding:"max=500"`
Author string `json:"author" binding:"max=64"`
Config map[string]string `json:"config"`
AutoStart bool `json:"auto_start"`
}
// JSPluginUpdateRequest 更新 JS 插件请求
// @Description 更新 JS 插件
type JSPluginUpdateRequest struct {
Source string `json:"source"`
Signature string `json:"signature"`
Description string `json:"description" binding:"max=500"`
Author string `json:"author" binding:"max=64"`
Config map[string]string `json:"config"`
AutoStart bool `json:"auto_start"`
Enabled bool `json:"enabled"`
}
// JSPluginInstallRequest JS 插件安装请求
// @Description 安装 JS 插件到客户端
type JSPluginInstallRequest struct {
PluginName string `json:"plugin_name" binding:"required"`
Source string `json:"source" binding:"required"`
Signature string `json:"signature"`
RuleName string `json:"rule_name"`
RemotePort int `json:"remote_port"`
Config map[string]string `json:"config"`
AutoStart bool `json:"auto_start"`
}
// StorePluginInfo 扩展商店插件信息
// @Description 插件商店中的插件信息
type StorePluginInfo struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Description string `json:"description"`
Author string `json:"author"`
Icon string `json:"icon,omitempty"`
DownloadURL string `json:"download_url,omitempty"`
SignatureURL string `json:"signature_url,omitempty"`
ConfigSchema []ConfigField `json:"config_schema,omitempty"`
}
// StoreInstallRequest 从商店安装插件请求
// @Description 从插件商店安装插件到客户端
type StoreInstallRequest struct {
PluginName string `json:"plugin_name" binding:"required"`
Version string `json:"version"`
DownloadURL string `json:"download_url" binding:"required,url"`
SignatureURL string `json:"signature_url" binding:"required,url"`
ClientID string `json:"client_id" binding:"required"`
RemotePort int `json:"remote_port"`
ConfigSchema []ConfigField `json:"config_schema,omitempty"`
// HTTP Basic Auth 配置
AuthEnabled bool `json:"auth_enabled,omitempty"`
AuthUsername string `json:"auth_username,omitempty"`
AuthPassword string `json:"auth_password,omitempty"`
}
// JSPluginPushRequest 推送 JS 插件到客户端请求
// @Description 推送 JS 插件到指定客户端
type JSPluginPushRequest struct {
RemotePort int `json:"remote_port"`
}

View File

@@ -6,7 +6,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/internal/server/router/dto"
"github.com/gotunnel/pkg/protocol"
)
// ClientHandler 客户端处理器
@@ -117,34 +116,6 @@ func (h *ClientHandler) Get(c *gin.Context) {
online, lastPing, remoteAddr, clientName, clientOS, clientArch, clientVersion := h.app.GetServer().GetClientStatus(clientID)
// 复制插件列表
plugins := make([]db.ClientPlugin, len(client.Plugins))
copy(plugins, client.Plugins)
// 如果客户端在线,获取实时插件运行状态
if online {
if statusList, err := h.app.GetServer().GetClientPluginStatus(clientID); err == nil {
// 创建运行中插件的映射
runningPlugins := make(map[string]bool)
for _, s := range statusList {
runningPlugins[s.PluginName] = s.Running
}
// 更新插件状态
for i := range plugins {
if running, ok := runningPlugins[plugins[i].Name]; ok {
plugins[i].Running = running
} else {
plugins[i].Running = false
}
}
}
} else {
// 客户端离线时,所有插件都标记为未运行
for i := range plugins {
plugins[i].Running = false
}
}
// 如果客户端在线且有名称,优先使用在线名称
nickname := client.Nickname
if online && clientName != "" && nickname == "" {
@@ -155,7 +126,6 @@ func (h *ClientHandler) Get(c *gin.Context) {
ID: client.ID,
Nickname: nickname,
Rules: client.Rules,
Plugins: plugins,
Online: online,
LastPing: lastPing,
RemoteAddr: remoteAddr,
@@ -196,9 +166,6 @@ func (h *ClientHandler) Update(c *gin.Context) {
client.Nickname = req.Nickname
client.Rules = req.Rules
if req.Plugins != nil {
client.Plugins = req.Plugins
}
if err := h.app.GetClientStore().UpdateClient(client); err != nil {
InternalError(c, err.Error())
@@ -301,159 +268,12 @@ func (h *ClientHandler) Restart(c *gin.Context) {
SuccessWithMessage(c, gin.H{"status": "ok"}, "client restart initiated")
}
// InstallPlugins 安装插件到客户端
// @Summary 安装插件
// @Description 将指定插件安装到客户端
// @Tags 客户端
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "客户端ID"
// @Param request body dto.InstallPluginsRequest true "插件列表"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/client/{id}/install-plugins [post]
func (h *ClientHandler) InstallPlugins(c *gin.Context) {
clientID := c.Param("id")
if !h.app.GetServer().IsClientOnline(clientID) {
ClientNotOnline(c)
return
}
var req dto.InstallPluginsRequest
if !BindJSON(c, &req) {
return
}
if err := h.app.GetServer().InstallPluginsToClient(clientID, req.Plugins); err != nil {
InternalError(c, err.Error())
return
}
Success(c, gin.H{"status": "ok"})
}
// PluginAction 客户端插件操作
// @Summary 插件操作
// @Description 对客户端插件执行操作(start/stop/restart/config/delete)
// @Tags 客户端
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "客户端ID"
// @Param pluginID path string true "插件实例ID"
// @Param action path string true "操作类型" Enums(start, stop, restart, config, delete)
// @Param request body dto.ClientPluginActionRequest false "操作参数"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/client/{id}/plugin/{pluginID}/{action} [post]
func (h *ClientHandler) PluginAction(c *gin.Context) {
clientID := c.Param("id")
pluginID := c.Param("pluginID")
action := c.Param("action")
var req dto.ClientPluginActionRequest
c.ShouldBindJSON(&req) // 忽略错误,使用默认值
// 通过 pluginID 查找插件信息
client, err := h.app.GetClientStore().GetClient(clientID)
if err != nil {
NotFound(c, "client not found")
return
}
var pluginName string
for _, p := range client.Plugins {
if p.ID == pluginID {
pluginName = p.Name
break
}
}
if pluginName == "" {
NotFound(c, "plugin not found")
return
}
if req.RuleName == "" {
req.RuleName = pluginName
}
switch action {
case "start":
err = h.app.GetServer().StartClientPlugin(clientID, pluginID, pluginName, req.RuleName)
case "stop":
err = h.app.GetServer().StopClientPlugin(clientID, pluginID, pluginName, req.RuleName)
case "restart":
err = h.app.GetServer().RestartClientPlugin(clientID, pluginID, pluginName, req.RuleName)
case "config":
if req.Config == nil {
BadRequest(c, "config required")
return
}
err = h.app.GetServer().UpdateClientPluginConfig(clientID, pluginID, pluginName, req.RuleName, req.Config, req.Restart)
case "delete":
err = h.deleteClientPlugin(clientID, pluginID)
default:
BadRequest(c, "unknown action: "+action)
return
}
if err != nil {
InternalError(c, err.Error())
return
}
Success(c, gin.H{
"status": "ok",
"action": action,
"plugin_id": pluginID,
"plugin": pluginName,
})
}
func (h *ClientHandler) deleteClientPlugin(clientID, pluginID string) error {
client, err := h.app.GetClientStore().GetClient(clientID)
if err != nil {
return fmt.Errorf("client not found")
}
var newPlugins []db.ClientPlugin
var pluginName string
var pluginPort int
found := false
for _, p := range client.Plugins {
if p.ID == pluginID {
found = true
pluginName = p.Name
pluginPort = p.RemotePort
continue
}
newPlugins = append(newPlugins, p)
}
if !found {
return fmt.Errorf("plugin %s not found", pluginID)
}
// 删除插件管理的代理规则
var newRules []protocol.ProxyRule
for _, r := range client.Rules {
if r.PluginManaged && r.Name == pluginName {
continue // 跳过此插件的规则
}
newRules = append(newRules, r)
}
// 停止端口监听器
if pluginPort > 0 {
h.app.GetServer().StopPluginRule(clientID, pluginPort)
}
client.Plugins = newPlugins
client.Rules = newRules
return h.app.GetClientStore().UpdateClient(client)
}
// GetSystemStats 获取客户端系统状态
func (h *ClientHandler) GetSystemStats(c *gin.Context) {

View File

@@ -47,9 +47,6 @@ func (h *ConfigHandler) Get(c *gin.Context) {
Username: cfg.Server.Web.Username,
Password: "****",
},
PluginStore: dto.PluginStoreConfigInfo{
URL: cfg.Server.PluginStore.URL,
},
}
Success(c, resp)
@@ -103,11 +100,6 @@ func (h *ConfigHandler) Update(c *gin.Context) {
cfg.Server.Web.Password = req.Web.Password
}
// 更新 PluginStore 配置
if req.PluginStore != nil {
cfg.Server.PluginStore.URL = req.PluginStore.URL
}
if err := h.app.SaveConfig(); err != nil {
InternalError(c, err.Error())
return

View File

@@ -0,0 +1,108 @@
package handler
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/gotunnel/internal/server/db"
)
// InstallHandler 安装处理器
type InstallHandler struct {
app AppInterface
}
// NewInstallHandler 创建安装处理器
func NewInstallHandler(app AppInterface) *InstallHandler {
return &InstallHandler{app: app}
}
// GenerateInstallCommandRequest 生成安装命令请求
type GenerateInstallCommandRequest struct {
ClientID string `json:"client_id" binding:"required"`
}
// InstallCommandResponse 安装命令响应
type InstallCommandResponse struct {
Token string `json:"token"`
Commands map[string]string `json:"commands"`
ExpiresAt int64 `json:"expires_at"`
ServerAddr string `json:"server_addr"`
}
// GenerateInstallCommand 生成安装命令
// @Summary 生成客户端安装命令
// @Tags install
// @Accept json
// @Produce json
// @Param body body GenerateInstallCommandRequest true "客户端ID"
// @Success 200 {object} InstallCommandResponse
// @Router /api/install/generate [post]
func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) {
var req GenerateInstallCommandRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 生成随机token
tokenBytes := make([]byte, 32)
if _, err := rand.Read(tokenBytes); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"})
return
}
token := hex.EncodeToString(tokenBytes)
// 保存到数据库
now := time.Now().Unix()
installToken := &db.InstallToken{
Token: token,
ClientID: req.ClientID,
CreatedAt: now,
Used: false,
}
store, ok := h.app.GetClientStore().(db.InstallTokenStore)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "存储不支持安装token"})
return
}
if err := store.CreateInstallToken(installToken); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存token失败"})
return
}
// 获取服务器地址
serverAddr := fmt.Sprintf("%s:%d", h.app.GetConfig().Server.BindAddr, h.app.GetServer().GetBindPort())
if h.app.GetConfig().Server.BindAddr == "" || h.app.GetConfig().Server.BindAddr == "0.0.0.0" {
serverAddr = fmt.Sprintf("your-server-ip:%d", h.app.GetServer().GetBindPort())
}
// 生成安装命令
expiresAt := now + 3600 // 1小时过期
tlsFlag := ""
if h.app.GetConfig().Server.TLSDisabled {
tlsFlag = " -no-tls"
}
commands := map[string]string{
"linux": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s -id %s%s",
serverAddr, token, req.ClientID, tlsFlag),
"macos": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s -id %s%s",
serverAddr, token, req.ClientID, tlsFlag),
"windows": fmt.Sprintf("powershell -c \"irm https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.ps1 | iex; Install-GoTunnel -Server '%s' -Token '%s' -ClientID '%s'%s\"",
serverAddr, token, req.ClientID, tlsFlag),
}
c.JSON(http.StatusOK, InstallCommandResponse{
Token: token,
Commands: commands,
ExpiresAt: expiresAt,
ServerAddr: serverAddr,
})
}

View File

@@ -13,7 +13,6 @@ type AppInterface interface {
GetConfig() *config.ServerConfig
GetConfigPath() string
SaveConfig() error
GetJSPluginStore() db.JSPluginStore
GetTrafficStore() db.TrafficStore
}
@@ -35,32 +34,13 @@ type ServerInterface interface {
GetBindPort() int
PushConfigToClient(clientID string) error
DisconnectClient(clientID string) error
GetPluginList() []PluginInfo
EnablePlugin(name string) error
DisablePlugin(name string) error
InstallPluginsToClient(clientID string, plugins []string) error
GetPluginConfigSchema(name string) ([]ConfigField, error)
SyncPluginConfigToClient(clientID string, pluginName string, config map[string]string) error
InstallJSPluginToClient(clientID string, req JSPluginInstallRequest) error
RestartClient(clientID string) error
StartClientPlugin(clientID, pluginID, pluginName, ruleName string) error
StopClientPlugin(clientID, pluginID, pluginName, ruleName string) error
RestartClientPlugin(clientID, pluginID, pluginName, ruleName string) error
UpdateClientPluginConfig(clientID, pluginID, pluginName, ruleName string, config map[string]string, restart bool) error
SendUpdateToClient(clientID, downloadURL string) error
// 日志流
StartClientLogStream(clientID, sessionID string, lines int, follow bool, level string) (<-chan protocol.LogEntry, error)
StopClientLogStream(sessionID string)
// 插件状态查询
GetClientPluginStatus(clientID string) ([]protocol.PluginStatusEntry, error)
// 插件规则管理
StartPluginRule(clientID string, rule protocol.ProxyRule) error
StopPluginRule(clientID string, remotePort int) error
// 端口检查
IsPortAvailable(port int, excludeClientID string) bool
// 插件 API 代理
ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error)
// 系统状态
// 系统状态
GetClientSystemStats(clientID string) (*protocol.SystemStatsResponse, error)
// 截图
@@ -68,44 +48,3 @@ type ServerInterface interface {
// Shell 执行
ExecuteClientShell(clientID, command string, timeout int) (*protocol.ShellExecuteResponse, error)
}
// ConfigField 配置字段
type ConfigField struct {
Key string `json:"key"`
Label string `json:"label"`
Type string `json:"type"`
Default string `json:"default,omitempty"`
Required bool `json:"required,omitempty"`
Options []string `json:"options,omitempty"`
Description string `json:"description,omitempty"`
}
// RuleSchema 规则表单模式
type RuleSchema struct {
NeedsLocalAddr bool `json:"needs_local_addr"`
ExtraFields []ConfigField `json:"extra_fields,omitempty"`
}
// PluginInfo 插件信息
type PluginInfo struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Description string `json:"description"`
Source string `json:"source"`
Icon string `json:"icon,omitempty"`
Enabled bool `json:"enabled"`
RuleSchema *RuleSchema `json:"rule_schema,omitempty"`
}
// JSPluginInstallRequest JS 插件安装请求
type JSPluginInstallRequest struct {
PluginID string `json:"plugin_id"`
PluginName string `json:"plugin_name"`
Source string `json:"source"`
Signature string `json:"signature"`
RuleName string `json:"rule_name"`
RemotePort int `json:"remote_port"`
Config map[string]string `json:"config"`
AutoStart bool `json:"auto_start"`
}

View File

@@ -1,219 +0,0 @@
package handler
import (
"github.com/gin-gonic/gin"
"github.com/gotunnel/internal/server/db"
// removed router import
"github.com/gotunnel/internal/server/router/dto"
)
// JSPluginHandler JS 插件处理器
type JSPluginHandler struct {
app AppInterface
}
// NewJSPluginHandler 创建 JS 插件处理器
func NewJSPluginHandler(app AppInterface) *JSPluginHandler {
return &JSPluginHandler{app: app}
}
// List 获取 JS 插件列表
// @Summary 获取所有 JS 插件
// @Description 返回所有注册的 JS 插件
// @Tags JS插件
// @Produce json
// @Security Bearer
// @Success 200 {object} Response{data=[]db.JSPlugin}
// @Router /api/js-plugins [get]
func (h *JSPluginHandler) List(c *gin.Context) {
plugins, err := h.app.GetJSPluginStore().GetAllJSPlugins()
if err != nil {
InternalError(c, err.Error())
return
}
if plugins == nil {
plugins = []db.JSPlugin{}
}
Success(c, plugins)
}
// Create 创建 JS 插件
// @Summary 创建 JS 插件
// @Description 创建新的 JS 插件
// @Tags JS插件
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body dto.JSPluginCreateRequest true "插件信息"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/js-plugins [post]
func (h *JSPluginHandler) Create(c *gin.Context) {
var req dto.JSPluginCreateRequest
if !BindJSON(c, &req) {
return
}
plugin := &db.JSPlugin{
Name: req.Name,
Source: req.Source,
Signature: req.Signature,
Description: req.Description,
Author: req.Author,
Config: req.Config,
AutoStart: req.AutoStart,
Enabled: true,
}
if err := h.app.GetJSPluginStore().SaveJSPlugin(plugin); err != nil {
InternalError(c, err.Error())
return
}
Success(c, gin.H{"status": "ok"})
}
// Get 获取单个 JS 插件
// @Summary 获取 JS 插件详情
// @Description 获取指定 JS 插件的详细信息
// @Tags JS插件
// @Produce json
// @Security Bearer
// @Param name path string true "插件名称"
// @Success 200 {object} Response{data=db.JSPlugin}
// @Failure 404 {object} Response
// @Router /api/js-plugin/{name} [get]
func (h *JSPluginHandler) Get(c *gin.Context) {
name := c.Param("name")
plugin, err := h.app.GetJSPluginStore().GetJSPlugin(name)
if err != nil {
NotFound(c, "plugin not found")
return
}
Success(c, plugin)
}
// Update 更新 JS 插件
// @Summary 更新 JS 插件
// @Description 更新指定 JS 插件的信息
// @Tags JS插件
// @Accept json
// @Produce json
// @Security Bearer
// @Param name path string true "插件名称"
// @Param request body dto.JSPluginUpdateRequest true "更新内容"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/js-plugin/{name} [put]
func (h *JSPluginHandler) Update(c *gin.Context) {
name := c.Param("name")
var req dto.JSPluginUpdateRequest
if !BindJSON(c, &req) {
return
}
plugin := &db.JSPlugin{
Name: name,
Source: req.Source,
Signature: req.Signature,
Description: req.Description,
Author: req.Author,
Config: req.Config,
AutoStart: req.AutoStart,
Enabled: req.Enabled,
}
if err := h.app.GetJSPluginStore().SaveJSPlugin(plugin); err != nil {
InternalError(c, err.Error())
return
}
Success(c, gin.H{"status": "ok"})
}
// Delete 删除 JS 插件
// @Summary 删除 JS 插件
// @Description 删除指定的 JS 插件
// @Tags JS插件
// @Produce json
// @Security Bearer
// @Param name path string true "插件名称"
// @Success 200 {object} Response
// @Router /api/js-plugin/{name} [delete]
func (h *JSPluginHandler) Delete(c *gin.Context) {
name := c.Param("name")
if err := h.app.GetJSPluginStore().DeleteJSPlugin(name); err != nil {
InternalError(c, err.Error())
return
}
Success(c, gin.H{"status": "ok"})
}
// PushToClient 推送 JS 插件到客户端
// @Summary 推送插件到客户端
// @Description 将 JS 插件推送到指定客户端
// @Tags JS插件
// @Accept json
// @Produce json
// @Security Bearer
// @Param name path string true "插件名称"
// @Param clientID path string true "客户端ID"
// @Param request body dto.JSPluginPushRequest false "推送配置"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Failure 404 {object} Response
// @Router /api/js-plugin/{name}/push/{clientID} [post]
func (h *JSPluginHandler) PushToClient(c *gin.Context) {
pluginName := c.Param("name")
clientID := c.Param("clientID")
// 解析请求体(可选)
var pushReq dto.JSPluginPushRequest
c.ShouldBindJSON(&pushReq) // 忽略错误,允许空请求体
// 检查客户端是否在线
if !h.app.GetServer().IsClientOnline(clientID) {
ClientNotOnline(c)
return
}
// 获取插件
plugin, err := h.app.GetJSPluginStore().GetJSPlugin(pluginName)
if err != nil {
NotFound(c, "plugin not found")
return
}
if !plugin.Enabled {
Error(c, 400, CodePluginDisabled, "plugin is disabled")
return
}
// 推送到客户端
req := JSPluginInstallRequest{
PluginName: plugin.Name,
Source: plugin.Source,
Signature: plugin.Signature,
RuleName: plugin.Name,
RemotePort: pushReq.RemotePort,
Config: plugin.Config,
AutoStart: plugin.AutoStart,
}
if err := h.app.GetServer().InstallJSPluginToClient(clientID, req); err != nil {
InternalError(c, err.Error())
return
}
Success(c, gin.H{
"status": "ok",
"plugin": pluginName,
"client": clientID,
"remote_port": pushReq.RemotePort,
})
}

View File

@@ -1,416 +0,0 @@
package handler
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/internal/server/router/dto"
"github.com/gotunnel/pkg/plugin"
)
// PluginHandler 插件处理器
type PluginHandler struct {
app AppInterface
}
// NewPluginHandler 创建插件处理器
func NewPluginHandler(app AppInterface) *PluginHandler {
return &PluginHandler{app: app}
}
// List 获取插件列表
// @Summary 获取所有插件
// @Description 返回服务端所有注册的插件
// @Tags 插件
// @Produce json
// @Security Bearer
// @Success 200 {object} Response{data=[]dto.PluginInfo}
// @Router /api/plugins [get]
func (h *PluginHandler) List(c *gin.Context) {
plugins := h.app.GetServer().GetPluginList()
result := make([]dto.PluginInfo, len(plugins))
for i, p := range plugins {
result[i] = dto.PluginInfo{
Name: p.Name,
Version: p.Version,
Type: p.Type,
Description: p.Description,
Source: p.Source,
Icon: p.Icon,
Enabled: p.Enabled,
}
if p.RuleSchema != nil {
result[i].RuleSchema = &dto.RuleSchema{
NeedsLocalAddr: p.RuleSchema.NeedsLocalAddr,
ExtraFields: convertRouterConfigFields(p.RuleSchema.ExtraFields),
}
}
}
Success(c, result)
}
// Enable 启用插件
// @Summary 启用插件
// @Description 启用指定插件
// @Tags 插件
// @Produce json
// @Security Bearer
// @Param name path string true "插件名称"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/plugin/{name}/enable [post]
func (h *PluginHandler) Enable(c *gin.Context) {
name := c.Param("name")
if err := h.app.GetServer().EnablePlugin(name); err != nil {
BadRequest(c, err.Error())
return
}
Success(c, gin.H{"status": "ok"})
}
// Disable 禁用插件
// @Summary 禁用插件
// @Description 禁用指定插件
// @Tags 插件
// @Produce json
// @Security Bearer
// @Param name path string true "插件名称"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/plugin/{name}/disable [post]
func (h *PluginHandler) Disable(c *gin.Context) {
name := c.Param("name")
if err := h.app.GetServer().DisablePlugin(name); err != nil {
BadRequest(c, err.Error())
return
}
Success(c, gin.H{"status": "ok"})
}
// GetRuleSchemas 获取规则配置模式
// @Summary 获取规则模式
// @Description 返回所有协议类型的配置模式
// @Tags 插件
// @Produce json
// @Security Bearer
// @Success 200 {object} Response{data=map[string]dto.RuleSchema}
// @Router /api/rule-schemas [get]
func (h *PluginHandler) GetRuleSchemas(c *gin.Context) {
// 获取内置协议模式
schemas := make(map[string]dto.RuleSchema)
for name, schema := range plugin.BuiltinRuleSchemas() {
schemas[name] = dto.RuleSchema{
NeedsLocalAddr: schema.NeedsLocalAddr,
ExtraFields: convertConfigFields(schema.ExtraFields),
}
}
// 添加已注册插件的模式
plugins := h.app.GetServer().GetPluginList()
for _, p := range plugins {
if p.RuleSchema != nil {
schemas[p.Name] = dto.RuleSchema{
NeedsLocalAddr: p.RuleSchema.NeedsLocalAddr,
ExtraFields: convertRouterConfigFields(p.RuleSchema.ExtraFields),
}
}
}
Success(c, schemas)
}
// GetClientConfig 获取客户端插件配置
// @Summary 获取客户端插件配置
// @Description 获取客户端上指定插件的配置
// @Tags 插件
// @Produce json
// @Security Bearer
// @Param clientID path string true "客户端ID"
// @Param pluginName path string true "插件名称"
// @Success 200 {object} Response{data=dto.PluginConfigResponse}
// @Failure 404 {object} Response
// @Router /api/client-plugin/{clientID}/{pluginName}/config [get]
func (h *PluginHandler) GetClientConfig(c *gin.Context) {
clientID := c.Param("clientID")
pluginName := c.Param("pluginName")
client, err := h.app.GetClientStore().GetClient(clientID)
if err != nil {
NotFound(c, "client not found")
return
}
// 查找客户端的插件
var clientPlugin *db.ClientPlugin
for i, p := range client.Plugins {
if p.Name == pluginName {
clientPlugin = &client.Plugins[i]
break
}
}
if clientPlugin == nil {
NotFound(c, "plugin not installed on client")
return
}
var schemaFields []dto.ConfigField
// 优先使用客户端插件保存的 ConfigSchema
if len(clientPlugin.ConfigSchema) > 0 {
for _, f := range clientPlugin.ConfigSchema {
schemaFields = append(schemaFields, dto.ConfigField{
Key: f.Key,
Label: f.Label,
Type: f.Type,
Default: f.Default,
Required: f.Required,
Options: f.Options,
Description: f.Description,
})
}
} else {
// 尝试从内置插件获取配置模式
schema, err := h.app.GetServer().GetPluginConfigSchema(pluginName)
if err != nil {
// 如果内置插件中找不到,尝试从 JS 插件获取
jsPlugin, jsErr := h.app.GetJSPluginStore().GetJSPlugin(pluginName)
if jsErr == nil {
// 使用 JS 插件的 config 作为动态 schema
for key := range jsPlugin.Config {
schemaFields = append(schemaFields, dto.ConfigField{
Key: key,
Label: key,
Type: "string",
})
}
}
} else {
schemaFields = convertRouterConfigFields(schema)
}
}
// 添加 remote_port 作为系统配置字段(始终显示)
schemaFields = append([]dto.ConfigField{{
Key: "remote_port",
Label: "远程端口",
Type: "number",
Description: "服务端监听端口,修改后需重启插件生效",
}}, schemaFields...)
// 添加 Auth 配置字段
schemaFields = append(schemaFields, dto.ConfigField{
Key: "auth_enabled",
Label: "启用认证",
Type: "bool",
Description: "启用 HTTP Basic Auth 保护",
}, dto.ConfigField{
Key: "auth_username",
Label: "认证用户名",
Type: "string",
Description: "HTTP Basic Auth 用户名",
}, dto.ConfigField{
Key: "auth_password",
Label: "认证密码",
Type: "password",
Description: "HTTP Basic Auth 密码",
})
// 构建配置值
config := clientPlugin.Config
if config == nil {
config = make(map[string]string)
}
// 将 remote_port 加入配置
if clientPlugin.RemotePort > 0 {
config["remote_port"] = fmt.Sprintf("%d", clientPlugin.RemotePort)
}
// 将 Auth 配置加入
if clientPlugin.AuthEnabled {
config["auth_enabled"] = "true"
} else {
config["auth_enabled"] = "false"
}
config["auth_username"] = clientPlugin.AuthUsername
config["auth_password"] = clientPlugin.AuthPassword
Success(c, dto.PluginConfigResponse{
PluginName: pluginName,
Schema: schemaFields,
Config: config,
})
}
// UpdateClientConfig 更新客户端插件配置
// @Summary 更新客户端插件配置
// @Description 更新客户端上指定插件的配置
// @Tags 插件
// @Accept json
// @Produce json
// @Security Bearer
// @Param clientID path string true "客户端ID"
// @Param pluginName path string true "插件名称"
// @Param request body dto.PluginConfigRequest true "配置内容"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Failure 404 {object} Response
// @Router /api/client-plugin/{clientID}/{pluginName}/config [put]
func (h *PluginHandler) UpdateClientConfig(c *gin.Context) {
clientID := c.Param("clientID")
pluginName := c.Param("pluginName")
var req dto.PluginConfigRequest
if !BindJSON(c, &req) {
return
}
client, err := h.app.GetClientStore().GetClient(clientID)
if err != nil {
NotFound(c, "client not found")
return
}
// 更新插件配置
found := false
portChanged := false
authChanged := false
var oldPort, newPort int
for i, p := range client.Plugins {
if p.Name == pluginName {
oldPort = client.Plugins[i].RemotePort
// 提取 remote_port 并单独处理
if portStr, ok := req.Config["remote_port"]; ok {
fmt.Sscanf(portStr, "%d", &newPort)
if newPort > 0 && newPort != oldPort {
// 检查新端口是否可用
if !h.app.GetServer().IsPortAvailable(newPort, clientID) {
BadRequest(c, fmt.Sprintf("port %d is already in use", newPort))
return
}
client.Plugins[i].RemotePort = newPort
portChanged = true
}
delete(req.Config, "remote_port") // 不保存到 Config map
}
// 提取 Auth 配置并单独处理
if authEnabledStr, ok := req.Config["auth_enabled"]; ok {
newAuthEnabled := authEnabledStr == "true"
if newAuthEnabled != client.Plugins[i].AuthEnabled {
client.Plugins[i].AuthEnabled = newAuthEnabled
authChanged = true
}
delete(req.Config, "auth_enabled")
}
if authUsername, ok := req.Config["auth_username"]; ok {
if authUsername != client.Plugins[i].AuthUsername {
client.Plugins[i].AuthUsername = authUsername
authChanged = true
}
delete(req.Config, "auth_username")
}
if authPassword, ok := req.Config["auth_password"]; ok {
if authPassword != client.Plugins[i].AuthPassword {
client.Plugins[i].AuthPassword = authPassword
authChanged = true
}
delete(req.Config, "auth_password")
}
client.Plugins[i].Config = req.Config
found = true
break
}
}
if !found {
NotFound(c, "plugin not installed on client")
return
}
// 如果端口变更,同步更新代理规则
if portChanged {
for i, r := range client.Rules {
if r.Name == pluginName && r.PluginManaged {
client.Rules[i].RemotePort = newPort
break
}
}
// 停止旧端口监听器
if oldPort > 0 {
h.app.GetServer().StopPluginRule(clientID, oldPort)
}
}
// 如果 Auth 配置变更,同步更新代理规则
if authChanged {
for i, p := range client.Plugins {
if p.Name == pluginName {
for j, r := range client.Rules {
if r.Name == pluginName && r.PluginManaged {
client.Rules[j].AuthEnabled = client.Plugins[i].AuthEnabled
client.Rules[j].AuthUsername = client.Plugins[i].AuthUsername
client.Rules[j].AuthPassword = client.Plugins[i].AuthPassword
break
}
}
break
}
}
}
// 保存到数据库
if err := h.app.GetClientStore().UpdateClient(client); err != nil {
InternalError(c, err.Error())
return
}
// 如果客户端在线,同步配置
if h.app.GetServer().IsClientOnline(clientID) {
if err := h.app.GetServer().SyncPluginConfigToClient(clientID, pluginName, req.Config); err != nil {
PartialSuccess(c, gin.H{"status": "partial", "port_changed": portChanged}, "config saved but sync failed: "+err.Error())
return
}
}
Success(c, gin.H{"status": "ok", "port_changed": portChanged})
}
// convertConfigFields 转换插件配置字段到 DTO
func convertConfigFields(fields []plugin.ConfigField) []dto.ConfigField {
result := make([]dto.ConfigField, len(fields))
for i, f := range fields {
result[i] = dto.ConfigField{
Key: f.Key,
Label: f.Label,
Type: string(f.Type),
Default: f.Default,
Required: f.Required,
Options: f.Options,
Description: f.Description,
}
}
return result
}
// convertRouterConfigFields 转换 ConfigField 到 dto.ConfigField
func convertRouterConfigFields(fields []ConfigField) []dto.ConfigField {
result := make([]dto.ConfigField, len(fields))
for i, f := range fields {
result[i] = dto.ConfigField{
Key: f.Key,
Label: f.Label,
Type: f.Type,
Default: f.Default,
Required: f.Required,
Options: f.Options,
Description: f.Description,
}
}
return result
}

View File

@@ -1,139 +0,0 @@
package handler
import (
"io"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gotunnel/pkg/protocol"
)
// PluginAPIHandler 插件 API 代理处理器
type PluginAPIHandler struct {
app AppInterface
}
// NewPluginAPIHandler 创建插件 API 代理处理器
func NewPluginAPIHandler(app AppInterface) *PluginAPIHandler {
return &PluginAPIHandler{app: app}
}
// ProxyRequest 代理请求到客户端插件
// @Summary 代理插件 API 请求
// @Description 将请求代理到客户端的 JS 插件处理
// @Tags 插件 API
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "客户端 ID"
// @Param pluginID path string true "插件实例 ID"
// @Param route path string true "插件路由"
// @Success 200 {object} object
// @Failure 404 {object} Response
// @Failure 502 {object} Response
// @Router /api/client/{id}/plugin-api/{pluginID}/{route} [get]
func (h *PluginAPIHandler) ProxyRequest(c *gin.Context) {
clientID := c.Param("id")
pluginID := c.Param("pluginID")
route := c.Param("route")
// 确保路由以 / 开头
if !strings.HasPrefix(route, "/") {
route = "/" + route
}
// 检查客户端是否在线
if !h.app.GetServer().IsClientOnline(clientID) {
ClientNotOnline(c)
return
}
// 读取请求体
var body string
if c.Request.Body != nil {
bodyBytes, _ := io.ReadAll(c.Request.Body)
body = string(bodyBytes)
}
// 构建请求头
headers := make(map[string]string)
for key, values := range c.Request.Header {
if len(values) > 0 {
headers[key] = values[0]
}
}
// 构建 API 请求
apiReq := protocol.PluginAPIRequest{
PluginID: pluginID,
Method: c.Request.Method,
Path: route,
Query: c.Request.URL.RawQuery,
Headers: headers,
Body: body,
}
// 发送请求到客户端
resp, err := h.app.GetServer().ProxyPluginAPIRequest(clientID, apiReq)
if err != nil {
BadGateway(c, "Plugin request failed: "+err.Error())
return
}
// 检查错误
if resp.Error != "" {
c.JSON(http.StatusBadGateway, gin.H{
"code": 502,
"message": resp.Error,
})
return
}
// 设置响应头
for key, value := range resp.Headers {
c.Header(key, value)
}
// 返回响应
c.String(resp.Status, resp.Body)
}
// ProxyPluginAPIRequest 接口方法声明 - 添加到 ServerInterface
type PluginAPIProxyInterface interface {
ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error)
}
// AuthConfig 认证配置
type AuthConfig struct {
Type string `json:"type"` // none, basic, token
Username string `json:"username"` // Basic Auth 用户名
Password string `json:"password"` // Basic Auth 密码
Token string `json:"token"` // Token 认证
}
// BasicAuthMiddleware 创建 Basic Auth 中间件
func BasicAuthMiddleware(username, password string) gin.HandlerFunc {
return func(c *gin.Context) {
user, pass, ok := c.Request.BasicAuth()
if !ok || user != username || pass != password {
c.Header("WWW-Authenticate", `Basic realm="Plugin"`)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Unauthorized",
})
return
}
c.Next()
}
}
// WithTimeout 带超时的请求处理
func WithTimeout(timeout time.Duration, handler gin.HandlerFunc) gin.HandlerFunc {
return func(c *gin.Context) {
// 设置请求超时
c.Request = c.Request.WithContext(c.Request.Context())
handler(c)
}
}

View File

@@ -1,292 +0,0 @@
package handler
import (
"fmt"
"io"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/internal/server/router/dto"
"github.com/gotunnel/pkg/protocol"
)
// StoreHandler 插件商店处理器
type StoreHandler struct {
app AppInterface
}
// NewStoreHandler 创建插件商店处理器
func NewStoreHandler(app AppInterface) *StoreHandler {
return &StoreHandler{app: app}
}
// ListPlugins 获取商店插件列表
// @Summary 获取商店插件
// @Description 从远程插件商店获取可用插件列表
// @Tags 插件商店
// @Produce json
// @Security Bearer
// @Success 200 {object} Response{data=object{plugins=[]dto.StorePluginInfo}}
// @Failure 502 {object} Response
// @Router /api/store/plugins [get]
func (h *StoreHandler) ListPlugins(c *gin.Context) {
cfg := h.app.GetConfig()
storeURL := cfg.Server.PluginStore.GetPluginStoreURL()
// 从远程 URL 获取插件列表
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(storeURL)
if err != nil {
BadGateway(c, "Failed to fetch store: "+err.Error())
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
BadGateway(c, "Store returned error")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
InternalError(c, "Failed to read response")
return
}
// 直接返回原始 JSON已经是数组格式
c.Header("Content-Type", "application/json")
c.Writer.Write([]byte(`{"code":0,"data":{"plugins":`))
c.Writer.Write(body)
c.Writer.Write([]byte(`}}`))
}
// Install 从商店安装插件到客户端
// @Summary 安装商店插件
// @Description 从插件商店下载并安装插件到指定客户端
// @Tags 插件商店
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body dto.StoreInstallRequest true "安装请求"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Failure 502 {object} Response
// @Router /api/store/install [post]
func (h *StoreHandler) Install(c *gin.Context) {
var req dto.StoreInstallRequest
if !BindJSON(c, &req) {
return
}
// 检查客户端是否在线
if !h.app.GetServer().IsClientOnline(req.ClientID) {
ClientNotOnline(c)
return
}
// 下载插件
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(req.DownloadURL)
if err != nil {
BadGateway(c, "Failed to download plugin: "+err.Error())
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
BadGateway(c, "Plugin download failed with status: "+resp.Status)
return
}
source, err := io.ReadAll(resp.Body)
if err != nil {
InternalError(c, "Failed to read plugin: "+err.Error())
return
}
// 下载签名文件
sigResp, err := client.Get(req.SignatureURL)
if err != nil {
BadGateway(c, "Failed to download signature: "+err.Error())
return
}
defer sigResp.Body.Close()
if sigResp.StatusCode != http.StatusOK {
BadGateway(c, "Signature download failed with status: "+sigResp.Status)
return
}
signature, err := io.ReadAll(sigResp.Body)
if err != nil {
InternalError(c, "Failed to read signature: "+err.Error())
return
}
// 检查插件是否已存在,决定使用已有 ID 还是生成新 ID
pluginID := ""
dbClient, err := h.app.GetClientStore().GetClient(req.ClientID)
if err == nil {
for _, p := range dbClient.Plugins {
if p.Name == req.PluginName && p.ID != "" {
pluginID = p.ID
break
}
}
}
if pluginID == "" {
pluginID = uuid.New().String()
}
// 安装到客户端
installReq := JSPluginInstallRequest{
PluginID: pluginID,
PluginName: req.PluginName,
Source: string(source),
Signature: string(signature),
RuleName: req.PluginName,
RemotePort: req.RemotePort,
AutoStart: true,
}
if err := h.app.GetServer().InstallJSPluginToClient(req.ClientID, installReq); err != nil {
InternalError(c, "Failed to install plugin: "+err.Error())
return
}
// 将插件保存到 JSPluginStore用于客户端重连时恢复
jsPlugin := &db.JSPlugin{
Name: req.PluginName,
Source: string(source),
Signature: string(signature),
AutoStart: true,
Enabled: true,
}
// 尝试保存,忽略错误(可能已存在)
h.app.GetJSPluginStore().SaveJSPlugin(jsPlugin)
// 将插件信息保存到客户端记录
// 重新获取 dbClient可能已被修改
dbClient, err = h.app.GetClientStore().GetClient(req.ClientID)
if err == nil {
// 检查插件是否已存在(通过名称匹配)
pluginExists := false
for i, p := range dbClient.Plugins {
if p.Name == req.PluginName {
dbClient.Plugins[i].Enabled = true
dbClient.Plugins[i].RemotePort = req.RemotePort
dbClient.Plugins[i].AuthEnabled = req.AuthEnabled
dbClient.Plugins[i].AuthUsername = req.AuthUsername
dbClient.Plugins[i].AuthPassword = req.AuthPassword
// 确保有 ID
if dbClient.Plugins[i].ID == "" {
dbClient.Plugins[i].ID = pluginID
}
pluginExists = true
break
}
}
if !pluginExists {
version := req.Version
if version == "" {
version = "1.0.0"
}
// 转换 ConfigSchema
var configSchema []db.ConfigField
for _, f := range req.ConfigSchema {
configSchema = append(configSchema, db.ConfigField{
Key: f.Key,
Label: f.Label,
Type: f.Type,
Default: f.Default,
Required: f.Required,
Options: f.Options,
Description: f.Description,
})
}
dbClient.Plugins = append(dbClient.Plugins, db.ClientPlugin{
ID: pluginID,
Name: req.PluginName,
Version: version,
Enabled: true,
RemotePort: req.RemotePort,
ConfigSchema: configSchema,
AuthEnabled: req.AuthEnabled,
AuthUsername: req.AuthUsername,
AuthPassword: req.AuthPassword,
})
}
// 自动创建代理规则(如果指定了端口)
if req.RemotePort > 0 {
// 检查端口是否可用
if !h.app.GetServer().IsPortAvailable(req.RemotePort, req.ClientID) {
InternalError(c, fmt.Sprintf("port %d is already in use", req.RemotePort))
return
}
ruleExists := false
for i, r := range dbClient.Rules {
if r.Name == req.PluginName {
// 更新现有规则
dbClient.Rules[i].Type = req.PluginName
dbClient.Rules[i].RemotePort = req.RemotePort
dbClient.Rules[i].Enabled = boolPtr(true)
dbClient.Rules[i].PluginID = pluginID
dbClient.Rules[i].AuthEnabled = req.AuthEnabled
dbClient.Rules[i].AuthUsername = req.AuthUsername
dbClient.Rules[i].AuthPassword = req.AuthPassword
dbClient.Rules[i].PluginManaged = true
ruleExists = true
break
}
}
if !ruleExists {
// 创建新规则
dbClient.Rules = append(dbClient.Rules, protocol.ProxyRule{
Name: req.PluginName,
Type: req.PluginName,
RemotePort: req.RemotePort,
Enabled: boolPtr(true),
PluginID: pluginID,
AuthEnabled: req.AuthEnabled,
AuthUsername: req.AuthUsername,
AuthPassword: req.AuthPassword,
PluginManaged: true,
})
}
}
h.app.GetClientStore().UpdateClient(dbClient)
}
// 启动服务端监听器(让外部用户可以通过 RemotePort 访问插件)
if req.RemotePort > 0 {
pluginRule := protocol.ProxyRule{
Name: req.PluginName,
Type: req.PluginName, // 使用插件名作为类型,让 isClientPlugin 识别
RemotePort: req.RemotePort,
Enabled: boolPtr(true),
PluginID: pluginID,
AuthEnabled: req.AuthEnabled,
AuthUsername: req.AuthUsername,
AuthPassword: req.AuthPassword,
}
// 启动监听器(忽略错误,可能端口已被占用)
h.app.GetServer().StartPluginRule(req.ClientID, pluginRule)
}
Success(c, gin.H{
"status": "ok",
"plugin": req.PluginName,
"plugin_id": pluginID,
"client": req.ClientID,
})
}
// boolPtr 返回 bool 值的指针
func boolPtr(b bool) *bool {
return &b
}

View File

@@ -67,8 +67,6 @@ func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth,
api.POST("/client/:id/push", clientHandler.PushConfig)
api.POST("/client/:id/disconnect", clientHandler.Disconnect)
api.POST("/client/:id/restart", clientHandler.Restart)
api.POST("/client/:id/install-plugins", clientHandler.InstallPlugins)
api.POST("/client/:id/plugin/:pluginID/:action", clientHandler.PluginAction)
api.GET("/client/:id/system-stats", clientHandler.GetSystemStats)
api.GET("/client/:id/screenshot", clientHandler.GetScreenshot)
api.POST("/client/:id/shell", clientHandler.ExecuteShell)
@@ -79,29 +77,6 @@ func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth,
api.PUT("/config", configHandler.Update)
api.POST("/config/reload", configHandler.Reload)
// 插件管理
pluginHandler := handler.NewPluginHandler(app)
api.GET("/plugins", pluginHandler.List)
api.POST("/plugin/:name/enable", pluginHandler.Enable)
api.POST("/plugin/:name/disable", pluginHandler.Disable)
api.GET("/rule-schemas", pluginHandler.GetRuleSchemas)
api.GET("/client-plugin/:clientID/:pluginName/config", pluginHandler.GetClientConfig)
api.PUT("/client-plugin/:clientID/:pluginName/config", pluginHandler.UpdateClientConfig)
// JS 插件管理
jsPluginHandler := handler.NewJSPluginHandler(app)
api.GET("/js-plugins", jsPluginHandler.List)
api.POST("/js-plugins", jsPluginHandler.Create)
api.GET("/js-plugin/:name", jsPluginHandler.Get)
api.PUT("/js-plugin/:name", jsPluginHandler.Update)
api.DELETE("/js-plugin/:name", jsPluginHandler.Delete)
api.POST("/js-plugin/:name/push/:clientID", jsPluginHandler.PushToClient)
// 插件商店
storeHandler := handler.NewStoreHandler(app)
api.GET("/store/plugins", storeHandler.ListPlugins)
api.POST("/store/install", storeHandler.Install)
// 更新管理
updateHandler := handler.NewUpdateHandler(app)
api.GET("/update/check/server", updateHandler.CheckServer)
@@ -118,9 +93,9 @@ func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth,
api.GET("/traffic/stats", trafficHandler.GetStats)
api.GET("/traffic/hourly", trafficHandler.GetHourly)
// 插件 API 代理 (通过 Web API 访问客户端插件)
pluginAPIHandler := handler.NewPluginAPIHandler(app)
api.Any("/client/:id/plugin-api/:pluginID/*route", pluginAPIHandler.ProxyRequest)
// 安装命令生成
installHandler := handler.NewInstallHandler(app)
api.POST("/install/generate", installHandler.GenerateInstallCommand)
}
}
@@ -200,10 +175,6 @@ func isStaticAsset(path string) bool {
// Re-export types from handler package for backward compatibility
type (
ServerInterface = handler.ServerInterface
AppInterface = handler.AppInterface
ConfigField = handler.ConfigField
RuleSchema = handler.RuleSchema
PluginInfo = handler.PluginInfo
JSPluginInstallRequest = handler.JSPluginInstallRequest
ServerInterface = handler.ServerInterface
AppInterface = handler.AppInterface
)