From 4116d8934c6395cad8bf7d0c1e6126417909b869 Mon Sep 17 00:00:00 2001 From: Flik Date: Mon, 29 Dec 2025 17:18:26 +0800 Subject: [PATCH] update --- cmd/client/main.go | 8 +- cmd/server/main.go | 38 ++- go.mod | 5 + go.sum | 8 + internal/client/plugin/manager.go | 24 +- internal/client/tunnel/client.go | 76 +++++- internal/server/app/app.go | 25 +- internal/server/config/config.go | 10 + internal/server/db/interface.go | 22 ++ internal/server/db/sqlite.go | 120 +++++++++ internal/server/plugin/manager.go | 23 +- internal/server/router/api.go | 211 ++++++++++++++- internal/server/tunnel/server.go | 116 ++++++++- pkg/plugin/api.go | 103 -------- pkg/plugin/base.go | 90 ------- pkg/plugin/builtin.go | 11 - pkg/plugin/builtin/echo.go | 6 +- pkg/plugin/builtin/register.go | 21 +- pkg/plugin/builtin/socks5.go | 6 +- pkg/plugin/builtin/vnc.go | 8 +- pkg/plugin/client_api.go | 161 ------------ pkg/plugin/registry.go | 52 ++-- pkg/plugin/script/js.go | 412 ++++++++++++++++++++++++++++++ pkg/plugin/server_api.go | 180 ------------- pkg/plugin/types.go | 302 +++++++--------------- pkg/protocol/message.go | 21 ++ plugins/echo.js | 30 +++ plugins/filemanager.js | 194 ++++++++++++++ web/src/api/index.ts | 11 +- web/src/types/index.ts | 12 + web/src/views/PluginsView.vue | 126 ++++++++- 31 files changed, 1570 insertions(+), 862 deletions(-) delete mode 100644 pkg/plugin/api.go delete mode 100644 pkg/plugin/base.go delete mode 100644 pkg/plugin/builtin.go delete mode 100644 pkg/plugin/client_api.go create mode 100644 pkg/plugin/script/js.go delete mode 100644 pkg/plugin/server_api.go create mode 100644 plugins/echo.js create mode 100644 plugins/filemanager.js diff --git a/cmd/client/main.go b/cmd/client/main.go index 6cd98d1..bb7ca10 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -32,11 +32,13 @@ func main() { // 初始化插件系统 registry := plugin.NewRegistry() - if err := registry.RegisterAll(builtin.GetAll()); err != nil { - log.Fatalf("[Plugin] Register error: %v", err) + for _, h := range builtin.GetClientPlugins() { + if err := registry.RegisterClient(h); err != nil { + log.Fatalf("[Plugin] Register error: %v", err) + } } client.SetPluginRegistry(registry) - log.Printf("[Plugin] Registered %d plugins", len(builtin.GetAll())) + log.Printf("[Plugin] Registered %d plugins", len(builtin.GetClientPlugins())) client.Run() } diff --git a/cmd/server/main.go b/cmd/server/main.go index 6f25425..bb4dbae 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -4,6 +4,7 @@ import ( "flag" "fmt" "log" + "os" "github.com/gotunnel/internal/server/app" "github.com/gotunnel/internal/server/config" @@ -56,15 +57,21 @@ func main() { // 初始化插件系统 registry := plugin.NewRegistry() - if err := registry.RegisterAll(builtin.GetAll()); err != nil { + if err := registry.RegisterAllServer(builtin.GetServerPlugins()); err != nil { log.Fatalf("[Plugin] Register error: %v", err) } server.SetPluginRegistry(registry) - log.Printf("[Plugin] Registered %d plugins", len(builtin.GetAll())) + log.Printf("[Plugin] Registered %d plugins", len(builtin.GetServerPlugins())) + + // 加载 JS 插件配置 + if len(cfg.JSPlugins) > 0 { + jsPlugins := loadJSPlugins(cfg.JSPlugins) + server.LoadJSPlugins(jsPlugins) + } // 启动 Web 控制台 if cfg.Web.Enabled { - ws := app.NewWebServer(clientStore, server, cfg, *configPath) + ws := app.NewWebServer(clientStore, server, cfg, *configPath, clientStore) addr := fmt.Sprintf("%s:%d", cfg.Web.BindAddr, cfg.Web.BindPort) go func() { @@ -82,3 +89,28 @@ func main() { log.Fatal(server.Run()) } + +// loadJSPlugins 加载 JS 插件文件 +func loadJSPlugins(configs []config.JSPluginConfig) []tunnel.JSPluginEntry { + var plugins []tunnel.JSPluginEntry + + for _, cfg := range configs { + source, err := os.ReadFile(cfg.Path) + if err != nil { + log.Printf("[JSPlugin] Failed to load %s: %v", cfg.Path, err) + continue + } + + plugins = append(plugins, tunnel.JSPluginEntry{ + Name: cfg.Name, + Source: string(source), + AutoPush: cfg.AutoPush, + Config: cfg.Config, + AutoStart: cfg.AutoStart, + }) + + log.Printf("[JSPlugin] Loaded: %s from %s", cfg.Name, cfg.Path) + } + + return plugins +} diff --git a/go.mod b/go.mod index 45e2d7d..5b65c0d 100644 --- a/go.mod +++ b/go.mod @@ -10,13 +10,18 @@ require ( ) require ( + github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/dop251/goja v0.0.0-20251201205617-2bb4c724c0f9 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect + github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.3.8 // indirect modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index d29685e..f5645c0 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dop251/goja v0.0.0-20251201205617-2bb4c724c0f9 h1:3uSSOd6mVlwcX3k5OYOpiDqFgRmaE2dBfLvVIFWWHrw= +github.com/dop251/goja v0.0.0-20251201205617-2bb4c724c0f9/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= @@ -23,6 +29,8 @@ golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/client/plugin/manager.go b/internal/client/plugin/manager.go index 42cb2a7..d962b70 100644 --- a/internal/client/plugin/manager.go +++ b/internal/client/plugin/manager.go @@ -22,7 +22,6 @@ func NewManager() (*Manager, error) { registry: registry, } - // 注册内置 plugins if err := m.registerBuiltins(); err != nil { return nil, err } @@ -32,22 +31,21 @@ func NewManager() (*Manager, error) { // registerBuiltins 注册内置 plugins func (m *Manager) registerBuiltins() error { - // 注册服务端插件 - if err := m.registry.RegisterAll(builtin.GetAll()); err != nil { - return err - } - // 注册客户端插件 - for _, h := range builtin.GetAllClientPlugins() { - if err := m.registry.RegisterClientPlugin(h); err != nil { + for _, h := range builtin.GetClientPlugins() { + if err := m.registry.RegisterClient(h); err != nil { return err } } - log.Printf("[Plugin] Registered %d server plugins, %d client plugins", - len(builtin.GetAll()), len(builtin.GetAllClientPlugins())) + log.Printf("[Plugin] Registered %d client plugins", len(builtin.GetClientPlugins())) return nil } -// GetHandler 返回指定代理类型的 handler -func (m *Manager) GetHandler(proxyType string) (plugin.ProxyHandler, error) { - return m.registry.Get(proxyType) +// GetClient 返回客户端插件 +func (m *Manager) GetClient(name string) (plugin.ClientPlugin, error) { + return m.registry.GetClient(name) +} + +// GetRegistry 返回插件注册表 +func (m *Manager) GetRegistry() *plugin.Registry { + return m.registry } diff --git a/internal/client/tunnel/client.go b/internal/client/tunnel/client.go index 5ce76d6..f7db276 100644 --- a/internal/client/tunnel/client.go +++ b/internal/client/tunnel/client.go @@ -11,6 +11,7 @@ import ( "time" "github.com/gotunnel/pkg/plugin" + "github.com/gotunnel/pkg/plugin/script" "github.com/gotunnel/pkg/protocol" "github.com/gotunnel/pkg/relay" "github.com/hashicorp/yamux" @@ -38,7 +39,7 @@ type Client struct { rules []protocol.ProxyRule mu sync.RWMutex pluginRegistry *plugin.Registry - runningPlugins map[string]plugin.ClientHandler // 运行中的客户端插件 + runningPlugins map[string]plugin.ClientPlugin // 运行中的客户端插件 pluginMu sync.RWMutex } @@ -51,7 +52,7 @@ func NewClient(serverAddr, token, id string) *Client { ServerAddr: serverAddr, Token: token, ID: id, - runningPlugins: make(map[string]plugin.ClientHandler), + runningPlugins: make(map[string]plugin.ClientPlugin), } } @@ -203,6 +204,8 @@ func (c *Client) handleStream(stream net.Conn) { c.handleClientPluginStart(stream, msg) case protocol.MsgTypeClientPluginConn: c.handleClientPluginConn(stream, msg) + case protocol.MsgTypeJSPluginInstall: + c.handleJSPluginInstall(stream, msg) } } @@ -368,7 +371,7 @@ func (c *Client) handlePluginConfig(msg *protocol.Message) { // 应用配置到插件 if c.pluginRegistry != nil { - handler, err := c.pluginRegistry.Get(cfg.PluginName) + handler, err := c.pluginRegistry.GetClient(cfg.PluginName) if err != nil { log.Printf("[Client] Plugin %s not found: %v", cfg.PluginName, err) return @@ -399,7 +402,7 @@ func (c *Client) handleClientPluginStart(stream net.Conn, msg *protocol.Message) return } - handler, err := c.pluginRegistry.GetClientPlugin(req.PluginName) + handler, err := c.pluginRegistry.GetClient(req.PluginName) if err != nil { c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", err.Error()) return @@ -462,3 +465,68 @@ func (c *Client) handleClientPluginConn(stream net.Conn, msg *protocol.Message) // 让插件处理连接 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 + } + + log.Printf("[Client] Installing JS plugin: %s", 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) + } + + log.Printf("[Client] JS plugin %s installed", req.PluginName) + c.sendJSPluginResult(stream, req.PluginName, true, "") + + // 自动启动 + if req.AutoStart { + c.startJSPlugin(jsPlugin, req) + } +} + +// 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 { + log.Printf("[Client] JS plugin %s init error: %v", req.PluginName, err) + return + } + + localAddr, err := handler.Start() + if err != nil { + log.Printf("[Client] JS plugin %s start error: %v", req.PluginName, err) + return + } + + key := req.PluginName + ":" + req.RuleName + c.pluginMu.Lock() + c.runningPlugins[key] = handler + c.pluginMu.Unlock() + + log.Printf("[Client] JS plugin %s started at %s", req.PluginName, localAddr) +} diff --git a/internal/server/app/app.go b/internal/server/app/app.go index 8a2bb40..d7415de 100644 --- a/internal/server/app/app.go +++ b/internal/server/app/app.go @@ -45,19 +45,21 @@ func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // WebServer Web控制台服务 type WebServer struct { - ClientStore db.ClientStore - Server router.ServerInterface - Config *config.ServerConfig - ConfigPath string + ClientStore db.ClientStore + Server router.ServerInterface + Config *config.ServerConfig + ConfigPath string + JSPluginStore db.JSPluginStore } // NewWebServer 创建Web服务 -func NewWebServer(cs db.ClientStore, srv router.ServerInterface, cfg *config.ServerConfig, cfgPath string) *WebServer { +func NewWebServer(cs db.ClientStore, srv router.ServerInterface, cfg *config.ServerConfig, cfgPath string, jsStore db.JSPluginStore) *WebServer { return &WebServer{ - ClientStore: cs, - Server: srv, - Config: cfg, - ConfigPath: cfgPath, + ClientStore: cs, + Server: srv, + Config: cfg, + ConfigPath: cfgPath, + JSPluginStore: jsStore, } } @@ -146,3 +148,8 @@ func (w *WebServer) GetConfigPath() string { func (w *WebServer) SaveConfig() error { return config.SaveServerConfig(w.ConfigPath, w.Config) } + +// GetJSPluginStore 获取 JS 插件存储 +func (w *WebServer) GetJSPluginStore() db.JSPluginStore { + return w.JSPluginStore +} diff --git a/internal/server/config/config.go b/internal/server/config/config.go index 71886cf..d13b8ef 100644 --- a/internal/server/config/config.go +++ b/internal/server/config/config.go @@ -13,6 +13,16 @@ type ServerConfig struct { Server ServerSettings `yaml:"server"` Web WebSettings `yaml:"web"` PluginStore PluginStoreSettings `yaml:"plugin_store"` + JSPlugins []JSPluginConfig `yaml:"js_plugins,omitempty"` +} + +// JSPluginConfig JS 插件配置 +type JSPluginConfig struct { + Name string `yaml:"name"` + Path string `yaml:"path"` // JS 文件路径 + AutoPush []string `yaml:"auto_push,omitempty"` // 自动推送到的客户端 ID 列表 + Config map[string]string `yaml:"config,omitempty"` // 插件配置 + AutoStart bool `yaml:"auto_start,omitempty"` // 是否自动启动 } // PluginStoreSettings 扩展商店设置 diff --git a/internal/server/db/interface.go b/internal/server/db/interface.go index 0a70515..3dbfb15 100644 --- a/internal/server/db/interface.go +++ b/internal/server/db/interface.go @@ -33,6 +33,18 @@ type PluginData struct { WASMData []byte `json:"-"` } +// JSPlugin JS 插件数据 +type JSPlugin struct { + Name string `json:"name"` + Source string `json:"source"` + Description string `json:"description"` + Author string `json:"author"` + AutoPush []string `json:"auto_push"` + Config map[string]string `json:"config"` + AutoStart bool `json:"auto_start"` + Enabled bool `json:"enabled"` +} + // ClientStore 客户端存储接口 type ClientStore interface { GetAllClients() ([]Client, error) @@ -55,9 +67,19 @@ type PluginStore interface { GetPluginWASM(name string) ([]byte, 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 +} + // Store 统一存储接口 type Store interface { ClientStore PluginStore + JSPluginStore Close() error } diff --git a/internal/server/db/sqlite.go b/internal/server/db/sqlite.go index 4479ede..a2f9089 100644 --- a/internal/server/db/sqlite.go +++ b/internal/server/db/sqlite.go @@ -75,6 +75,24 @@ func (s *SQLiteStore) init() error { // 迁移:添加 icon 列 s.db.Exec(`ALTER TABLE plugins ADD COLUMN icon TEXT`) + // 创建 JS 插件表 + _, err = s.db.Exec(` + CREATE TABLE IF NOT EXISTS js_plugins ( + name TEXT PRIMARY KEY, + source TEXT NOT NULL, + description TEXT, + author TEXT, + 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 + ) + `) + if err != nil { + return err + } + return nil } @@ -296,3 +314,105 @@ func (s *SQLiteStore) GetPluginWASM(name string) ([]byte, error) { err := s.db.QueryRow(`SELECT wasm_data FROM plugins WHERE name = ?`, name).Scan(&data) return data, err } + +// ========== JS 插件存储方法 ========== + +// GetAllJSPlugins 获取所有 JS 插件 +func (s *SQLiteStore) GetAllJSPlugins() ([]JSPlugin, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + rows, err := s.db.Query(` + SELECT name, source, description, author, 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 autoStart, enabled int + err := rows.Scan(&p.Name, &p.Source, &p.Description, &p.Author, + &autoPushJSON, &configJSON, &autoStart, &enabled) + if err != nil { + return nil, err + } + 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 autoStart, enabled int + err := s.db.QueryRow(` + SELECT name, source, description, author, auto_push, config, auto_start, enabled + FROM js_plugins WHERE name = ? + `, name).Scan(&p.Name, &p.Source, &p.Description, &p.Author, + &autoPushJSON, &configJSON, &autoStart, &enabled) + if err != nil { + return nil, err + } + 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, description, author, auto_push, config, auto_start, enabled) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, p.Name, p.Source, p.Description, p.Author, + 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 = ? WHERE name = ?`, val, name) + return err +} diff --git a/internal/server/plugin/manager.go b/internal/server/plugin/manager.go index fc8f868..57f29b8 100644 --- a/internal/server/plugin/manager.go +++ b/internal/server/plugin/manager.go @@ -22,7 +22,6 @@ func NewManager() (*Manager, error) { registry: registry, } - // 注册内置 plugins if err := m.registerBuiltins(); err != nil { return nil, err } @@ -32,28 +31,26 @@ func NewManager() (*Manager, error) { // registerBuiltins 注册内置 plugins func (m *Manager) registerBuiltins() error { - // 注册服务端插件 - if err := m.registry.RegisterAll(builtin.GetAll()); err != nil { + if err := m.registry.RegisterAllServer(builtin.GetServerPlugins()); err != nil { return err } - // 注册客户端插件 - for _, h := range builtin.GetAllClientPlugins() { - if err := m.registry.RegisterClientPlugin(h); err != nil { + for _, h := range builtin.GetClientPlugins() { + if err := m.registry.RegisterClient(h); err != nil { return err } } - log.Printf("[Plugin] Registered %d server plugins, %d client plugins", - len(builtin.GetAll()), len(builtin.GetAllClientPlugins())) + log.Printf("[Plugin] Registered %d server, %d client plugins", + len(builtin.GetServerPlugins()), len(builtin.GetClientPlugins())) return nil } -// GetHandler 返回指定代理类型的 handler -func (m *Manager) GetHandler(proxyType string) (plugin.ProxyHandler, error) { - return m.registry.Get(proxyType) +// GetServer 返回服务端插件 +func (m *Manager) GetServer(name string) (plugin.ServerPlugin, error) { + return m.registry.GetServer(name) } -// ListPlugins 返回所有可用的 plugins -func (m *Manager) ListPlugins() []plugin.PluginInfo { +// ListPlugins 返回所有插件 +func (m *Manager) ListPlugins() []plugin.Info { return m.registry.List() } diff --git a/internal/server/router/api.go b/internal/server/router/api.go index 88c5079..af00ea1 100644 --- a/internal/server/router/api.go +++ b/internal/server/router/api.go @@ -51,6 +51,18 @@ type ServerInterface interface { // 插件配置 GetPluginConfigSchema(name string) ([]ConfigField, error) SyncPluginConfigToClient(clientID string, pluginName string, config map[string]string) error + // JS 插件 + InstallJSPluginToClient(clientID string, req JSPluginInstallRequest) error +} + +// JSPluginInstallRequest JS 插件安装请求 +type JSPluginInstallRequest struct { + PluginName string `json:"plugin_name"` + Source string `json:"source"` + RuleName string `json:"rule_name"` + RemotePort int `json:"remote_port"` + Config map[string]string `json:"config"` + AutoStart bool `json:"auto_start"` } // ConfigField 配置字段(从 plugin 包导出) @@ -89,21 +101,24 @@ type AppInterface interface { GetConfig() *config.ServerConfig GetConfigPath() string SaveConfig() error + GetJSPluginStore() db.JSPluginStore } // APIHandler API处理器 type APIHandler struct { - clientStore db.ClientStore - server ServerInterface - app AppInterface + clientStore db.ClientStore + server ServerInterface + app AppInterface + jsPluginStore db.JSPluginStore } // RegisterRoutes 注册所有 API 路由 func RegisterRoutes(r *Router, app AppInterface) { h := &APIHandler{ - clientStore: app.GetClientStore(), - server: app.GetServer(), - app: app, + clientStore: app.GetClientStore(), + server: app.GetServer(), + app: app, + jsPluginStore: app.GetJSPluginStore(), } api := r.Group("/api") @@ -116,6 +131,8 @@ func RegisterRoutes(r *Router, app AppInterface) { api.HandleFunc("/plugin/", h.handlePlugin) api.HandleFunc("/store/plugins", h.handleStorePlugins) api.HandleFunc("/client-plugin/", h.handleClientPlugin) + api.HandleFunc("/js-plugin/", h.handleJSPlugin) + api.HandleFunc("/js-plugins", h.handleJSPlugins) } func (h *APIHandler) handleStatus(rw http.ResponseWriter, r *http.Request) { @@ -752,3 +769,185 @@ func (h *APIHandler) updateClientPluginConfig(rw http.ResponseWriter, r *http.Re h.jsonResponse(rw, map[string]string{"status": "ok"}) } + +// handleJSPlugin 处理单个 JS 插件操作 +// GET/PUT/DELETE /api/js-plugin/{name} +// POST /api/js-plugin/{name}/push/{clientID} +func (h *APIHandler) handleJSPlugin(rw http.ResponseWriter, r *http.Request) { + path := r.URL.Path[len("/api/js-plugin/"):] + if path == "" { + http.Error(rw, "plugin name required", http.StatusBadRequest) + return + } + + parts := splitPathMulti(path) + + // POST /api/js-plugin/{name}/push/{clientID} + if len(parts) == 3 && parts[1] == "push" { + if r.Method != http.MethodPost { + http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) + return + } + h.pushJSPluginToClient(rw, parts[0], parts[2]) + return + } + + // GET/PUT/DELETE /api/js-plugin/{name} + pluginName := parts[0] + switch r.Method { + case http.MethodGet: + h.getJSPlugin(rw, pluginName) + case http.MethodPut: + h.updateJSPlugin(rw, r, pluginName) + case http.MethodDelete: + h.deleteJSPlugin(rw, pluginName) + default: + http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +// installJSPluginToClient 安装 JS 插件到客户端 +func (h *APIHandler) installJSPluginToClient(rw http.ResponseWriter, r *http.Request, clientID string) { + online, _, _ := h.server.GetClientStatus(clientID) + if !online { + http.Error(rw, "client not online", http.StatusBadRequest) + return + } + + var req JSPluginInstallRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + if req.PluginName == "" || req.Source == "" { + http.Error(rw, "plugin_name and source required", http.StatusBadRequest) + return + } + + if err := h.server.InstallJSPluginToClient(clientID, req); err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + h.jsonResponse(rw, map[string]interface{}{ + "status": "ok", + "plugin": req.PluginName, + }) +} + +// handleJSPlugins 处理 JS 插件列表和创建 +// GET /api/js-plugins - 获取所有 JS 插件 +// POST /api/js-plugins - 创建新 JS 插件 +func (h *APIHandler) handleJSPlugins(rw http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + h.getJSPlugins(rw) + case http.MethodPost: + h.createJSPlugin(rw, r) + default: + http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +func (h *APIHandler) getJSPlugins(rw http.ResponseWriter) { + plugins, err := h.jsPluginStore.GetAllJSPlugins() + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + if plugins == nil { + plugins = []db.JSPlugin{} + } + h.jsonResponse(rw, plugins) +} + +func (h *APIHandler) createJSPlugin(rw http.ResponseWriter, r *http.Request) { + var req db.JSPlugin + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + if req.Name == "" || req.Source == "" { + http.Error(rw, "name and source required", http.StatusBadRequest) + return + } + + req.Enabled = true + if err := h.jsPluginStore.SaveJSPlugin(&req); err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + h.jsonResponse(rw, map[string]string{"status": "ok"}) +} + +func (h *APIHandler) getJSPlugin(rw http.ResponseWriter, name string) { + p, err := h.jsPluginStore.GetJSPlugin(name) + if err != nil { + http.Error(rw, "plugin not found", http.StatusNotFound) + return + } + h.jsonResponse(rw, p) +} + +func (h *APIHandler) updateJSPlugin(rw http.ResponseWriter, r *http.Request, name string) { + var req db.JSPlugin + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + req.Name = name + if err := h.jsPluginStore.SaveJSPlugin(&req); err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + h.jsonResponse(rw, map[string]string{"status": "ok"}) +} + +func (h *APIHandler) deleteJSPlugin(rw http.ResponseWriter, name string) { + if err := h.jsPluginStore.DeleteJSPlugin(name); err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + h.jsonResponse(rw, map[string]string{"status": "ok"}) +} + +// pushJSPluginToClient 推送 JS 插件到指定客户端 +func (h *APIHandler) pushJSPluginToClient(rw http.ResponseWriter, pluginName, clientID string) { + // 检查客户端是否在线 + online, _, _ := h.server.GetClientStatus(clientID) + if !online { + http.Error(rw, "client not online", http.StatusBadRequest) + return + } + + // 获取插件 + p, err := h.jsPluginStore.GetJSPlugin(pluginName) + if err != nil { + http.Error(rw, "plugin not found", http.StatusNotFound) + return + } + + if !p.Enabled { + http.Error(rw, "plugin is disabled", http.StatusBadRequest) + return + } + + // 推送到客户端 + req := JSPluginInstallRequest{ + PluginName: p.Name, + Source: p.Source, + RuleName: p.Name, + Config: p.Config, + AutoStart: p.AutoStart, + } + + if err := h.server.InstallJSPluginToClient(clientID, req); err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + h.jsonResponse(rw, map[string]string{"status": "ok", "plugin": pluginName, "client": clientID}) +} diff --git a/internal/server/tunnel/server.go b/internal/server/tunnel/server.go index 42a2cbc..ba4461c 100644 --- a/internal/server/tunnel/server.go +++ b/internal/server/tunnel/server.go @@ -47,6 +47,16 @@ type Server struct { mu sync.RWMutex tlsConfig *tls.Config pluginRegistry *plugin.Registry + jsPlugins []JSPluginEntry // 配置的 JS 插件 +} + +// JSPluginEntry JS 插件条目 +type JSPluginEntry struct { + Name string + Source string + AutoPush []string + Config map[string]string + AutoStart bool } // ClientSession 客户端会话 @@ -85,6 +95,12 @@ func (s *Server) SetPluginRegistry(registry *plugin.Registry) { s.pluginRegistry = registry } +// LoadJSPlugins 加载 JS 插件配置 +func (s *Server) LoadJSPlugins(plugins []JSPluginEntry) { + s.jsPlugins = plugins + log.Printf("[Server] Loaded %d JS plugin configs", len(plugins)) +} + // Run 启动服务端 func (s *Server) Run() error { addr := fmt.Sprintf("%s:%d", s.bindAddr, s.bindPort) @@ -210,6 +226,9 @@ func (s *Server) setupClientSession(conn net.Conn, clientID string, rules []prot return } + // 自动推送 JS 插件 + s.autoPushJSPlugins(cs) + s.startProxyListeners(cs) go s.heartbeatLoop(cs) @@ -362,7 +381,7 @@ func (s *Server) acceptProxyServerConns(cs *ClientSession, ln net.Listener, rule // 优先使用插件系统 if s.pluginRegistry != nil { - if handler, err := s.pluginRegistry.Get(rule.Type); err == nil { + if handler, err := s.pluginRegistry.GetServer(rule.Type); err == nil { handler.Init(rule.PluginConfig) for { conn, err := ln.Accept() @@ -662,7 +681,7 @@ func (s *Server) InstallPluginsToClient(clientID string, plugins []string) error if !found { // 获取插件信息 version := "1.0.0" - if handler, err := s.pluginRegistry.Get(pluginName); err == nil && handler != nil { + if handler, err := s.pluginRegistry.GetServer(pluginName); err == nil && handler != nil { version = handler.Metadata().Version } client.Plugins = append(client.Plugins, db.ClientPlugin{ @@ -782,7 +801,7 @@ func (s *Server) GetPluginConfigSchema(name string) ([]router.ConfigField, error return nil, fmt.Errorf("plugin registry not initialized") } - handler, err := s.pluginRegistry.Get(name) + handler, err := s.pluginRegistry.GetServer(name) if err != nil { return nil, fmt.Errorf("plugin %s not found", name) } @@ -835,12 +854,65 @@ func (s *Server) sendPluginConfig(session *yamux.Session, pluginName string, con return protocol.WriteMessage(stream, msg) } +// InstallJSPluginToClient 安装 JS 插件到客户端 +func (s *Server) InstallJSPluginToClient(clientID string, req router.JSPluginInstallRequest) error { + s.mu.RLock() + cs, ok := s.clients[clientID] + s.mu.RUnlock() + + if !ok { + return fmt.Errorf("client %s not online", clientID) + } + + stream, err := cs.Session.Open() + if err != nil { + return err + } + defer stream.Close() + + installReq := protocol.JSPluginInstallRequest{ + PluginName: req.PluginName, + Source: req.Source, + RuleName: req.RuleName, + RemotePort: req.RemotePort, + Config: req.Config, + AutoStart: req.AutoStart, + } + + msg, err := protocol.NewMessage(protocol.MsgTypeJSPluginInstall, installReq) + if err != nil { + return err + } + + if err := protocol.WriteMessage(stream, msg); err != nil { + return err + } + + // 等待安装结果 + resp, err := protocol.ReadMessage(stream) + if err != nil { + return err + } + + var result protocol.JSPluginInstallResult + if err := resp.ParsePayload(&result); err != nil { + return err + } + + if !result.Success { + return fmt.Errorf("install failed: %s", result.Error) + } + + log.Printf("[Server] JS plugin %s installed on client %s", req.PluginName, clientID) + return nil +} + // isClientPlugin 检查是否为客户端插件 func (s *Server) isClientPlugin(pluginType string) bool { if s.pluginRegistry == nil { return false } - handler, err := s.pluginRegistry.GetClientPlugin(pluginType) + handler, err := s.pluginRegistry.GetClient(pluginType) if err != nil { return false } @@ -950,3 +1022,39 @@ func (s *Server) handleClientPluginConn(cs *ClientSession, conn net.Conn, rule p relay.Relay(conn, stream) } + +// autoPushJSPlugins 自动推送 JS 插件到客户端 +func (s *Server) autoPushJSPlugins(cs *ClientSession) { + for _, jp := range s.jsPlugins { + if !s.shouldPushToClient(jp.AutoPush, cs.ID) { + continue + } + + log.Printf("[Server] Auto-pushing JS plugin %s to client %s", jp.Name, cs.ID) + + req := router.JSPluginInstallRequest{ + PluginName: jp.Name, + Source: jp.Source, + RuleName: jp.Name, + Config: jp.Config, + AutoStart: jp.AutoStart, + } + + if err := s.InstallJSPluginToClient(cs.ID, req); err != nil { + log.Printf("[Server] Failed to push JS plugin %s: %v", jp.Name, err) + } + } +} + +// shouldPushToClient 检查是否应推送到指定客户端 +func (s *Server) shouldPushToClient(autoPush []string, clientID string) bool { + if len(autoPush) == 0 { + return true + } + for _, id := range autoPush { + if id == clientID || id == "*" { + return true + } + } + return false +} diff --git a/pkg/plugin/api.go b/pkg/plugin/api.go deleted file mode 100644 index 89a1eaf..0000000 --- a/pkg/plugin/api.go +++ /dev/null @@ -1,103 +0,0 @@ -package plugin - -import ( - "net" - "time" -) - -// ============================================================================= -// 核心接口定义 - 按职责分离 -// ============================================================================= - -// Dialer 网络拨号接口(已在 types.go 中定义,此处为文档说明) -// type Dialer interface { -// Dial(network, address string) (net.Conn, error) -// } - -// PortManager 端口管理接口(仅服务端可用) -type PortManager interface { - // ReservePort 预留端口,返回错误如果端口已被占用 - ReservePort(port int) error - // ReleasePort 释放端口 - ReleasePort(port int) - // IsPortAvailable 检查端口是否可用 - IsPortAvailable(port int) bool -} - -// RuleManager 代理规则管理接口(仅服务端可用) -type RuleManager interface { - // CreateRule 创建代理规则 - CreateRule(rule *RuleConfig) error - // DeleteRule 删除代理规则 - DeleteRule(clientID, ruleName string) error - // GetRules 获取客户端的代理规则 - GetRules(clientID string) ([]RuleConfig, error) - // UpdateRule 更新代理规则 - UpdateRule(clientID string, rule *RuleConfig) error -} - -// ClientManager 客户端管理接口(仅服务端可用) -type ClientManager interface { - // GetClientList 获取所有客户端列表 - GetClientList() ([]ClientInfo, error) - // IsClientOnline 检查客户端是否在线 - IsClientOnline(clientID string) bool -} - -// Logger 日志接口 -type Logger interface { - // Log 记录日志 - Log(level LogLevel, format string, args ...interface{}) -} - -// ConfigStore 配置存储接口 -type ConfigStore interface { - // GetConfig 获取配置值 - GetConfig(key string) string - // SetConfig 设置配置值 - SetConfig(key, value string) -} - -// EventBus 事件总线接口 -type EventBus interface { - // OnEvent 订阅事件 - OnEvent(eventType EventType, handler EventHandler) - // EmitEvent 发送事件 - EmitEvent(event *Event) -} - -// ============================================================================= -// 组合接口 -// ============================================================================= - -// PluginAPI 插件 API 主接口,组合所有子接口 -// 插件可以通过此接口访问 GoTunnel 的功能 -type PluginAPI interface { - // 网络操作 - Dial(network, address string) (net.Conn, error) - DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) - Listen(network, address string) (net.Listener, error) - - // 端口管理(服务端) - PortManager - - // 规则管理(服务端) - RuleManager - - // 客户端管理(服务端) - ClientManager - - // 日志 - Logger - - // 配置 - ConfigStore - - // 事件 - EventBus - - // 上下文 - GetContext() *Context - GetClientID() string - GetServerInfo() *ServerInfo -} diff --git a/pkg/plugin/base.go b/pkg/plugin/base.go deleted file mode 100644 index 20d8bee..0000000 --- a/pkg/plugin/base.go +++ /dev/null @@ -1,90 +0,0 @@ -package plugin - -import ( - "fmt" - "log" - "sync" -) - -// ============================================================================= -// 基础实现 - 提取公共代码 -// ============================================================================= - -// baseAPI 包含服务端和客户端共享的基础功能 -type baseAPI struct { - pluginName string - config map[string]string - configMu sync.RWMutex - - eventHandlers map[EventType][]EventHandler - eventMu sync.RWMutex -} - -// newBaseAPI 创建基础 API -func newBaseAPI(pluginName string, config map[string]string) *baseAPI { - cfg := config - if cfg == nil { - cfg = make(map[string]string) - } - return &baseAPI{ - pluginName: pluginName, - config: cfg, - eventHandlers: make(map[EventType][]EventHandler), - } -} - -// Log 记录日志 -func (b *baseAPI) Log(level LogLevel, format string, args ...interface{}) { - prefix := fmt.Sprintf("[Plugin:%s] ", b.pluginName) - msg := fmt.Sprintf(format, args...) - log.Printf("%s%s", prefix, msg) -} - -// GetConfig 获取配置值 -func (b *baseAPI) GetConfig(key string) string { - b.configMu.RLock() - defer b.configMu.RUnlock() - return b.config[key] -} - -// SetConfig 设置配置值 -func (b *baseAPI) SetConfig(key, value string) { - b.configMu.Lock() - defer b.configMu.Unlock() - b.config[key] = value -} - -// OnEvent 订阅事件 -func (b *baseAPI) OnEvent(eventType EventType, handler EventHandler) { - b.eventMu.Lock() - defer b.eventMu.Unlock() - b.eventHandlers[eventType] = append(b.eventHandlers[eventType], handler) -} - -// EmitEvent 发送事件(复制切片避免竞态条件) -func (b *baseAPI) EmitEvent(event *Event) { - b.eventMu.RLock() - handlers := make([]EventHandler, len(b.eventHandlers[event.Type])) - copy(handlers, b.eventHandlers[event.Type]) - b.eventMu.RUnlock() - - for _, handler := range handlers { - go handler(event) - } -} - -// getPluginName 获取插件名称 -func (b *baseAPI) getPluginName() string { - return b.pluginName -} - -// getConfigMap 获取配置副本 -func (b *baseAPI) getConfigMap() map[string]string { - b.configMu.RLock() - defer b.configMu.RUnlock() - result := make(map[string]string, len(b.config)) - for k, v := range b.config { - result[k] = v - } - return result -} diff --git a/pkg/plugin/builtin.go b/pkg/plugin/builtin.go deleted file mode 100644 index 4420b28..0000000 --- a/pkg/plugin/builtin.go +++ /dev/null @@ -1,11 +0,0 @@ -package plugin - -// RegisterBuiltins 注册所有内置 plugins -func RegisterBuiltins(registry *Registry, handlers ...ProxyHandler) error { - for _, handler := range handlers { - if err := registry.RegisterBuiltin(handler); err != nil { - return err - } - } - return nil -} diff --git a/pkg/plugin/builtin/echo.go b/pkg/plugin/builtin/echo.go index c6e7618..463327c 100644 --- a/pkg/plugin/builtin/echo.go +++ b/pkg/plugin/builtin/echo.go @@ -10,7 +10,7 @@ import ( ) func init() { - RegisterClientPlugin(NewEchoPlugin()) + RegisterClient(NewEchoPlugin()) } // EchoPlugin 回显插件 - 客户端插件示例 @@ -27,8 +27,8 @@ func NewEchoPlugin() *EchoPlugin { } // Metadata 返回插件信息 -func (p *EchoPlugin) Metadata() plugin.PluginMetadata { - return plugin.PluginMetadata{ +func (p *EchoPlugin) Metadata() plugin.Metadata { + return plugin.Metadata{ Name: "echo", Version: "1.0.0", Type: plugin.PluginTypeApp, diff --git a/pkg/plugin/builtin/register.go b/pkg/plugin/builtin/register.go index e1b7f4b..fdedd68 100644 --- a/pkg/plugin/builtin/register.go +++ b/pkg/plugin/builtin/register.go @@ -2,28 +2,27 @@ package builtin import "github.com/gotunnel/pkg/plugin" -// 全局插件注册表 var ( - serverPlugins []plugin.ProxyHandler - clientPlugins []plugin.ClientHandler + serverPlugins []plugin.ServerPlugin + clientPlugins []plugin.ClientPlugin ) -// Register 注册服务端插件 -func Register(handler plugin.ProxyHandler) { +// RegisterServer 注册服务端插件 +func RegisterServer(handler plugin.ServerPlugin) { serverPlugins = append(serverPlugins, handler) } -// RegisterClientPlugin 注册客户端插件 -func RegisterClientPlugin(handler plugin.ClientHandler) { +// RegisterClient 注册客户端插件 +func RegisterClient(handler plugin.ClientPlugin) { clientPlugins = append(clientPlugins, handler) } -// GetAll 返回所有服务端插件 -func GetAll() []plugin.ProxyHandler { +// GetServerPlugins 返回所有服务端插件 +func GetServerPlugins() []plugin.ServerPlugin { return serverPlugins } -// GetAllClientPlugins 返回所有客户端插件 -func GetAllClientPlugins() []plugin.ClientHandler { +// GetClientPlugins 返回所有客户端插件 +func GetClientPlugins() []plugin.ClientPlugin { return clientPlugins } diff --git a/pkg/plugin/builtin/socks5.go b/pkg/plugin/builtin/socks5.go index 7136a2a..5b0f2cf 100644 --- a/pkg/plugin/builtin/socks5.go +++ b/pkg/plugin/builtin/socks5.go @@ -11,7 +11,7 @@ import ( ) func init() { - Register(NewSOCKS5Plugin()) + RegisterServer(NewSOCKS5Plugin()) } const ( @@ -39,8 +39,8 @@ func NewSOCKS5Plugin() *SOCKS5Plugin { } // Metadata 返回 plugin 信息 -func (p *SOCKS5Plugin) Metadata() plugin.PluginMetadata { - return plugin.PluginMetadata{ +func (p *SOCKS5Plugin) Metadata() plugin.Metadata { + return plugin.Metadata{ Name: "socks5", Version: "1.0.0", Type: plugin.PluginTypeProxy, diff --git a/pkg/plugin/builtin/vnc.go b/pkg/plugin/builtin/vnc.go index 7197ca2..065fedd 100644 --- a/pkg/plugin/builtin/vnc.go +++ b/pkg/plugin/builtin/vnc.go @@ -9,7 +9,7 @@ import ( ) func init() { - Register(NewVNCPlugin()) + RegisterServer(NewVNCPlugin()) } // VNCPlugin VNC 远程桌面插件 @@ -23,13 +23,13 @@ func NewVNCPlugin() *VNCPlugin { } // Metadata 返回 plugin 信息 -func (p *VNCPlugin) Metadata() plugin.PluginMetadata { - return plugin.PluginMetadata{ +func (p *VNCPlugin) Metadata() plugin.Metadata { + return plugin.Metadata{ Name: "vnc", Version: "1.0.0", Type: plugin.PluginTypeApp, Source: plugin.PluginSourceBuiltin, - RunAt: plugin.SideServer, // 当前为服务端中继模式 + RunAt: plugin.SideServer, Description: "VNC remote desktop relay", Author: "GoTunnel", RuleSchema: &plugin.RuleSchema{ diff --git a/pkg/plugin/client_api.go b/pkg/plugin/client_api.go deleted file mode 100644 index 0a567cf..0000000 --- a/pkg/plugin/client_api.go +++ /dev/null @@ -1,161 +0,0 @@ -package plugin - -import ( - "context" - "fmt" - "net" - "time" -) - -// ============================================================================= -// 客户端 API 实现 -// ============================================================================= - -// ClientAPI 客户端 PluginAPI 实现 -type ClientAPI struct { - *baseAPI - clientID string - dialer Dialer -} - -// ClientAPIOption 客户端 API 配置选项 -type ClientAPIOption struct { - PluginName string - ClientID string - Config map[string]string - Dialer Dialer -} - -// NewClientAPI 创建客户端 API -func NewClientAPI(opt ClientAPIOption) *ClientAPI { - return &ClientAPI{ - baseAPI: newBaseAPI(opt.PluginName, opt.Config), - clientID: opt.ClientID, - dialer: opt.Dialer, - } -} - -// --- 网络操作 --- - -// Dial 通过隧道建立连接 -func (c *ClientAPI) Dial(network, address string) (net.Conn, error) { - if c.dialer == nil { - return nil, ErrNotConnected - } - return c.dialer.Dial(network, address) -} - -// DialTimeout 带超时的连接(使用 context 避免 goroutine 泄漏) -func (c *ClientAPI) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { - if c.dialer == nil { - return nil, ErrNotConnected - } - - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - type result struct { - conn net.Conn - err error - } - ch := make(chan result, 1) - - go func() { - conn, err := c.dialer.Dial(network, address) - select { - case ch <- result{conn, err}: - case <-ctx.Done(): - if conn != nil { - conn.Close() - } - } - }() - - select { - case r := <-ch: - return r.conn, r.err - case <-ctx.Done(): - return nil, fmt.Errorf("dial timeout") - } -} - -// Listen 客户端不支持监听 -func (c *ClientAPI) Listen(network, address string) (net.Listener, error) { - return nil, ErrNotSupported -} - -// --- 端口管理(客户端不支持)--- - -// ReservePort 客户端不支持 -func (c *ClientAPI) ReservePort(port int) error { - return ErrNotSupported -} - -// ReleasePort 客户端不支持 -func (c *ClientAPI) ReleasePort(port int) {} - -// IsPortAvailable 检查本地端口是否可用 -func (c *ClientAPI) IsPortAvailable(port int) bool { - ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) - if err != nil { - return false - } - ln.Close() - return true -} - -// --- 规则管理(客户端不支持)--- - -// CreateRule 客户端不支持 -func (c *ClientAPI) CreateRule(rule *RuleConfig) error { - return ErrNotSupported -} - -// DeleteRule 客户端不支持 -func (c *ClientAPI) DeleteRule(clientID, ruleName string) error { - return ErrNotSupported -} - -// GetRules 客户端不支持 -func (c *ClientAPI) GetRules(clientID string) ([]RuleConfig, error) { - return nil, ErrNotSupported -} - -// UpdateRule 客户端不支持 -func (c *ClientAPI) UpdateRule(clientID string, rule *RuleConfig) error { - return ErrNotSupported -} - -// --- 客户端管理 --- - -// GetClientID 获取当前客户端 ID -func (c *ClientAPI) GetClientID() string { - return c.clientID -} - -// GetClientList 客户端不支持 -func (c *ClientAPI) GetClientList() ([]ClientInfo, error) { - return nil, ErrNotSupported -} - -// IsClientOnline 客户端不支持 -func (c *ClientAPI) IsClientOnline(clientID string) bool { - return false -} - -// --- 上下文 --- - -// GetContext 获取当前上下文 -func (c *ClientAPI) GetContext() *Context { - return &Context{ - PluginName: c.getPluginName(), - Side: SideClient, - ClientID: c.clientID, - Config: c.getConfigMap(), - } -} - -// GetServerInfo 客户端不支持 -func (c *ClientAPI) GetServerInfo() *ServerInfo { - return nil -} diff --git a/pkg/plugin/registry.go b/pkg/plugin/registry.go index ad1d689..da70541 100644 --- a/pkg/plugin/registry.go +++ b/pkg/plugin/registry.go @@ -8,23 +8,23 @@ import ( // Registry 管理可用的 plugins type Registry struct { - serverPlugins map[string]ProxyHandler // 服务端插件 - clientPlugins map[string]ClientHandler // 客户端插件 - enabled map[string]bool // 启用状态 + serverPlugins map[string]ServerPlugin // 服务端插件 + clientPlugins map[string]ClientPlugin // 客户端插件 + enabled map[string]bool // 启用状态 mu sync.RWMutex } // NewRegistry 创建 plugin 注册表 func NewRegistry() *Registry { return &Registry{ - serverPlugins: make(map[string]ProxyHandler), - clientPlugins: make(map[string]ClientHandler), + serverPlugins: make(map[string]ServerPlugin), + clientPlugins: make(map[string]ClientPlugin), enabled: make(map[string]bool), } } -// RegisterBuiltin 注册服务端插件 -func (r *Registry) RegisterBuiltin(handler ProxyHandler) error { +// RegisterServer 注册服务端插件 +func (r *Registry) RegisterServer(handler ServerPlugin) error { r.mu.Lock() defer r.mu.Unlock() @@ -42,8 +42,8 @@ func (r *Registry) RegisterBuiltin(handler ProxyHandler) error { return nil } -// RegisterClientPlugin 注册客户端插件 -func (r *Registry) RegisterClientPlugin(handler ClientHandler) error { +// RegisterClient 注册客户端插件 +func (r *Registry) RegisterClient(handler ClientPlugin) error { r.mu.Lock() defer r.mu.Unlock() @@ -61,23 +61,22 @@ func (r *Registry) RegisterClientPlugin(handler ClientHandler) error { return nil } -// Get 返回指定代理类型的服务端 handler -func (r *Registry) Get(proxyType string) (ProxyHandler, error) { +// GetServer 返回服务端插件 +func (r *Registry) GetServer(name string) (ServerPlugin, error) { r.mu.RLock() defer r.mu.RUnlock() - if handler, ok := r.serverPlugins[proxyType]; ok { - if !r.enabled[proxyType] { - return nil, fmt.Errorf("plugin %s is disabled", proxyType) + if handler, ok := r.serverPlugins[name]; ok { + if !r.enabled[name] { + return nil, fmt.Errorf("plugin %s is disabled", name) } return handler, nil } - - return nil, fmt.Errorf("plugin %s not found", proxyType) + return nil, fmt.Errorf("plugin %s not found", name) } -// GetClientPlugin 返回指定类型的客户端 handler -func (r *Registry) GetClientPlugin(name string) (ClientHandler, error) { +// GetClient 返回客户端插件 +func (r *Registry) GetClient(name string) (ClientPlugin, error) { r.mu.RLock() defer r.mu.RUnlock() @@ -87,29 +86,26 @@ func (r *Registry) GetClientPlugin(name string) (ClientHandler, error) { } return handler, nil } - return nil, fmt.Errorf("client plugin %s not found", name) } // List 返回所有可用的 plugins -func (r *Registry) List() []PluginInfo { +func (r *Registry) List() []Info { r.mu.RLock() defer r.mu.RUnlock() - var plugins []PluginInfo + var plugins []Info - // 服务端插件 for name, handler := range r.serverPlugins { - plugins = append(plugins, PluginInfo{ + plugins = append(plugins, Info{ Metadata: handler.Metadata(), Loaded: true, Enabled: r.enabled[name], }) } - // 客户端插件 for name, handler := range r.clientPlugins { - plugins = append(plugins, PluginInfo{ + plugins = append(plugins, Info{ Metadata: handler.Metadata(), Loaded: true, Enabled: r.enabled[name], @@ -187,10 +183,10 @@ func (r *Registry) IsEnabled(name string) bool { return r.enabled[name] } -// RegisterAll 批量注册插件 -func (r *Registry) RegisterAll(handlers []ProxyHandler) error { +// RegisterAllServer 批量注册服务端插件 +func (r *Registry) RegisterAllServer(handlers []ServerPlugin) error { for _, handler := range handlers { - if err := r.RegisterBuiltin(handler); err != nil { + if err := r.RegisterServer(handler); err != nil { return err } } diff --git a/pkg/plugin/script/js.go b/pkg/plugin/script/js.go new file mode 100644 index 0000000..7baea40 --- /dev/null +++ b/pkg/plugin/script/js.go @@ -0,0 +1,412 @@ +package script + +import ( + "encoding/json" + "fmt" + "io" + "net" + "os" + "path/filepath" + "sync" + + "github.com/dop251/goja" + "github.com/gotunnel/pkg/plugin" +) + +// JSPlugin JavaScript 脚本插件 +type JSPlugin struct { + name string + source string + vm *goja.Runtime + metadata plugin.Metadata + config map[string]string + running bool + mu sync.Mutex +} + +// NewJSPlugin 从 JS 源码创建插件 +func NewJSPlugin(name, source string) (*JSPlugin, error) { + p := &JSPlugin{ + name: name, + source: source, + vm: goja.New(), + } + + if err := p.init(); err != nil { + return nil, err + } + + return p, nil +} + +// init 初始化 JS 运行时 +func (p *JSPlugin) init() error { + // 注入基础 API + p.vm.Set("log", p.jsLog) + p.vm.Set("config", p.jsGetConfig) + + // 注入文件 API + p.vm.Set("fs", p.createFsAPI()) + + // 注入 HTTP API + p.vm.Set("http", p.createHttpAPI()) + + // 执行脚本 + _, err := p.vm.RunString(p.source) + if err != nil { + return fmt.Errorf("run script: %w", err) + } + + // 获取元数据 + if err := p.loadMetadata(); err != nil { + return err + } + + return nil +} + +// loadMetadata 从 JS 获取元数据 +func (p *JSPlugin) loadMetadata() error { + fn, ok := goja.AssertFunction(p.vm.Get("metadata")) + if !ok { + // 使用默认元数据 + p.metadata = plugin.Metadata{ + Name: p.name, + Type: plugin.PluginTypeApp, + Source: plugin.PluginSourceScript, + RunAt: plugin.SideClient, + } + return nil + } + + result, err := fn(goja.Undefined()) + if err != nil { + return err + } + + obj := result.ToObject(p.vm) + p.metadata = plugin.Metadata{ + Name: getString(obj, "name", p.name), + Version: getString(obj, "version", "1.0.0"), + Type: plugin.PluginType(getString(obj, "type", "app")), + Source: plugin.PluginSourceScript, + RunAt: plugin.Side(getString(obj, "run_at", "client")), + Description: getString(obj, "description", ""), + Author: getString(obj, "author", ""), + } + return nil +} + +// Metadata 返回插件元数据 +func (p *JSPlugin) Metadata() plugin.Metadata { + return p.metadata +} + +// Init 初始化插件配置 +func (p *JSPlugin) Init(config map[string]string) error { + p.config = config + p.vm.Set("config", config) + return nil +} + +// Start 启动插件 +func (p *JSPlugin) Start() (string, error) { + p.mu.Lock() + defer p.mu.Unlock() + + if p.running { + return "", nil + } + + fn, ok := goja.AssertFunction(p.vm.Get("start")) + if ok { + _, err := fn(goja.Undefined()) + if err != nil { + return "", err + } + } + + p.running = true + return "script-plugin", nil +} + +// HandleConn 处理连接 +func (p *JSPlugin) HandleConn(conn net.Conn) error { + defer conn.Close() + + // 创建连接包装器 + jsConn := newJSConn(conn) + p.vm.Set("conn", jsConn) + + fn, ok := goja.AssertFunction(p.vm.Get("handleConn")) + if !ok { + return fmt.Errorf("handleConn not defined") + } + + _, err := fn(goja.Undefined(), p.vm.ToValue(jsConn)) + return err +} + +// Stop 停止插件 +func (p *JSPlugin) Stop() error { + p.mu.Lock() + defer p.mu.Unlock() + + if !p.running { + return nil + } + + fn, ok := goja.AssertFunction(p.vm.Get("stop")) + if ok { + fn(goja.Undefined()) + } + + p.running = false + return nil +} + +// jsLog JS 日志函数 +func (p *JSPlugin) jsLog(msg string) { + fmt.Printf("[JS:%s] %s\n", p.name, msg) +} + +// jsGetConfig 获取配置 +func (p *JSPlugin) jsGetConfig(key string) string { + if p.config == nil { + return "" + } + return p.config[key] +} + +// getString 从 JS 对象获取字符串 +func getString(obj *goja.Object, key, def string) string { + v := obj.Get(key) + if v == nil || goja.IsUndefined(v) { + return def + } + return v.String() +} + +// jsConn JS 连接包装器 +type jsConn struct { + conn net.Conn +} + +func newJSConn(conn net.Conn) *jsConn { + return &jsConn{conn: conn} +} + +func (c *jsConn) Read(size int) []byte { + buf := make([]byte, size) + n, err := c.conn.Read(buf) + if err != nil { + return nil + } + return buf[:n] +} + +func (c *jsConn) Write(data []byte) int { + n, _ := c.conn.Write(data) + return n +} + +func (c *jsConn) Close() { + c.conn.Close() +} + +// ============================================================================= +// 文件系统 API +// ============================================================================= + +// createFsAPI 创建文件系统 API +func (p *JSPlugin) createFsAPI() map[string]interface{} { + return map[string]interface{}{ + "readFile": p.fsReadFile, + "writeFile": p.fsWriteFile, + "readDir": p.fsReadDir, + "stat": p.fsStat, + "exists": p.fsExists, + "mkdir": p.fsMkdir, + "remove": p.fsRemove, + } +} + +func (p *JSPlugin) fsReadFile(path string) string { + data, err := os.ReadFile(path) + if err != nil { + return "" + } + return string(data) +} + +func (p *JSPlugin) fsWriteFile(path, content string) bool { + return os.WriteFile(path, []byte(content), 0644) == nil +} + +func (p *JSPlugin) fsReadDir(path string) []map[string]interface{} { + entries, err := os.ReadDir(path) + if err != nil { + return nil + } + var result []map[string]interface{} + for _, e := range entries { + info, _ := e.Info() + result = append(result, map[string]interface{}{ + "name": e.Name(), + "isDir": e.IsDir(), + "size": info.Size(), + }) + } + return result +} + +func (p *JSPlugin) fsStat(path string) map[string]interface{} { + info, err := os.Stat(path) + if err != nil { + return nil + } + return map[string]interface{}{ + "name": info.Name(), + "size": info.Size(), + "isDir": info.IsDir(), + "modTime": info.ModTime().Unix(), + } +} + +func (p *JSPlugin) fsExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func (p *JSPlugin) fsMkdir(path string) bool { + return os.MkdirAll(path, 0755) == nil +} + +func (p *JSPlugin) fsRemove(path string) bool { + return os.RemoveAll(path) == nil +} + +// ============================================================================= +// HTTP 服务 API +// ============================================================================= + +// createHttpAPI 创建 HTTP API +func (p *JSPlugin) createHttpAPI() map[string]interface{} { + return map[string]interface{}{ + "serve": p.httpServe, + "json": p.httpJSON, + "sendFile": p.httpSendFile, + } +} + +// httpServe 启动 HTTP 服务处理连接 +func (p *JSPlugin) httpServe(conn net.Conn, handler func(map[string]interface{}) map[string]interface{}) { + defer conn.Close() + + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { + return + } + + req := parseHTTPRequest(buf[:n]) + resp := handler(req) + writeHTTPResponse(conn, resp) +} + +func (p *JSPlugin) httpJSON(data interface{}) string { + b, _ := json.Marshal(data) + return string(b) +} + +func (p *JSPlugin) httpSendFile(conn net.Conn, filePath string) { + f, err := os.Open(filePath) + if err != nil { + conn.Write([]byte("HTTP/1.1 404 Not Found\r\n\r\n")) + return + } + defer f.Close() + + info, _ := f.Stat() + contentType := getContentType(filePath) + + header := fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n", + contentType, info.Size()) + conn.Write([]byte(header)) + io.Copy(conn, f) +} + +// parseHTTPRequest 解析 HTTP 请求 +func parseHTTPRequest(data []byte) map[string]interface{} { + lines := string(data) + req := map[string]interface{}{ + "method": "GET", + "path": "/", + "body": "", + } + + // 解析请求行 + if idx := indexOf(lines, " "); idx > 0 { + req["method"] = lines[:idx] + rest := lines[idx+1:] + if idx2 := indexOf(rest, " "); idx2 > 0 { + req["path"] = rest[:idx2] + } + } + + // 解析 body + if idx := indexOf(lines, "\r\n\r\n"); idx > 0 { + req["body"] = lines[idx+4:] + } + + return req +} + +// writeHTTPResponse 写入 HTTP 响应 +func writeHTTPResponse(conn net.Conn, resp map[string]interface{}) { + status := 200 + if s, ok := resp["status"].(int); ok { + status = s + } + + body := "" + if b, ok := resp["body"].(string); ok { + body = b + } + + contentType := "application/json" + if ct, ok := resp["contentType"].(string); ok { + contentType = ct + } + + header := fmt.Sprintf("HTTP/1.1 %d OK\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n", + status, contentType, len(body)) + conn.Write([]byte(header + body)) +} + +func indexOf(s, substr string) int { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return i + } + } + return -1 +} + +func getContentType(path string) string { + ext := filepath.Ext(path) + types := map[string]string{ + ".html": "text/html", + ".css": "text/css", + ".js": "application/javascript", + ".json": "application/json", + ".png": "image/png", + ".jpg": "image/jpeg", + ".gif": "image/gif", + ".txt": "text/plain", + } + if ct, ok := types[ext]; ok { + return ct + } + return "application/octet-stream" +} diff --git a/pkg/plugin/server_api.go b/pkg/plugin/server_api.go deleted file mode 100644 index bc9ace9..0000000 --- a/pkg/plugin/server_api.go +++ /dev/null @@ -1,180 +0,0 @@ -package plugin - -import ( - "net" - "time" -) - -// ============================================================================= -// 服务端依赖接口(依赖注入) -// ============================================================================= - -// PortStore 端口存储接口 -type PortStore interface { - Reserve(port int, owner string) error - Release(port int) - IsAvailable(port int) bool -} - -// RuleStore 规则存储接口 -type RuleStore interface { - GetAll(clientID string) ([]RuleConfig, error) - Create(clientID string, rule *RuleConfig) error - Update(clientID string, rule *RuleConfig) error - Delete(clientID, ruleName string) error -} - -// ClientStore 客户端存储接口 -type ClientStore interface { - GetAll() ([]ClientInfo, error) - IsOnline(clientID string) bool -} - -// ============================================================================= -// 服务端 API 实现 -// ============================================================================= - -// ServerAPI 服务端 PluginAPI 实现 -type ServerAPI struct { - *baseAPI - portStore PortStore - ruleStore RuleStore - clientStore ClientStore - serverInfo *ServerInfo -} - -// ServerAPIOption 服务端 API 配置选项 -type ServerAPIOption struct { - PluginName string - Config map[string]string - PortStore PortStore - RuleStore RuleStore - ClientStore ClientStore - ServerInfo *ServerInfo -} - -// NewServerAPI 创建服务端 API -func NewServerAPI(opt ServerAPIOption) *ServerAPI { - return &ServerAPI{ - baseAPI: newBaseAPI(opt.PluginName, opt.Config), - portStore: opt.PortStore, - ruleStore: opt.RuleStore, - clientStore: opt.ClientStore, - serverInfo: opt.ServerInfo, - } -} - -// --- 网络操作 --- - -// Dial 服务端不支持隧道拨号 -func (s *ServerAPI) Dial(network, address string) (net.Conn, error) { - return nil, ErrNotSupported -} - -// DialTimeout 服务端不支持隧道拨号 -func (s *ServerAPI) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { - return nil, ErrNotSupported -} - -// Listen 在指定地址监听 -func (s *ServerAPI) Listen(network, address string) (net.Listener, error) { - return net.Listen(network, address) -} - -// --- 端口管理 --- - -// ReservePort 预留端口 -func (s *ServerAPI) ReservePort(port int) error { - if s.portStore == nil { - return ErrNotSupported - } - return s.portStore.Reserve(port, s.getPluginName()) -} - -// ReleasePort 释放端口 -func (s *ServerAPI) ReleasePort(port int) { - if s.portStore != nil { - s.portStore.Release(port) - } -} - -// IsPortAvailable 检查端口是否可用 -func (s *ServerAPI) IsPortAvailable(port int) bool { - if s.portStore == nil { - return false - } - return s.portStore.IsAvailable(port) -} - -// --- 规则管理 --- - -// CreateRule 创建代理规则 -func (s *ServerAPI) CreateRule(rule *RuleConfig) error { - if s.ruleStore == nil { - return ErrNotSupported - } - return s.ruleStore.Create(rule.ClientID, rule) -} - -// DeleteRule 删除代理规则 -func (s *ServerAPI) DeleteRule(clientID, ruleName string) error { - if s.ruleStore == nil { - return ErrNotSupported - } - return s.ruleStore.Delete(clientID, ruleName) -} - -// GetRules 获取客户端的代理规则 -func (s *ServerAPI) GetRules(clientID string) ([]RuleConfig, error) { - if s.ruleStore == nil { - return nil, ErrNotSupported - } - return s.ruleStore.GetAll(clientID) -} - -// UpdateRule 更新代理规则 -func (s *ServerAPI) UpdateRule(clientID string, rule *RuleConfig) error { - if s.ruleStore == nil { - return ErrNotSupported - } - return s.ruleStore.Update(clientID, rule) -} - -// --- 客户端管理 --- - -// GetClientID 服务端返回空 -func (s *ServerAPI) GetClientID() string { - return "" -} - -// GetClientList 获取所有客户端列表 -func (s *ServerAPI) GetClientList() ([]ClientInfo, error) { - if s.clientStore == nil { - return nil, ErrNotSupported - } - return s.clientStore.GetAll() -} - -// IsClientOnline 检查客户端是否在线 -func (s *ServerAPI) IsClientOnline(clientID string) bool { - if s.clientStore == nil { - return false - } - return s.clientStore.IsOnline(clientID) -} - -// --- 上下文 --- - -// GetContext 获取当前上下文 -func (s *ServerAPI) GetContext() *Context { - return &Context{ - PluginName: s.getPluginName(), - Side: SideServer, - Config: s.getConfigMap(), - } -} - -// GetServerInfo 获取服务端信息 -func (s *ServerAPI) GetServerInfo() *ServerInfo { - return s.serverInfo -} diff --git a/pkg/plugin/types.go b/pkg/plugin/types.go index 6925dc1..a70bcd1 100644 --- a/pkg/plugin/types.go +++ b/pkg/plugin/types.go @@ -5,138 +5,8 @@ import ( "time" ) -// PluginType 定义 plugin 类别 -type PluginType string - -const ( - PluginTypeProxy PluginType = "proxy" // 代理协议插件 (SOCKS5 等) - PluginTypeApp PluginType = "app" // 应用插件 (VNC, 文件管理等) - PluginTypeService PluginType = "service" // 服务插件 (Web服务等) - PluginTypeTool PluginType = "tool" // 工具插件 (监控、日志等) -) - -// PluginSource 表示 plugin 来源 -type PluginSource string - -const ( - PluginSourceBuiltin PluginSource = "builtin" // 内置编译 -) - -// ConfigFieldType 配置字段类型 -type ConfigFieldType string - -const ( - ConfigFieldString ConfigFieldType = "string" - ConfigFieldNumber ConfigFieldType = "number" - ConfigFieldBool ConfigFieldType = "bool" - ConfigFieldSelect ConfigFieldType = "select" // 下拉选择 - ConfigFieldPassword ConfigFieldType = "password" // 密码输入 -) - -// ConfigField 配置字段定义 -type ConfigField struct { - Key string `json:"key"` // 配置键名 - Label string `json:"label"` // 显示标签 - Type ConfigFieldType `json:"type"` // 字段类型 - Default string `json:"default,omitempty"` // 默认值 - Required bool `json:"required,omitempty"` // 是否必填 - Options []string `json:"options,omitempty"` // select 类型的选项 - Description string `json:"description,omitempty"` // 字段描述 -} - -// RuleSchema 规则表单模式定义 -type RuleSchema struct { - NeedsLocalAddr bool `json:"needs_local_addr"` // 是否需要本地地址 - ExtraFields []ConfigField `json:"extra_fields,omitempty"` // 额外字段 -} - -// PluginMetadata 描述一个 plugin -type PluginMetadata struct { - Name string `json:"name"` // 唯一标识符 - Version string `json:"version"` // 语义化版本 - Type PluginType `json:"type"` // Plugin 类别 - Source PluginSource `json:"source"` // builtin - RunAt Side `json:"run_at"` // 运行位置: server 或 client - Description string `json:"description"` // 人类可读描述 - Author string `json:"author"` // Plugin 作者 - Icon string `json:"icon,omitempty"` // 图标文件名 - Capabilities []string `json:"capabilities,omitempty"` // 所需能力 - ConfigSchema []ConfigField `json:"config_schema,omitempty"` // 插件配置模式 - RuleSchema *RuleSchema `json:"rule_schema,omitempty"` // 规则表单模式 -} - -// PluginInfo 组合元数据和运行时状态 -type PluginInfo struct { - Metadata PluginMetadata `json:"metadata"` - Loaded bool `json:"loaded"` - Enabled bool `json:"enabled"` - LoadedAt time.Time `json:"loaded_at,omitempty"` - Error string `json:"error,omitempty"` -} - -// Dialer 用于建立连接的接口 -type Dialer interface { - Dial(network, address string) (net.Conn, error) -} - -// ProxyHandler 是所有 proxy plugin 必须实现的接口 -// 运行在服务端,处理外部连接并通过隧道转发 -type ProxyHandler interface { - // Metadata 返回 plugin 信息 - Metadata() PluginMetadata - - // Init 使用配置初始化 plugin - Init(config map[string]string) error - - // HandleConn 处理传入连接 - // dialer 用于通过隧道建立连接 - HandleConn(conn net.Conn, dialer Dialer) error - - // Close 释放 plugin 资源 - Close() error -} - -// ClientHandler 客户端插件接口 -// 运行在客户端,提供本地服务(如 VNC 服务器、文件管理等) -type ClientHandler interface { - // Metadata 返回 plugin 信息 - Metadata() PluginMetadata - - // Init 使用配置初始化 plugin - Init(config map[string]string) error - - // Start 启动客户端服务 - // 返回服务监听的本地地址(如 "127.0.0.1:5900") - Start() (localAddr string, err error) - - // HandleConn 处理来自隧道的连接 - HandleConn(conn net.Conn) error - - // Stop 停止客户端服务 - Stop() error -} - -// ExtendedProxyHandler 扩展的代理处理器接口 -// 支持 PluginAPI 的插件应实现此接口 -type ExtendedProxyHandler interface { - ProxyHandler - - // SetAPI 设置 PluginAPI,允许插件调用系统功能 - SetAPI(api PluginAPI) -} - -// LogLevel 日志级别 -type LogLevel uint8 - -const ( - LogDebug LogLevel = iota - LogInfo - LogWarn - LogError -) - // ============================================================================= -// API 相关类型 +// 基础类型 // ============================================================================= // Side 运行侧 @@ -147,86 +17,104 @@ const ( SideClient Side = "client" ) -// Context 插件运行上下文 -type Context struct { - PluginName string - Side Side - ClientID string - Config map[string]string -} - -// ServerInfo 服务端信息 -type ServerInfo struct { - BindAddr string - BindPort int - Version string -} - -// RuleConfig 代理规则配置 -type RuleConfig struct { - ClientID string `json:"client_id"` - Name string `json:"name"` - Type string `json:"type"` - LocalIP string `json:"local_ip"` - LocalPort int `json:"local_port"` - RemotePort int `json:"remote_port"` - Enabled bool `json:"enabled"` - PluginName string `json:"plugin_name,omitempty"` - PluginConfig map[string]string `json:"plugin_config,omitempty"` -} - -// ClientInfo 客户端信息 -type ClientInfo struct { - ID string `json:"id"` - Nickname string `json:"nickname"` - Online bool `json:"online"` - LastPing string `json:"last_ping,omitempty"` -} - -// EventType 事件类型 -type EventType string +// PluginType 插件类别 +type PluginType string const ( - EventClientConnect EventType = "client_connect" - EventClientDisconnect EventType = "client_disconnect" - EventRuleCreated EventType = "rule_created" - EventRuleDeleted EventType = "rule_deleted" - EventProxyConnect EventType = "proxy_connect" - EventProxyDisconnect EventType = "proxy_disconnect" + PluginTypeProxy PluginType = "proxy" // 代理协议 (SOCKS5 等) + PluginTypeApp PluginType = "app" // 应用服务 (VNC, Echo 等) ) -// Event 事件 -type Event struct { - Type EventType `json:"type"` - Timestamp time.Time `json:"timestamp"` - Data map[string]interface{} `json:"data"` -} +// PluginSource 插件来源 +type PluginSource string -// EventHandler 事件处理函数 -type EventHandler func(event *Event) - -// ============================================================================= -// 错误定义 -// ============================================================================= - -// APIError API 错误 -type APIError struct { - Code int - Message string -} - -func (e *APIError) Error() string { - return e.Message -} - -// 常见 API 错误 -var ( - ErrNotSupported = &APIError{Code: 1, Message: "operation not supported"} - ErrClientNotFound = &APIError{Code: 2, Message: "client not found"} - ErrPortOccupied = &APIError{Code: 3, Message: "port already occupied"} - ErrRuleNotFound = &APIError{Code: 4, Message: "rule not found"} - ErrRuleExists = &APIError{Code: 5, Message: "rule already exists"} - ErrNotConnected = &APIError{Code: 6, Message: "not connected"} - ErrInvalidConfig = &APIError{Code: 7, Message: "invalid configuration"} +const ( + PluginSourceBuiltin PluginSource = "builtin" // 内置编译 + PluginSourceScript PluginSource = "script" // 脚本插件 ) +// ============================================================================= +// 配置相关 +// ============================================================================= + +// ConfigFieldType 配置字段类型 +type ConfigFieldType string + +const ( + ConfigFieldString ConfigFieldType = "string" + ConfigFieldNumber ConfigFieldType = "number" + ConfigFieldBool ConfigFieldType = "bool" + ConfigFieldSelect ConfigFieldType = "select" + ConfigFieldPassword ConfigFieldType = "password" +) + +// ConfigField 配置字段定义 +type ConfigField struct { + Key string `json:"key"` + Label string `json:"label"` + Type ConfigFieldType `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"` +} + +// ============================================================================= +// 元数据 +// ============================================================================= + +// Metadata 插件元数据 +type Metadata struct { + Name string `json:"name"` + Version string `json:"version"` + Type PluginType `json:"type"` + Source PluginSource `json:"source"` + RunAt Side `json:"run_at"` + Description string `json:"description"` + Author string `json:"author,omitempty"` + ConfigSchema []ConfigField `json:"config_schema,omitempty"` + RuleSchema *RuleSchema `json:"rule_schema,omitempty"` +} + +// Info 插件运行时信息 +type Info struct { + Metadata Metadata `json:"metadata"` + Loaded bool `json:"loaded"` + Enabled bool `json:"enabled"` + LoadedAt time.Time `json:"loaded_at,omitempty"` + Error string `json:"error,omitempty"` +} + +// ============================================================================= +// 核心接口 +// ============================================================================= + +// Dialer 网络拨号接口 +type Dialer interface { + Dial(network, address string) (net.Conn, error) +} + +// ServerPlugin 服务端插件接口 +// 运行在服务端,处理外部连接并通过隧道转发到客户端 +type ServerPlugin interface { + Metadata() Metadata + Init(config map[string]string) error + HandleConn(conn net.Conn, dialer Dialer) error + Close() error +} + +// ClientPlugin 客户端插件接口 +// 运行在客户端,提供本地服务 +type ClientPlugin interface { + Metadata() Metadata + Init(config map[string]string) error + Start() (localAddr string, err error) + HandleConn(conn net.Conn) error + Stop() error +} diff --git a/pkg/protocol/message.go b/pkg/protocol/message.go index 575577d..46ff6d2 100644 --- a/pkg/protocol/message.go +++ b/pkg/protocol/message.go @@ -44,6 +44,10 @@ const ( MsgTypeClientPluginStop uint8 = 41 // 停止客户端插件 MsgTypeClientPluginStatus uint8 = 42 // 客户端插件状态 MsgTypeClientPluginConn uint8 = 43 // 客户端插件连接请求 + + // JS 插件动态安装 + MsgTypeJSPluginInstall uint8 = 50 // 安装 JS 插件 + MsgTypeJSPluginResult uint8 = 51 // 安装结果 ) // Message 基础消息结构 @@ -204,6 +208,23 @@ type ClientPluginConnRequest struct { RuleName string `json:"rule_name"` // 规则名称 } +// JSPluginInstallRequest JS 插件安装请求 +type JSPluginInstallRequest struct { + PluginName string `json:"plugin_name"` // 插件名称 + Source string `json:"source"` // JS 源码 + RuleName string `json:"rule_name"` // 规则名称 + RemotePort int `json:"remote_port"` // 服务端监听端口 + Config map[string]string `json:"config"` // 插件配置 + AutoStart bool `json:"auto_start"` // 是否自动启动 +} + +// JSPluginInstallResult JS 插件安装结果 +type JSPluginInstallResult struct { + PluginName string `json:"plugin_name"` + Success bool `json:"success"` + Error string `json:"error,omitempty"` +} + // WriteMessage 写入消息到 writer func WriteMessage(w io.Writer, msg *Message) error { header := make([]byte, HeaderSize) diff --git a/plugins/echo.js b/plugins/echo.js new file mode 100644 index 0000000..8858a38 --- /dev/null +++ b/plugins/echo.js @@ -0,0 +1,30 @@ +// Echo JS Plugin - 回显插件示例 +function metadata() { + return { + name: "echo-js", + version: "1.0.0", + type: "app", + run_at: "client", + description: "Echo plugin written in JavaScript", + author: "GoTunnel" + }; +} + +function start() { + log("Echo JS plugin started"); +} + +function handleConn(conn) { + log("New connection"); + while (true) { + var data = conn.Read(4096); + if (!data || data.length === 0) { + break; + } + conn.Write(data); + } +} + +function stop() { + log("Echo JS plugin stopped"); +} diff --git a/plugins/filemanager.js b/plugins/filemanager.js new file mode 100644 index 0000000..a43cf86 --- /dev/null +++ b/plugins/filemanager.js @@ -0,0 +1,194 @@ +// FileManager JS Plugin - 文件管理插件 +// 提供 HTTP API 管理客户端本地文件 + +var authToken = ""; +var basePath = "/"; + +function metadata() { + return { + name: "filemanager", + version: "1.0.0", + type: "app", + run_at: "client", + description: "File manager with HTTP API", + author: "GoTunnel" + }; +} + +function start() { + authToken = config("auth_token") || "admin"; + basePath = config("base_path") || "/"; + log("FileManager started, base: " + basePath); +} + +function stop() { + log("FileManager stopped"); +} + +// 处理连接 +function handleConn(conn) { + var data = conn.Read(4096); + if (!data) return; + + var req = parseRequest(String.fromCharCode.apply(null, data)); + var resp = handleRequest(req); + conn.Write(stringToBytes(resp)); +} + +// 解析 HTTP 请求 +function parseRequest(raw) { + var lines = raw.split("\r\n"); + var first = lines[0].split(" "); + var req = { + method: first[0] || "GET", + path: first[1] || "/", + headers: {}, + body: "" + }; + + var bodyStart = raw.indexOf("\r\n\r\n"); + if (bodyStart > 0) { + req.body = raw.substring(bodyStart + 4); + } + return req; +} + +// 处理请求 +function handleRequest(req) { + // 检查认证 + if (req.path.indexOf("?token=" + authToken) < 0) { + return httpResponse(401, {error: "Unauthorized"}); + } + + var path = req.path.split("?")[0]; + + if (path === "/api/list") { + return handleList(req); + } else if (path === "/api/read") { + return handleRead(req); + } else if (path === "/api/write") { + return handleWrite(req); + } else if (path === "/api/delete") { + return handleDelete(req); + } + + return httpResponse(404, {error: "Not found"}); +} + +// 获取查询参数 +function getQueryParam(req, name) { + var query = req.path.split("?")[1] || ""; + var params = query.split("&"); + for (var i = 0; i < params.length; i++) { + var pair = params[i].split("="); + if (pair[0] === name) { + return decodeURIComponent(pair[1] || ""); + } + } + return ""; +} + +// 安全路径检查 +function safePath(path) { + if (!path) return basePath; + // 防止路径遍历 + if (path.indexOf("..") >= 0) return null; + if (path.charAt(0) !== "/") { + path = basePath + "/" + path; + } + return path; +} + +// 列出目录 +function handleList(req) { + var dir = safePath(getQueryParam(req, "path")); + if (!dir) { + return httpResponse(400, {error: "Invalid path"}); + } + + var entries = fs.readDir(dir); + if (!entries) { + return httpResponse(404, {error: "Directory not found"}); + } + + return httpResponse(200, {path: dir, entries: entries}); +} + +// 读取文件 +function handleRead(req) { + var file = safePath(getQueryParam(req, "path")); + if (!file) { + return httpResponse(400, {error: "Invalid path"}); + } + + var stat = fs.stat(file); + if (!stat) { + return httpResponse(404, {error: "File not found"}); + } + if (stat.isDir) { + return httpResponse(400, {error: "Cannot read directory"}); + } + + var content = fs.readFile(file); + return httpResponse(200, {path: file, content: content, size: stat.size}); +} + +// 写入文件 +function handleWrite(req) { + var file = safePath(getQueryParam(req, "path")); + if (!file) { + return httpResponse(400, {error: "Invalid path"}); + } + + if (req.method !== "POST") { + return httpResponse(405, {error: "Method not allowed"}); + } + + if (fs.writeFile(file, req.body)) { + return httpResponse(200, {success: true, path: file}); + } + return httpResponse(500, {error: "Write failed"}); +} + +// 删除文件 +function handleDelete(req) { + var file = safePath(getQueryParam(req, "path")); + if (!file) { + return httpResponse(400, {error: "Invalid path"}); + } + + if (!fs.exists(file)) { + return httpResponse(404, {error: "File not found"}); + } + + if (fs.remove(file)) { + return httpResponse(200, {success: true, path: file}); + } + return httpResponse(500, {error: "Delete failed"}); +} + +// 构建 HTTP 响应 +function httpResponse(status, data) { + var body = JSON.stringify(data); + var statusText = status === 200 ? "OK" : + status === 400 ? "Bad Request" : + status === 401 ? "Unauthorized" : + status === 404 ? "Not Found" : + status === 405 ? "Method Not Allowed" : + status === 500 ? "Internal Server Error" : "Unknown"; + + return "HTTP/1.1 " + status + " " + statusText + "\r\n" + + "Content-Type: application/json\r\n" + + "Content-Length: " + body.length + "\r\n" + + "Access-Control-Allow-Origin: *\r\n" + + "\r\n" + body; +} + +// 字符串转字节数组 +function stringToBytes(str) { + var bytes = []; + for (var i = 0; i < str.length; i++) { + bytes.push(str.charCodeAt(i)); + } + return bytes; +} diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 748213b..c6efbff 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -1,5 +1,5 @@ import { get, post, put, del } from '../config/axios' -import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, PluginInfo, StorePluginInfo, PluginConfigResponse } from '../types' +import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, PluginInfo, StorePluginInfo, PluginConfigResponse, JSPlugin } from '../types' // 重新导出 token 管理方法 export { getToken, setToken, removeToken } from '../config/axios' @@ -39,3 +39,12 @@ export const getClientPluginConfig = (clientId: string, pluginName: string) => get(`/client-plugin/${clientId}/${pluginName}/config`) export const updateClientPluginConfig = (clientId: string, pluginName: string, config: Record) => put(`/client-plugin/${clientId}/${pluginName}/config`, { config }) + +// JS 插件管理 +export const getJSPlugins = () => get('/js-plugins') +export const createJSPlugin = (plugin: JSPlugin) => post('/js-plugins', plugin) +export const getJSPlugin = (name: string) => get(`/js-plugin/${name}`) +export const updateJSPlugin = (name: string, plugin: JSPlugin) => put(`/js-plugin/${name}`, plugin) +export const deleteJSPlugin = (name: string) => del(`/js-plugin/${name}`) +export const pushJSPluginToClient = (pluginName: string, clientId: string) => + post(`/js-plugin/${pluginName}/push/${clientId}`) diff --git a/web/src/types/index.ts b/web/src/types/index.ts index ce1fa4f..2277775 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -111,3 +111,15 @@ export interface StorePluginInfo { icon?: string download_url?: string } + +// JS 插件信息 +export interface JSPlugin { + name: string + source: string + description: string + author: string + auto_push: string[] + config: Record + auto_start: boolean + enabled: boolean +} diff --git a/web/src/views/PluginsView.vue b/web/src/views/PluginsView.vue index c337fab..26e43ff 100644 --- a/web/src/views/PluginsView.vue +++ b/web/src/views/PluginsView.vue @@ -3,19 +3,23 @@ import { ref, onMounted, computed } from 'vue' import { useRouter } from 'vue-router' import { NCard, NButton, NSpace, NTag, NStatistic, NGrid, NGi, - NEmpty, NSpin, NIcon, NSwitch, NTabs, NTabPane, useMessage + NEmpty, NSpin, NIcon, NSwitch, NTabs, NTabPane, useMessage, + NSelect } from 'naive-ui' -import { ArrowBackOutline, ExtensionPuzzleOutline, StorefrontOutline } from '@vicons/ionicons5' -import { getPlugins, enablePlugin, disablePlugin, getStorePlugins } from '../api' -import type { PluginInfo, StorePluginInfo } from '../types' +import { ArrowBackOutline, ExtensionPuzzleOutline, StorefrontOutline, CodeSlashOutline } from '@vicons/ionicons5' +import { getPlugins, enablePlugin, disablePlugin, getStorePlugins, getJSPlugins, pushJSPluginToClient, getClients } from '../api' +import type { PluginInfo, StorePluginInfo, JSPlugin, ClientStatus } from '../types' const router = useRouter() const message = useMessage() const plugins = ref([]) const storePlugins = ref([]) +const jsPlugins = ref([]) +const clients = ref([]) const storeUrl = ref('') const loading = ref(true) const storeLoading = ref(false) +const jsLoading = ref(false) const activeTab = ref('installed') const loadPlugins = async () => { @@ -89,9 +93,58 @@ const handleTabChange = (tab: string) => { if (tab === 'store' && storePlugins.value.length === 0) { loadStorePlugins() } + if (tab === 'js' && jsPlugins.value.length === 0) { + loadJSPlugins() + } } -onMounted(loadPlugins) +// JS 插件相关 +/* 安全加固:暂时禁用创建/删除功能 +const showJSModal = ref(false) +const jsForm = ref({...}) +const configItems = ref>([]) +const configToObject = () => {...} +const handleCreateJSPlugin = async () => {...} +const handleDeleteJSPlugin = async (name: string) => {...} +const resetJSForm = () => {...} +*/ + +const loadJSPlugins = async () => { + jsLoading.value = true + try { + const { data } = await getJSPlugins() + jsPlugins.value = data || [] + } catch (e) { + console.error('Failed to load JS plugins', e) + } finally { + jsLoading.value = false + } +} + +const loadClients = async () => { + try { + const { data } = await getClients() + clients.value = data || [] + } catch (e) { + console.error('Failed to load clients', e) + } +} + +const handlePushJSPlugin = async (pluginName: string, clientId: string) => { + try { + await pushJSPluginToClient(pluginName, clientId) + message.success(`已推送 ${pluginName} 到 ${clientId}`) + } catch (e) { + message.error('推送失败') + } +} + +const onlineClients = computed(() => clients.value.filter(c => c.online)) + +onMounted(() => { + loadPlugins() + loadClients() +})