feat: remove unused plugin version comparison and types, refactor proxy server to support authentication

- Deleted version comparison logic from `pkg/plugin/sign/version.go`.
- Removed unused types and constants from `pkg/plugin/types.go`.
- Updated `pkg/protocol/message.go` to remove plugin-related message types.
- Enhanced `pkg/proxy/http.go` and `pkg/proxy/socks5.go` to include username/password authentication for HTTP and SOCKS5 proxies.
- Modified `pkg/proxy/server.go` to pass authentication parameters to server constructors.
- Added new API endpoint to generate installation commands with a token for clients.
- Created database functions to manage installation tokens in `internal/server/db/install_token.go`.
- Implemented the installation command generation logic in `internal/server/router/handler/install.go`.
- Updated web frontend to support installation command generation and display in `web/src/views/ClientsView.vue`.
This commit is contained in:
2026-03-17 23:16:30 +08:00
parent dcfd2f4466
commit 5a03d9e1f1
42 changed files with 638 additions and 6161 deletions

View File

@@ -11,13 +11,9 @@ import (
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/gotunnel/pkg/plugin"
"github.com/gotunnel/pkg/plugin/script"
"github.com/gotunnel/pkg/plugin/sign"
"github.com/gotunnel/pkg/protocol"
"github.com/gotunnel/pkg/relay"
"github.com/gotunnel/pkg/update"
@@ -38,21 +34,17 @@ const (
// Client 隧道客户端
type Client struct {
ServerAddr string
Token string
ID string
Name string // 客户端名称(主机名)
TLSEnabled bool
TLSConfig *tls.Config
DataDir string // 数据目录
session *yamux.Session
rules []protocol.ProxyRule
mu sync.RWMutex
pluginRegistry *plugin.Registry
runningPlugins map[string]plugin.ClientPlugin
versionStore *PluginVersionStore
pluginMu sync.RWMutex
logger *Logger // 日志收集器
ServerAddr string
Token string
ID string
Name string // 客户端名称(主机名)
TLSEnabled bool
TLSConfig *tls.Config
DataDir string // 数据目录
session *yamux.Session
rules []protocol.ProxyRule
mu sync.RWMutex
logger *Logger // 日志收集器
}
// NewClient 创建客户端
@@ -93,30 +85,20 @@ func NewClient(serverAddr, token, id string) *Client {
}
return &Client{
ServerAddr: serverAddr,
Token: token,
ID: id,
Name: hostname,
DataDir: dataDir,
runningPlugins: make(map[string]plugin.ClientPlugin),
logger: logger,
ServerAddr: serverAddr,
Token: token,
ID: id,
Name: hostname,
DataDir: dataDir,
logger: logger,
}
}
// InitVersionStore 初始化版本存储
func (c *Client) InitVersionStore() error {
store, err := NewPluginVersionStore(c.DataDir)
if err != nil {
return err
}
c.versionStore = store
return nil
}
// SetPluginRegistry 设置插件注册表
func (c *Client) SetPluginRegistry(registry *plugin.Registry) {
c.pluginRegistry = registry
}
// logf 安全地记录日志(同时输出到标准日志和日志收集器)
func (c *Client) logf(format string, args ...interface{}) {
@@ -261,31 +243,14 @@ 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)
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)
case protocol.MsgTypeUpdateDownload:
c.handleUpdateDownload(stream, msg)
case protocol.MsgTypeLogRequest:
go c.handleLogRequest(stream, msg)
case protocol.MsgTypeLogStop:
c.handleLogStop(stream, msg)
case protocol.MsgTypePluginStatusQuery:
c.handlePluginStatusQuery(stream, msg)
case protocol.MsgTypePluginAPIRequest:
c.handlePluginAPIRequest(stream, msg)
case protocol.MsgTypeSystemStatsRequest:
c.handleSystemStatsRequest(stream, msg)
case protocol.MsgTypeScreenshotRequest:
@@ -451,312 +416,14 @@ 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 {
c.logErrorf("Parse plugin config error: %v", err)
return
}
c.logf("Received config for plugin: %s", cfg.PluginName)
// 应用配置到插件
if c.pluginRegistry != nil {
handler, err := c.pluginRegistry.GetClient(cfg.PluginName)
if err != nil {
c.logWarnf("Plugin %s not found: %v", cfg.PluginName, err)
return
}
if err := handler.Init(cfg.Config); err != nil {
c.logErrorf("Plugin %s init error: %v", cfg.PluginName, err)
return
}
c.logf("Plugin %s config applied", cfg.PluginName)
}
}
// handleClientPluginStart 处理客户端插件启动请求
func (c *Client) handleClientPluginStart(stream net.Conn, msg *protocol.Message) {
defer stream.Close()
var req protocol.ClientPluginStartRequest
if err := msg.ParsePayload(&req); err != nil {
c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", err.Error())
return
}
c.logf("Starting plugin %s for rule %s", req.PluginName, req.RuleName)
// 获取插件
if c.pluginRegistry == nil {
c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", "plugin registry not set")
return
}
handler, err := c.pluginRegistry.GetClient(req.PluginName)
if err != nil {
c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", err.Error())
return
}
// 初始化并启动
if err := handler.Init(req.Config); err != nil {
c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", err.Error())
return
}
localAddr, err := handler.Start()
if err != nil {
c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", err.Error())
return
}
// 保存运行中的插件
key := req.PluginName + ":" + req.RuleName
c.pluginMu.Lock()
c.runningPlugins[key] = handler
c.pluginMu.Unlock()
c.logf("Plugin %s started at %s", req.PluginName, localAddr)
c.sendPluginStatus(stream, req.PluginName, req.RuleName, true, localAddr, "")
}
// sendPluginStatus 发送插件状态响应
func (c *Client) sendPluginStatus(stream net.Conn, pluginName, ruleName string, running bool, localAddr, errMsg string) {
resp := protocol.ClientPluginStatusResponse{
PluginName: pluginName,
RuleName: ruleName,
Running: running,
LocalAddr: localAddr,
Error: errMsg,
}
msg, _ := protocol.NewMessage(protocol.MsgTypeClientPluginStatus, resp)
protocol.WriteMessage(stream, msg)
}
// handleClientPluginConn 处理客户端插件连接
func (c *Client) handleClientPluginConn(stream net.Conn, msg *protocol.Message) {
var req protocol.ClientPluginConnRequest
if err := msg.ParsePayload(&req); err != nil {
stream.Close()
return
}
c.pluginMu.RLock()
var handler plugin.ClientPlugin
var ok bool
// 优先使用 PluginID 查找
if req.PluginID != "" {
handler, ok = c.runningPlugins[req.PluginID]
}
// 如果没找到,回退到 pluginName:ruleName
if !ok {
key := req.PluginName + ":" + req.RuleName
handler, ok = c.runningPlugins[key]
}
c.pluginMu.RUnlock()
if !ok {
c.logWarnf("Plugin %s (ID: %s) not running", req.PluginName, req.PluginID)
stream.Close()
return
}
// 让插件处理连接
handler.HandleConn(stream)
}
// handleJSPluginInstall 处理 JS 插件安装请求
func (c *Client) handleJSPluginInstall(stream net.Conn, msg *protocol.Message) {
defer stream.Close()
var req protocol.JSPluginInstallRequest
if err := msg.ParsePayload(&req); err != nil {
c.sendJSPluginResult(stream, "", false, err.Error())
return
}
c.logf("Installing JS plugin: %s (ID: %s)", req.PluginName, req.PluginID)
// 使用 PluginID 作为 key如果有否则回退到 pluginName:ruleName
key := req.PluginID
if key == "" {
key = req.PluginName + ":" + req.RuleName
}
// 如果插件已经在运行,先停止它
c.pluginMu.Lock()
if existingHandler, ok := c.runningPlugins[key]; ok {
c.logf("Stopping existing plugin %s before reinstall", key)
if err := existingHandler.Stop(); err != nil {
c.logErrorf("Stop existing plugin error: %v", err)
}
delete(c.runningPlugins, key)
}
c.pluginMu.Unlock()
// 验证官方签名
if err := c.verifyJSPluginSignature(req.PluginName, req.Source, req.Signature); err != nil {
c.logErrorf("JS plugin %s signature verification failed: %v", req.PluginName, err)
c.sendJSPluginResult(stream, req.PluginName, false, "signature verification failed: "+err.Error())
return
}
c.logf("JS plugin %s signature verified", req.PluginName)
// 创建 JS 插件
jsPlugin, err := script.NewJSPlugin(req.PluginName, req.Source)
if err != nil {
c.sendJSPluginResult(stream, req.PluginName, false, err.Error())
return
}
// 注册到 registry
if c.pluginRegistry != nil {
c.pluginRegistry.RegisterClient(jsPlugin)
}
c.logf("JS plugin %s installed", req.PluginName)
// 保存版本信息(防止降级攻击)
if c.versionStore != nil {
signed, _ := sign.DecodeSignedPlugin(req.Signature)
if signed != nil {
c.versionStore.SetVersion(req.PluginName, signed.Payload.Version)
}
}
// 先启动插件,再发送安装结果
// 这样服务端收到结果后启动监听器时,客户端插件已经准备好了
if req.AutoStart {
c.startJSPlugin(jsPlugin, req)
}
c.sendJSPluginResult(stream, req.PluginName, true, "")
}
// sendJSPluginResult 发送 JS 插件安装结果
func (c *Client) sendJSPluginResult(stream net.Conn, name string, success bool, errMsg string) {
result := protocol.JSPluginInstallResult{
PluginName: name,
Success: success,
Error: errMsg,
}
msg, _ := protocol.NewMessage(protocol.MsgTypeJSPluginResult, result)
protocol.WriteMessage(stream, msg)
}
// startJSPlugin 启动 JS 插件
func (c *Client) startJSPlugin(handler plugin.ClientPlugin, req protocol.JSPluginInstallRequest) {
if err := handler.Init(req.Config); err != nil {
c.logErrorf("JS plugin %s init error: %v", req.PluginName, err)
return
}
localAddr, err := handler.Start()
if err != nil {
c.logErrorf("JS plugin %s start error: %v", req.PluginName, err)
return
}
// 使用 PluginID 作为 key如果有否则回退到 pluginName:ruleName
key := req.PluginID
if key == "" {
key = req.PluginName + ":" + req.RuleName
}
c.pluginMu.Lock()
c.runningPlugins[key] = handler
c.pluginMu.Unlock()
c.logf("JS plugin %s (ID: %s) started at %s", req.PluginName, req.PluginID, localAddr)
}
// verifyJSPluginSignature 验证 JS 插件签名
func (c *Client) verifyJSPluginSignature(pluginName, source, signature string) error {
if signature == "" {
return fmt.Errorf("missing signature")
}
// 解码签名
signed, err := sign.DecodeSignedPlugin(signature)
if err != nil {
return fmt.Errorf("decode signature: %w", err)
}
// 根据 KeyID 获取对应公钥
pubKey, err := sign.GetPublicKeyByID(signed.Payload.KeyID)
if err != nil {
return fmt.Errorf("get public key: %w", err)
}
// 验证插件名称匹配
if signed.Payload.Name != pluginName {
return fmt.Errorf("plugin name mismatch: expected %s, got %s",
pluginName, signed.Payload.Name)
}
// 验证签名和源码哈希
if err := sign.VerifyPlugin(pubKey, signed, source); err != nil {
return err
}
// 检查版本降级攻击
if c.versionStore != nil {
currentVer := c.versionStore.GetVersion(pluginName)
if currentVer != "" {
cmp := sign.CompareVersions(signed.Payload.Version, currentVer)
if cmp < 0 {
return fmt.Errorf("version downgrade rejected: %s < %s",
signed.Payload.Version, currentVer)
}
}
}
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
}
c.pluginMu.Lock()
var handler plugin.ClientPlugin
var key string
var ok bool
// 优先使用 PluginID 查找
if req.PluginID != "" {
handler, ok = c.runningPlugins[req.PluginID]
if ok {
key = req.PluginID
}
}
// 如果没找到,回退到 pluginName:ruleName
if !ok {
key = req.PluginName + ":" + req.RuleName
handler, ok = c.runningPlugins[key]
}
if ok {
if err := handler.Stop(); err != nil {
c.logErrorf("Plugin %s stop error: %v", key, err)
}
delete(c.runningPlugins, key)
}
c.pluginMu.Unlock()
c.logf("Plugin %s stopped", key)
c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", "")
}
// handleClientRestart 处理客户端重启请求
func (c *Client) handleClientRestart(stream net.Conn, msg *protocol.Message) {
@@ -776,99 +443,13 @@ func (c *Client) handleClientRestart(stream net.Conn, msg *protocol.Message) {
protocol.WriteMessage(stream, respMsg)
// 停止所有运行中的插件
c.pluginMu.Lock()
for key, handler := range c.runningPlugins {
c.logf("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
}
c.pluginMu.RLock()
var handler plugin.ClientPlugin
var key string
var ok bool
// 优先使用 PluginID 查找
if req.PluginID != "" {
handler, ok = c.runningPlugins[req.PluginID]
if ok {
key = req.PluginID
}
}
// 如果没找到,回退到 pluginName:ruleName
if !ok {
key = req.PluginName + ":" + req.RuleName
handler, ok = c.runningPlugins[key]
}
c.pluginMu.RUnlock()
c.logf("Config update for plugin %s", key)
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 {
c.logErrorf("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()
c.logf("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)
}
// handleUpdateDownload 处理更新下载请求
func (c *Client) handleUpdateDownload(stream net.Conn, msg *protocol.Message) {
@@ -957,9 +538,6 @@ func (c *Client) performSelfUpdate(downloadURL string) error {
c.logf("Will install to fallback path: %s", targetPath)
}
// 停止所有插件
c.stopAllPlugins()
if fallbackDir == "" {
// 原地替换:备份 → 复制 → 清理
backupPath := currentPath + ".bak"
@@ -1030,16 +608,6 @@ func (c *Client) checkUpdatePermissions(execPath string) error {
return nil
}
// stopAllPlugins 停止所有运行中的插件
func (c *Client) stopAllPlugins() {
c.pluginMu.Lock()
for key, handler := range c.runningPlugins {
c.logf("Stopping plugin %s for update", key)
handler.Stop()
}
c.runningPlugins = make(map[string]plugin.ClientPlugin)
c.pluginMu.Unlock()
}
// performWindowsClientUpdate Windows 平台更新
func performWindowsClientUpdate(newFile, currentPath, serverAddr, token, id string) error {
@@ -1092,33 +660,6 @@ func restartClientProcess(path, serverAddr, token, id string) {
os.Exit(0)
}
// handlePluginStatusQuery 处理插件状态查询
func (c *Client) handlePluginStatusQuery(stream net.Conn, msg *protocol.Message) {
defer stream.Close()
c.pluginMu.RLock()
plugins := make([]protocol.PluginStatusEntry, 0, len(c.runningPlugins))
for key, handler := range c.runningPlugins {
// 从插件的 Metadata 获取真正的插件名称
pluginName := handler.Metadata().Name
// 如果 Metadata 没有名称,回退到从 key 解析
if pluginName == "" {
parts := strings.SplitN(key, ":", 2)
pluginName = parts[0]
}
plugins = append(plugins, protocol.PluginStatusEntry{
PluginName: pluginName,
Running: true,
})
}
c.pluginMu.RUnlock()
resp := protocol.PluginStatusQueryResponse{
Plugins: plugins,
}
respMsg, _ := protocol.NewMessage(protocol.MsgTypePluginStatusQueryResp, resp)
protocol.WriteMessage(stream, respMsg)
}
// handleLogRequest 处理日志请求
func (c *Client) handleLogRequest(stream net.Conn, msg *protocol.Message) {
@@ -1196,72 +737,7 @@ func (c *Client) handleLogStop(stream net.Conn, msg *protocol.Message) {
c.logger.Unsubscribe(req.SessionID)
}
// handlePluginAPIRequest 处理插件 API 请求
func (c *Client) handlePluginAPIRequest(stream net.Conn, msg *protocol.Message) {
defer stream.Close()
var req protocol.PluginAPIRequest
if err := msg.ParsePayload(&req); err != nil {
c.sendPluginAPIResponse(stream, 400, nil, "", "invalid request: "+err.Error())
return
}
c.logf("Plugin API request: %s %s for plugin %s (ID: %s)", req.Method, req.Path, req.PluginName, req.PluginID)
// 查找运行中的插件
c.pluginMu.RLock()
var handler plugin.ClientPlugin
// 优先使用 PluginID 查找
if req.PluginID != "" {
handler = c.runningPlugins[req.PluginID]
}
// 如果没找到,尝试通过 PluginName 匹配(向后兼容)
if handler == nil && req.PluginName != "" {
for key, p := range c.runningPlugins {
// key 可能是 PluginID 或 "pluginName:ruleName" 格式
if strings.HasPrefix(key, req.PluginName+":") {
handler = p
break
}
}
}
c.pluginMu.RUnlock()
if handler == nil {
c.sendPluginAPIResponse(stream, 404, nil, "", "plugin not running: "+req.PluginName)
return
}
// 类型断言为 JSPlugin
jsPlugin, ok := handler.(*script.JSPlugin)
if !ok {
c.sendPluginAPIResponse(stream, 500, nil, "", "plugin does not support API routing")
return
}
// 调用插件的 API 处理函数
status, headers, body, err := jsPlugin.HandleAPIRequest(req.Method, req.Path, req.Query, req.Headers, req.Body)
if err != nil {
c.sendPluginAPIResponse(stream, 500, nil, "", err.Error())
return
}
c.sendPluginAPIResponse(stream, status, headers, body, "")
}
// sendPluginAPIResponse 发送插件 API 响应
func (c *Client) sendPluginAPIResponse(stream net.Conn, status int, headers map[string]string, body, errMsg string) {
resp := protocol.PluginAPIResponse{
Status: status,
Headers: headers,
Body: body,
Error: errMsg,
}
msg, _ := protocol.NewMessage(protocol.MsgTypePluginAPIResponse, resp)
protocol.WriteMessage(stream, msg)
}
// handleSystemStatsRequest 处理系统状态请求
func (c *Client) handleSystemStatsRequest(stream net.Conn, msg *protocol.Message) {

View File

@@ -16,23 +16,21 @@ var staticFiles embed.FS
// WebServer Web控制台服务
type WebServer struct {
ClientStore db.ClientStore
Server router.ServerInterface
Config *config.ServerConfig
ConfigPath string
JSPluginStore db.JSPluginStore
TrafficStore db.TrafficStore
ClientStore db.ClientStore
Server router.ServerInterface
Config *config.ServerConfig
ConfigPath string
TrafficStore db.TrafficStore
}
// NewWebServer 创建Web服务
func NewWebServer(cs db.ClientStore, srv router.ServerInterface, cfg *config.ServerConfig, cfgPath string, store db.Store) *WebServer {
return &WebServer{
ClientStore: cs,
Server: srv,
Config: cfg,
ConfigPath: cfgPath,
JSPluginStore: store,
TrafficStore: store,
ClientStore: cs,
Server: srv,
Config: cfg,
ConfigPath: cfgPath,
TrafficStore: store,
}
}
@@ -107,11 +105,6 @@ func (w *WebServer) SaveConfig() error {
return config.SaveServerConfig(w.ConfigPath, w.Config)
}
// GetJSPluginStore 获取 JS 插件存储
func (w *WebServer) GetJSPluginStore() db.JSPluginStore {
return w.JSPluginStore
}
// GetTrafficStore 获取流量存储
func (w *WebServer) GetTrafficStore() db.TrafficStore {
return w.TrafficStore

View File

@@ -13,33 +13,16 @@ type ServerConfig struct {
Server ServerSettings `yaml:"server"`
}
// PluginStoreSettings 插件仓库设置
type PluginStoreSettings struct {
URL string `yaml:"url"` // 插件仓库 URL为空则使用默认值
}
// 默认插件仓库 URL
const DefaultPluginStoreURL = "https://git.92coco.cn/flik/GoTunnel-Plugins/raw/branch/main/store.json"
// GetPluginStoreURL 获取插件仓库 URL
func (s *PluginStoreSettings) GetPluginStoreURL() string {
if s.URL != "" {
return s.URL
}
return DefaultPluginStoreURL
}
// ServerSettings 服务端设置
type ServerSettings struct {
BindAddr string `yaml:"bind_addr"`
BindPort int `yaml:"bind_port"`
Token string `yaml:"token"`
HeartbeatSec int `yaml:"heartbeat_sec"`
HeartbeatTimeout int `yaml:"heartbeat_timeout"`
DBPath string `yaml:"db_path"`
TLSDisabled bool `yaml:"tls_disabled"`
Web WebSettings `yaml:"web"`
PluginStore PluginStoreSettings `yaml:"plugin_store"`
BindAddr string `yaml:"bind_addr"`
BindPort int `yaml:"bind_port"`
Token string `yaml:"token"`
HeartbeatSec int `yaml:"heartbeat_sec"`
HeartbeatTimeout int `yaml:"heartbeat_timeout"`
DBPath string `yaml:"db_path"`
TLSDisabled bool `yaml:"tls_disabled"`
Web WebSettings `yaml:"web"`
}
// WebSettings Web控制台设置

View File

@@ -0,0 +1,45 @@
package db
// CreateInstallToken 创建安装token
func (s *SQLiteStore) CreateInstallToken(token *InstallToken) error {
s.mu.Lock()
defer s.mu.Unlock()
_, err := s.db.Exec(`INSERT INTO install_tokens (token, client_id, created_at, used) VALUES (?, ?, ?, ?)`,
token.Token, token.ClientID, token.CreatedAt, 0)
return err
}
// GetInstallToken 获取安装token
func (s *SQLiteStore) GetInstallToken(token string) (*InstallToken, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var t InstallToken
var used int
err := s.db.QueryRow(`SELECT token, client_id, created_at, used FROM install_tokens WHERE token = ?`, token).
Scan(&t.Token, &t.ClientID, &t.CreatedAt, &used)
if err != nil {
return nil, err
}
t.Used = used == 1
return &t, nil
}
// MarkTokenUsed 标记token已使用
func (s *SQLiteStore) MarkTokenUsed(token string) error {
s.mu.Lock()
defer s.mu.Unlock()
_, err := s.db.Exec(`UPDATE install_tokens SET used = 1 WHERE token = ?`, token)
return err
}
// DeleteExpiredTokens 删除过期token
func (s *SQLiteStore) DeleteExpiredTokens(expireTime int64) error {
s.mu.Lock()
defer s.mu.Unlock()
_, err := s.db.Exec(`DELETE FROM install_tokens WHERE created_at < ?`, expireTime)
return err
}

View File

@@ -2,52 +2,11 @@ package db
import "github.com/gotunnel/pkg/protocol"
// ConfigField 配置字段定义
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"`
}
// ClientPlugin 客户端已安装的插件
type ClientPlugin struct {
ID string `json:"id"` // 插件实例唯一 ID
Name string `json:"name"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
Running bool `json:"running"` // 运行状态
Config map[string]string `json:"config,omitempty"` // 插件配置
RemotePort int `json:"remote_port,omitempty"` // 远程监听端口
ConfigSchema []ConfigField `json:"config_schema,omitempty"` // 配置模式
AuthEnabled bool `json:"auth_enabled,omitempty"` // 是否启用认证
AuthUsername string `json:"auth_username,omitempty"` // 认证用户名
AuthPassword string `json:"auth_password,omitempty"` // 认证密码
}
// Client 客户端数据
type Client struct {
ID string `json:"id"`
Nickname string `json:"nickname,omitempty"`
Rules []protocol.ProxyRule `json:"rules"`
Plugins []ClientPlugin `json:"plugins,omitempty"` // 已安装的插件
}
// JSPlugin JS 插件数据
type JSPlugin struct {
Name string `json:"name"`
Source string `json:"source"`
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"`
Enabled bool `json:"enabled"`
}
// ClientStore 客户端存储接口
@@ -62,20 +21,9 @@ type ClientStore interface {
Close() error
}
// JSPluginStore JS 插件存储接口
type JSPluginStore interface {
GetAllJSPlugins() ([]JSPlugin, error)
GetJSPlugin(name string) (*JSPlugin, error)
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
JSPluginStore
TrafficStore
Close() error
}
@@ -94,3 +42,19 @@ type TrafficStore interface {
Get24HourTraffic() (inbound, outbound int64, err error)
GetHourlyTraffic(hours int) ([]TrafficRecord, error)
}
// InstallToken 安装token
type InstallToken struct {
Token string `json:"token"`
ClientID string `json:"client_id"`
CreatedAt int64 `json:"created_at"`
Used bool `json:"used"`
}
// InstallTokenStore 安装token存储接口
type InstallTokenStore interface {
CreateInstallToken(token *InstallToken) error
GetInstallToken(token string) (*InstallToken, error)
MarkTokenUsed(token string) error
DeleteExpiredTokens(expireTime int64) error
}

View File

@@ -40,8 +40,7 @@ func (s *SQLiteStore) init() error {
CREATE TABLE IF NOT EXISTS clients (
id TEXT PRIMARY KEY,
nickname TEXT NOT NULL DEFAULT '',
rules TEXT NOT NULL DEFAULT '[]',
plugins TEXT NOT NULL DEFAULT '[]'
rules TEXT NOT NULL DEFAULT '[]'
)
`)
if err != nil {
@@ -50,36 +49,6 @@ func (s *SQLiteStore) init() error {
// 迁移:添加 nickname 列
s.db.Exec(`ALTER TABLE clients ADD COLUMN nickname TEXT NOT NULL DEFAULT ''`)
// 迁移:添加 plugins 列
s.db.Exec(`ALTER TABLE clients ADD COLUMN plugins TEXT NOT NULL DEFAULT '[]'`)
// 创建 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 '{}',
auto_start INTEGER DEFAULT 1,
enabled INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return err
}
// 迁移:添加 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`)
// 创建流量统计表
_, err = s.db.Exec(`
@@ -108,6 +77,19 @@ func (s *SQLiteStore) init() error {
// 初始化总流量记录
s.db.Exec(`INSERT OR IGNORE INTO traffic_total (id, inbound, outbound) VALUES (1, 0, 0)`)
// 创建安装token表
_, err = s.db.Exec(`
CREATE TABLE IF NOT EXISTS install_tokens (
token TEXT PRIMARY KEY,
client_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
used INTEGER NOT NULL DEFAULT 0
)
`)
if err != nil {
return err
}
return nil
}
@@ -121,7 +103,7 @@ func (s *SQLiteStore) GetAllClients() ([]Client, error) {
s.mu.RLock()
defer s.mu.RUnlock()
rows, err := s.db.Query(`SELECT id, nickname, rules, plugins FROM clients`)
rows, err := s.db.Query(`SELECT id, nickname, rules FROM clients`)
if err != nil {
return nil, err
}
@@ -130,16 +112,13 @@ func (s *SQLiteStore) GetAllClients() ([]Client, error) {
var clients []Client
for rows.Next() {
var c Client
var rulesJSON, pluginsJSON string
if err := rows.Scan(&c.ID, &c.Nickname, &rulesJSON, &pluginsJSON); err != nil {
var rulesJSON string
if err := rows.Scan(&c.ID, &c.Nickname, &rulesJSON); err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(rulesJSON), &c.Rules); err != nil {
c.Rules = []protocol.ProxyRule{}
}
if err := json.Unmarshal([]byte(pluginsJSON), &c.Plugins); err != nil {
c.Plugins = []ClientPlugin{}
}
clients = append(clients, c)
}
return clients, nil
@@ -151,17 +130,14 @@ func (s *SQLiteStore) GetClient(id string) (*Client, error) {
defer s.mu.RUnlock()
var c Client
var rulesJSON, pluginsJSON string
err := s.db.QueryRow(`SELECT id, nickname, rules, plugins FROM clients WHERE id = ?`, id).Scan(&c.ID, &c.Nickname, &rulesJSON, &pluginsJSON)
var rulesJSON string
err := s.db.QueryRow(`SELECT id, nickname, rules FROM clients WHERE id = ?`, id).Scan(&c.ID, &c.Nickname, &rulesJSON)
if err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(rulesJSON), &c.Rules); err != nil {
c.Rules = []protocol.ProxyRule{}
}
if err := json.Unmarshal([]byte(pluginsJSON), &c.Plugins); err != nil {
c.Plugins = []ClientPlugin{}
}
return &c, nil
}
@@ -174,12 +150,8 @@ func (s *SQLiteStore) CreateClient(c *Client) error {
if err != nil {
return err
}
pluginsJSON, err := json.Marshal(c.Plugins)
if err != nil {
return err
}
_, err = s.db.Exec(`INSERT INTO clients (id, nickname, rules, plugins) VALUES (?, ?, ?, ?)`,
c.ID, c.Nickname, string(rulesJSON), string(pluginsJSON))
_, err = s.db.Exec(`INSERT INTO clients (id, nickname, rules) VALUES (?, ?, ?)`,
c.ID, c.Nickname, string(rulesJSON))
return err
}
@@ -192,12 +164,8 @@ func (s *SQLiteStore) UpdateClient(c *Client) error {
if err != nil {
return err
}
pluginsJSON, err := json.Marshal(c.Plugins)
if err != nil {
return err
}
_, err = s.db.Exec(`UPDATE clients SET nickname = ?, rules = ?, plugins = ? WHERE id = ?`,
c.Nickname, string(rulesJSON), string(pluginsJSON), c.ID)
_, err = s.db.Exec(`UPDATE clients SET nickname = ?, rules = ? WHERE id = ?`,
c.Nickname, string(rulesJSON), c.ID)
return err
}
@@ -229,121 +197,6 @@ func (s *SQLiteStore) GetClientRules(id string) ([]protocol.ProxyRule, error) {
return c.Rules, nil
}
// ========== JS 插件存储方法 ==========
// GetAllJSPlugins 获取所有 JS 插件
func (s *SQLiteStore) GetAllJSPlugins() ([]JSPlugin, error) {
s.mu.RLock()
defer s.mu.RUnlock()
rows, err := s.db.Query(`
SELECT name, source, signature, description, author, version, auto_push, config, auto_start, enabled
FROM js_plugins
`)
if err != nil {
return nil, err
}
defer rows.Close()
var plugins []JSPlugin
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,
&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
p.Enabled = enabled == 1
plugins = append(plugins, p)
}
return plugins, nil
}
// GetJSPlugin 获取单个 JS 插件
func (s *SQLiteStore) GetJSPlugin(name string) (*JSPlugin, error) {
s.mu.RLock()
defer s.mu.RUnlock()
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, version, auto_push, config, auto_start, enabled
FROM js_plugins WHERE name = ?
`, name).Scan(&p.Name, &p.Source, &p.Signature, &p.Description, &p.Author,
&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
p.Enabled = enabled == 1
return &p, nil
}
// SaveJSPlugin 保存 JS 插件
func (s *SQLiteStore) SaveJSPlugin(p *JSPlugin) error {
s.mu.Lock()
defer s.mu.Unlock()
autoPushJSON, _ := json.Marshal(p.AutoPush)
configJSON, _ := json.Marshal(p.Config)
autoStart, enabled := 0, 0
if p.AutoStart {
autoStart = 1
}
if p.Enabled {
enabled = 1
}
_, err := s.db.Exec(`
INSERT OR REPLACE INTO js_plugins
(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
}
// DeleteJSPlugin 删除 JS 插件
func (s *SQLiteStore) DeleteJSPlugin(name string) error {
s.mu.Lock()
defer s.mu.Unlock()
_, err := s.db.Exec(`DELETE FROM js_plugins WHERE name = ?`, name)
return err
}
// SetJSPluginEnabled 设置 JS 插件启用状态
func (s *SQLiteStore) SetJSPluginEnabled(name string, enabled bool) error {
s.mu.Lock()
defer s.mu.Unlock()
val := 0
if enabled {
val = 1
}
_, 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
}
// ========== 流量统计方法 ==========
// getHourTimestamp 获取当前小时的时间戳

View File

@@ -1,34 +0,0 @@
package plugin
import (
"sync"
"github.com/gotunnel/pkg/plugin"
)
// Manager 服务端 plugin 管理器
type Manager struct {
registry *plugin.Registry
mu sync.RWMutex
}
// NewManager 创建 plugin 管理器
func NewManager() (*Manager, error) {
registry := plugin.NewRegistry()
m := &Manager{
registry: registry,
}
return m, nil
}
// ListPlugins 返回所有插件
func (m *Manager) ListPlugins() []plugin.Info {
return m.registry.List()
}
// GetRegistry 返回插件注册表
func (m *Manager) GetRegistry() *plugin.Registry {
return m.registry
}

View File

@@ -1,7 +1,6 @@
package dto
import (
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/pkg/protocol"
)
@@ -17,7 +16,6 @@ type CreateClientRequest struct {
type UpdateClientRequest struct {
Nickname string `json:"nickname" binding:"max=128" example:"My Client"`
Rules []protocol.ProxyRule `json:"rules"`
Plugins []db.ClientPlugin `json:"plugins"`
}
// ClientResponse 客户端详情响应
@@ -26,7 +24,6 @@ type ClientResponse struct {
ID string `json:"id" example:"client-001"`
Nickname string `json:"nickname,omitempty" example:"My Client"`
Rules []protocol.ProxyRule `json:"rules"`
Plugins []db.ClientPlugin `json:"plugins,omitempty"`
Online bool `json:"online" example:"true"`
LastPing string `json:"last_ping,omitempty" example:"2025-01-02T10:30:00Z"`
RemoteAddr string `json:"remote_addr,omitempty" example:"192.168.1.100:54321"`
@@ -47,17 +44,3 @@ type ClientListItem struct {
OS string `json:"os,omitempty" example:"linux"`
Arch string `json:"arch,omitempty" example:"amd64"`
}
// InstallPluginsRequest 安装插件到客户端请求
// @Description 安装插件到指定客户端
type InstallPluginsRequest struct {
Plugins []string `json:"plugins" binding:"required,min=1,dive,required" example:"socks5,http-proxy"`
}
// ClientPluginActionRequest 客户端插件操作请求
// @Description 对客户端插件执行操作
type ClientPluginActionRequest struct {
RuleName string `json:"rule_name"`
Config map[string]string `json:"config,omitempty"`
Restart bool `json:"restart"`
}

View File

@@ -1,119 +0,0 @@
package dto
// PluginConfigRequest 更新插件配置请求
// @Description 更新客户端插件配置
type PluginConfigRequest struct {
Config map[string]string `json:"config" binding:"required"`
}
// PluginConfigResponse 插件配置响应
// @Description 插件配置详情
type PluginConfigResponse struct {
PluginName string `json:"plugin_name"`
Schema []ConfigField `json:"schema"`
Config map[string]string `json:"config"`
}
// ConfigField 配置字段定义
// @Description 配置表单字段
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 规则表单模式
// @Description 代理规则的配置模式
type RuleSchema struct {
NeedsLocalAddr bool `json:"needs_local_addr"`
ExtraFields []ConfigField `json:"extra_fields,omitempty"`
}
// PluginInfo 插件信息
// @Description 服务端插件信息
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"`
}
// JSPluginCreateRequest 创建 JS 插件请求
// @Description 创建新的 JS 插件
type JSPluginCreateRequest struct {
Name string `json:"name" binding:"required,min=1,max=64"`
Source string `json:"source" binding:"required"`
Signature string `json:"signature"`
Description string `json:"description" binding:"max=500"`
Author string `json:"author" binding:"max=64"`
Config map[string]string `json:"config"`
AutoStart bool `json:"auto_start"`
}
// JSPluginUpdateRequest 更新 JS 插件请求
// @Description 更新 JS 插件
type JSPluginUpdateRequest struct {
Source string `json:"source"`
Signature string `json:"signature"`
Description string `json:"description" binding:"max=500"`
Author string `json:"author" binding:"max=64"`
Config map[string]string `json:"config"`
AutoStart bool `json:"auto_start"`
Enabled bool `json:"enabled"`
}
// JSPluginInstallRequest JS 插件安装请求
// @Description 安装 JS 插件到客户端
type JSPluginInstallRequest struct {
PluginName string `json:"plugin_name" binding:"required"`
Source string `json:"source" binding:"required"`
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"`
}
// StorePluginInfo 扩展商店插件信息
// @Description 插件商店中的插件信息
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"`
ConfigSchema []ConfigField `json:"config_schema,omitempty"`
}
// StoreInstallRequest 从商店安装插件请求
// @Description 从插件商店安装插件到客户端
type StoreInstallRequest struct {
PluginName string `json:"plugin_name" binding:"required"`
Version string `json:"version"`
DownloadURL string `json:"download_url" binding:"required,url"`
SignatureURL string `json:"signature_url" binding:"required,url"`
ClientID string `json:"client_id" binding:"required"`
RemotePort int `json:"remote_port"`
ConfigSchema []ConfigField `json:"config_schema,omitempty"`
// HTTP Basic Auth 配置
AuthEnabled bool `json:"auth_enabled,omitempty"`
AuthUsername string `json:"auth_username,omitempty"`
AuthPassword string `json:"auth_password,omitempty"`
}
// JSPluginPushRequest 推送 JS 插件到客户端请求
// @Description 推送 JS 插件到指定客户端
type JSPluginPushRequest struct {
RemotePort int `json:"remote_port"`
}

View File

@@ -6,7 +6,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/internal/server/router/dto"
"github.com/gotunnel/pkg/protocol"
)
// ClientHandler 客户端处理器
@@ -117,34 +116,6 @@ func (h *ClientHandler) Get(c *gin.Context) {
online, lastPing, remoteAddr, clientName, clientOS, clientArch, clientVersion := h.app.GetServer().GetClientStatus(clientID)
// 复制插件列表
plugins := make([]db.ClientPlugin, len(client.Plugins))
copy(plugins, client.Plugins)
// 如果客户端在线,获取实时插件运行状态
if online {
if statusList, err := h.app.GetServer().GetClientPluginStatus(clientID); err == nil {
// 创建运行中插件的映射
runningPlugins := make(map[string]bool)
for _, s := range statusList {
runningPlugins[s.PluginName] = s.Running
}
// 更新插件状态
for i := range plugins {
if running, ok := runningPlugins[plugins[i].Name]; ok {
plugins[i].Running = running
} else {
plugins[i].Running = false
}
}
}
} else {
// 客户端离线时,所有插件都标记为未运行
for i := range plugins {
plugins[i].Running = false
}
}
// 如果客户端在线且有名称,优先使用在线名称
nickname := client.Nickname
if online && clientName != "" && nickname == "" {
@@ -155,7 +126,6 @@ func (h *ClientHandler) Get(c *gin.Context) {
ID: client.ID,
Nickname: nickname,
Rules: client.Rules,
Plugins: plugins,
Online: online,
LastPing: lastPing,
RemoteAddr: remoteAddr,
@@ -196,9 +166,6 @@ func (h *ClientHandler) Update(c *gin.Context) {
client.Nickname = req.Nickname
client.Rules = req.Rules
if req.Plugins != nil {
client.Plugins = req.Plugins
}
if err := h.app.GetClientStore().UpdateClient(client); err != nil {
InternalError(c, err.Error())
@@ -301,159 +268,12 @@ func (h *ClientHandler) Restart(c *gin.Context) {
SuccessWithMessage(c, gin.H{"status": "ok"}, "client restart initiated")
}
// InstallPlugins 安装插件到客户端
// @Summary 安装插件
// @Description 将指定插件安装到客户端
// @Tags 客户端
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "客户端ID"
// @Param request body dto.InstallPluginsRequest true "插件列表"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/client/{id}/install-plugins [post]
func (h *ClientHandler) InstallPlugins(c *gin.Context) {
clientID := c.Param("id")
if !h.app.GetServer().IsClientOnline(clientID) {
ClientNotOnline(c)
return
}
var req dto.InstallPluginsRequest
if !BindJSON(c, &req) {
return
}
if err := h.app.GetServer().InstallPluginsToClient(clientID, req.Plugins); err != nil {
InternalError(c, err.Error())
return
}
Success(c, gin.H{"status": "ok"})
}
// PluginAction 客户端插件操作
// @Summary 插件操作
// @Description 对客户端插件执行操作(start/stop/restart/config/delete)
// @Tags 客户端
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "客户端ID"
// @Param pluginID path string true "插件实例ID"
// @Param action path string true "操作类型" Enums(start, stop, restart, config, delete)
// @Param request body dto.ClientPluginActionRequest false "操作参数"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/client/{id}/plugin/{pluginID}/{action} [post]
func (h *ClientHandler) PluginAction(c *gin.Context) {
clientID := c.Param("id")
pluginID := c.Param("pluginID")
action := c.Param("action")
var req dto.ClientPluginActionRequest
c.ShouldBindJSON(&req) // 忽略错误,使用默认值
// 通过 pluginID 查找插件信息
client, err := h.app.GetClientStore().GetClient(clientID)
if err != nil {
NotFound(c, "client not found")
return
}
var pluginName string
for _, p := range client.Plugins {
if p.ID == pluginID {
pluginName = p.Name
break
}
}
if pluginName == "" {
NotFound(c, "plugin not found")
return
}
if req.RuleName == "" {
req.RuleName = pluginName
}
switch action {
case "start":
err = h.app.GetServer().StartClientPlugin(clientID, pluginID, pluginName, req.RuleName)
case "stop":
err = h.app.GetServer().StopClientPlugin(clientID, pluginID, pluginName, req.RuleName)
case "restart":
err = h.app.GetServer().RestartClientPlugin(clientID, pluginID, pluginName, req.RuleName)
case "config":
if req.Config == nil {
BadRequest(c, "config required")
return
}
err = h.app.GetServer().UpdateClientPluginConfig(clientID, pluginID, pluginName, req.RuleName, req.Config, req.Restart)
case "delete":
err = h.deleteClientPlugin(clientID, pluginID)
default:
BadRequest(c, "unknown action: "+action)
return
}
if err != nil {
InternalError(c, err.Error())
return
}
Success(c, gin.H{
"status": "ok",
"action": action,
"plugin_id": pluginID,
"plugin": pluginName,
})
}
func (h *ClientHandler) deleteClientPlugin(clientID, pluginID string) error {
client, err := h.app.GetClientStore().GetClient(clientID)
if err != nil {
return fmt.Errorf("client not found")
}
var newPlugins []db.ClientPlugin
var pluginName string
var pluginPort int
found := false
for _, p := range client.Plugins {
if p.ID == pluginID {
found = true
pluginName = p.Name
pluginPort = p.RemotePort
continue
}
newPlugins = append(newPlugins, p)
}
if !found {
return fmt.Errorf("plugin %s not found", pluginID)
}
// 删除插件管理的代理规则
var newRules []protocol.ProxyRule
for _, r := range client.Rules {
if r.PluginManaged && r.Name == pluginName {
continue // 跳过此插件的规则
}
newRules = append(newRules, r)
}
// 停止端口监听器
if pluginPort > 0 {
h.app.GetServer().StopPluginRule(clientID, pluginPort)
}
client.Plugins = newPlugins
client.Rules = newRules
return h.app.GetClientStore().UpdateClient(client)
}
// GetSystemStats 获取客户端系统状态
func (h *ClientHandler) GetSystemStats(c *gin.Context) {

View File

@@ -47,9 +47,6 @@ func (h *ConfigHandler) Get(c *gin.Context) {
Username: cfg.Server.Web.Username,
Password: "****",
},
PluginStore: dto.PluginStoreConfigInfo{
URL: cfg.Server.PluginStore.URL,
},
}
Success(c, resp)
@@ -103,11 +100,6 @@ func (h *ConfigHandler) Update(c *gin.Context) {
cfg.Server.Web.Password = req.Web.Password
}
// 更新 PluginStore 配置
if req.PluginStore != nil {
cfg.Server.PluginStore.URL = req.PluginStore.URL
}
if err := h.app.SaveConfig(); err != nil {
InternalError(c, err.Error())
return

View File

@@ -0,0 +1,108 @@
package handler
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/gotunnel/internal/server/db"
)
// InstallHandler 安装处理器
type InstallHandler struct {
app AppInterface
}
// NewInstallHandler 创建安装处理器
func NewInstallHandler(app AppInterface) *InstallHandler {
return &InstallHandler{app: app}
}
// GenerateInstallCommandRequest 生成安装命令请求
type GenerateInstallCommandRequest struct {
ClientID string `json:"client_id" binding:"required"`
}
// InstallCommandResponse 安装命令响应
type InstallCommandResponse struct {
Token string `json:"token"`
Commands map[string]string `json:"commands"`
ExpiresAt int64 `json:"expires_at"`
ServerAddr string `json:"server_addr"`
}
// GenerateInstallCommand 生成安装命令
// @Summary 生成客户端安装命令
// @Tags install
// @Accept json
// @Produce json
// @Param body body GenerateInstallCommandRequest true "客户端ID"
// @Success 200 {object} InstallCommandResponse
// @Router /api/install/generate [post]
func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) {
var req GenerateInstallCommandRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 生成随机token
tokenBytes := make([]byte, 32)
if _, err := rand.Read(tokenBytes); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"})
return
}
token := hex.EncodeToString(tokenBytes)
// 保存到数据库
now := time.Now().Unix()
installToken := &db.InstallToken{
Token: token,
ClientID: req.ClientID,
CreatedAt: now,
Used: false,
}
store, ok := h.app.GetClientStore().(db.InstallTokenStore)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "存储不支持安装token"})
return
}
if err := store.CreateInstallToken(installToken); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存token失败"})
return
}
// 获取服务器地址
serverAddr := fmt.Sprintf("%s:%d", h.app.GetConfig().Server.BindAddr, h.app.GetServer().GetBindPort())
if h.app.GetConfig().Server.BindAddr == "" || h.app.GetConfig().Server.BindAddr == "0.0.0.0" {
serverAddr = fmt.Sprintf("your-server-ip:%d", h.app.GetServer().GetBindPort())
}
// 生成安装命令
expiresAt := now + 3600 // 1小时过期
tlsFlag := ""
if h.app.GetConfig().Server.TLSDisabled {
tlsFlag = " -no-tls"
}
commands := map[string]string{
"linux": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s -id %s%s",
serverAddr, token, req.ClientID, tlsFlag),
"macos": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s -id %s%s",
serverAddr, token, req.ClientID, tlsFlag),
"windows": fmt.Sprintf("powershell -c \"irm https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.ps1 | iex; Install-GoTunnel -Server '%s' -Token '%s' -ClientID '%s'%s\"",
serverAddr, token, req.ClientID, tlsFlag),
}
c.JSON(http.StatusOK, InstallCommandResponse{
Token: token,
Commands: commands,
ExpiresAt: expiresAt,
ServerAddr: serverAddr,
})
}

View File

@@ -13,7 +13,6 @@ type AppInterface interface {
GetConfig() *config.ServerConfig
GetConfigPath() string
SaveConfig() error
GetJSPluginStore() db.JSPluginStore
GetTrafficStore() db.TrafficStore
}
@@ -35,32 +34,13 @@ type ServerInterface interface {
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
InstallJSPluginToClient(clientID string, req JSPluginInstallRequest) error
RestartClient(clientID string) error
StartClientPlugin(clientID, pluginID, pluginName, ruleName string) error
StopClientPlugin(clientID, pluginID, pluginName, ruleName string) error
RestartClientPlugin(clientID, pluginID, pluginName, ruleName string) error
UpdateClientPluginConfig(clientID, pluginID, pluginName, ruleName string, config map[string]string, restart bool) error
SendUpdateToClient(clientID, downloadURL string) error
// 日志流
StartClientLogStream(clientID, sessionID string, lines int, follow bool, level string) (<-chan protocol.LogEntry, error)
StopClientLogStream(sessionID string)
// 插件状态查询
GetClientPluginStatus(clientID string) ([]protocol.PluginStatusEntry, error)
// 插件规则管理
StartPluginRule(clientID string, rule protocol.ProxyRule) error
StopPluginRule(clientID string, remotePort int) error
// 端口检查
IsPortAvailable(port int, excludeClientID string) bool
// 插件 API 代理
ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error)
// 系统状态
// 系统状态
GetClientSystemStats(clientID string) (*protocol.SystemStatsResponse, error)
// 截图
@@ -68,44 +48,3 @@ type ServerInterface interface {
// Shell 执行
ExecuteClientShell(clientID, command string, timeout int) (*protocol.ShellExecuteResponse, error)
}
// ConfigField 配置字段
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"`
}
// JSPluginInstallRequest JS 插件安装请求
type JSPluginInstallRequest struct {
PluginID string `json:"plugin_id"`
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"`
}

View File

@@ -1,219 +0,0 @@
package handler
import (
"github.com/gin-gonic/gin"
"github.com/gotunnel/internal/server/db"
// removed router import
"github.com/gotunnel/internal/server/router/dto"
)
// JSPluginHandler JS 插件处理器
type JSPluginHandler struct {
app AppInterface
}
// NewJSPluginHandler 创建 JS 插件处理器
func NewJSPluginHandler(app AppInterface) *JSPluginHandler {
return &JSPluginHandler{app: app}
}
// List 获取 JS 插件列表
// @Summary 获取所有 JS 插件
// @Description 返回所有注册的 JS 插件
// @Tags JS插件
// @Produce json
// @Security Bearer
// @Success 200 {object} Response{data=[]db.JSPlugin}
// @Router /api/js-plugins [get]
func (h *JSPluginHandler) List(c *gin.Context) {
plugins, err := h.app.GetJSPluginStore().GetAllJSPlugins()
if err != nil {
InternalError(c, err.Error())
return
}
if plugins == nil {
plugins = []db.JSPlugin{}
}
Success(c, plugins)
}
// Create 创建 JS 插件
// @Summary 创建 JS 插件
// @Description 创建新的 JS 插件
// @Tags JS插件
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body dto.JSPluginCreateRequest true "插件信息"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/js-plugins [post]
func (h *JSPluginHandler) Create(c *gin.Context) {
var req dto.JSPluginCreateRequest
if !BindJSON(c, &req) {
return
}
plugin := &db.JSPlugin{
Name: req.Name,
Source: req.Source,
Signature: req.Signature,
Description: req.Description,
Author: req.Author,
Config: req.Config,
AutoStart: req.AutoStart,
Enabled: true,
}
if err := h.app.GetJSPluginStore().SaveJSPlugin(plugin); err != nil {
InternalError(c, err.Error())
return
}
Success(c, gin.H{"status": "ok"})
}
// Get 获取单个 JS 插件
// @Summary 获取 JS 插件详情
// @Description 获取指定 JS 插件的详细信息
// @Tags JS插件
// @Produce json
// @Security Bearer
// @Param name path string true "插件名称"
// @Success 200 {object} Response{data=db.JSPlugin}
// @Failure 404 {object} Response
// @Router /api/js-plugin/{name} [get]
func (h *JSPluginHandler) Get(c *gin.Context) {
name := c.Param("name")
plugin, err := h.app.GetJSPluginStore().GetJSPlugin(name)
if err != nil {
NotFound(c, "plugin not found")
return
}
Success(c, plugin)
}
// Update 更新 JS 插件
// @Summary 更新 JS 插件
// @Description 更新指定 JS 插件的信息
// @Tags JS插件
// @Accept json
// @Produce json
// @Security Bearer
// @Param name path string true "插件名称"
// @Param request body dto.JSPluginUpdateRequest true "更新内容"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/js-plugin/{name} [put]
func (h *JSPluginHandler) Update(c *gin.Context) {
name := c.Param("name")
var req dto.JSPluginUpdateRequest
if !BindJSON(c, &req) {
return
}
plugin := &db.JSPlugin{
Name: name,
Source: req.Source,
Signature: req.Signature,
Description: req.Description,
Author: req.Author,
Config: req.Config,
AutoStart: req.AutoStart,
Enabled: req.Enabled,
}
if err := h.app.GetJSPluginStore().SaveJSPlugin(plugin); err != nil {
InternalError(c, err.Error())
return
}
Success(c, gin.H{"status": "ok"})
}
// Delete 删除 JS 插件
// @Summary 删除 JS 插件
// @Description 删除指定的 JS 插件
// @Tags JS插件
// @Produce json
// @Security Bearer
// @Param name path string true "插件名称"
// @Success 200 {object} Response
// @Router /api/js-plugin/{name} [delete]
func (h *JSPluginHandler) Delete(c *gin.Context) {
name := c.Param("name")
if err := h.app.GetJSPluginStore().DeleteJSPlugin(name); err != nil {
InternalError(c, err.Error())
return
}
Success(c, gin.H{"status": "ok"})
}
// PushToClient 推送 JS 插件到客户端
// @Summary 推送插件到客户端
// @Description 将 JS 插件推送到指定客户端
// @Tags JS插件
// @Accept json
// @Produce json
// @Security Bearer
// @Param name path string true "插件名称"
// @Param clientID path string true "客户端ID"
// @Param request body dto.JSPluginPushRequest false "推送配置"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Failure 404 {object} Response
// @Router /api/js-plugin/{name}/push/{clientID} [post]
func (h *JSPluginHandler) PushToClient(c *gin.Context) {
pluginName := c.Param("name")
clientID := c.Param("clientID")
// 解析请求体(可选)
var pushReq dto.JSPluginPushRequest
c.ShouldBindJSON(&pushReq) // 忽略错误,允许空请求体
// 检查客户端是否在线
if !h.app.GetServer().IsClientOnline(clientID) {
ClientNotOnline(c)
return
}
// 获取插件
plugin, err := h.app.GetJSPluginStore().GetJSPlugin(pluginName)
if err != nil {
NotFound(c, "plugin not found")
return
}
if !plugin.Enabled {
Error(c, 400, CodePluginDisabled, "plugin is disabled")
return
}
// 推送到客户端
req := JSPluginInstallRequest{
PluginName: plugin.Name,
Source: plugin.Source,
Signature: plugin.Signature,
RuleName: plugin.Name,
RemotePort: pushReq.RemotePort,
Config: plugin.Config,
AutoStart: plugin.AutoStart,
}
if err := h.app.GetServer().InstallJSPluginToClient(clientID, req); err != nil {
InternalError(c, err.Error())
return
}
Success(c, gin.H{
"status": "ok",
"plugin": pluginName,
"client": clientID,
"remote_port": pushReq.RemotePort,
})
}

View File

@@ -1,416 +0,0 @@
package handler
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/internal/server/router/dto"
"github.com/gotunnel/pkg/plugin"
)
// PluginHandler 插件处理器
type PluginHandler struct {
app AppInterface
}
// NewPluginHandler 创建插件处理器
func NewPluginHandler(app AppInterface) *PluginHandler {
return &PluginHandler{app: app}
}
// List 获取插件列表
// @Summary 获取所有插件
// @Description 返回服务端所有注册的插件
// @Tags 插件
// @Produce json
// @Security Bearer
// @Success 200 {object} Response{data=[]dto.PluginInfo}
// @Router /api/plugins [get]
func (h *PluginHandler) List(c *gin.Context) {
plugins := h.app.GetServer().GetPluginList()
result := make([]dto.PluginInfo, len(plugins))
for i, p := range plugins {
result[i] = dto.PluginInfo{
Name: p.Name,
Version: p.Version,
Type: p.Type,
Description: p.Description,
Source: p.Source,
Icon: p.Icon,
Enabled: p.Enabled,
}
if p.RuleSchema != nil {
result[i].RuleSchema = &dto.RuleSchema{
NeedsLocalAddr: p.RuleSchema.NeedsLocalAddr,
ExtraFields: convertRouterConfigFields(p.RuleSchema.ExtraFields),
}
}
}
Success(c, result)
}
// Enable 启用插件
// @Summary 启用插件
// @Description 启用指定插件
// @Tags 插件
// @Produce json
// @Security Bearer
// @Param name path string true "插件名称"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/plugin/{name}/enable [post]
func (h *PluginHandler) Enable(c *gin.Context) {
name := c.Param("name")
if err := h.app.GetServer().EnablePlugin(name); err != nil {
BadRequest(c, err.Error())
return
}
Success(c, gin.H{"status": "ok"})
}
// Disable 禁用插件
// @Summary 禁用插件
// @Description 禁用指定插件
// @Tags 插件
// @Produce json
// @Security Bearer
// @Param name path string true "插件名称"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Router /api/plugin/{name}/disable [post]
func (h *PluginHandler) Disable(c *gin.Context) {
name := c.Param("name")
if err := h.app.GetServer().DisablePlugin(name); err != nil {
BadRequest(c, err.Error())
return
}
Success(c, gin.H{"status": "ok"})
}
// GetRuleSchemas 获取规则配置模式
// @Summary 获取规则模式
// @Description 返回所有协议类型的配置模式
// @Tags 插件
// @Produce json
// @Security Bearer
// @Success 200 {object} Response{data=map[string]dto.RuleSchema}
// @Router /api/rule-schemas [get]
func (h *PluginHandler) GetRuleSchemas(c *gin.Context) {
// 获取内置协议模式
schemas := make(map[string]dto.RuleSchema)
for name, schema := range plugin.BuiltinRuleSchemas() {
schemas[name] = dto.RuleSchema{
NeedsLocalAddr: schema.NeedsLocalAddr,
ExtraFields: convertConfigFields(schema.ExtraFields),
}
}
// 添加已注册插件的模式
plugins := h.app.GetServer().GetPluginList()
for _, p := range plugins {
if p.RuleSchema != nil {
schemas[p.Name] = dto.RuleSchema{
NeedsLocalAddr: p.RuleSchema.NeedsLocalAddr,
ExtraFields: convertRouterConfigFields(p.RuleSchema.ExtraFields),
}
}
}
Success(c, schemas)
}
// GetClientConfig 获取客户端插件配置
// @Summary 获取客户端插件配置
// @Description 获取客户端上指定插件的配置
// @Tags 插件
// @Produce json
// @Security Bearer
// @Param clientID path string true "客户端ID"
// @Param pluginName path string true "插件名称"
// @Success 200 {object} Response{data=dto.PluginConfigResponse}
// @Failure 404 {object} Response
// @Router /api/client-plugin/{clientID}/{pluginName}/config [get]
func (h *PluginHandler) GetClientConfig(c *gin.Context) {
clientID := c.Param("clientID")
pluginName := c.Param("pluginName")
client, err := h.app.GetClientStore().GetClient(clientID)
if err != nil {
NotFound(c, "client not found")
return
}
// 查找客户端的插件
var clientPlugin *db.ClientPlugin
for i, p := range client.Plugins {
if p.Name == pluginName {
clientPlugin = &client.Plugins[i]
break
}
}
if clientPlugin == nil {
NotFound(c, "plugin not installed on client")
return
}
var schemaFields []dto.ConfigField
// 优先使用客户端插件保存的 ConfigSchema
if len(clientPlugin.ConfigSchema) > 0 {
for _, f := range clientPlugin.ConfigSchema {
schemaFields = append(schemaFields, dto.ConfigField{
Key: f.Key,
Label: f.Label,
Type: f.Type,
Default: f.Default,
Required: f.Required,
Options: f.Options,
Description: f.Description,
})
}
} else {
// 尝试从内置插件获取配置模式
schema, err := h.app.GetServer().GetPluginConfigSchema(pluginName)
if err != nil {
// 如果内置插件中找不到,尝试从 JS 插件获取
jsPlugin, jsErr := h.app.GetJSPluginStore().GetJSPlugin(pluginName)
if jsErr == nil {
// 使用 JS 插件的 config 作为动态 schema
for key := range jsPlugin.Config {
schemaFields = append(schemaFields, dto.ConfigField{
Key: key,
Label: key,
Type: "string",
})
}
}
} else {
schemaFields = convertRouterConfigFields(schema)
}
}
// 添加 remote_port 作为系统配置字段(始终显示)
schemaFields = append([]dto.ConfigField{{
Key: "remote_port",
Label: "远程端口",
Type: "number",
Description: "服务端监听端口,修改后需重启插件生效",
}}, schemaFields...)
// 添加 Auth 配置字段
schemaFields = append(schemaFields, dto.ConfigField{
Key: "auth_enabled",
Label: "启用认证",
Type: "bool",
Description: "启用 HTTP Basic Auth 保护",
}, dto.ConfigField{
Key: "auth_username",
Label: "认证用户名",
Type: "string",
Description: "HTTP Basic Auth 用户名",
}, dto.ConfigField{
Key: "auth_password",
Label: "认证密码",
Type: "password",
Description: "HTTP Basic Auth 密码",
})
// 构建配置值
config := clientPlugin.Config
if config == nil {
config = make(map[string]string)
}
// 将 remote_port 加入配置
if clientPlugin.RemotePort > 0 {
config["remote_port"] = fmt.Sprintf("%d", clientPlugin.RemotePort)
}
// 将 Auth 配置加入
if clientPlugin.AuthEnabled {
config["auth_enabled"] = "true"
} else {
config["auth_enabled"] = "false"
}
config["auth_username"] = clientPlugin.AuthUsername
config["auth_password"] = clientPlugin.AuthPassword
Success(c, dto.PluginConfigResponse{
PluginName: pluginName,
Schema: schemaFields,
Config: config,
})
}
// UpdateClientConfig 更新客户端插件配置
// @Summary 更新客户端插件配置
// @Description 更新客户端上指定插件的配置
// @Tags 插件
// @Accept json
// @Produce json
// @Security Bearer
// @Param clientID path string true "客户端ID"
// @Param pluginName path string true "插件名称"
// @Param request body dto.PluginConfigRequest true "配置内容"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Failure 404 {object} Response
// @Router /api/client-plugin/{clientID}/{pluginName}/config [put]
func (h *PluginHandler) UpdateClientConfig(c *gin.Context) {
clientID := c.Param("clientID")
pluginName := c.Param("pluginName")
var req dto.PluginConfigRequest
if !BindJSON(c, &req) {
return
}
client, err := h.app.GetClientStore().GetClient(clientID)
if err != nil {
NotFound(c, "client not found")
return
}
// 更新插件配置
found := false
portChanged := false
authChanged := false
var oldPort, newPort int
for i, p := range client.Plugins {
if p.Name == pluginName {
oldPort = client.Plugins[i].RemotePort
// 提取 remote_port 并单独处理
if portStr, ok := req.Config["remote_port"]; ok {
fmt.Sscanf(portStr, "%d", &newPort)
if newPort > 0 && newPort != oldPort {
// 检查新端口是否可用
if !h.app.GetServer().IsPortAvailable(newPort, clientID) {
BadRequest(c, fmt.Sprintf("port %d is already in use", newPort))
return
}
client.Plugins[i].RemotePort = newPort
portChanged = true
}
delete(req.Config, "remote_port") // 不保存到 Config map
}
// 提取 Auth 配置并单独处理
if authEnabledStr, ok := req.Config["auth_enabled"]; ok {
newAuthEnabled := authEnabledStr == "true"
if newAuthEnabled != client.Plugins[i].AuthEnabled {
client.Plugins[i].AuthEnabled = newAuthEnabled
authChanged = true
}
delete(req.Config, "auth_enabled")
}
if authUsername, ok := req.Config["auth_username"]; ok {
if authUsername != client.Plugins[i].AuthUsername {
client.Plugins[i].AuthUsername = authUsername
authChanged = true
}
delete(req.Config, "auth_username")
}
if authPassword, ok := req.Config["auth_password"]; ok {
if authPassword != client.Plugins[i].AuthPassword {
client.Plugins[i].AuthPassword = authPassword
authChanged = true
}
delete(req.Config, "auth_password")
}
client.Plugins[i].Config = req.Config
found = true
break
}
}
if !found {
NotFound(c, "plugin not installed on client")
return
}
// 如果端口变更,同步更新代理规则
if portChanged {
for i, r := range client.Rules {
if r.Name == pluginName && r.PluginManaged {
client.Rules[i].RemotePort = newPort
break
}
}
// 停止旧端口监听器
if oldPort > 0 {
h.app.GetServer().StopPluginRule(clientID, oldPort)
}
}
// 如果 Auth 配置变更,同步更新代理规则
if authChanged {
for i, p := range client.Plugins {
if p.Name == pluginName {
for j, r := range client.Rules {
if r.Name == pluginName && r.PluginManaged {
client.Rules[j].AuthEnabled = client.Plugins[i].AuthEnabled
client.Rules[j].AuthUsername = client.Plugins[i].AuthUsername
client.Rules[j].AuthPassword = client.Plugins[i].AuthPassword
break
}
}
break
}
}
}
// 保存到数据库
if err := h.app.GetClientStore().UpdateClient(client); err != nil {
InternalError(c, err.Error())
return
}
// 如果客户端在线,同步配置
if h.app.GetServer().IsClientOnline(clientID) {
if err := h.app.GetServer().SyncPluginConfigToClient(clientID, pluginName, req.Config); err != nil {
PartialSuccess(c, gin.H{"status": "partial", "port_changed": portChanged}, "config saved but sync failed: "+err.Error())
return
}
}
Success(c, gin.H{"status": "ok", "port_changed": portChanged})
}
// convertConfigFields 转换插件配置字段到 DTO
func convertConfigFields(fields []plugin.ConfigField) []dto.ConfigField {
result := make([]dto.ConfigField, len(fields))
for i, f := range fields {
result[i] = dto.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
}
// convertRouterConfigFields 转换 ConfigField 到 dto.ConfigField
func convertRouterConfigFields(fields []ConfigField) []dto.ConfigField {
result := make([]dto.ConfigField, len(fields))
for i, f := range fields {
result[i] = dto.ConfigField{
Key: f.Key,
Label: f.Label,
Type: f.Type,
Default: f.Default,
Required: f.Required,
Options: f.Options,
Description: f.Description,
}
}
return result
}

View File

@@ -1,139 +0,0 @@
package handler
import (
"io"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gotunnel/pkg/protocol"
)
// PluginAPIHandler 插件 API 代理处理器
type PluginAPIHandler struct {
app AppInterface
}
// NewPluginAPIHandler 创建插件 API 代理处理器
func NewPluginAPIHandler(app AppInterface) *PluginAPIHandler {
return &PluginAPIHandler{app: app}
}
// ProxyRequest 代理请求到客户端插件
// @Summary 代理插件 API 请求
// @Description 将请求代理到客户端的 JS 插件处理
// @Tags 插件 API
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path string true "客户端 ID"
// @Param pluginID path string true "插件实例 ID"
// @Param route path string true "插件路由"
// @Success 200 {object} object
// @Failure 404 {object} Response
// @Failure 502 {object} Response
// @Router /api/client/{id}/plugin-api/{pluginID}/{route} [get]
func (h *PluginAPIHandler) ProxyRequest(c *gin.Context) {
clientID := c.Param("id")
pluginID := c.Param("pluginID")
route := c.Param("route")
// 确保路由以 / 开头
if !strings.HasPrefix(route, "/") {
route = "/" + route
}
// 检查客户端是否在线
if !h.app.GetServer().IsClientOnline(clientID) {
ClientNotOnline(c)
return
}
// 读取请求体
var body string
if c.Request.Body != nil {
bodyBytes, _ := io.ReadAll(c.Request.Body)
body = string(bodyBytes)
}
// 构建请求头
headers := make(map[string]string)
for key, values := range c.Request.Header {
if len(values) > 0 {
headers[key] = values[0]
}
}
// 构建 API 请求
apiReq := protocol.PluginAPIRequest{
PluginID: pluginID,
Method: c.Request.Method,
Path: route,
Query: c.Request.URL.RawQuery,
Headers: headers,
Body: body,
}
// 发送请求到客户端
resp, err := h.app.GetServer().ProxyPluginAPIRequest(clientID, apiReq)
if err != nil {
BadGateway(c, "Plugin request failed: "+err.Error())
return
}
// 检查错误
if resp.Error != "" {
c.JSON(http.StatusBadGateway, gin.H{
"code": 502,
"message": resp.Error,
})
return
}
// 设置响应头
for key, value := range resp.Headers {
c.Header(key, value)
}
// 返回响应
c.String(resp.Status, resp.Body)
}
// ProxyPluginAPIRequest 接口方法声明 - 添加到 ServerInterface
type PluginAPIProxyInterface interface {
ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRequest) (*protocol.PluginAPIResponse, error)
}
// AuthConfig 认证配置
type AuthConfig struct {
Type string `json:"type"` // none, basic, token
Username string `json:"username"` // Basic Auth 用户名
Password string `json:"password"` // Basic Auth 密码
Token string `json:"token"` // Token 认证
}
// BasicAuthMiddleware 创建 Basic Auth 中间件
func BasicAuthMiddleware(username, password string) gin.HandlerFunc {
return func(c *gin.Context) {
user, pass, ok := c.Request.BasicAuth()
if !ok || user != username || pass != password {
c.Header("WWW-Authenticate", `Basic realm="Plugin"`)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Unauthorized",
})
return
}
c.Next()
}
}
// WithTimeout 带超时的请求处理
func WithTimeout(timeout time.Duration, handler gin.HandlerFunc) gin.HandlerFunc {
return func(c *gin.Context) {
// 设置请求超时
c.Request = c.Request.WithContext(c.Request.Context())
handler(c)
}
}

View File

@@ -1,292 +0,0 @@
package handler
import (
"fmt"
"io"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/internal/server/router/dto"
"github.com/gotunnel/pkg/protocol"
)
// StoreHandler 插件商店处理器
type StoreHandler struct {
app AppInterface
}
// NewStoreHandler 创建插件商店处理器
func NewStoreHandler(app AppInterface) *StoreHandler {
return &StoreHandler{app: app}
}
// ListPlugins 获取商店插件列表
// @Summary 获取商店插件
// @Description 从远程插件商店获取可用插件列表
// @Tags 插件商店
// @Produce json
// @Security Bearer
// @Success 200 {object} Response{data=object{plugins=[]dto.StorePluginInfo}}
// @Failure 502 {object} Response
// @Router /api/store/plugins [get]
func (h *StoreHandler) ListPlugins(c *gin.Context) {
cfg := h.app.GetConfig()
storeURL := cfg.Server.PluginStore.GetPluginStoreURL()
// 从远程 URL 获取插件列表
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(storeURL)
if err != nil {
BadGateway(c, "Failed to fetch store: "+err.Error())
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
BadGateway(c, "Store returned error")
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
InternalError(c, "Failed to read response")
return
}
// 直接返回原始 JSON已经是数组格式
c.Header("Content-Type", "application/json")
c.Writer.Write([]byte(`{"code":0,"data":{"plugins":`))
c.Writer.Write(body)
c.Writer.Write([]byte(`}}`))
}
// Install 从商店安装插件到客户端
// @Summary 安装商店插件
// @Description 从插件商店下载并安装插件到指定客户端
// @Tags 插件商店
// @Accept json
// @Produce json
// @Security Bearer
// @Param request body dto.StoreInstallRequest true "安装请求"
// @Success 200 {object} Response
// @Failure 400 {object} Response
// @Failure 502 {object} Response
// @Router /api/store/install [post]
func (h *StoreHandler) Install(c *gin.Context) {
var req dto.StoreInstallRequest
if !BindJSON(c, &req) {
return
}
// 检查客户端是否在线
if !h.app.GetServer().IsClientOnline(req.ClientID) {
ClientNotOnline(c)
return
}
// 下载插件
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(req.DownloadURL)
if err != nil {
BadGateway(c, "Failed to download plugin: "+err.Error())
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
BadGateway(c, "Plugin download failed with status: "+resp.Status)
return
}
source, err := io.ReadAll(resp.Body)
if err != nil {
InternalError(c, "Failed to read plugin: "+err.Error())
return
}
// 下载签名文件
sigResp, err := client.Get(req.SignatureURL)
if err != nil {
BadGateway(c, "Failed to download signature: "+err.Error())
return
}
defer sigResp.Body.Close()
if sigResp.StatusCode != http.StatusOK {
BadGateway(c, "Signature download failed with status: "+sigResp.Status)
return
}
signature, err := io.ReadAll(sigResp.Body)
if err != nil {
InternalError(c, "Failed to read signature: "+err.Error())
return
}
// 检查插件是否已存在,决定使用已有 ID 还是生成新 ID
pluginID := ""
dbClient, err := h.app.GetClientStore().GetClient(req.ClientID)
if err == nil {
for _, p := range dbClient.Plugins {
if p.Name == req.PluginName && p.ID != "" {
pluginID = p.ID
break
}
}
}
if pluginID == "" {
pluginID = uuid.New().String()
}
// 安装到客户端
installReq := JSPluginInstallRequest{
PluginID: pluginID,
PluginName: req.PluginName,
Source: string(source),
Signature: string(signature),
RuleName: req.PluginName,
RemotePort: req.RemotePort,
AutoStart: true,
}
if err := h.app.GetServer().InstallJSPluginToClient(req.ClientID, installReq); err != nil {
InternalError(c, "Failed to install plugin: "+err.Error())
return
}
// 将插件保存到 JSPluginStore用于客户端重连时恢复
jsPlugin := &db.JSPlugin{
Name: req.PluginName,
Source: string(source),
Signature: string(signature),
AutoStart: true,
Enabled: true,
}
// 尝试保存,忽略错误(可能已存在)
h.app.GetJSPluginStore().SaveJSPlugin(jsPlugin)
// 将插件信息保存到客户端记录
// 重新获取 dbClient可能已被修改
dbClient, err = h.app.GetClientStore().GetClient(req.ClientID)
if err == nil {
// 检查插件是否已存在(通过名称匹配)
pluginExists := false
for i, p := range dbClient.Plugins {
if p.Name == req.PluginName {
dbClient.Plugins[i].Enabled = true
dbClient.Plugins[i].RemotePort = req.RemotePort
dbClient.Plugins[i].AuthEnabled = req.AuthEnabled
dbClient.Plugins[i].AuthUsername = req.AuthUsername
dbClient.Plugins[i].AuthPassword = req.AuthPassword
// 确保有 ID
if dbClient.Plugins[i].ID == "" {
dbClient.Plugins[i].ID = pluginID
}
pluginExists = true
break
}
}
if !pluginExists {
version := req.Version
if version == "" {
version = "1.0.0"
}
// 转换 ConfigSchema
var configSchema []db.ConfigField
for _, f := range req.ConfigSchema {
configSchema = append(configSchema, db.ConfigField{
Key: f.Key,
Label: f.Label,
Type: f.Type,
Default: f.Default,
Required: f.Required,
Options: f.Options,
Description: f.Description,
})
}
dbClient.Plugins = append(dbClient.Plugins, db.ClientPlugin{
ID: pluginID,
Name: req.PluginName,
Version: version,
Enabled: true,
RemotePort: req.RemotePort,
ConfigSchema: configSchema,
AuthEnabled: req.AuthEnabled,
AuthUsername: req.AuthUsername,
AuthPassword: req.AuthPassword,
})
}
// 自动创建代理规则(如果指定了端口)
if req.RemotePort > 0 {
// 检查端口是否可用
if !h.app.GetServer().IsPortAvailable(req.RemotePort, req.ClientID) {
InternalError(c, fmt.Sprintf("port %d is already in use", req.RemotePort))
return
}
ruleExists := false
for i, r := range dbClient.Rules {
if r.Name == req.PluginName {
// 更新现有规则
dbClient.Rules[i].Type = req.PluginName
dbClient.Rules[i].RemotePort = req.RemotePort
dbClient.Rules[i].Enabled = boolPtr(true)
dbClient.Rules[i].PluginID = pluginID
dbClient.Rules[i].AuthEnabled = req.AuthEnabled
dbClient.Rules[i].AuthUsername = req.AuthUsername
dbClient.Rules[i].AuthPassword = req.AuthPassword
dbClient.Rules[i].PluginManaged = true
ruleExists = true
break
}
}
if !ruleExists {
// 创建新规则
dbClient.Rules = append(dbClient.Rules, protocol.ProxyRule{
Name: req.PluginName,
Type: req.PluginName,
RemotePort: req.RemotePort,
Enabled: boolPtr(true),
PluginID: pluginID,
AuthEnabled: req.AuthEnabled,
AuthUsername: req.AuthUsername,
AuthPassword: req.AuthPassword,
PluginManaged: true,
})
}
}
h.app.GetClientStore().UpdateClient(dbClient)
}
// 启动服务端监听器(让外部用户可以通过 RemotePort 访问插件)
if req.RemotePort > 0 {
pluginRule := protocol.ProxyRule{
Name: req.PluginName,
Type: req.PluginName, // 使用插件名作为类型,让 isClientPlugin 识别
RemotePort: req.RemotePort,
Enabled: boolPtr(true),
PluginID: pluginID,
AuthEnabled: req.AuthEnabled,
AuthUsername: req.AuthUsername,
AuthPassword: req.AuthPassword,
}
// 启动监听器(忽略错误,可能端口已被占用)
h.app.GetServer().StartPluginRule(req.ClientID, pluginRule)
}
Success(c, gin.H{
"status": "ok",
"plugin": req.PluginName,
"plugin_id": pluginID,
"client": req.ClientID,
})
}
// boolPtr 返回 bool 值的指针
func boolPtr(b bool) *bool {
return &b
}

View File

@@ -67,8 +67,6 @@ func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth,
api.POST("/client/:id/push", clientHandler.PushConfig)
api.POST("/client/:id/disconnect", clientHandler.Disconnect)
api.POST("/client/:id/restart", clientHandler.Restart)
api.POST("/client/:id/install-plugins", clientHandler.InstallPlugins)
api.POST("/client/:id/plugin/:pluginID/:action", clientHandler.PluginAction)
api.GET("/client/:id/system-stats", clientHandler.GetSystemStats)
api.GET("/client/:id/screenshot", clientHandler.GetScreenshot)
api.POST("/client/:id/shell", clientHandler.ExecuteShell)
@@ -79,29 +77,6 @@ func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth,
api.PUT("/config", configHandler.Update)
api.POST("/config/reload", configHandler.Reload)
// 插件管理
pluginHandler := handler.NewPluginHandler(app)
api.GET("/plugins", pluginHandler.List)
api.POST("/plugin/:name/enable", pluginHandler.Enable)
api.POST("/plugin/:name/disable", pluginHandler.Disable)
api.GET("/rule-schemas", pluginHandler.GetRuleSchemas)
api.GET("/client-plugin/:clientID/:pluginName/config", pluginHandler.GetClientConfig)
api.PUT("/client-plugin/:clientID/:pluginName/config", pluginHandler.UpdateClientConfig)
// JS 插件管理
jsPluginHandler := handler.NewJSPluginHandler(app)
api.GET("/js-plugins", jsPluginHandler.List)
api.POST("/js-plugins", jsPluginHandler.Create)
api.GET("/js-plugin/:name", jsPluginHandler.Get)
api.PUT("/js-plugin/:name", jsPluginHandler.Update)
api.DELETE("/js-plugin/:name", jsPluginHandler.Delete)
api.POST("/js-plugin/:name/push/:clientID", jsPluginHandler.PushToClient)
// 插件商店
storeHandler := handler.NewStoreHandler(app)
api.GET("/store/plugins", storeHandler.ListPlugins)
api.POST("/store/install", storeHandler.Install)
// 更新管理
updateHandler := handler.NewUpdateHandler(app)
api.GET("/update/check/server", updateHandler.CheckServer)
@@ -118,9 +93,9 @@ func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth,
api.GET("/traffic/stats", trafficHandler.GetStats)
api.GET("/traffic/hourly", trafficHandler.GetHourly)
// 插件 API 代理 (通过 Web API 访问客户端插件)
pluginAPIHandler := handler.NewPluginAPIHandler(app)
api.Any("/client/:id/plugin-api/:pluginID/*route", pluginAPIHandler.ProxyRequest)
// 安装命令生成
installHandler := handler.NewInstallHandler(app)
api.POST("/install/generate", installHandler.GenerateInstallCommand)
}
}
@@ -200,10 +175,6 @@ func isStaticAsset(path string) bool {
// Re-export types from handler package for backward compatibility
type (
ServerInterface = handler.ServerInterface
AppInterface = handler.AppInterface
ConfigField = handler.ConfigField
RuleSchema = handler.RuleSchema
PluginInfo = handler.PluginInfo
JSPluginInstallRequest = handler.JSPluginInstallRequest
ServerInterface = handler.ServerInterface
AppInterface = handler.AppInterface
)

File diff suppressed because it is too large Load Diff