feat: implement plugin API request handling with HTTP Basic Auth support
Some checks failed
Build Multi-Platform Binaries / build-frontend (push) Successful in 31s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 4m16s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 2m21s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 5m31s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Has been cancelled
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
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Has been cancelled

This commit is contained in:
2026-01-04 20:32:21 +08:00
parent 458bb35005
commit 78982a26b0
9 changed files with 620 additions and 48 deletions

View File

@@ -49,6 +49,8 @@ type ServerInterface interface {
GetClientPluginStatus(clientID string) ([]protocol.PluginStatusEntry, error)
// 插件规则管理
StartPluginRule(clientID string, rule protocol.ProxyRule) error
// 插件 API 代理
ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error)
}
// ConfigField 配置字段

View File

@@ -0,0 +1,140 @@
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 clientID path string true "客户端 ID"
// @Param pluginName path string true "插件名称"
// @Param route path string true "插件路由"
// @Success 200 {object} object
// @Failure 404 {object} Response
// @Failure 502 {object} Response
// @Router /api/client/{clientID}/plugin/{pluginName}/{route} [get]
func (h *PluginAPIHandler) ProxyRequest(c *gin.Context) {
clientID := c.Param("clientID")
pluginName := c.Param("pluginName")
route := c.Param("route")
// 确保路由以 / 开头
if !strings.HasPrefix(route, "/") {
route = "/" + route
}
// 检查客户端是否在线
online, _, _ := h.app.GetServer().GetClientStatus(clientID)
if !online {
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{
PluginName: pluginName,
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

@@ -155,15 +155,16 @@ func (h *StoreHandler) Install(c *gin.Context) {
dbClient, err := h.app.GetClientStore().GetClient(req.ClientID)
if err == nil {
// 检查插件是否已存在
exists := false
pluginExists := false
for i, p := range dbClient.Plugins {
if p.Name == req.PluginName {
dbClient.Plugins[i].Enabled = true
exists = true
dbClient.Plugins[i].RemotePort = req.RemotePort
pluginExists = true
break
}
}
if !exists {
if !pluginExists {
version := req.Version
if version == "" {
version = "1.0.0"
@@ -189,16 +190,50 @@ func (h *StoreHandler) Install(c *gin.Context) {
ConfigSchema: configSchema,
})
}
// 自动创建代理规则(如果指定了端口)
if req.RemotePort > 0 {
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].AuthEnabled = req.AuthEnabled
dbClient.Rules[i].AuthUsername = req.AuthUsername
dbClient.Rules[i].AuthPassword = req.AuthPassword
ruleExists = true
break
}
}
if !ruleExists {
// 创建新规则
dbClient.Rules = append(dbClient.Rules, protocol.ProxyRule{
Name: req.PluginName,
Type: req.PluginName,
RemotePort: req.RemotePort,
Enabled: boolPtr(true),
AuthEnabled: req.AuthEnabled,
AuthUsername: req.AuthUsername,
AuthPassword: req.AuthPassword,
})
}
}
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),
Name: req.PluginName,
Type: req.PluginName, // 使用插件名作为类型,让 isClientPlugin 识别
RemotePort: req.RemotePort,
Enabled: boolPtr(true),
AuthEnabled: req.AuthEnabled,
AuthUsername: req.AuthUsername,
AuthPassword: req.AuthPassword,
}
// 启动监听器(忽略错误,可能端口已被占用)
h.app.GetServer().StartPluginRule(req.ClientID, pluginRule)