update
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 48s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 48s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m47s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 57s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 49s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m5s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 50s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 51s
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 48s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 48s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m47s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 57s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 49s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m5s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 50s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 51s
This commit is contained in:
@@ -32,11 +32,13 @@ func main() {
|
|||||||
|
|
||||||
// 初始化插件系统
|
// 初始化插件系统
|
||||||
registry := plugin.NewRegistry()
|
registry := plugin.NewRegistry()
|
||||||
if err := registry.RegisterAll(builtin.GetAll()); err != nil {
|
for _, h := range builtin.GetClientPlugins() {
|
||||||
log.Fatalf("[Plugin] Register error: %v", err)
|
if err := registry.RegisterClient(h); err != nil {
|
||||||
|
log.Fatalf("[Plugin] Register error: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
client.SetPluginRegistry(registry)
|
client.SetPluginRegistry(registry)
|
||||||
log.Printf("[Plugin] Registered %d plugins", len(builtin.GetAll()))
|
log.Printf("[Plugin] Registered %d plugins", len(builtin.GetClientPlugins()))
|
||||||
|
|
||||||
client.Run()
|
client.Run()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/gotunnel/internal/server/app"
|
"github.com/gotunnel/internal/server/app"
|
||||||
"github.com/gotunnel/internal/server/config"
|
"github.com/gotunnel/internal/server/config"
|
||||||
@@ -56,15 +57,21 @@ func main() {
|
|||||||
|
|
||||||
// 初始化插件系统
|
// 初始化插件系统
|
||||||
registry := plugin.NewRegistry()
|
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)
|
log.Fatalf("[Plugin] Register error: %v", err)
|
||||||
}
|
}
|
||||||
server.SetPluginRegistry(registry)
|
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 控制台
|
// 启动 Web 控制台
|
||||||
if cfg.Web.Enabled {
|
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)
|
addr := fmt.Sprintf("%s:%d", cfg.Web.BindAddr, cfg.Web.BindPort)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -82,3 +89,28 @@ func main() {
|
|||||||
|
|
||||||
log.Fatal(server.Run())
|
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
|
||||||
|
}
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -10,13 +10,18 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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/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/google/uuid v1.6.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
golang.org/x/sys v0.38.0 // 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/libc v1.66.10 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
|||||||
8
go.sum
8
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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
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 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
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=
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
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/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 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ func NewManager() (*Manager, error) {
|
|||||||
registry: registry,
|
registry: registry,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册内置 plugins
|
|
||||||
if err := m.registerBuiltins(); err != nil {
|
if err := m.registerBuiltins(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -32,22 +31,21 @@ func NewManager() (*Manager, error) {
|
|||||||
|
|
||||||
// registerBuiltins 注册内置 plugins
|
// registerBuiltins 注册内置 plugins
|
||||||
func (m *Manager) registerBuiltins() error {
|
func (m *Manager) registerBuiltins() error {
|
||||||
// 注册服务端插件
|
for _, h := range builtin.GetClientPlugins() {
|
||||||
if err := m.registry.RegisterAll(builtin.GetAll()); err != nil {
|
if err := m.registry.RegisterClient(h); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
// 注册客户端插件
|
|
||||||
for _, h := range builtin.GetAllClientPlugins() {
|
|
||||||
if err := m.registry.RegisterClientPlugin(h); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("[Plugin] Registered %d server plugins, %d client plugins",
|
log.Printf("[Plugin] Registered %d client plugins", len(builtin.GetClientPlugins()))
|
||||||
len(builtin.GetAll()), len(builtin.GetAllClientPlugins()))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHandler 返回指定代理类型的 handler
|
// GetClient 返回客户端插件
|
||||||
func (m *Manager) GetHandler(proxyType string) (plugin.ProxyHandler, error) {
|
func (m *Manager) GetClient(name string) (plugin.ClientPlugin, error) {
|
||||||
return m.registry.Get(proxyType)
|
return m.registry.GetClient(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRegistry 返回插件注册表
|
||||||
|
func (m *Manager) GetRegistry() *plugin.Registry {
|
||||||
|
return m.registry
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gotunnel/pkg/plugin"
|
"github.com/gotunnel/pkg/plugin"
|
||||||
|
"github.com/gotunnel/pkg/plugin/script"
|
||||||
"github.com/gotunnel/pkg/protocol"
|
"github.com/gotunnel/pkg/protocol"
|
||||||
"github.com/gotunnel/pkg/relay"
|
"github.com/gotunnel/pkg/relay"
|
||||||
"github.com/hashicorp/yamux"
|
"github.com/hashicorp/yamux"
|
||||||
@@ -38,7 +39,7 @@ type Client struct {
|
|||||||
rules []protocol.ProxyRule
|
rules []protocol.ProxyRule
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
pluginRegistry *plugin.Registry
|
pluginRegistry *plugin.Registry
|
||||||
runningPlugins map[string]plugin.ClientHandler // 运行中的客户端插件
|
runningPlugins map[string]plugin.ClientPlugin // 运行中的客户端插件
|
||||||
pluginMu sync.RWMutex
|
pluginMu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ func NewClient(serverAddr, token, id string) *Client {
|
|||||||
ServerAddr: serverAddr,
|
ServerAddr: serverAddr,
|
||||||
Token: token,
|
Token: token,
|
||||||
ID: id,
|
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)
|
c.handleClientPluginStart(stream, msg)
|
||||||
case protocol.MsgTypeClientPluginConn:
|
case protocol.MsgTypeClientPluginConn:
|
||||||
c.handleClientPluginConn(stream, msg)
|
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 {
|
if c.pluginRegistry != nil {
|
||||||
handler, err := c.pluginRegistry.Get(cfg.PluginName)
|
handler, err := c.pluginRegistry.GetClient(cfg.PluginName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[Client] Plugin %s not found: %v", cfg.PluginName, err)
|
log.Printf("[Client] Plugin %s not found: %v", cfg.PluginName, err)
|
||||||
return
|
return
|
||||||
@@ -399,7 +402,7 @@ func (c *Client) handleClientPluginStart(stream net.Conn, msg *protocol.Message)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, err := c.pluginRegistry.GetClientPlugin(req.PluginName)
|
handler, err := c.pluginRegistry.GetClient(req.PluginName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", err.Error())
|
c.sendPluginStatus(stream, req.PluginName, req.RuleName, false, "", err.Error())
|
||||||
return
|
return
|
||||||
@@ -462,3 +465,68 @@ func (c *Client) handleClientPluginConn(stream net.Conn, msg *protocol.Message)
|
|||||||
// 让插件处理连接
|
// 让插件处理连接
|
||||||
handler.HandleConn(stream)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,19 +45,21 @@ func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// WebServer Web控制台服务
|
// WebServer Web控制台服务
|
||||||
type WebServer struct {
|
type WebServer struct {
|
||||||
ClientStore db.ClientStore
|
ClientStore db.ClientStore
|
||||||
Server router.ServerInterface
|
Server router.ServerInterface
|
||||||
Config *config.ServerConfig
|
Config *config.ServerConfig
|
||||||
ConfigPath string
|
ConfigPath string
|
||||||
|
JSPluginStore db.JSPluginStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWebServer 创建Web服务
|
// 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{
|
return &WebServer{
|
||||||
ClientStore: cs,
|
ClientStore: cs,
|
||||||
Server: srv,
|
Server: srv,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
ConfigPath: cfgPath,
|
ConfigPath: cfgPath,
|
||||||
|
JSPluginStore: jsStore,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,3 +148,8 @@ func (w *WebServer) GetConfigPath() string {
|
|||||||
func (w *WebServer) SaveConfig() error {
|
func (w *WebServer) SaveConfig() error {
|
||||||
return config.SaveServerConfig(w.ConfigPath, w.Config)
|
return config.SaveServerConfig(w.ConfigPath, w.Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetJSPluginStore 获取 JS 插件存储
|
||||||
|
func (w *WebServer) GetJSPluginStore() db.JSPluginStore {
|
||||||
|
return w.JSPluginStore
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ type ServerConfig struct {
|
|||||||
Server ServerSettings `yaml:"server"`
|
Server ServerSettings `yaml:"server"`
|
||||||
Web WebSettings `yaml:"web"`
|
Web WebSettings `yaml:"web"`
|
||||||
PluginStore PluginStoreSettings `yaml:"plugin_store"`
|
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 扩展商店设置
|
// PluginStoreSettings 扩展商店设置
|
||||||
|
|||||||
@@ -33,6 +33,18 @@ type PluginData struct {
|
|||||||
WASMData []byte `json:"-"`
|
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 客户端存储接口
|
// ClientStore 客户端存储接口
|
||||||
type ClientStore interface {
|
type ClientStore interface {
|
||||||
GetAllClients() ([]Client, error)
|
GetAllClients() ([]Client, error)
|
||||||
@@ -55,9 +67,19 @@ type PluginStore interface {
|
|||||||
GetPluginWASM(name string) ([]byte, error)
|
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 统一存储接口
|
// Store 统一存储接口
|
||||||
type Store interface {
|
type Store interface {
|
||||||
ClientStore
|
ClientStore
|
||||||
PluginStore
|
PluginStore
|
||||||
|
JSPluginStore
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,24 @@ func (s *SQLiteStore) init() error {
|
|||||||
// 迁移:添加 icon 列
|
// 迁移:添加 icon 列
|
||||||
s.db.Exec(`ALTER TABLE plugins ADD COLUMN icon TEXT`)
|
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
|
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)
|
err := s.db.QueryRow(`SELECT wasm_data FROM plugins WHERE name = ?`, name).Scan(&data)
|
||||||
return data, err
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ func NewManager() (*Manager, error) {
|
|||||||
registry: registry,
|
registry: registry,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册内置 plugins
|
|
||||||
if err := m.registerBuiltins(); err != nil {
|
if err := m.registerBuiltins(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -32,28 +31,26 @@ func NewManager() (*Manager, error) {
|
|||||||
|
|
||||||
// registerBuiltins 注册内置 plugins
|
// registerBuiltins 注册内置 plugins
|
||||||
func (m *Manager) registerBuiltins() error {
|
func (m *Manager) registerBuiltins() error {
|
||||||
// 注册服务端插件
|
if err := m.registry.RegisterAllServer(builtin.GetServerPlugins()); err != nil {
|
||||||
if err := m.registry.RegisterAll(builtin.GetAll()); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 注册客户端插件
|
for _, h := range builtin.GetClientPlugins() {
|
||||||
for _, h := range builtin.GetAllClientPlugins() {
|
if err := m.registry.RegisterClient(h); err != nil {
|
||||||
if err := m.registry.RegisterClientPlugin(h); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("[Plugin] Registered %d server plugins, %d client plugins",
|
log.Printf("[Plugin] Registered %d server, %d client plugins",
|
||||||
len(builtin.GetAll()), len(builtin.GetAllClientPlugins()))
|
len(builtin.GetServerPlugins()), len(builtin.GetClientPlugins()))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHandler 返回指定代理类型的 handler
|
// GetServer 返回服务端插件
|
||||||
func (m *Manager) GetHandler(proxyType string) (plugin.ProxyHandler, error) {
|
func (m *Manager) GetServer(name string) (plugin.ServerPlugin, error) {
|
||||||
return m.registry.Get(proxyType)
|
return m.registry.GetServer(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPlugins 返回所有可用的 plugins
|
// ListPlugins 返回所有插件
|
||||||
func (m *Manager) ListPlugins() []plugin.PluginInfo {
|
func (m *Manager) ListPlugins() []plugin.Info {
|
||||||
return m.registry.List()
|
return m.registry.List()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,18 @@ type ServerInterface interface {
|
|||||||
// 插件配置
|
// 插件配置
|
||||||
GetPluginConfigSchema(name string) ([]ConfigField, error)
|
GetPluginConfigSchema(name string) ([]ConfigField, error)
|
||||||
SyncPluginConfigToClient(clientID string, pluginName string, config map[string]string) 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 包导出)
|
// ConfigField 配置字段(从 plugin 包导出)
|
||||||
@@ -89,21 +101,24 @@ type AppInterface interface {
|
|||||||
GetConfig() *config.ServerConfig
|
GetConfig() *config.ServerConfig
|
||||||
GetConfigPath() string
|
GetConfigPath() string
|
||||||
SaveConfig() error
|
SaveConfig() error
|
||||||
|
GetJSPluginStore() db.JSPluginStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIHandler API处理器
|
// APIHandler API处理器
|
||||||
type APIHandler struct {
|
type APIHandler struct {
|
||||||
clientStore db.ClientStore
|
clientStore db.ClientStore
|
||||||
server ServerInterface
|
server ServerInterface
|
||||||
app AppInterface
|
app AppInterface
|
||||||
|
jsPluginStore db.JSPluginStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterRoutes 注册所有 API 路由
|
// RegisterRoutes 注册所有 API 路由
|
||||||
func RegisterRoutes(r *Router, app AppInterface) {
|
func RegisterRoutes(r *Router, app AppInterface) {
|
||||||
h := &APIHandler{
|
h := &APIHandler{
|
||||||
clientStore: app.GetClientStore(),
|
clientStore: app.GetClientStore(),
|
||||||
server: app.GetServer(),
|
server: app.GetServer(),
|
||||||
app: app,
|
app: app,
|
||||||
|
jsPluginStore: app.GetJSPluginStore(),
|
||||||
}
|
}
|
||||||
|
|
||||||
api := r.Group("/api")
|
api := r.Group("/api")
|
||||||
@@ -116,6 +131,8 @@ func RegisterRoutes(r *Router, app AppInterface) {
|
|||||||
api.HandleFunc("/plugin/", h.handlePlugin)
|
api.HandleFunc("/plugin/", h.handlePlugin)
|
||||||
api.HandleFunc("/store/plugins", h.handleStorePlugins)
|
api.HandleFunc("/store/plugins", h.handleStorePlugins)
|
||||||
api.HandleFunc("/client-plugin/", h.handleClientPlugin)
|
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) {
|
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"})
|
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})
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ type Server struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
pluginRegistry *plugin.Registry
|
pluginRegistry *plugin.Registry
|
||||||
|
jsPlugins []JSPluginEntry // 配置的 JS 插件
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSPluginEntry JS 插件条目
|
||||||
|
type JSPluginEntry struct {
|
||||||
|
Name string
|
||||||
|
Source string
|
||||||
|
AutoPush []string
|
||||||
|
Config map[string]string
|
||||||
|
AutoStart bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientSession 客户端会话
|
// ClientSession 客户端会话
|
||||||
@@ -85,6 +95,12 @@ func (s *Server) SetPluginRegistry(registry *plugin.Registry) {
|
|||||||
s.pluginRegistry = 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 启动服务端
|
// Run 启动服务端
|
||||||
func (s *Server) Run() error {
|
func (s *Server) Run() error {
|
||||||
addr := fmt.Sprintf("%s:%d", s.bindAddr, s.bindPort)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自动推送 JS 插件
|
||||||
|
s.autoPushJSPlugins(cs)
|
||||||
|
|
||||||
s.startProxyListeners(cs)
|
s.startProxyListeners(cs)
|
||||||
go s.heartbeatLoop(cs)
|
go s.heartbeatLoop(cs)
|
||||||
|
|
||||||
@@ -362,7 +381,7 @@ func (s *Server) acceptProxyServerConns(cs *ClientSession, ln net.Listener, rule
|
|||||||
|
|
||||||
// 优先使用插件系统
|
// 优先使用插件系统
|
||||||
if s.pluginRegistry != nil {
|
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)
|
handler.Init(rule.PluginConfig)
|
||||||
for {
|
for {
|
||||||
conn, err := ln.Accept()
|
conn, err := ln.Accept()
|
||||||
@@ -662,7 +681,7 @@ func (s *Server) InstallPluginsToClient(clientID string, plugins []string) error
|
|||||||
if !found {
|
if !found {
|
||||||
// 获取插件信息
|
// 获取插件信息
|
||||||
version := "1.0.0"
|
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
|
version = handler.Metadata().Version
|
||||||
}
|
}
|
||||||
client.Plugins = append(client.Plugins, db.ClientPlugin{
|
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")
|
return nil, fmt.Errorf("plugin registry not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, err := s.pluginRegistry.Get(name)
|
handler, err := s.pluginRegistry.GetServer(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("plugin %s not found", name)
|
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)
|
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 检查是否为客户端插件
|
// isClientPlugin 检查是否为客户端插件
|
||||||
func (s *Server) isClientPlugin(pluginType string) bool {
|
func (s *Server) isClientPlugin(pluginType string) bool {
|
||||||
if s.pluginRegistry == nil {
|
if s.pluginRegistry == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
handler, err := s.pluginRegistry.GetClientPlugin(pluginType)
|
handler, err := s.pluginRegistry.GetClient(pluginType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -950,3 +1022,39 @@ func (s *Server) handleClientPluginConn(cs *ClientSession, conn net.Conn, rule p
|
|||||||
|
|
||||||
relay.Relay(conn, stream)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterClientPlugin(NewEchoPlugin())
|
RegisterClient(NewEchoPlugin())
|
||||||
}
|
}
|
||||||
|
|
||||||
// EchoPlugin 回显插件 - 客户端插件示例
|
// EchoPlugin 回显插件 - 客户端插件示例
|
||||||
@@ -27,8 +27,8 @@ func NewEchoPlugin() *EchoPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Metadata 返回插件信息
|
// Metadata 返回插件信息
|
||||||
func (p *EchoPlugin) Metadata() plugin.PluginMetadata {
|
func (p *EchoPlugin) Metadata() plugin.Metadata {
|
||||||
return plugin.PluginMetadata{
|
return plugin.Metadata{
|
||||||
Name: "echo",
|
Name: "echo",
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
Type: plugin.PluginTypeApp,
|
Type: plugin.PluginTypeApp,
|
||||||
|
|||||||
@@ -2,28 +2,27 @@ package builtin
|
|||||||
|
|
||||||
import "github.com/gotunnel/pkg/plugin"
|
import "github.com/gotunnel/pkg/plugin"
|
||||||
|
|
||||||
// 全局插件注册表
|
|
||||||
var (
|
var (
|
||||||
serverPlugins []plugin.ProxyHandler
|
serverPlugins []plugin.ServerPlugin
|
||||||
clientPlugins []plugin.ClientHandler
|
clientPlugins []plugin.ClientPlugin
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register 注册服务端插件
|
// RegisterServer 注册服务端插件
|
||||||
func Register(handler plugin.ProxyHandler) {
|
func RegisterServer(handler plugin.ServerPlugin) {
|
||||||
serverPlugins = append(serverPlugins, handler)
|
serverPlugins = append(serverPlugins, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterClientPlugin 注册客户端插件
|
// RegisterClient 注册客户端插件
|
||||||
func RegisterClientPlugin(handler plugin.ClientHandler) {
|
func RegisterClient(handler plugin.ClientPlugin) {
|
||||||
clientPlugins = append(clientPlugins, handler)
|
clientPlugins = append(clientPlugins, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAll 返回所有服务端插件
|
// GetServerPlugins 返回所有服务端插件
|
||||||
func GetAll() []plugin.ProxyHandler {
|
func GetServerPlugins() []plugin.ServerPlugin {
|
||||||
return serverPlugins
|
return serverPlugins
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllClientPlugins 返回所有客户端插件
|
// GetClientPlugins 返回所有客户端插件
|
||||||
func GetAllClientPlugins() []plugin.ClientHandler {
|
func GetClientPlugins() []plugin.ClientPlugin {
|
||||||
return clientPlugins
|
return clientPlugins
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Register(NewSOCKS5Plugin())
|
RegisterServer(NewSOCKS5Plugin())
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -39,8 +39,8 @@ func NewSOCKS5Plugin() *SOCKS5Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Metadata 返回 plugin 信息
|
// Metadata 返回 plugin 信息
|
||||||
func (p *SOCKS5Plugin) Metadata() plugin.PluginMetadata {
|
func (p *SOCKS5Plugin) Metadata() plugin.Metadata {
|
||||||
return plugin.PluginMetadata{
|
return plugin.Metadata{
|
||||||
Name: "socks5",
|
Name: "socks5",
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
Type: plugin.PluginTypeProxy,
|
Type: plugin.PluginTypeProxy,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Register(NewVNCPlugin())
|
RegisterServer(NewVNCPlugin())
|
||||||
}
|
}
|
||||||
|
|
||||||
// VNCPlugin VNC 远程桌面插件
|
// VNCPlugin VNC 远程桌面插件
|
||||||
@@ -23,13 +23,13 @@ func NewVNCPlugin() *VNCPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Metadata 返回 plugin 信息
|
// Metadata 返回 plugin 信息
|
||||||
func (p *VNCPlugin) Metadata() plugin.PluginMetadata {
|
func (p *VNCPlugin) Metadata() plugin.Metadata {
|
||||||
return plugin.PluginMetadata{
|
return plugin.Metadata{
|
||||||
Name: "vnc",
|
Name: "vnc",
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
Type: plugin.PluginTypeApp,
|
Type: plugin.PluginTypeApp,
|
||||||
Source: plugin.PluginSourceBuiltin,
|
Source: plugin.PluginSourceBuiltin,
|
||||||
RunAt: plugin.SideServer, // 当前为服务端中继模式
|
RunAt: plugin.SideServer,
|
||||||
Description: "VNC remote desktop relay",
|
Description: "VNC remote desktop relay",
|
||||||
Author: "GoTunnel",
|
Author: "GoTunnel",
|
||||||
RuleSchema: &plugin.RuleSchema{
|
RuleSchema: &plugin.RuleSchema{
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -8,23 +8,23 @@ import (
|
|||||||
|
|
||||||
// Registry 管理可用的 plugins
|
// Registry 管理可用的 plugins
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
serverPlugins map[string]ProxyHandler // 服务端插件
|
serverPlugins map[string]ServerPlugin // 服务端插件
|
||||||
clientPlugins map[string]ClientHandler // 客户端插件
|
clientPlugins map[string]ClientPlugin // 客户端插件
|
||||||
enabled map[string]bool // 启用状态
|
enabled map[string]bool // 启用状态
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRegistry 创建 plugin 注册表
|
// NewRegistry 创建 plugin 注册表
|
||||||
func NewRegistry() *Registry {
|
func NewRegistry() *Registry {
|
||||||
return &Registry{
|
return &Registry{
|
||||||
serverPlugins: make(map[string]ProxyHandler),
|
serverPlugins: make(map[string]ServerPlugin),
|
||||||
clientPlugins: make(map[string]ClientHandler),
|
clientPlugins: make(map[string]ClientPlugin),
|
||||||
enabled: make(map[string]bool),
|
enabled: make(map[string]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterBuiltin 注册服务端插件
|
// RegisterServer 注册服务端插件
|
||||||
func (r *Registry) RegisterBuiltin(handler ProxyHandler) error {
|
func (r *Registry) RegisterServer(handler ServerPlugin) error {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ func (r *Registry) RegisterBuiltin(handler ProxyHandler) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterClientPlugin 注册客户端插件
|
// RegisterClient 注册客户端插件
|
||||||
func (r *Registry) RegisterClientPlugin(handler ClientHandler) error {
|
func (r *Registry) RegisterClient(handler ClientPlugin) error {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
@@ -61,23 +61,22 @@ func (r *Registry) RegisterClientPlugin(handler ClientHandler) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 返回指定代理类型的服务端 handler
|
// GetServer 返回服务端插件
|
||||||
func (r *Registry) Get(proxyType string) (ProxyHandler, error) {
|
func (r *Registry) GetServer(name string) (ServerPlugin, error) {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
if handler, ok := r.serverPlugins[proxyType]; ok {
|
if handler, ok := r.serverPlugins[name]; ok {
|
||||||
if !r.enabled[proxyType] {
|
if !r.enabled[name] {
|
||||||
return nil, fmt.Errorf("plugin %s is disabled", proxyType)
|
return nil, fmt.Errorf("plugin %s is disabled", name)
|
||||||
}
|
}
|
||||||
return handler, nil
|
return handler, nil
|
||||||
}
|
}
|
||||||
|
return nil, fmt.Errorf("plugin %s not found", name)
|
||||||
return nil, fmt.Errorf("plugin %s not found", proxyType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClientPlugin 返回指定类型的客户端 handler
|
// GetClient 返回客户端插件
|
||||||
func (r *Registry) GetClientPlugin(name string) (ClientHandler, error) {
|
func (r *Registry) GetClient(name string) (ClientPlugin, error) {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
@@ -87,29 +86,26 @@ func (r *Registry) GetClientPlugin(name string) (ClientHandler, error) {
|
|||||||
}
|
}
|
||||||
return handler, nil
|
return handler, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("client plugin %s not found", name)
|
return nil, fmt.Errorf("client plugin %s not found", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List 返回所有可用的 plugins
|
// List 返回所有可用的 plugins
|
||||||
func (r *Registry) List() []PluginInfo {
|
func (r *Registry) List() []Info {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
var plugins []PluginInfo
|
var plugins []Info
|
||||||
|
|
||||||
// 服务端插件
|
|
||||||
for name, handler := range r.serverPlugins {
|
for name, handler := range r.serverPlugins {
|
||||||
plugins = append(plugins, PluginInfo{
|
plugins = append(plugins, Info{
|
||||||
Metadata: handler.Metadata(),
|
Metadata: handler.Metadata(),
|
||||||
Loaded: true,
|
Loaded: true,
|
||||||
Enabled: r.enabled[name],
|
Enabled: r.enabled[name],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 客户端插件
|
|
||||||
for name, handler := range r.clientPlugins {
|
for name, handler := range r.clientPlugins {
|
||||||
plugins = append(plugins, PluginInfo{
|
plugins = append(plugins, Info{
|
||||||
Metadata: handler.Metadata(),
|
Metadata: handler.Metadata(),
|
||||||
Loaded: true,
|
Loaded: true,
|
||||||
Enabled: r.enabled[name],
|
Enabled: r.enabled[name],
|
||||||
@@ -187,10 +183,10 @@ func (r *Registry) IsEnabled(name string) bool {
|
|||||||
return r.enabled[name]
|
return r.enabled[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterAll 批量注册插件
|
// RegisterAllServer 批量注册服务端插件
|
||||||
func (r *Registry) RegisterAll(handlers []ProxyHandler) error {
|
func (r *Registry) RegisterAllServer(handlers []ServerPlugin) error {
|
||||||
for _, handler := range handlers {
|
for _, handler := range handlers {
|
||||||
if err := r.RegisterBuiltin(handler); err != nil {
|
if err := r.RegisterServer(handler); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
412
pkg/plugin/script/js.go
Normal file
412
pkg/plugin/script/js.go
Normal file
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -5,138 +5,8 @@ import (
|
|||||||
"time"
|
"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 运行侧
|
// Side 运行侧
|
||||||
@@ -147,86 +17,104 @@ const (
|
|||||||
SideClient Side = "client"
|
SideClient Side = "client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Context 插件运行上下文
|
// PluginType 插件类别
|
||||||
type Context struct {
|
type PluginType string
|
||||||
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
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EventClientConnect EventType = "client_connect"
|
PluginTypeProxy PluginType = "proxy" // 代理协议 (SOCKS5 等)
|
||||||
EventClientDisconnect EventType = "client_disconnect"
|
PluginTypeApp PluginType = "app" // 应用服务 (VNC, Echo 等)
|
||||||
EventRuleCreated EventType = "rule_created"
|
|
||||||
EventRuleDeleted EventType = "rule_deleted"
|
|
||||||
EventProxyConnect EventType = "proxy_connect"
|
|
||||||
EventProxyDisconnect EventType = "proxy_disconnect"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event 事件
|
// PluginSource 插件来源
|
||||||
type Event struct {
|
type PluginSource string
|
||||||
Type EventType `json:"type"`
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
Data map[string]interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventHandler 事件处理函数
|
const (
|
||||||
type EventHandler func(event *Event)
|
PluginSourceBuiltin PluginSource = "builtin" // 内置编译
|
||||||
|
PluginSourceScript PluginSource = "script" // 脚本插件
|
||||||
// =============================================================================
|
|
||||||
// 错误定义
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
// 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"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 配置相关
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ const (
|
|||||||
MsgTypeClientPluginStop uint8 = 41 // 停止客户端插件
|
MsgTypeClientPluginStop uint8 = 41 // 停止客户端插件
|
||||||
MsgTypeClientPluginStatus uint8 = 42 // 客户端插件状态
|
MsgTypeClientPluginStatus uint8 = 42 // 客户端插件状态
|
||||||
MsgTypeClientPluginConn uint8 = 43 // 客户端插件连接请求
|
MsgTypeClientPluginConn uint8 = 43 // 客户端插件连接请求
|
||||||
|
|
||||||
|
// JS 插件动态安装
|
||||||
|
MsgTypeJSPluginInstall uint8 = 50 // 安装 JS 插件
|
||||||
|
MsgTypeJSPluginResult uint8 = 51 // 安装结果
|
||||||
)
|
)
|
||||||
|
|
||||||
// Message 基础消息结构
|
// Message 基础消息结构
|
||||||
@@ -204,6 +208,23 @@ type ClientPluginConnRequest struct {
|
|||||||
RuleName string `json:"rule_name"` // 规则名称
|
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
|
// WriteMessage 写入消息到 writer
|
||||||
func WriteMessage(w io.Writer, msg *Message) error {
|
func WriteMessage(w io.Writer, msg *Message) error {
|
||||||
header := make([]byte, HeaderSize)
|
header := make([]byte, HeaderSize)
|
||||||
|
|||||||
30
plugins/echo.js
Normal file
30
plugins/echo.js
Normal file
@@ -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");
|
||||||
|
}
|
||||||
194
plugins/filemanager.js
Normal file
194
plugins/filemanager.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { get, post, put, del } from '../config/axios'
|
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 管理方法
|
// 重新导出 token 管理方法
|
||||||
export { getToken, setToken, removeToken } from '../config/axios'
|
export { getToken, setToken, removeToken } from '../config/axios'
|
||||||
@@ -39,3 +39,12 @@ export const getClientPluginConfig = (clientId: string, pluginName: string) =>
|
|||||||
get<PluginConfigResponse>(`/client-plugin/${clientId}/${pluginName}/config`)
|
get<PluginConfigResponse>(`/client-plugin/${clientId}/${pluginName}/config`)
|
||||||
export const updateClientPluginConfig = (clientId: string, pluginName: string, config: Record<string, string>) =>
|
export const updateClientPluginConfig = (clientId: string, pluginName: string, config: Record<string, string>) =>
|
||||||
put(`/client-plugin/${clientId}/${pluginName}/config`, { config })
|
put(`/client-plugin/${clientId}/${pluginName}/config`, { config })
|
||||||
|
|
||||||
|
// JS 插件管理
|
||||||
|
export const getJSPlugins = () => get<JSPlugin[]>('/js-plugins')
|
||||||
|
export const createJSPlugin = (plugin: JSPlugin) => post('/js-plugins', plugin)
|
||||||
|
export const getJSPlugin = (name: string) => get<JSPlugin>(`/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}`)
|
||||||
|
|||||||
@@ -111,3 +111,15 @@ export interface StorePluginInfo {
|
|||||||
icon?: string
|
icon?: string
|
||||||
download_url?: string
|
download_url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JS 插件信息
|
||||||
|
export interface JSPlugin {
|
||||||
|
name: string
|
||||||
|
source: string
|
||||||
|
description: string
|
||||||
|
author: string
|
||||||
|
auto_push: string[]
|
||||||
|
config: Record<string, string>
|
||||||
|
auto_start: boolean
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,19 +3,23 @@ import { ref, onMounted, computed } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
NCard, NButton, NSpace, NTag, NStatistic, NGrid, NGi,
|
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'
|
} from 'naive-ui'
|
||||||
import { ArrowBackOutline, ExtensionPuzzleOutline, StorefrontOutline } from '@vicons/ionicons5'
|
import { ArrowBackOutline, ExtensionPuzzleOutline, StorefrontOutline, CodeSlashOutline } from '@vicons/ionicons5'
|
||||||
import { getPlugins, enablePlugin, disablePlugin, getStorePlugins } from '../api'
|
import { getPlugins, enablePlugin, disablePlugin, getStorePlugins, getJSPlugins, pushJSPluginToClient, getClients } from '../api'
|
||||||
import type { PluginInfo, StorePluginInfo } from '../types'
|
import type { PluginInfo, StorePluginInfo, JSPlugin, ClientStatus } from '../types'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const plugins = ref<PluginInfo[]>([])
|
const plugins = ref<PluginInfo[]>([])
|
||||||
const storePlugins = ref<StorePluginInfo[]>([])
|
const storePlugins = ref<StorePluginInfo[]>([])
|
||||||
|
const jsPlugins = ref<JSPlugin[]>([])
|
||||||
|
const clients = ref<ClientStatus[]>([])
|
||||||
const storeUrl = ref('')
|
const storeUrl = ref('')
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const storeLoading = ref(false)
|
const storeLoading = ref(false)
|
||||||
|
const jsLoading = ref(false)
|
||||||
const activeTab = ref('installed')
|
const activeTab = ref('installed')
|
||||||
|
|
||||||
const loadPlugins = async () => {
|
const loadPlugins = async () => {
|
||||||
@@ -89,9 +93,58 @@ const handleTabChange = (tab: string) => {
|
|||||||
if (tab === 'store' && storePlugins.value.length === 0) {
|
if (tab === 'store' && storePlugins.value.length === 0) {
|
||||||
loadStorePlugins()
|
loadStorePlugins()
|
||||||
}
|
}
|
||||||
|
if (tab === 'js' && jsPlugins.value.length === 0) {
|
||||||
|
loadJSPlugins()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadPlugins)
|
// JS 插件相关
|
||||||
|
/* 安全加固:暂时禁用创建/删除功能
|
||||||
|
const showJSModal = ref(false)
|
||||||
|
const jsForm = ref<JSPlugin>({...})
|
||||||
|
const configItems = ref<Array<{ key: string; value: string }>>([])
|
||||||
|
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()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -193,6 +246,69 @@ onMounted(loadPlugins)
|
|||||||
</n-grid>
|
</n-grid>
|
||||||
</n-spin>
|
</n-spin>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<!-- JS 插件 -->
|
||||||
|
<n-tab-pane name="js" tab="JS 插件">
|
||||||
|
<!-- 安全加固:暂时禁用 Web UI 创建功能
|
||||||
|
<n-space justify="end" style="margin-bottom: 16px;">
|
||||||
|
<n-button type="primary" @click="showJSModal = true">
|
||||||
|
<template #icon><n-icon><AddOutline /></n-icon></template>
|
||||||
|
新建 JS 插件
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<n-spin :show="jsLoading">
|
||||||
|
<n-empty v-if="!jsLoading && jsPlugins.length === 0" description="暂无 JS 插件" />
|
||||||
|
|
||||||
|
<n-grid v-else :cols="2" :x-gap="16" :y-gap="16" responsive="screen" cols-s="1">
|
||||||
|
<n-gi v-for="plugin in jsPlugins" :key="plugin.name">
|
||||||
|
<n-card hoverable>
|
||||||
|
<template #header>
|
||||||
|
<n-space align="center">
|
||||||
|
<n-icon size="24" color="#f0a020"><CodeSlashOutline /></n-icon>
|
||||||
|
<span>{{ plugin.name }}</span>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
<template #header-extra>
|
||||||
|
<n-space>
|
||||||
|
<n-select
|
||||||
|
v-if="onlineClients.length > 0"
|
||||||
|
placeholder="推送到..."
|
||||||
|
size="small"
|
||||||
|
style="width: 120px;"
|
||||||
|
:options="onlineClients.map(c => ({ label: c.nickname || c.id, value: c.id }))"
|
||||||
|
@update:value="(v: string) => handlePushJSPlugin(plugin.name, v)"
|
||||||
|
/>
|
||||||
|
<!-- 安全加固:暂时禁用删除功能
|
||||||
|
<n-popconfirm @positive-click="handleDeleteJSPlugin(plugin.name)">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button size="small" type="error" quaternary>删除</n-button>
|
||||||
|
</template>
|
||||||
|
确定删除此插件?
|
||||||
|
</n-popconfirm>
|
||||||
|
-->
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
<n-space vertical :size="8">
|
||||||
|
<n-space>
|
||||||
|
<n-tag size="small" type="warning">JS</n-tag>
|
||||||
|
<n-tag v-if="plugin.auto_start" size="small" type="success">自动启动</n-tag>
|
||||||
|
</n-space>
|
||||||
|
<p style="margin: 0; color: #666;">{{ plugin.description || '无描述' }}</p>
|
||||||
|
<p v-if="plugin.author" style="margin: 0; color: #999; font-size: 12px;">作者: {{ plugin.author }}</p>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</n-spin>
|
||||||
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
|
|
||||||
|
<!-- 安全加固:暂时禁用创建 JS 插件 Modal
|
||||||
|
<n-modal v-model:show="showJSModal" preset="card" title="新建 JS 插件" style="width: 600px;">
|
||||||
|
... 已屏蔽 ...
|
||||||
|
</n-modal>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user