111
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m26s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 49s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m1s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 54s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 39s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m32s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 51s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m0s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 58s

This commit is contained in:
Flik
2025-12-28 00:05:21 +08:00
parent f43256d33d
commit abfc235357
10 changed files with 513 additions and 15 deletions

View File

@@ -194,6 +194,9 @@ func (c *Client) handleStream(stream net.Conn) {
c.handleProxyConnect(stream, msg)
case protocol.MsgTypeUDPData:
c.handleUDPData(stream, msg)
case protocol.MsgTypePluginConfig:
defer stream.Close()
c.handlePluginConfig(msg)
}
}
@@ -346,3 +349,28 @@ func (c *Client) findRuleByPort(port int) *protocol.ProxyRule {
}
return nil
}
// handlePluginConfig 处理插件配置同步
func (c *Client) handlePluginConfig(msg *protocol.Message) {
var cfg protocol.PluginConfigSync
if err := msg.ParsePayload(&cfg); err != nil {
log.Printf("[Client] Parse plugin config error: %v", err)
return
}
log.Printf("[Client] Received config for plugin: %s", cfg.PluginName)
// 应用配置到插件
if c.pluginRegistry != nil {
handler, err := c.pluginRegistry.Get(cfg.PluginName)
if err != nil {
log.Printf("[Client] Plugin %s not found: %v", cfg.PluginName, err)
return
}
if err := handler.Init(cfg.Config); err != nil {
log.Printf("[Client] Plugin %s init error: %v", cfg.PluginName, err)
return
}
log.Printf("[Client] Plugin %s config applied", cfg.PluginName)
}
}

View File

@@ -4,9 +4,10 @@ import "github.com/gotunnel/pkg/protocol"
// ClientPlugin 客户端已安装的插件
type ClientPlugin struct {
Name string `json:"name"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
Name string `json:"name"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
Config map[string]string `json:"config,omitempty"` // 插件配置
}
// Client 客户端数据

View File

