Files
GoTunnel/internal/server/router/api.go
Flik 82c1a6a266
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 2m28s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 2m25s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 2m24s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 3m12s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m17s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 3m23s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 2m1s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m59s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m15s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 2m21s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m50s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 2m43s
1
2026-01-01 22:24:19 +08:00

1205 lines
33 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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})
}