package router import ( "encoding/json" "io" "net/http" "regexp" "time" "github.com/gotunnel/internal/server/config" "github.com/gotunnel/internal/server/db" "github.com/gotunnel/pkg/plugin" "github.com/gotunnel/pkg/protocol" ) // 客户端 ID 验证规则 var clientIDRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,64}$`) // validateClientID 验证客户端 ID 格式 func validateClientID(id string) bool { return clientIDRegex.MatchString(id) } // ClientStatus 客户端状态 type ClientStatus struct { ID string `json:"id"` Nickname string `json:"nickname,omitempty"` Online bool `json:"online"` LastPing string `json:"last_ping,omitempty"` RemoteAddr string `json:"remote_addr,omitempty"` RuleCount int `json:"rule_count"` } // ServerInterface 服务端接口 type ServerInterface interface { GetClientStatus(clientID string) (online bool, lastPing string, remoteAddr string) GetAllClientStatus() map[string]struct { Online bool LastPing string RemoteAddr string } ReloadConfig() error GetBindAddr() string GetBindPort() int // 客户端控制 PushConfigToClient(clientID string) error DisconnectClient(clientID string) error GetPluginList() []PluginInfo EnablePlugin(name string) error DisablePlugin(name string) error InstallPluginsToClient(clientID string, plugins []string) error // 插件配置 GetPluginConfigSchema(name string) ([]ConfigField, error) SyncPluginConfigToClient(clientID string, pluginName string, config map[string]string) error // JS 插件 InstallJSPluginToClient(clientID string, req JSPluginInstallRequest) error // 客户端/插件重启 RestartClient(clientID string) error StopClientPlugin(clientID, pluginName, ruleName string) error RestartClientPlugin(clientID, pluginName, ruleName string) error UpdateClientPluginConfig(clientID, pluginName, ruleName string, config map[string]string, restart bool) error } // JSPluginInstallRequest JS 插件安装请求 type JSPluginInstallRequest struct { PluginName string `json:"plugin_name"` Source string `json:"source"` Signature string `json:"signature"` RuleName string `json:"rule_name"` RemotePort int `json:"remote_port"` Config map[string]string `json:"config"` AutoStart bool `json:"auto_start"` } // ConfigField 配置字段(从 plugin 包导出) type ConfigField struct { Key string `json:"key"` Label string `json:"label"` Type string `json:"type"` Default string `json:"default,omitempty"` Required bool `json:"required,omitempty"` Options []string `json:"options,omitempty"` Description string `json:"description,omitempty"` } // RuleSchema 规则表单模式 type RuleSchema struct { NeedsLocalAddr bool `json:"needs_local_addr"` ExtraFields []ConfigField `json:"extra_fields,omitempty"` } // PluginInfo 插件信息 type PluginInfo struct { Name string `json:"name"` Version string `json:"version"` Type string `json:"type"` Description string `json:"description"` Source string `json:"source"` Icon string `json:"icon,omitempty"` Enabled bool `json:"enabled"` RuleSchema *RuleSchema `json:"rule_schema,omitempty"` } // AppInterface 应用接口 type AppInterface interface { GetClientStore() db.ClientStore GetServer() ServerInterface GetConfig() *config.ServerConfig GetConfigPath() string SaveConfig() error GetJSPluginStore() db.JSPluginStore } // APIHandler API处理器 type APIHandler struct { clientStore db.ClientStore server ServerInterface app AppInterface jsPluginStore db.JSPluginStore } // RegisterRoutes 注册所有 API 路由 func RegisterRoutes(r *Router, app AppInterface) { h := &APIHandler{ clientStore: app.GetClientStore(), server: app.GetServer(), app: app, jsPluginStore: app.GetJSPluginStore(), } api := r.Group("/api") api.HandleFunc("/status", h.handleStatus) api.HandleFunc("/clients", h.handleClients) api.HandleFunc("/client/", h.handleClient) api.HandleFunc("/config", h.handleConfig) api.HandleFunc("/config/reload", h.handleReload) api.HandleFunc("/plugins", h.handlePlugins) api.HandleFunc("/plugin/", h.handlePlugin) api.HandleFunc("/store/plugins", h.handleStorePlugins) api.HandleFunc("/store/install", h.handleStoreInstall) api.HandleFunc("/client-plugin/", h.handleClientPlugin) api.HandleFunc("/js-plugin/", h.handleJSPlugin) api.HandleFunc("/js-plugins", h.handleJSPlugins) api.HandleFunc("/rule-schemas", h.handleRuleSchemas) } func (h *APIHandler) handleStatus(rw http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } clients, _ := h.clientStore.GetAllClients() status := map[string]interface{}{ "server": map[string]interface{}{ "bind_addr": h.server.GetBindAddr(), "bind_port": h.server.GetBindPort(), }, "client_count": len(clients), } h.jsonResponse(rw, status) } func (h *APIHandler) handleClients(rw http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: h.getClients(rw) case http.MethodPost: h.addClient(rw, r) default: http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) } } func (h *APIHandler) getClients(rw http.ResponseWriter) { clients, err := h.clientStore.GetAllClients() if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } statusMap := h.server.GetAllClientStatus() var result []ClientStatus for _, c := range clients { cs := ClientStatus{ID: c.ID, Nickname: c.Nickname, RuleCount: len(c.Rules)} if s, ok := statusMap[c.ID]; ok { cs.Online = s.Online cs.LastPing = s.LastPing cs.RemoteAddr = s.RemoteAddr } result = append(result, cs) } h.jsonResponse(rw, result) } func (h *APIHandler) addClient(rw http.ResponseWriter, r *http.Request) { var req struct { ID string `json:"id"` Rules []protocol.ProxyRule `json:"rules"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } if req.ID == "" { http.Error(rw, "client id required", http.StatusBadRequest) return } if !validateClientID(req.ID) { http.Error(rw, "invalid client id: must be 1-64 alphanumeric characters, underscore or hyphen", http.StatusBadRequest) return } exists, _ := h.clientStore.ClientExists(req.ID) if exists { http.Error(rw, "client already exists", http.StatusConflict) return } client := &db.Client{ID: req.ID, Rules: req.Rules} if err := h.clientStore.CreateClient(client); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok"}) } func (h *APIHandler) handleClient(rw http.ResponseWriter, r *http.Request) { clientID := r.URL.Path[len("/api/client/"):] if clientID == "" { http.Error(rw, "client id required", http.StatusBadRequest) return } // 处理子路径操作 if idx := len(clientID) - 1; idx > 0 { if clientID[idx] == '/' { clientID = clientID[:idx] } } // 检查是否是特殊操作 parts := splitPath(clientID) if len(parts) == 2 { clientID = parts[0] action := parts[1] switch action { case "push": h.pushConfigToClient(rw, r, clientID) return case "disconnect": h.disconnectClient(rw, r, clientID) return case "install-plugins": h.installPluginsToClient(rw, r, clientID) return case "restart": h.restartClient(rw, r, clientID) return } } // 检查是否是插件操作: /api/client/{id}/plugin/{name}/{action} if len(parts) >= 2 && parts[1] == "plugin" { // 重新解析路径 remaining := clientID[len(parts[0])+1:] // "plugin/xxx/action" pluginParts := splitPath(remaining[7:]) // 跳过 "plugin/" if len(pluginParts) >= 2 { pluginName := pluginParts[0] pluginAction := pluginParts[1] h.handleClientPluginAction(rw, r, parts[0], pluginName, pluginAction) return } } switch r.Method { case http.MethodGet: h.getClient(rw, clientID) case http.MethodPut: h.updateClient(rw, r, clientID) case http.MethodDelete: h.deleteClient(rw, clientID) default: http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) } } // splitPath 分割路径 func splitPath(path string) []string { for i, c := range path { if c == '/' { return []string{path[:i], path[i+1:]} } } return []string{path} } func (h *APIHandler) getClient(rw http.ResponseWriter, clientID string) { client, err := h.clientStore.GetClient(clientID) if err != nil { http.Error(rw, "client not found", http.StatusNotFound) return } online, lastPing, remoteAddr := h.server.GetClientStatus(clientID) h.jsonResponse(rw, map[string]interface{}{ "id": client.ID, "nickname": client.Nickname, "rules": client.Rules, "plugins": client.Plugins, "online": online, "last_ping": lastPing, "remote_addr": remoteAddr, }) } func (h *APIHandler) updateClient(rw http.ResponseWriter, r *http.Request, clientID string) { var req struct { Nickname string `json:"nickname"` Rules []protocol.ProxyRule `json:"rules"` Plugins []db.ClientPlugin `json:"plugins"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } client, err := h.clientStore.GetClient(clientID) if err != nil { http.Error(rw, "client not found", http.StatusNotFound) return } client.Nickname = req.Nickname client.Rules = req.Rules if req.Plugins != nil { client.Plugins = req.Plugins } if err := h.clientStore.UpdateClient(client); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok"}) } func (h *APIHandler) deleteClient(rw http.ResponseWriter, clientID string) { exists, _ := h.clientStore.ClientExists(clientID) if !exists { http.Error(rw, "client not found", http.StatusNotFound) return } if err := h.clientStore.DeleteClient(clientID); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok"}) } func (h *APIHandler) handleConfig(rw http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: h.getConfig(rw) case http.MethodPut: h.updateConfig(rw, r) default: http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) } } func (h *APIHandler) getConfig(rw http.ResponseWriter) { cfg := h.app.GetConfig() // Token 脱敏处理,只显示前4位 maskedToken := cfg.Server.Token if len(maskedToken) > 4 { maskedToken = maskedToken[:4] + "****" } h.jsonResponse(rw, map[string]interface{}{ "server": map[string]interface{}{ "bind_addr": cfg.Server.BindAddr, "bind_port": cfg.Server.BindPort, "token": maskedToken, "heartbeat_sec": cfg.Server.HeartbeatSec, "heartbeat_timeout": cfg.Server.HeartbeatTimeout, }, "web": map[string]interface{}{ "enabled": cfg.Web.Enabled, "bind_addr": cfg.Web.BindAddr, "bind_port": cfg.Web.BindPort, "username": cfg.Web.Username, "password": "****", }, }) } func (h *APIHandler) updateConfig(rw http.ResponseWriter, r *http.Request) { var req struct { Server *struct { BindAddr string `json:"bind_addr"` BindPort int `json:"bind_port"` Token string `json:"token"` HeartbeatSec int `json:"heartbeat_sec"` HeartbeatTimeout int `json:"heartbeat_timeout"` } `json:"server"` Web *struct { Enabled bool `json:"enabled"` BindAddr string `json:"bind_addr"` BindPort int `json:"bind_port"` Username string `json:"username"` Password string `json:"password"` } `json:"web"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } cfg := h.app.GetConfig() // 更新 Server 配置 if req.Server != nil { if req.Server.BindAddr != "" { cfg.Server.BindAddr = req.Server.BindAddr } if req.Server.BindPort > 0 { cfg.Server.BindPort = req.Server.BindPort } if req.Server.Token != "" { cfg.Server.Token = req.Server.Token } if req.Server.HeartbeatSec > 0 { cfg.Server.HeartbeatSec = req.Server.HeartbeatSec } if req.Server.HeartbeatTimeout > 0 { cfg.Server.HeartbeatTimeout = req.Server.HeartbeatTimeout } } // 更新 Web 配置 if req.Web != nil { cfg.Web.Enabled = req.Web.Enabled if req.Web.BindAddr != "" { cfg.Web.BindAddr = req.Web.BindAddr } if req.Web.BindPort > 0 { cfg.Web.BindPort = req.Web.BindPort } cfg.Web.Username = req.Web.Username cfg.Web.Password = req.Web.Password } if err := h.app.SaveConfig(); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok"}) } func (h *APIHandler) handleReload(rw http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } if err := h.server.ReloadConfig(); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok"}) } func (h *APIHandler) jsonResponse(rw http.ResponseWriter, data interface{}) { rw.Header().Set("Content-Type", "application/json") json.NewEncoder(rw).Encode(data) } // pushConfigToClient 推送配置到客户端 func (h *APIHandler) pushConfigToClient(rw http.ResponseWriter, r *http.Request, clientID string) { if r.Method != http.MethodPost { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } online, _, _ := h.server.GetClientStatus(clientID) if !online { http.Error(rw, "client not online", http.StatusBadRequest) return } if err := h.server.PushConfigToClient(clientID); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok"}) } // disconnectClient 断开客户端连接 func (h *APIHandler) disconnectClient(rw http.ResponseWriter, r *http.Request, clientID string) { if r.Method != http.MethodPost { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } if err := h.server.DisconnectClient(clientID); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok"}) } // handlePlugins 处理插件列表 func (h *APIHandler) handlePlugins(rw http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } plugins := h.server.GetPluginList() h.jsonResponse(rw, plugins) } // handlePlugin 处理单个插件操作 func (h *APIHandler) handlePlugin(rw http.ResponseWriter, r *http.Request) { path := r.URL.Path[len("/api/plugin/"):] if path == "" { http.Error(rw, "plugin name required", http.StatusBadRequest) return } parts := splitPath(path) pluginName := parts[0] if len(parts) == 2 { action := parts[1] switch action { case "enable": h.enablePlugin(rw, r, pluginName) return case "disable": h.disablePlugin(rw, r, pluginName) return } } http.Error(rw, "invalid action", http.StatusBadRequest) } func (h *APIHandler) enablePlugin(rw http.ResponseWriter, r *http.Request, name string) { if r.Method != http.MethodPost { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } if err := h.server.EnablePlugin(name); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } h.jsonResponse(rw, map[string]string{"status": "ok"}) } func (h *APIHandler) disablePlugin(rw http.ResponseWriter, r *http.Request, name string) { if r.Method != http.MethodPost { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } if err := h.server.DisablePlugin(name); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } h.jsonResponse(rw, map[string]string{"status": "ok"}) } // installPluginsToClient 安装插件到客户端 func (h *APIHandler) installPluginsToClient(rw http.ResponseWriter, r *http.Request, clientID string) { if r.Method != http.MethodPost { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } online, _, _ := h.server.GetClientStatus(clientID) if !online { http.Error(rw, "client not online", http.StatusBadRequest) return } var req struct { Plugins []string `json:"plugins"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } if len(req.Plugins) == 0 { http.Error(rw, "no plugins specified", http.StatusBadRequest) return } if err := h.server.InstallPluginsToClient(clientID, req.Plugins); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok"}) } // StorePluginInfo 扩展商店插件信息 type StorePluginInfo struct { Name string `json:"name"` Version string `json:"version"` Type string `json:"type"` Description string `json:"description"` Author string `json:"author"` Icon string `json:"icon,omitempty"` DownloadURL string `json:"download_url,omitempty"` SignatureURL string `json:"signature_url,omitempty"` } // StorePluginInstallRequest 从商店安装插件的请求 type StorePluginInstallRequest struct { PluginName string `json:"plugin_name"` DownloadURL string `json:"download_url"` SignatureURL string `json:"signature_url"` ClientID string `json:"client_id"` } // handleStorePlugins 处理扩展商店插件列表 func (h *APIHandler) handleStorePlugins(rw http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } cfg := h.app.GetConfig() storeURL := cfg.PluginStore.GetPluginStoreURL() // 从远程URL获取插件列表 client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Get(storeURL) if err != nil { http.Error(rw, "Failed to fetch store: "+err.Error(), http.StatusBadGateway) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { http.Error(rw, "Store returned error", http.StatusBadGateway) return } body, err := io.ReadAll(resp.Body) if err != nil { http.Error(rw, "Failed to read response", http.StatusInternalServerError) return } var plugins []StorePluginInfo if err := json.Unmarshal(body, &plugins); err != nil { http.Error(rw, "Invalid store format", http.StatusBadGateway) return } h.jsonResponse(rw, map[string]interface{}{ "plugins": plugins, }) } // handleStoreInstall 从商店安装插件到客户端 func (h *APIHandler) handleStoreInstall(rw http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } var req StorePluginInstallRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } if req.PluginName == "" || req.DownloadURL == "" || req.ClientID == "" || req.SignatureURL == "" { http.Error(rw, "plugin_name, download_url, signature_url and client_id required", http.StatusBadRequest) return } // 检查客户端是否在线 online, _, _ := h.server.GetClientStatus(req.ClientID) if !online { http.Error(rw, "client not online", http.StatusBadRequest) return } // 下载插件 client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Get(req.DownloadURL) if err != nil { http.Error(rw, "Failed to download plugin: "+err.Error(), http.StatusBadGateway) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { http.Error(rw, "Plugin download failed with status: "+resp.Status, http.StatusBadGateway) return } source, err := io.ReadAll(resp.Body) if err != nil { http.Error(rw, "Failed to read plugin: "+err.Error(), http.StatusInternalServerError) return } // 下载签名文件 sigResp, err := client.Get(req.SignatureURL) if err != nil { http.Error(rw, "Failed to download signature: "+err.Error(), http.StatusBadGateway) return } defer sigResp.Body.Close() if sigResp.StatusCode != http.StatusOK { http.Error(rw, "Signature download failed with status: "+sigResp.Status, http.StatusBadGateway) return } signature, err := io.ReadAll(sigResp.Body) if err != nil { http.Error(rw, "Failed to read signature: "+err.Error(), http.StatusInternalServerError) return } // 安装到客户端 installReq := JSPluginInstallRequest{ PluginName: req.PluginName, Source: string(source), Signature: string(signature), RuleName: req.PluginName, AutoStart: true, } if err := h.server.InstallJSPluginToClient(req.ClientID, installReq); err != nil { http.Error(rw, "Failed to install plugin: "+err.Error(), http.StatusInternalServerError) return } // 将插件信息保存到数据库 dbClient, err := h.clientStore.GetClient(req.ClientID) if err == nil { // 检查插件是否已存在 exists := false for i, p := range dbClient.Plugins { if p.Name == req.PluginName { // 更新已存在的插件 dbClient.Plugins[i].Enabled = true exists = true break } } if !exists { // 添加新插件 dbClient.Plugins = append(dbClient.Plugins, db.ClientPlugin{ Name: req.PluginName, Version: "1.0.0", Enabled: true, }) } _ = h.clientStore.UpdateClient(dbClient) } h.jsonResponse(rw, map[string]interface{}{ "status": "ok", "plugin": req.PluginName, "client": req.ClientID, }) } // handleClientPlugin 处理客户端插件配置 // 路由: /api/client-plugin/{clientID}/{pluginName}/config func (h *APIHandler) handleClientPlugin(rw http.ResponseWriter, r *http.Request) { path := r.URL.Path[len("/api/client-plugin/"):] if path == "" { http.Error(rw, "client id required", http.StatusBadRequest) return } // 解析路径: clientID/pluginName/action parts := splitPathMulti(path) if len(parts) < 3 { http.Error(rw, "invalid path, expected: /api/client-plugin/{clientID}/{pluginName}/config", http.StatusBadRequest) return } clientID := parts[0] pluginName := parts[1] action := parts[2] if action != "config" { http.Error(rw, "invalid action", http.StatusBadRequest) return } switch r.Method { case http.MethodGet: h.getClientPluginConfig(rw, clientID, pluginName) case http.MethodPut: h.updateClientPluginConfig(rw, r, clientID, pluginName) default: http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) } } // splitPathMulti 分割路径为多个部分 func splitPathMulti(path string) []string { var parts []string start := 0 for i, c := range path { if c == '/' { if i > start { parts = append(parts, path[start:i]) } start = i + 1 } } if start < len(path) { parts = append(parts, path[start:]) } return parts } // getClientPluginConfig 获取客户端插件配置 func (h *APIHandler) getClientPluginConfig(rw http.ResponseWriter, clientID, pluginName string) { client, err := h.clientStore.GetClient(clientID) if err != nil { http.Error(rw, "client not found", http.StatusNotFound) return } // 尝试从内置插件获取配置模式 schema, err := h.server.GetPluginConfigSchema(pluginName) if err != nil { // 如果内置插件中找不到,尝试从 JS 插件获取 jsPlugin, jsErr := h.jsPluginStore.GetJSPlugin(pluginName) if jsErr != nil { // 两者都找不到,返回空 schema(允许配置但没有预定义的 schema) schema = []ConfigField{} } else { // 使用 JS 插件的 config 作为动态 schema for key := range jsPlugin.Config { schema = append(schema, ConfigField{ Key: key, Label: key, Type: "string", }) } } } // 查找客户端的插件配置 var config map[string]string for _, p := range client.Plugins { if p.Name == pluginName { config = p.Config break } } if config == nil { config = make(map[string]string) } h.jsonResponse(rw, map[string]interface{}{ "plugin_name": pluginName, "schema": schema, "config": config, }) } // updateClientPluginConfig 更新客户端插件配置 func (h *APIHandler) updateClientPluginConfig(rw http.ResponseWriter, r *http.Request, clientID, pluginName string) { var req struct { Config map[string]string `json:"config"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } client, err := h.clientStore.GetClient(clientID) if err != nil { http.Error(rw, "client not found", http.StatusNotFound) return } // 更新插件配置 found := false for i, p := range client.Plugins { if p.Name == pluginName { client.Plugins[i].Config = req.Config found = true break } } if !found { http.Error(rw, "plugin not installed on client", http.StatusNotFound) return } // 保存到数据库 if err := h.clientStore.UpdateClient(client); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } // 如果客户端在线,同步配置 online, _, _ := h.server.GetClientStatus(clientID) if online { if err := h.server.SyncPluginConfigToClient(clientID, pluginName, req.Config); err != nil { // 配置已保存,但同步失败,返回警告 h.jsonResponse(rw, map[string]interface{}{ "status": "partial", "message": "config saved but sync failed: " + err.Error(), }) return } } h.jsonResponse(rw, map[string]string{"status": "ok"}) } // handleJSPlugin 处理单个 JS 插件操作 // GET/PUT/DELETE /api/js-plugin/{name} // POST /api/js-plugin/{name}/push/{clientID} func (h *APIHandler) handleJSPlugin(rw http.ResponseWriter, r *http.Request) { path := r.URL.Path[len("/api/js-plugin/"):] if path == "" { http.Error(rw, "plugin name required", http.StatusBadRequest) return } parts := splitPathMulti(path) // POST /api/js-plugin/{name}/push/{clientID} if len(parts) == 3 && parts[1] == "push" { if r.Method != http.MethodPost { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } h.pushJSPluginToClient(rw, parts[0], parts[2]) return } // GET/PUT/DELETE /api/js-plugin/{name} pluginName := parts[0] switch r.Method { case http.MethodGet: h.getJSPlugin(rw, pluginName) case http.MethodPut: h.updateJSPlugin(rw, r, pluginName) case http.MethodDelete: h.deleteJSPlugin(rw, pluginName) default: http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) } } // installJSPluginToClient 安装 JS 插件到客户端 func (h *APIHandler) installJSPluginToClient(rw http.ResponseWriter, r *http.Request, clientID string) { online, _, _ := h.server.GetClientStatus(clientID) if !online { http.Error(rw, "client not online", http.StatusBadRequest) return } var req JSPluginInstallRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } if req.PluginName == "" || req.Source == "" { http.Error(rw, "plugin_name and source required", http.StatusBadRequest) return } if err := h.server.InstallJSPluginToClient(clientID, req); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]interface{}{ "status": "ok", "plugin": req.PluginName, }) } // handleJSPlugins 处理 JS 插件列表和创建 // GET /api/js-plugins - 获取所有 JS 插件 // POST /api/js-plugins - 创建新 JS 插件 func (h *APIHandler) handleJSPlugins(rw http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: h.getJSPlugins(rw) case http.MethodPost: h.createJSPlugin(rw, r) default: http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) } } func (h *APIHandler) getJSPlugins(rw http.ResponseWriter) { plugins, err := h.jsPluginStore.GetAllJSPlugins() if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if plugins == nil { plugins = []db.JSPlugin{} } h.jsonResponse(rw, plugins) } func (h *APIHandler) createJSPlugin(rw http.ResponseWriter, r *http.Request) { var req db.JSPlugin if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } if req.Name == "" || req.Source == "" { http.Error(rw, "name and source required", http.StatusBadRequest) return } req.Enabled = true if err := h.jsPluginStore.SaveJSPlugin(&req); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok"}) } func (h *APIHandler) getJSPlugin(rw http.ResponseWriter, name string) { p, err := h.jsPluginStore.GetJSPlugin(name) if err != nil { http.Error(rw, "plugin not found", http.StatusNotFound) return } h.jsonResponse(rw, p) } func (h *APIHandler) updateJSPlugin(rw http.ResponseWriter, r *http.Request, name string) { var req db.JSPlugin if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(rw, err.Error(), http.StatusBadRequest) return } req.Name = name if err := h.jsPluginStore.SaveJSPlugin(&req); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok"}) } func (h *APIHandler) deleteJSPlugin(rw http.ResponseWriter, name string) { if err := h.jsPluginStore.DeleteJSPlugin(name); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok"}) } // pushJSPluginToClient 推送 JS 插件到指定客户端 func (h *APIHandler) pushJSPluginToClient(rw http.ResponseWriter, pluginName, clientID string) { // 检查客户端是否在线 online, _, _ := h.server.GetClientStatus(clientID) if !online { http.Error(rw, "client not online", http.StatusBadRequest) return } // 获取插件 p, err := h.jsPluginStore.GetJSPlugin(pluginName) if err != nil { http.Error(rw, "plugin not found", http.StatusNotFound) return } if !p.Enabled { http.Error(rw, "plugin is disabled", http.StatusBadRequest) return } // 推送到客户端 req := JSPluginInstallRequest{ PluginName: p.Name, Source: p.Source, Signature: p.Signature, RuleName: p.Name, Config: p.Config, AutoStart: p.AutoStart, } if err := h.server.InstallJSPluginToClient(clientID, req); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok", "plugin": pluginName, "client": clientID}) } // handleRuleSchemas 返回所有协议类型的配置模式 func (h *APIHandler) handleRuleSchemas(rw http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } // 获取内置协议模式 schemas := make(map[string]RuleSchema) for name, schema := range plugin.BuiltinRuleSchemas() { schemas[name] = RuleSchema{ NeedsLocalAddr: schema.NeedsLocalAddr, ExtraFields: convertConfigFields(schema.ExtraFields), } } // 添加已注册插件的模式 plugins := h.server.GetPluginList() for _, p := range plugins { if p.RuleSchema != nil { schemas[p.Name] = *p.RuleSchema } } h.jsonResponse(rw, schemas) } // convertConfigFields 将 plugin.ConfigField 转换为 router.ConfigField func convertConfigFields(fields []plugin.ConfigField) []ConfigField { result := make([]ConfigField, len(fields)) for i, f := range fields { result[i] = ConfigField{ Key: f.Key, Label: f.Label, Type: string(f.Type), Default: f.Default, Required: f.Required, Options: f.Options, Description: f.Description, } } return result } // restartClient 重启客户端 func (h *APIHandler) restartClient(rw http.ResponseWriter, r *http.Request, clientID string) { if r.Method != http.MethodPost { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } if err := h.server.RestartClient(clientID); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok", "message": "client restart initiated"}) } // handleClientPluginAction 处理客户端插件操作 func (h *APIHandler) handleClientPluginAction(rw http.ResponseWriter, r *http.Request, clientID, pluginName, action string) { if r.Method != http.MethodPost && r.Method != http.MethodPut { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) return } // 获取规则名称(从请求体或使用插件名作为默认值) var req struct { RuleName string `json:"rule_name"` Config map[string]string `json:"config"` Restart bool `json:"restart"` } json.NewDecoder(r.Body).Decode(&req) if req.RuleName == "" { req.RuleName = pluginName } var err error switch action { case "stop": err = h.server.StopClientPlugin(clientID, pluginName, req.RuleName) case "restart": err = h.server.RestartClientPlugin(clientID, pluginName, req.RuleName) case "config": if req.Config == nil { http.Error(rw, "config required", http.StatusBadRequest) return } err = h.server.UpdateClientPluginConfig(clientID, pluginName, req.RuleName, req.Config, req.Restart) default: http.Error(rw, "unknown action", http.StatusBadRequest) return } if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } h.jsonResponse(rw, map[string]string{"status": "ok", "action": action, "plugin": pluginName}) }