update
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 59s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m8s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m17s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 53s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m14s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 2m8s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m10s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 57s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m11s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 55s
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 59s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m8s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m17s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 53s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m14s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 2m8s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m10s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 57s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m11s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 55s
This commit is contained in:
@@ -221,10 +221,16 @@ func (c *Client) handleStream(stream net.Conn) {
|
||||
c.handlePluginConfig(msg)
|
||||
case protocol.MsgTypeClientPluginStart:
|
||||
c.handleClientPluginStart(stream, msg)
|
||||
case protocol.MsgTypeClientPluginStop:
|
||||
c.handleClientPluginStop(stream, msg)
|
||||
case protocol.MsgTypeClientPluginConn:
|
||||
c.handleClientPluginConn(stream, msg)
|
||||
case protocol.MsgTypeJSPluginInstall:
|
||||
c.handleJSPluginInstall(stream, msg)
|
||||
case protocol.MsgTypeClientRestart:
|
||||
c.handleClientRestart(stream, msg)
|
||||
case protocol.MsgTypePluginConfigUpdate:
|
||||
c.handlePluginConfigUpdate(stream, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,3 +615,126 @@ func (c *Client) verifyJSPluginSignature(pluginName, source, signature string) e
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleClientPluginStop 处理客户端插件停止请求
|
||||
func (c *Client) handleClientPluginStop(stream net.Conn, msg *protocol.Message) {
|
||||
defer stream.Close()
|
||||
|
||||
var req protocol.ClientPluginStopRequest
|
||||
if err := msg.ParsePayload(&req); err != nil {
|
||||
c.sendPluginStatus(stream, req.PluginName, req.RuleName, true, "", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
key := req.PluginName + ":" + req.RuleName
|
||||
|
||||
c.pluginMu.Lock()
|
||||
handler, ok := c.runningPlugins[key]
|
||||
if ok {
|
||||
if err := handler.Stop(); err != nil {
|
||||
log.Printf("[Client] Plugin %s stop error: %v", key, err)
|
||||
}
|
||||
delete(c.runningPlugins, key)
|
||||
}
|
||||
c.pluginMu.Unlock()
|
||||
|
||||
log.Printf("[Client] Plugin %s stopped", key)
|
||||
c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", "")
|
||||
}
|
||||
|
||||
// handleClientRestart 处理客户端重启请求
|
||||
func (c *Client) handleClientRestart(stream net.Conn, msg *protocol.Message) {
|
||||
defer stream.Close()
|
||||
|
||||
var req protocol.ClientRestartRequest
|
||||
msg.ParsePayload(&req)
|
||||
|
||||
log.Printf("[Client] Restart requested: %s", req.Reason)
|
||||
|
||||
// 发送响应
|
||||
resp := protocol.ClientRestartResponse{
|
||||
Success: true,
|
||||
Message: "restarting",
|
||||
}
|
||||
respMsg, _ := protocol.NewMessage(protocol.MsgTypeClientRestart, resp)
|
||||
protocol.WriteMessage(stream, respMsg)
|
||||
|
||||
// 停止所有运行中的插件
|
||||
c.pluginMu.Lock()
|
||||
for key, handler := range c.runningPlugins {
|
||||
log.Printf("[Client] Stopping plugin %s for restart", key)
|
||||
handler.Stop()
|
||||
}
|
||||
c.runningPlugins = make(map[string]plugin.ClientPlugin)
|
||||
c.pluginMu.Unlock()
|
||||
|
||||
// 关闭会话(会触发重连)
|
||||
if c.session != nil {
|
||||
c.session.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// handlePluginConfigUpdate 处理插件配置更新请求
|
||||
func (c *Client) handlePluginConfigUpdate(stream net.Conn, msg *protocol.Message) {
|
||||
defer stream.Close()
|
||||
|
||||
var req protocol.PluginConfigUpdateRequest
|
||||
if err := msg.ParsePayload(&req); err != nil {
|
||||
c.sendPluginConfigUpdateResult(stream, req.PluginName, req.RuleName, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
key := req.PluginName + ":" + req.RuleName
|
||||
log.Printf("[Client] Config update for plugin %s", key)
|
||||
|
||||
c.pluginMu.RLock()
|
||||
handler, ok := c.runningPlugins[key]
|
||||
c.pluginMu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
c.sendPluginConfigUpdateResult(stream, req.PluginName, req.RuleName, false, "plugin not running")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Restart {
|
||||
// 停止并重启插件
|
||||
c.pluginMu.Lock()
|
||||
if err := handler.Stop(); err != nil {
|
||||
log.Printf("[Client] Plugin %s stop error: %v", key, err)
|
||||
}
|
||||
delete(c.runningPlugins, key)
|
||||
c.pluginMu.Unlock()
|
||||
|
||||
// 重新初始化和启动
|
||||
if err := handler.Init(req.Config); err != nil {
|
||||
c.sendPluginConfigUpdateResult(stream, req.PluginName, req.RuleName, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
localAddr, err := handler.Start()
|
||||
if err != nil {
|
||||
c.sendPluginConfigUpdateResult(stream, req.PluginName, req.RuleName, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.pluginMu.Lock()
|
||||
c.runningPlugins[key] = handler
|
||||
c.pluginMu.Unlock()
|
||||
|
||||
log.Printf("[Client] Plugin %s restarted at %s with new config", key, localAddr)
|
||||
}
|
||||
|
||||
c.sendPluginConfigUpdateResult(stream, req.PluginName, req.RuleName, true, "")
|
||||
}
|
||||
|
||||
// sendPluginConfigUpdateResult 发送插件配置更新结果
|
||||
func (c *Client) sendPluginConfigUpdateResult(stream net.Conn, pluginName, ruleName string, success bool, errMsg string) {
|
||||
result := protocol.PluginConfigUpdateResponse{
|
||||
PluginName: pluginName,
|
||||
RuleName: ruleName,
|
||||
Success: success,
|
||||
Error: errMsg,
|
||||
}
|
||||
msg, _ := protocol.NewMessage(protocol.MsgTypePluginConfigUpdate, result)
|
||||
protocol.WriteMessage(stream, msg)
|
||||
}
|
||||
|
||||
@@ -18,21 +18,6 @@ type Client struct {
|
||||
Plugins []ClientPlugin `json:"plugins,omitempty"` // 已安装的插件
|
||||
}
|
||||
|
||||
// PluginData 插件数据
|
||||
type PluginData struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
Source string `json:"source"`
|
||||
Description string `json:"description"`
|
||||
Author string `json:"author"`
|
||||
Icon string `json:"icon"`
|
||||
Checksum string `json:"checksum"`
|
||||
Size int64 `json:"size"`
|
||||
Enabled bool `json:"enabled"`
|
||||
WASMData []byte `json:"-"`
|
||||
}
|
||||
|
||||
// JSPlugin JS 插件数据
|
||||
type JSPlugin struct {
|
||||
Name string `json:"name"`
|
||||
@@ -40,6 +25,7 @@ type JSPlugin struct {
|
||||
Signature string `json:"signature"` // 官方签名 (Base64)
|
||||
Description string `json:"description"`
|
||||
Author string `json:"author"`
|
||||
Version string `json:"version,omitempty"`
|
||||
AutoPush []string `json:"auto_push"`
|
||||
Config map[string]string `json:"config"`
|
||||
AutoStart bool `json:"auto_start"`
|
||||
@@ -58,16 +44,6 @@ type ClientStore interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
// PluginStore 插件存储接口
|
||||
type PluginStore interface {
|
||||
GetAllPlugins() ([]PluginData, error)
|
||||
GetPlugin(name string) (*PluginData, error)
|
||||
SavePlugin(p *PluginData) error
|
||||
DeletePlugin(name string) error
|
||||
SetPluginEnabled(name string, enabled bool) error
|
||||
GetPluginWASM(name string) ([]byte, error)
|
||||
}
|
||||
|
||||
// JSPluginStore JS 插件存储接口
|
||||
type JSPluginStore interface {
|
||||
GetAllJSPlugins() ([]JSPlugin, error)
|
||||
@@ -75,12 +51,12 @@ type JSPluginStore interface {
|
||||
SaveJSPlugin(p *JSPlugin) error
|
||||
DeleteJSPlugin(name string) error
|
||||
SetJSPluginEnabled(name string, enabled bool) error
|
||||
UpdateJSPluginConfig(name string, config map[string]string) error
|
||||
}
|
||||
|
||||
// Store 统一存储接口
|
||||
type Store interface {
|
||||
ClientStore
|
||||
PluginStore
|
||||
JSPluginStore
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -52,41 +52,21 @@ func (s *SQLiteStore) init() error {
|
||||
// 迁移:添加 plugins 列
|
||||
s.db.Exec(`ALTER TABLE clients ADD COLUMN plugins TEXT NOT NULL DEFAULT '[]'`)
|
||||
|
||||
// 创建插件表
|
||||
_, err = s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS plugins (
|
||||
name TEXT PRIMARY KEY,
|
||||
version TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'proxy',
|
||||
source TEXT NOT NULL DEFAULT 'wasm',
|
||||
description TEXT,
|
||||
author TEXT,
|
||||
icon TEXT,
|
||||
checksum TEXT,
|
||||
size INTEGER DEFAULT 0,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
wasm_data BLOB
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 迁移:添加 icon 列
|
||||
s.db.Exec(`ALTER TABLE plugins ADD COLUMN icon TEXT`)
|
||||
|
||||
// 创建 JS 插件表
|
||||
_, err = s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS js_plugins (
|
||||
name TEXT PRIMARY KEY,
|
||||
source TEXT NOT NULL,
|
||||
signature TEXT NOT NULL DEFAULT '',
|
||||
description TEXT,
|
||||
author TEXT,
|
||||
version TEXT DEFAULT '',
|
||||
auto_push TEXT NOT NULL DEFAULT '[]',
|
||||
config TEXT NOT NULL DEFAULT '',
|
||||
config TEXT NOT NULL DEFAULT '{}',
|
||||
auto_start INTEGER DEFAULT 1,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
@@ -95,6 +75,10 @@ func (s *SQLiteStore) init() error {
|
||||
|
||||
// 迁移:添加 signature 列
|
||||
s.db.Exec(`ALTER TABLE js_plugins ADD COLUMN signature TEXT NOT NULL DEFAULT ''`)
|
||||
// 迁移:添加 version 列
|
||||
s.db.Exec(`ALTER TABLE js_plugins ADD COLUMN version TEXT DEFAULT ''`)
|
||||
// 迁移:添加 updated_at 列
|
||||
s.db.Exec(`ALTER TABLE js_plugins ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP`)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -217,107 +201,6 @@ func (s *SQLiteStore) GetClientRules(id string) ([]protocol.ProxyRule, error) {
|
||||
return c.Rules, nil
|
||||
}
|
||||
|
||||
// ========== 插件存储方法 ==========
|
||||
|
||||
// GetAllPlugins 获取所有插件
|
||||
func (s *SQLiteStore) GetAllPlugins() ([]PluginData, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
rows, err := s.db.Query(`
|
||||
SELECT name, version, type, source, description, author, icon, checksum, size, enabled
|
||||
FROM plugins
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var plugins []PluginData
|
||||
for rows.Next() {
|
||||
var p PluginData
|
||||
var enabled int
|
||||
var icon sql.NullString
|
||||
err := rows.Scan(&p.Name, &p.Version, &p.Type, &p.Source,
|
||||
&p.Description, &p.Author, &icon, &p.Checksum, &p.Size, &enabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Enabled = enabled == 1
|
||||
p.Icon = icon.String
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
return plugins, nil
|
||||
}
|
||||
|
||||
// GetPlugin 获取单个插件
|
||||
func (s *SQLiteStore) GetPlugin(name string) (*PluginData, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var p PluginData
|
||||
var enabled int
|
||||
var icon sql.NullString
|
||||
err := s.db.QueryRow(`
|
||||
SELECT name, version, type, source, description, author, icon, checksum, size, enabled
|
||||
FROM plugins WHERE name = ?
|
||||
`, name).Scan(&p.Name, &p.Version, &p.Type, &p.Source,
|
||||
&p.Description, &p.Author, &icon, &p.Checksum, &p.Size, &enabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Enabled = enabled == 1
|
||||
p.Icon = icon.String
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// SavePlugin 保存插件
|
||||
func (s *SQLiteStore) SavePlugin(p *PluginData) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
enabled := 0
|
||||
if p.Enabled {
|
||||
enabled = 1
|
||||
}
|
||||
_, err := s.db.Exec(`
|
||||
INSERT OR REPLACE INTO plugins
|
||||
(name, version, type, source, description, author, icon, checksum, size, enabled, wasm_data)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, p.Name, p.Version, p.Type, p.Source, p.Description, p.Author,
|
||||
p.Icon, p.Checksum, p.Size, enabled, p.WASMData)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeletePlugin 删除插件
|
||||
func (s *SQLiteStore) DeletePlugin(name string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
_, err := s.db.Exec(`DELETE FROM plugins WHERE name = ?`, name)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetPluginEnabled 设置插件启用状态
|
||||
func (s *SQLiteStore) SetPluginEnabled(name string, enabled bool) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
val := 0
|
||||
if enabled {
|
||||
val = 1
|
||||
}
|
||||
_, err := s.db.Exec(`UPDATE plugins SET enabled = ? WHERE name = ?`, val, name)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetPluginWASM 获取插件 WASM 数据
|
||||
func (s *SQLiteStore) GetPluginWASM(name string) ([]byte, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var data []byte
|
||||
err := s.db.QueryRow(`SELECT wasm_data FROM plugins WHERE name = ?`, name).Scan(&data)
|
||||
return data, err
|
||||
}
|
||||
|
||||
// ========== JS 插件存储方法 ==========
|
||||
|
||||
// GetAllJSPlugins 获取所有 JS 插件
|
||||
@@ -326,7 +209,7 @@ func (s *SQLiteStore) GetAllJSPlugins() ([]JSPlugin, error) {
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
rows, err := s.db.Query(`
|
||||
SELECT name, source, signature, description, author, auto_push, config, auto_start, enabled
|
||||
SELECT name, source, signature, description, author, version, auto_push, config, auto_start, enabled
|
||||
FROM js_plugins
|
||||
`)
|
||||
if err != nil {
|
||||
@@ -338,12 +221,14 @@ func (s *SQLiteStore) GetAllJSPlugins() ([]JSPlugin, error) {
|
||||
for rows.Next() {
|
||||
var p JSPlugin
|
||||
var autoPushJSON, configJSON string
|
||||
var version sql.NullString
|
||||
var autoStart, enabled int
|
||||
err := rows.Scan(&p.Name, &p.Source, &p.Signature, &p.Description, &p.Author,
|
||||
&autoPushJSON, &configJSON, &autoStart, &enabled)
|
||||
&version, &autoPushJSON, &configJSON, &autoStart, &enabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Version = version.String
|
||||
json.Unmarshal([]byte(autoPushJSON), &p.AutoPush)
|
||||
json.Unmarshal([]byte(configJSON), &p.Config)
|
||||
p.AutoStart = autoStart == 1
|
||||
@@ -360,15 +245,17 @@ func (s *SQLiteStore) GetJSPlugin(name string) (*JSPlugin, error) {
|
||||
|
||||
var p JSPlugin
|
||||
var autoPushJSON, configJSON string
|
||||
var version sql.NullString
|
||||
var autoStart, enabled int
|
||||
err := s.db.QueryRow(`
|
||||
SELECT name, source, signature, description, author, auto_push, config, auto_start, enabled
|
||||
SELECT name, source, signature, description, author, version, auto_push, config, auto_start, enabled
|
||||
FROM js_plugins WHERE name = ?
|
||||
`, name).Scan(&p.Name, &p.Source, &p.Signature, &p.Description, &p.Author,
|
||||
&autoPushJSON, &configJSON, &autoStart, &enabled)
|
||||
&version, &autoPushJSON, &configJSON, &autoStart, &enabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Version = version.String
|
||||
json.Unmarshal([]byte(autoPushJSON), &p.AutoPush)
|
||||
json.Unmarshal([]byte(configJSON), &p.Config)
|
||||
p.AutoStart = autoStart == 1
|
||||
@@ -393,9 +280,9 @@ func (s *SQLiteStore) SaveJSPlugin(p *JSPlugin) error {
|
||||
|
||||
_, err := s.db.Exec(`
|
||||
INSERT OR REPLACE INTO js_plugins
|
||||
(name, source, signature, description, author, auto_push, config, auto_start, enabled)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, p.Name, p.Source, p.Signature, p.Description, p.Author,
|
||||
(name, source, signature, description, author, version, auto_push, config, auto_start, enabled, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
`, p.Name, p.Source, p.Signature, p.Description, p.Author, p.Version,
|
||||
string(autoPushJSON), string(configJSON), autoStart, enabled)
|
||||
return err
|
||||
}
|
||||
@@ -416,6 +303,15 @@ func (s *SQLiteStore) SetJSPluginEnabled(name string, enabled bool) error {
|
||||
if enabled {
|
||||
val = 1
|
||||
}
|
||||
_, err := s.db.Exec(`UPDATE js_plugins SET enabled = ? WHERE name = ?`, val, name)
|
||||
_, err := s.db.Exec(`UPDATE js_plugins SET enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?`, val, name)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateJSPluginConfig 更新 JS 插件配置
|
||||
func (s *SQLiteStore) UpdateJSPluginConfig(name string, config map[string]string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
configJSON, _ := json.Marshal(config)
|
||||
_, err := s.db.Exec(`UPDATE js_plugins SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?`, string(configJSON), name)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/gotunnel/internal/server/config"
|
||||
"github.com/gotunnel/internal/server/db"
|
||||
"github.com/gotunnel/pkg/plugin"
|
||||
"github.com/gotunnel/pkg/protocol"
|
||||
)
|
||||
|
||||
@@ -53,6 +54,11 @@ type ServerInterface interface {
|
||||
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 插件安装请求
|
||||
@@ -135,6 +141,7 @@ func RegisterRoutes(r *Router, app AppInterface) {
|
||||
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) {
|
||||
@@ -247,6 +254,22 @@ func (h *APIHandler) handleClient(rw http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1064,3 +1087,105 @@ func (h *APIHandler) pushJSPluginToClient(rw http.ResponseWriter, pluginName, cl
|
||||
|
||||
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})
|
||||
}
|
||||
|
||||
@@ -1146,3 +1146,163 @@ func (s *Server) shouldPushToClient(autoPush []string, clientID string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RestartClient 重启客户端(通过断开连接,让客户端自动重连)
|
||||
func (s *Server) RestartClient(clientID string) error {
|
||||
s.mu.RLock()
|
||||
cs, ok := s.clients[clientID]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("client %s not found or not online", clientID)
|
||||
}
|
||||
|
||||
// 发送重启消息
|
||||
stream, err := cs.Session.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := protocol.ClientRestartRequest{
|
||||
Reason: "server requested restart",
|
||||
}
|
||||
msg, _ := protocol.NewMessage(protocol.MsgTypeClientRestart, req)
|
||||
protocol.WriteMessage(stream, msg)
|
||||
stream.Close()
|
||||
|
||||
// 等待一小段时间后断开连接
|
||||
time.AfterFunc(100*time.Millisecond, func() {
|
||||
cs.Session.Close()
|
||||
})
|
||||
|
||||
log.Printf("[Server] Restart initiated for client %s", clientID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopClientPlugin 停止客户端插件
|
||||
func (s *Server) StopClientPlugin(clientID, pluginName, ruleName string) error {
|
||||
s.mu.RLock()
|
||||
cs, ok := s.clients[clientID]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("client %s not found or not online", clientID)
|
||||
}
|
||||
|
||||
return s.sendClientPluginStop(cs.Session, pluginName, ruleName)
|
||||
}
|
||||
|
||||
// sendClientPluginStop 发送客户端插件停止命令
|
||||
func (s *Server) sendClientPluginStop(session *yamux.Session, pluginName, ruleName string) error {
|
||||
stream, err := session.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
req := protocol.ClientPluginStopRequest{
|
||||
PluginName: pluginName,
|
||||
RuleName: ruleName,
|
||||
}
|
||||
msg, err := protocol.NewMessage(protocol.MsgTypeClientPluginStop, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := protocol.WriteMessage(stream, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 等待响应
|
||||
resp, err := protocol.ReadMessage(stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Type != protocol.MsgTypeClientPluginStatus {
|
||||
return fmt.Errorf("unexpected response type: %d", resp.Type)
|
||||
}
|
||||
|
||||
var status protocol.ClientPluginStatusResponse
|
||||
if err := resp.ParsePayload(&status); err != nil {
|
||||
return err
|
||||
}
|
||||
if status.Running {
|
||||
return fmt.Errorf("plugin still running: %s", status.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestartClientPlugin 重启客户端插件
|
||||
func (s *Server) RestartClientPlugin(clientID, pluginName, ruleName string) error {
|
||||
s.mu.RLock()
|
||||
cs, ok := s.clients[clientID]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("client %s not found or not online", clientID)
|
||||
}
|
||||
|
||||
// 查找规则
|
||||
var rule *protocol.ProxyRule
|
||||
for _, r := range cs.Rules {
|
||||
if r.Name == ruleName && r.Type == pluginName {
|
||||
rule = &r
|
||||
break
|
||||
}
|
||||
}
|
||||
if rule == nil {
|
||||
return fmt.Errorf("rule %s not found for plugin %s", ruleName, pluginName)
|
||||
}
|
||||
|
||||
// 先停止
|
||||
if err := s.sendClientPluginStop(cs.Session, pluginName, ruleName); err != nil {
|
||||
log.Printf("[Server] Stop plugin warning: %v", err)
|
||||
}
|
||||
|
||||
// 再启动
|
||||
return s.sendClientPluginStart(cs.Session, *rule)
|
||||
}
|
||||
|
||||
// UpdateClientPluginConfig 更新客户端插件配置
|
||||
func (s *Server) UpdateClientPluginConfig(clientID, pluginName, ruleName string, config map[string]string, restart bool) error {
|
||||
s.mu.RLock()
|
||||
cs, ok := s.clients[clientID]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("client %s not found or not online", clientID)
|
||||
}
|
||||
|
||||
// 发送配置更新消息
|
||||
stream, err := cs.Session.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
req := protocol.PluginConfigUpdateRequest{
|
||||
PluginName: pluginName,
|
||||
RuleName: ruleName,
|
||||
Config: config,
|
||||
Restart: restart,
|
||||
}
|
||||
msg, _ := protocol.NewMessage(protocol.MsgTypePluginConfigUpdate, req)
|
||||
if err := protocol.WriteMessage(stream, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 等待响应
|
||||
resp, err := protocol.ReadMessage(stream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result protocol.PluginConfigUpdateResponse
|
||||
if err := resp.ParsePayload(&result); err != nil {
|
||||
return err
|
||||
}
|
||||
if !result.Success {
|
||||
return fmt.Errorf("config update failed: %s", result.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user