@@ -46,6 +46,20 @@ type ServerInterface interface {
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
}
// ConfigField 配置字段(从 plugin 包导出)
type ConfigField struct {
Key string `json:"key"`
Label string `json:"label"`
Type string `json:"type"`
Default string `json:"default,omitempty"`
Required bool `json:"required,omitempty"`
Options []string `json:"options,omitempty"`
Description string `json:"description,omitempty"`
}
// PluginInfo 插件信息
@@ -92,6 +106,7 @@ func RegisterRoutes(r *Router, app AppInterface) {
api.HandleFunc("/plugins", h.handlePlugins)
api.HandleFunc("/plugin/", h.handlePlugin)
api.HandleFunc("/store/plugins", h.handleStorePlugins)
api.HandleFunc("/client-plugin/", h.handleClientPlugin)
}
func (h *APIHandler) handleStatus(rw http.ResponseWriter, r *http.Request) {
@@ -586,3 +601,143 @@ func (h *APIHandler) handleStorePlugins(rw http.ResponseWriter, r *http.Request)
"store_url": storeURL,
})
}
// handleClientPlugin 处理客户端插件配置
// 路由: /api/client-plugin/{clientID}/{pluginName}/config
func (h *APIHandler) handleClientPlugin(rw http.ResponseWriter, r *http.Request) {
path := r.URL.Path[len("/api/client-plugin/"):]
if path == "" {
http.Error(rw, "client id required", http.StatusBadRequest)
return
}
// 解析路径: clientID/pluginName/action
parts := splitPathMulti(path)
if len(parts) < 3 {
http.Error(rw, "invalid path, expected: /api/client-plugin/{clientID}/{pluginName}/config", http.StatusBadRequest)
return
}
clientID := parts[0]
pluginName := parts[1]
action := parts[2]
if action != "config" {
http.Error(rw, "invalid action", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
h.getClientPluginConfig(rw, clientID, pluginName)
case http.MethodPut:
h.updateClientPluginConfig(rw, r, clientID, pluginName)
default:
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// splitPathMulti 分割路径为多个部分
func splitPathMulti(path string) []string {
var parts []string
start := 0
for i, c := range path {
if c == '/' {
if i > start {
parts = append(parts, path[start:i])
}
start = i + 1
}
}
if start < len(path) {
parts = append(parts, path[start:])
}
return parts
}
// getClientPluginConfig 获取客户端插件配置
func (h *APIHandler) getClientPluginConfig(rw http.ResponseWriter, clientID, pluginName string) {
client, err := h.clientStore.GetClient(clientID)
if err != nil {
http.Error(rw, "client not found", http.StatusNotFound)
return
}
// 获取插件配置模式
schema, err := h.server.GetPluginConfigSchema(pluginName)
if err != nil {
http.Error(rw, err.Error(), http.StatusNotFound)
return
}
// 查找客户端的插件配置
var config map[string]string
for _, p := range client.Plugins {
if p.Name == pluginName {
config = p.Config
break
}
}
if config == nil {
config = make(map[string]string)
}
h.jsonResponse(rw, map[string]interface{}{
"plugin_name": pluginName,
"schema": schema,
"config": config,
})
}
// updateClientPluginConfig 更新客户端插件配置
func (h *APIHandler) updateClientPluginConfig(rw http.ResponseWriter, r *http.Request, clientID, pluginName string) {
var req struct {
Config map[string]string `json:"config"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
client, err := h.clientStore.GetClient(clientID)
if err != nil {
http.Error(rw, "client not found", http.StatusNotFound)
return
}
// 更新插件配置
found := false
for i, p := range client.Plugins {
if p.Name == pluginName {
client.Plugins[i].Config = req.Config
found = true
break
}
}
if !found {
http.Error(rw, "plugin not installed on client", http.StatusNotFound)
return
}
// 保存到数据库
if err := h.clientStore.UpdateClient(client); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
// 如果客户端在线,同步配置
online, _ := h.server.GetClientStatus(clientID)
if online {
if err := h.server.SyncPluginConfigToClient(clientID, pluginName, req.Config); err != nil {
// 配置已保存,但同步失败,返回警告
h.jsonResponse(rw, map[string]interface{}{
"status": "partial",
"message": "config saved but sync failed: " + err.Error(),
})
return
}
}
h.jsonResponse(rw, map[string]string{"status": "ok"})
}

View File

@@ -700,3 +700,62 @@ func (s *Server) sendUDPPacket(cs *ClientSession, conn *net.UDPConn, clientAddr
conn.WriteToUDP(respPacket.Data, clientAddr)
}
}
// GetPluginConfigSchema 获取插件配置模式
func (s *Server) GetPluginConfigSchema(name string) ([]router.ConfigField, error) {
if s.pluginRegistry == nil {
return nil, fmt.Errorf("plugin registry not initialized")
}
handler, err := s.pluginRegistry.Get(name)
if err != nil {
return nil, fmt.Errorf("plugin %s not found", name)
}
metadata := handler.Metadata()
var result []router.ConfigField
for _, f := range metadata.ConfigSchema {
result = append(result, router.ConfigField{
Key: f.Key,
Label: f.Label,
Type: string(f.Type),
Default: f.Default,
Required: f.Required,
Options: f.Options,
Description: f.Description,
})
}
return result, nil
}
// SyncPluginConfigToClient 同步插件配置到客户端
func (s *Server) SyncPluginConfigToClient(clientID string, pluginName string, config map[string]string) error {
s.mu.RLock()
cs, ok := s.clients[clientID]
s.mu.RUnlock()
if !ok {
return fmt.Errorf("client %s not online", clientID)
}
return s.sendPluginConfig(cs.Session, pluginName, config)
}
// sendPluginConfig 发送插件配置到客户端
func (s *Server) sendPluginConfig(session *yamux.Session, pluginName string, config map[string]string) error {
stream, err := session.Open()
if err != nil {
return err
}
defer stream.Close()
req := protocol.PluginConfigSync{
PluginName: pluginName,
Config: config,
}
msg, err := protocol.NewMessage(protocol.MsgTypePluginConfig, req)
if err != nil {
return err
}
return protocol.WriteMessage(stream, msg)
}