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:
@@ -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
|
||||
|
||||
@@ -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控制台设置
|
||||
|
||||
45
internal/server/db/install_token.go
Normal file
45
internal/server/db/install_token.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 获取当前小时的时间戳
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
108
internal/server/router/handler/install.go
Normal file
108
internal/server/router/handler/install.go
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user