feat: remove unused plugin version comparison and types, refactor proxy server to support authentication
- Deleted version comparison logic from `pkg/plugin/sign/version.go`. - Removed unused types and constants from `pkg/plugin/types.go`. - Updated `pkg/protocol/message.go` to remove plugin-related message types. - Enhanced `pkg/proxy/http.go` and `pkg/proxy/socks5.go` to include username/password authentication for HTTP and SOCKS5 proxies. - Modified `pkg/proxy/server.go` to pass authentication parameters to server constructors. - Added new API endpoint to generate installation commands with a token for clients. - Created database functions to manage installation tokens in `internal/server/db/install_token.go`. - Implemented the installation command generation logic in `internal/server/router/handler/install.go`. - Updated web frontend to support installation command generation and display in `web/src/views/ClientsView.vue`.
This commit is contained in:
@@ -1,154 +0,0 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EventType 审计事件类型
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventPluginInstall EventType = "plugin_install"
|
||||
EventPluginUninstall EventType = "plugin_uninstall"
|
||||
EventPluginStart EventType = "plugin_start"
|
||||
EventPluginStop EventType = "plugin_stop"
|
||||
EventPluginVerify EventType = "plugin_verify"
|
||||
EventPluginReject EventType = "plugin_reject"
|
||||
EventConfigChange EventType = "config_change"
|
||||
)
|
||||
|
||||
// Event 审计事件
|
||||
type Event struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Type EventType `json:"type"`
|
||||
PluginName string `json:"plugin_name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Details map[string]string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// Logger 审计日志记录器
|
||||
type Logger struct {
|
||||
path string
|
||||
file *os.File
|
||||
mu sync.Mutex
|
||||
enabled bool
|
||||
}
|
||||
|
||||
var (
|
||||
defaultLogger *Logger
|
||||
loggerOnce sync.Once
|
||||
)
|
||||
|
||||
// NewLogger 创建审计日志记录器
|
||||
func NewLogger(dataDir string) (*Logger, error) {
|
||||
path := filepath.Join(dataDir, "audit.log")
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Logger{path: path, file: file, enabled: true}, nil
|
||||
}
|
||||
|
||||
// InitDefault 初始化默认日志记录器
|
||||
func InitDefault(dataDir string) error {
|
||||
var err error
|
||||
loggerOnce.Do(func() {
|
||||
defaultLogger, err = NewLogger(dataDir)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Log 记录审计事件
|
||||
func (l *Logger) Log(event Event) {
|
||||
if l == nil || !l.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
event.Timestamp = time.Now()
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
data, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
log.Printf("[Audit] Marshal error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := l.file.Write(append(data, '\n')); err != nil {
|
||||
log.Printf("[Audit] Write error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭日志文件
|
||||
func (l *Logger) Close() error {
|
||||
if l == nil || l.file == nil {
|
||||
return nil
|
||||
}
|
||||
return l.file.Close()
|
||||
}
|
||||
|
||||
// LogEvent 使用默认记录器记录事件
|
||||
func LogEvent(event Event) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.Log(event)
|
||||
}
|
||||
}
|
||||
|
||||
// LogPluginInstall 记录插件安装事件
|
||||
func LogPluginInstall(pluginName, version, clientID string, success bool, msg string) {
|
||||
LogEvent(Event{
|
||||
Type: EventPluginInstall,
|
||||
PluginName: pluginName,
|
||||
Version: version,
|
||||
ClientID: clientID,
|
||||
Success: success,
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
|
||||
// LogPluginVerify 记录插件验证事件
|
||||
func LogPluginVerify(pluginName, version string, success bool, msg string) {
|
||||
LogEvent(Event{
|
||||
Type: EventPluginVerify,
|
||||
PluginName: pluginName,
|
||||
Version: version,
|
||||
Success: success,
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
|
||||
// LogPluginReject 记录插件拒绝事件
|
||||
func LogPluginReject(pluginName, version, reason string) {
|
||||
LogEvent(Event{
|
||||
Type: EventPluginReject,
|
||||
PluginName: pluginName,
|
||||
Version: version,
|
||||
Success: false,
|
||||
Message: reason,
|
||||
})
|
||||
}
|
||||
|
||||
// LogWithDetails 记录带详情的事件
|
||||
func LogWithDetails(eventType EventType, pluginName string, success bool, msg string, details map[string]string) {
|
||||
LogEvent(Event{
|
||||
Type: eventType,
|
||||
PluginName: pluginName,
|
||||
Success: success,
|
||||
Message: msg,
|
||||
Details: details,
|
||||
})
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Registry 管理可用的 plugins (仅客户端插件)
|
||||
type Registry struct {
|
||||
clientPlugins map[string]ClientPlugin // 客户端插件
|
||||
enabled map[string]bool // 启用状态
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewRegistry 创建 plugin 注册表
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
clientPlugins: make(map[string]ClientPlugin),
|
||||
enabled: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterClient 注册客户端插件
|
||||
func (r *Registry) RegisterClient(handler ClientPlugin) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
meta := handler.Metadata()
|
||||
if meta.Name == "" {
|
||||
return fmt.Errorf("plugin name cannot be empty")
|
||||
}
|
||||
|
||||
if _, exists := r.clientPlugins[meta.Name]; exists {
|
||||
return fmt.Errorf("client plugin %s already registered", meta.Name)
|
||||
}
|
||||
|
||||
r.clientPlugins[meta.Name] = handler
|
||||
r.enabled[meta.Name] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetClient 返回客户端插件
|
||||
func (r *Registry) GetClient(name string) (ClientPlugin, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
if handler, ok := r.clientPlugins[name]; ok {
|
||||
if !r.enabled[name] {
|
||||
return nil, fmt.Errorf("client plugin %s is disabled", name)
|
||||
}
|
||||
return handler, nil
|
||||
}
|
||||
return nil, fmt.Errorf("client plugin %s not found", name)
|
||||
}
|
||||
|
||||
// List 返回所有可用的 plugins
|
||||
func (r *Registry) List() []Info {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
var plugins []Info
|
||||
|
||||
for name, handler := range r.clientPlugins {
|
||||
plugins = append(plugins, Info{
|
||||
Metadata: handler.Metadata(),
|
||||
Loaded: true,
|
||||
Enabled: r.enabled[name],
|
||||
})
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
// Has 检查 plugin 是否存在
|
||||
func (r *Registry) Has(name string) bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
_, ok := r.clientPlugins[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Close 关闭所有 plugins
|
||||
func (r *Registry) Close(ctx context.Context) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
var lastErr error
|
||||
for name, handler := range r.clientPlugins {
|
||||
if err := handler.Stop(); err != nil {
|
||||
lastErr = fmt.Errorf("failed to stop client plugin %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// Enable 启用插件
|
||||
func (r *Registry) Enable(name string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if !r.has(name) {
|
||||
return fmt.Errorf("plugin %s not found", name)
|
||||
}
|
||||
r.enabled[name] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disable 禁用插件
|
||||
func (r *Registry) Disable(name string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if !r.has(name) {
|
||||
return fmt.Errorf("plugin %s not found", name)
|
||||
}
|
||||
r.enabled[name] = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// has 内部检查(无锁)
|
||||
func (r *Registry) has(name string) bool {
|
||||
_, ok := r.clientPlugins[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsEnabled 检查插件是否启用
|
||||
func (r *Registry) IsEnabled(name string) bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.enabled[name]
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package plugin
|
||||
|
||||
// 内置协议类型配置模式
|
||||
|
||||
// BuiltinRuleSchemas 返回所有内置协议类型的配置模式
|
||||
func BuiltinRuleSchemas() map[string]RuleSchema {
|
||||
return map[string]RuleSchema{
|
||||
"tcp": {
|
||||
NeedsLocalAddr: true,
|
||||
ExtraFields: nil,
|
||||
},
|
||||
"udp": {
|
||||
NeedsLocalAddr: true,
|
||||
ExtraFields: nil,
|
||||
},
|
||||
"http": {
|
||||
NeedsLocalAddr: false,
|
||||
ExtraFields: []ConfigField{
|
||||
{
|
||||
Key: "auth_enabled",
|
||||
Label: "启用认证",
|
||||
Type: ConfigFieldBool,
|
||||
Default: "false",
|
||||
Description: "是否启用 HTTP Basic 认证",
|
||||
},
|
||||
{
|
||||
Key: "username",
|
||||
Label: "用户名",
|
||||
Type: ConfigFieldString,
|
||||
Description: "HTTP 代理认证用户名",
|
||||
},
|
||||
{
|
||||
Key: "password",
|
||||
Label: "密码",
|
||||
Type: ConfigFieldPassword,
|
||||
Description: "HTTP 代理认证密码",
|
||||
},
|
||||
},
|
||||
},
|
||||
"https": {
|
||||
NeedsLocalAddr: false,
|
||||
ExtraFields: []ConfigField{
|
||||
{
|
||||
Key: "auth_enabled",
|
||||
Label: "启用认证",
|
||||
Type: ConfigFieldBool,
|
||||
Default: "false",
|
||||
Description: "是否启用 HTTPS 代理认证",
|
||||
},
|
||||
{
|
||||
Key: "username",
|
||||
Label: "用户名",
|
||||
Type: ConfigFieldString,
|
||||
Description: "HTTPS 代理认证用户名",
|
||||
},
|
||||
{
|
||||
Key: "password",
|
||||
Label: "密码",
|
||||
Type: ConfigFieldPassword,
|
||||
Description: "HTTPS 代理认证密码",
|
||||
},
|
||||
},
|
||||
},
|
||||
"socks5": {
|
||||
NeedsLocalAddr: false,
|
||||
ExtraFields: []ConfigField{
|
||||
{
|
||||
Key: "auth_enabled",
|
||||
Label: "启用认证",
|
||||
Type: ConfigFieldBool,
|
||||
Default: "false",
|
||||
Description: "是否启用 SOCKS5 用户名/密码认证",
|
||||
},
|
||||
{
|
||||
Key: "username",
|
||||
Label: "用户名",
|
||||
Type: ConfigFieldString,
|
||||
Description: "SOCKS5 认证用户名",
|
||||
},
|
||||
{
|
||||
Key: "password",
|
||||
Label: "密码",
|
||||
Type: ConfigFieldPassword,
|
||||
Description: "SOCKS5 认证密码",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetRuleSchema 获取指定协议类型的配置模式
|
||||
func GetRuleSchema(proxyType string) *RuleSchema {
|
||||
schemas := BuiltinRuleSchemas()
|
||||
if schema, ok := schemas[proxyType]; ok {
|
||||
return &schema
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsBuiltinType 检查是否为内置协议类型
|
||||
func IsBuiltinType(proxyType string) bool {
|
||||
builtinTypes := []string{"tcp", "udp", "http", "https"}
|
||||
for _, t := range builtinTypes {
|
||||
if t == proxyType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,913 +0,0 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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
|
||||
sandbox *Sandbox
|
||||
running bool
|
||||
mu sync.Mutex
|
||||
eventListeners map[string][]func(goja.Value)
|
||||
storagePath string
|
||||
apiHandlers map[string]map[string]goja.Callable // method -> path -> handler
|
||||
}
|
||||
|
||||
// NewJSPlugin 从 JS 源码创建插件
|
||||
func NewJSPlugin(name, source string) (*JSPlugin, error) {
|
||||
p := &JSPlugin{
|
||||
name: name,
|
||||
source: source,
|
||||
vm: goja.New(),
|
||||
sandbox: DefaultSandbox(),
|
||||
eventListeners: make(map[string][]func(goja.Value)),
|
||||
storagePath: filepath.Join("plugin_data", name+".json"),
|
||||
apiHandlers: make(map[string]map[string]goja.Callable),
|
||||
}
|
||||
|
||||
// 确保存储目录存在
|
||||
os.MkdirAll("plugin_data", 0755)
|
||||
|
||||
if err := p.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// SetSandbox 设置沙箱配置
|
||||
func (p *JSPlugin) SetSandbox(sandbox *Sandbox) {
|
||||
p.sandbox = sandbox
|
||||
}
|
||||
|
||||
// init 初始化 JS 运行时
|
||||
func (p *JSPlugin) init() error {
|
||||
// 设置栈深度限制(防止递归攻击)
|
||||
if p.sandbox.MaxStackDepth > 0 {
|
||||
p.vm.SetMaxCallStackSize(p.sandbox.MaxStackDepth)
|
||||
}
|
||||
|
||||
// 注入基础 API
|
||||
p.vm.Set("log", p.jsLog)
|
||||
|
||||
// Config API (兼容旧的 config() 调用,同时支持 config.get/getAll)
|
||||
p.vm.Set("config", p.jsGetConfig)
|
||||
if configObj := p.vm.Get("config"); configObj != nil {
|
||||
obj := configObj.ToObject(p.vm)
|
||||
obj.Set("get", p.jsGetConfig)
|
||||
obj.Set("getAll", p.jsGetAllConfig)
|
||||
}
|
||||
|
||||
// 注入增强 API
|
||||
p.vm.Set("logger", p.createLoggerAPI())
|
||||
p.vm.Set("storage", p.createStorageAPI())
|
||||
p.vm.Set("event", p.createEventAPI())
|
||||
p.vm.Set("request", p.createRequestAPI())
|
||||
p.vm.Set("notify", p.createNotifyAPI())
|
||||
|
||||
// 注入文件 API
|
||||
p.vm.Set("fs", p.createFsAPI())
|
||||
|
||||
// 注入 HTTP API
|
||||
p.vm.Set("http", p.createHttpAPI())
|
||||
|
||||
// 注入路由 API
|
||||
p.vm.Set("api", p.createRouteAPI())
|
||||
|
||||
// 执行脚本
|
||||
_, 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
|
||||
|
||||
// 根据 root_path 配置设置沙箱允许的路径
|
||||
if rootPath := config["root_path"]; rootPath != "" {
|
||||
absPath, err := filepath.Abs(rootPath)
|
||||
if err == nil {
|
||||
p.sandbox.AllowedPaths = append(p.sandbox.AllowedPaths, absPath)
|
||||
p.sandbox.WritablePaths = append(p.sandbox.WritablePaths, absPath)
|
||||
}
|
||||
} else {
|
||||
// 如果没有配置 root_path,默认允许访问当前目录
|
||||
cwd, err := os.Getwd()
|
||||
if err == nil {
|
||||
p.sandbox.AllowedPaths = append(p.sandbox.AllowedPaths, cwd)
|
||||
p.sandbox.WritablePaths = append(p.sandbox.WritablePaths, cwd)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
// goja Runtime 不是线程安全的,需要加锁
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// 创建连接包装器
|
||||
jsConn := newJSConn(conn)
|
||||
|
||||
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) map[string]interface{} {
|
||||
if err := p.sandbox.ValidateReadPath(path); err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "data": ""}
|
||||
}
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "data": ""}
|
||||
}
|
||||
if info.Size() > p.sandbox.MaxReadSize {
|
||||
return map[string]interface{}{"error": "file too large", "data": ""}
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "data": ""}
|
||||
}
|
||||
return map[string]interface{}{"error": "", "data": string(data)}
|
||||
}
|
||||
|
||||
func (p *JSPlugin) fsWriteFile(path, content string) map[string]interface{} {
|
||||
if err := p.sandbox.ValidateWritePath(path); err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "ok": false}
|
||||
}
|
||||
|
||||
if int64(len(content)) > p.sandbox.MaxWriteSize {
|
||||
return map[string]interface{}{"error": "content too large", "ok": false}
|
||||
}
|
||||
|
||||
err := os.WriteFile(path, []byte(content), 0644)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "ok": false}
|
||||
}
|
||||
return map[string]interface{}{"error": "", "ok": true}
|
||||
}
|
||||
|
||||
func (p *JSPlugin) fsReadDir(path string) map[string]interface{} {
|
||||
if err := p.sandbox.ValidateReadPath(path); err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "entries": nil}
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "entries": 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 map[string]interface{}{"error": "", "entries": result}
|
||||
}
|
||||
|
||||
func (p *JSPlugin) fsStat(path string) map[string]interface{} {
|
||||
if err := p.sandbox.ValidateReadPath(path); err != nil {
|
||||
return map[string]interface{}{"error": err.Error()}
|
||||
}
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error()}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"error": "",
|
||||
"name": info.Name(),
|
||||
"size": info.Size(),
|
||||
"isDir": info.IsDir(),
|
||||
"modTime": info.ModTime().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *JSPlugin) fsExists(path string) map[string]interface{} {
|
||||
if err := p.sandbox.ValidateReadPath(path); err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "exists": false}
|
||||
}
|
||||
_, err := os.Stat(path)
|
||||
return map[string]interface{}{"error": "", "exists": err == nil}
|
||||
}
|
||||
|
||||
func (p *JSPlugin) fsMkdir(path string) map[string]interface{} {
|
||||
if err := p.sandbox.ValidateWritePath(path); err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "ok": false}
|
||||
}
|
||||
err := os.MkdirAll(path, 0755)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "ok": false}
|
||||
}
|
||||
return map[string]interface{}{"error": "", "ok": true}
|
||||
}
|
||||
|
||||
func (p *JSPlugin) fsRemove(path string) map[string]interface{} {
|
||||
if err := p.sandbox.ValidateWritePath(path); err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "ok": false}
|
||||
}
|
||||
err := os.RemoveAll(path)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "ok": false}
|
||||
}
|
||||
return map[string]interface{}{"error": "", "ok": true}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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(connObj interface{}, handler goja.Callable) {
|
||||
// 从 jsConn 包装器中提取原始 net.Conn
|
||||
var conn net.Conn
|
||||
if jc, ok := connObj.(*jsConn); ok {
|
||||
conn = jc.conn
|
||||
} else if nc, ok := connObj.(net.Conn); ok {
|
||||
conn = nc
|
||||
} else {
|
||||
fmt.Printf("[JS:%s] httpServe: invalid conn type: %T\n", p.name, connObj)
|
||||
return
|
||||
}
|
||||
|
||||
// 注意:不要在这里关闭连接,HandleConn 会负责关闭
|
||||
|
||||
// Use bufio to read the request properly
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
for {
|
||||
// 1. Read Request Line
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
fmt.Printf("[JS:%s] httpServe read error: %v\n", p.name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(line, " ")
|
||||
if len(parts) < 2 {
|
||||
fmt.Printf("[JS:%s] Invalid request line: %s\n", p.name, line)
|
||||
return
|
||||
}
|
||||
method := parts[0]
|
||||
path := parts[1]
|
||||
|
||||
fmt.Printf("[JS:%s] Request: %s %s\n", p.name, method, path)
|
||||
|
||||
// 2. Read Headers
|
||||
headers := make(map[string]string)
|
||||
contentLength := 0
|
||||
for {
|
||||
hLine, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
hLine = strings.TrimSpace(hLine)
|
||||
if hLine == "" {
|
||||
break
|
||||
}
|
||||
if idx := strings.Index(hLine, ":"); idx > 0 {
|
||||
key := strings.TrimSpace(hLine[:idx])
|
||||
val := strings.TrimSpace(hLine[idx+1:])
|
||||
headers[strings.ToLower(key)] = val
|
||||
if strings.ToLower(key) == "content-length" {
|
||||
contentLength, _ = strconv.Atoi(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Read Body
|
||||
body := ""
|
||||
if contentLength > 0 {
|
||||
bodyBuf := make([]byte, contentLength)
|
||||
if _, err := io.ReadFull(reader, bodyBuf); err == nil {
|
||||
body = string(bodyBuf)
|
||||
}
|
||||
}
|
||||
|
||||
req := map[string]interface{}{
|
||||
"method": method,
|
||||
"path": path,
|
||||
"headers": headers,
|
||||
"body": body,
|
||||
}
|
||||
|
||||
// 调用 JS handler 函数
|
||||
result, err := handler(goja.Undefined(), p.vm.ToValue(req))
|
||||
if err != nil {
|
||||
fmt.Printf("[JS:%s] HTTP handler error: %v\n", p.name, err)
|
||||
conn.Write([]byte("HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n"))
|
||||
return
|
||||
}
|
||||
|
||||
// 将结果转换为 map
|
||||
if result == nil || goja.IsUndefined(result) || goja.IsNull(result) {
|
||||
conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"))
|
||||
continue
|
||||
}
|
||||
|
||||
resp := make(map[string]interface{})
|
||||
respObj := result.ToObject(p.vm)
|
||||
for _, key := range respObj.Keys() {
|
||||
val := respObj.Get(key)
|
||||
resp[key] = val.Export()
|
||||
}
|
||||
|
||||
writeHTTPResponse(conn, resp)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *JSPlugin) httpJSON(data interface{}) string {
|
||||
b, _ := json.Marshal(data)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (p *JSPlugin) httpSendFile(connObj interface{}, filePath string) {
|
||||
// 从 jsConn 包装器中提取原始 net.Conn
|
||||
var conn net.Conn
|
||||
if jc, ok := connObj.(*jsConn); ok {
|
||||
conn = jc.conn
|
||||
} else if nc, ok := connObj.(net.Conn); ok {
|
||||
conn = nc
|
||||
} else {
|
||||
fmt.Printf("[JS:%s] httpSendFile: invalid conn type: %T\n", p.name, connObj)
|
||||
return
|
||||
}
|
||||
|
||||
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 is deprecated, logic moved to httpServe
|
||||
func parseHTTPRequest(data []byte) map[string]interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Logger API
|
||||
// =============================================================================
|
||||
|
||||
func (p *JSPlugin) createLoggerAPI() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"info": func(msg string) { fmt.Printf("[JS:%s][INFO] %s\n", p.name, msg) },
|
||||
"warn": func(msg string) { fmt.Printf("[JS:%s][WARN] %s\n", p.name, msg) },
|
||||
"error": func(msg string) { fmt.Printf("[JS:%s][ERROR] %s\n", p.name, msg) },
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Config API Enhancements
|
||||
// =============================================================================
|
||||
|
||||
func (p *JSPlugin) jsGetAllConfig() map[string]string {
|
||||
if p.config == nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
return p.config
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Storage API
|
||||
// =============================================================================
|
||||
|
||||
func (p *JSPlugin) createStorageAPI() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"get": p.storageGet,
|
||||
"set": p.storageSet,
|
||||
"delete": p.storageDelete,
|
||||
"keys": p.storageKeys,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *JSPlugin) loadStorage() map[string]interface{} {
|
||||
data := make(map[string]interface{})
|
||||
if _, err := os.Stat(p.storagePath); err == nil {
|
||||
content, _ := os.ReadFile(p.storagePath)
|
||||
json.Unmarshal(content, &data)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (p *JSPlugin) saveStorage(data map[string]interface{}) {
|
||||
content, _ := json.MarshalIndent(data, "", " ")
|
||||
os.WriteFile(p.storagePath, content, 0644)
|
||||
}
|
||||
|
||||
func (p *JSPlugin) storageGet(key string, def interface{}) interface{} {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
data := p.loadStorage()
|
||||
if v, ok := data[key]; ok {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func (p *JSPlugin) storageSet(key string, value interface{}) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
data := p.loadStorage()
|
||||
data[key] = value
|
||||
p.saveStorage(data)
|
||||
}
|
||||
|
||||
func (p *JSPlugin) storageDelete(key string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
data := p.loadStorage()
|
||||
delete(data, key)
|
||||
p.saveStorage(data)
|
||||
}
|
||||
|
||||
func (p *JSPlugin) storageKeys() []string {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
data := p.loadStorage()
|
||||
keys := make([]string, 0, len(data))
|
||||
for k := range data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Event API
|
||||
// =============================================================================
|
||||
|
||||
func (p *JSPlugin) createEventAPI() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"on": p.eventOn,
|
||||
"emit": p.eventEmit,
|
||||
"off": p.eventOff,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *JSPlugin) eventOn(event string, callback func(goja.Value)) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.eventListeners[event] = append(p.eventListeners[event], callback)
|
||||
}
|
||||
|
||||
func (p *JSPlugin) eventEmit(event string, data interface{}) {
|
||||
p.mu.Lock()
|
||||
listeners := p.eventListeners[event]
|
||||
p.mu.Unlock() // 释放锁以允许回调中操作
|
||||
|
||||
val := p.vm.ToValue(data)
|
||||
for _, cb := range listeners {
|
||||
cb(val)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *JSPlugin) eventOff(event string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
delete(p.eventListeners, event)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Request API (HTTP Client)
|
||||
// =============================================================================
|
||||
|
||||
func (p *JSPlugin) createRequestAPI() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"get": p.requestGet,
|
||||
"post": p.requestPost,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *JSPlugin) requestGet(url string) map[string]interface{} {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "status": 0}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return map[string]interface{}{
|
||||
"status": resp.StatusCode,
|
||||
"body": string(body),
|
||||
"error": "",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *JSPlugin) requestPost(url string, contentType, data string) map[string]interface{} {
|
||||
resp, err := http.Post(url, contentType, strings.NewReader(data))
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "status": 0}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return map[string]interface{}{
|
||||
"status": resp.StatusCode,
|
||||
"body": string(body),
|
||||
"error": "",
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Notify API
|
||||
// =============================================================================
|
||||
|
||||
func (p *JSPlugin) createNotifyAPI() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"send": func(title, msg string) {
|
||||
// 目前仅打印到日志,后续对接系统通知
|
||||
fmt.Printf("[NOTIFY][%s] %s: %s\n", p.name, title, msg)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Route API (用于 Web API 代理)
|
||||
// =============================================================================
|
||||
|
||||
func (p *JSPlugin) createRouteAPI() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"handle": p.apiHandle,
|
||||
"get": func(path string, handler goja.Callable) { p.apiRegister("GET", path, handler) },
|
||||
"post": func(path string, handler goja.Callable) { p.apiRegister("POST", path, handler) },
|
||||
"put": func(path string, handler goja.Callable) { p.apiRegister("PUT", path, handler) },
|
||||
"delete": func(path string, handler goja.Callable) { p.apiRegister("DELETE", path, handler) },
|
||||
}
|
||||
}
|
||||
|
||||
// apiHandle 注册 API 路由处理函数
|
||||
func (p *JSPlugin) apiHandle(method, path string, handler goja.Callable) {
|
||||
p.apiRegister(method, path, handler)
|
||||
}
|
||||
|
||||
// apiRegister 注册 API 路由
|
||||
func (p *JSPlugin) apiRegister(method, path string, handler goja.Callable) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.apiHandlers[method] == nil {
|
||||
p.apiHandlers[method] = make(map[string]goja.Callable)
|
||||
}
|
||||
p.apiHandlers[method][path] = handler
|
||||
fmt.Printf("[JS:%s] Registered API: %s %s\n", p.name, method, path)
|
||||
}
|
||||
|
||||
// HandleAPIRequest 处理 API 请求
|
||||
func (p *JSPlugin) HandleAPIRequest(method, path, query string, headers map[string]string, body string) (int, map[string]string, string, error) {
|
||||
p.mu.Lock()
|
||||
handlers := p.apiHandlers[method]
|
||||
p.mu.Unlock()
|
||||
|
||||
if handlers == nil {
|
||||
return 404, nil, `{"error":"method not allowed"}`, nil
|
||||
}
|
||||
|
||||
// 查找匹配的路由
|
||||
var handler goja.Callable
|
||||
var matchedPath string
|
||||
|
||||
for registeredPath, h := range handlers {
|
||||
if matchRoute(registeredPath, path) {
|
||||
handler = h
|
||||
matchedPath = registeredPath
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if handler == nil {
|
||||
return 404, nil, `{"error":"route not found"}`, nil
|
||||
}
|
||||
|
||||
// 构建请求对象
|
||||
reqObj := map[string]interface{}{
|
||||
"method": method,
|
||||
"path": path,
|
||||
"pattern": matchedPath,
|
||||
"query": query,
|
||||
"headers": headers,
|
||||
"body": body,
|
||||
"params": extractParams(matchedPath, path),
|
||||
}
|
||||
|
||||
// 调用处理函数
|
||||
result, err := handler(goja.Undefined(), p.vm.ToValue(reqObj))
|
||||
if err != nil {
|
||||
return 500, nil, fmt.Sprintf(`{"error":"%s"}`, err.Error()), nil
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
if result == nil || goja.IsUndefined(result) || goja.IsNull(result) {
|
||||
return 200, nil, "", nil
|
||||
}
|
||||
|
||||
respObj := result.ToObject(p.vm)
|
||||
status := 200
|
||||
if s := respObj.Get("status"); s != nil && !goja.IsUndefined(s) {
|
||||
status = int(s.ToInteger())
|
||||
}
|
||||
|
||||
respHeaders := make(map[string]string)
|
||||
if h := respObj.Get("headers"); h != nil && !goja.IsUndefined(h) {
|
||||
hObj := h.ToObject(p.vm)
|
||||
for _, key := range hObj.Keys() {
|
||||
respHeaders[key] = hObj.Get(key).String()
|
||||
}
|
||||
}
|
||||
|
||||
respBody := ""
|
||||
if b := respObj.Get("body"); b != nil && !goja.IsUndefined(b) {
|
||||
respBody = b.String()
|
||||
}
|
||||
|
||||
return status, respHeaders, respBody, nil
|
||||
}
|
||||
|
||||
// matchRoute 匹配路由 (支持简单的路径参数)
|
||||
func matchRoute(pattern, path string) bool {
|
||||
patternParts := strings.Split(strings.Trim(pattern, "/"), "/")
|
||||
pathParts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
|
||||
if len(patternParts) != len(pathParts) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, part := range patternParts {
|
||||
if strings.HasPrefix(part, ":") {
|
||||
continue // 路径参数,匹配任意值
|
||||
}
|
||||
if part != pathParts[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// extractParams 提取路径参数
|
||||
func extractParams(pattern, path string) map[string]string {
|
||||
params := make(map[string]string)
|
||||
patternParts := strings.Split(strings.Trim(pattern, "/"), "/")
|
||||
pathParts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
|
||||
for i, part := range patternParts {
|
||||
if strings.HasPrefix(part, ":") && i < len(pathParts) {
|
||||
paramName := strings.TrimPrefix(part, ":")
|
||||
params[paramName] = pathParts[i]
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Sandbox 插件沙箱配置
|
||||
type Sandbox struct {
|
||||
// 允许访问的路径列表(绝对路径)
|
||||
AllowedPaths []string
|
||||
// 允许写入的路径列表(必须是 AllowedPaths 的子集)
|
||||
WritablePaths []string
|
||||
// 禁止访问的路径(黑名单,优先级高于白名单)
|
||||
DeniedPaths []string
|
||||
// 是否允许网络访问
|
||||
AllowNetwork bool
|
||||
// 最大文件读取大小 (bytes)
|
||||
MaxReadSize int64
|
||||
// 最大文件写入大小 (bytes)
|
||||
MaxWriteSize int64
|
||||
// 最大内存使用量 (bytes),0 表示不限制
|
||||
MaxMemory int64
|
||||
// 最大调用栈深度
|
||||
MaxStackDepth int
|
||||
}
|
||||
|
||||
// DefaultSandbox 返回默认沙箱配置(最小权限)
|
||||
func DefaultSandbox() *Sandbox {
|
||||
return &Sandbox{
|
||||
AllowedPaths: []string{},
|
||||
WritablePaths: []string{},
|
||||
DeniedPaths: defaultDeniedPaths(),
|
||||
AllowNetwork: false,
|
||||
MaxReadSize: 10 * 1024 * 1024, // 10MB
|
||||
MaxWriteSize: 1 * 1024 * 1024, // 1MB
|
||||
MaxMemory: 64 * 1024 * 1024, // 64MB
|
||||
MaxStackDepth: 1000, // 最大调用栈深度
|
||||
}
|
||||
}
|
||||
|
||||
// defaultDeniedPaths 返回默认禁止访问的路径
|
||||
func defaultDeniedPaths() []string {
|
||||
home, _ := os.UserHomeDir()
|
||||
denied := []string{
|
||||
"/etc/passwd",
|
||||
"/etc/shadow",
|
||||
"/etc/sudoers",
|
||||
"/root",
|
||||
"/.ssh",
|
||||
"/.gnupg",
|
||||
"/.aws",
|
||||
"/.kube",
|
||||
"/proc",
|
||||
"/sys",
|
||||
}
|
||||
if home != "" {
|
||||
denied = append(denied,
|
||||
filepath.Join(home, ".ssh"),
|
||||
filepath.Join(home, ".gnupg"),
|
||||
filepath.Join(home, ".aws"),
|
||||
filepath.Join(home, ".kube"),
|
||||
filepath.Join(home, ".config"),
|
||||
filepath.Join(home, ".local"),
|
||||
)
|
||||
}
|
||||
return denied
|
||||
}
|
||||
|
||||
// ValidateReadPath 验证读取路径是否允许
|
||||
func (s *Sandbox) ValidateReadPath(path string) error {
|
||||
return s.validatePath(path, false)
|
||||
}
|
||||
|
||||
// ValidateWritePath 验证写入路径是否允许
|
||||
func (s *Sandbox) ValidateWritePath(path string) error {
|
||||
return s.validatePath(path, true)
|
||||
}
|
||||
|
||||
func (s *Sandbox) validatePath(path string, write bool) error {
|
||||
// 清理路径,防止路径遍历攻击
|
||||
cleanPath, err := s.cleanPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查黑名单(优先级最高)
|
||||
if s.isDenied(cleanPath) {
|
||||
return fmt.Errorf("access denied: path is in denied list")
|
||||
}
|
||||
|
||||
// 检查白名单
|
||||
allowedList := s.AllowedPaths
|
||||
if write {
|
||||
allowedList = s.WritablePaths
|
||||
}
|
||||
|
||||
if len(allowedList) == 0 {
|
||||
return fmt.Errorf("access denied: no paths allowed")
|
||||
}
|
||||
|
||||
if !s.isAllowed(cleanPath, allowedList) {
|
||||
if write {
|
||||
return fmt.Errorf("access denied: path not in writable list")
|
||||
}
|
||||
return fmt.Errorf("access denied: path not in allowed list")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanPath 清理并验证路径
|
||||
func (s *Sandbox) cleanPath(path string) (string, error) {
|
||||
// 转换为绝对路径
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid path: %w", err)
|
||||
}
|
||||
|
||||
// 清理路径(解析 .. 和 .)
|
||||
cleanPath := filepath.Clean(absPath)
|
||||
|
||||
// 检查符号链接(防止通过符号链接绕过限制)
|
||||
realPath, err := filepath.EvalSymlinks(cleanPath)
|
||||
if err != nil {
|
||||
// 文件可能不存在,使用清理后的路径
|
||||
if !os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("invalid path: %w", err)
|
||||
}
|
||||
realPath = cleanPath
|
||||
}
|
||||
|
||||
// 再次检查路径遍历
|
||||
if strings.Contains(realPath, "..") {
|
||||
return "", fmt.Errorf("path traversal detected")
|
||||
}
|
||||
|
||||
return realPath, nil
|
||||
}
|
||||
|
||||
// isDenied 检查路径是否在黑名单中
|
||||
func (s *Sandbox) isDenied(path string) bool {
|
||||
for _, denied := range s.DeniedPaths {
|
||||
if strings.HasPrefix(path, denied) || path == denied {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isAllowed 检查路径是否在白名单中
|
||||
func (s *Sandbox) isAllowed(path string, allowedList []string) bool {
|
||||
for _, allowed := range allowedList {
|
||||
if strings.HasPrefix(path, allowed) || path == allowed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package sign
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 官方固定公钥(客户端内置)
|
||||
const OfficialPublicKeyBase64 = "0A0xRthj0wgPg8X8GJZ6/EnNpAUw5v7O//XLty+P5Yw="
|
||||
|
||||
var (
|
||||
officialPubKey ed25519.PublicKey
|
||||
officialPubKeyOnce sync.Once
|
||||
officialPubKeyErr error
|
||||
)
|
||||
|
||||
// initOfficialKey 初始化官方公钥
|
||||
func initOfficialKey() {
|
||||
officialPubKey, officialPubKeyErr = DecodePublicKey(OfficialPublicKeyBase64)
|
||||
}
|
||||
|
||||
// GetOfficialPublicKey 获取官方公钥
|
||||
func GetOfficialPublicKey() (ed25519.PublicKey, error) {
|
||||
officialPubKeyOnce.Do(initOfficialKey)
|
||||
return officialPubKey, officialPubKeyErr
|
||||
}
|
||||
|
||||
// GetPublicKeyByID 根据 ID 获取公钥(兼容旧接口,忽略 keyID)
|
||||
func GetPublicKeyByID(keyID string) (ed25519.PublicKey, error) {
|
||||
return GetOfficialPublicKey()
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package sign
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PluginPayload 插件签名载荷
|
||||
type PluginPayload struct {
|
||||
Name string `json:"name"` // 插件名称
|
||||
Version string `json:"version"` // 版本号
|
||||
SourceHash string `json:"source_hash"` // 源码 SHA256
|
||||
KeyID string `json:"key_id"` // 签名密钥 ID
|
||||
Timestamp int64 `json:"timestamp"` // 签名时间戳
|
||||
}
|
||||
|
||||
// SignedPlugin 已签名的插件
|
||||
type SignedPlugin struct {
|
||||
Payload PluginPayload `json:"payload"`
|
||||
Signature string `json:"signature"` // Base64 签名
|
||||
}
|
||||
|
||||
// NormalizeSource 规范化源码(统一换行符)
|
||||
func NormalizeSource(source string) string {
|
||||
// 统一换行符为 LF
|
||||
normalized := strings.ReplaceAll(source, "\r\n", "\n")
|
||||
normalized = strings.ReplaceAll(normalized, "\r", "\n")
|
||||
// 去除尾部空白
|
||||
normalized = strings.TrimRight(normalized, " \t\n")
|
||||
return normalized
|
||||
}
|
||||
|
||||
// HashSource 计算源码哈希
|
||||
func HashSource(source string) string {
|
||||
normalized := NormalizeSource(source)
|
||||
hash := sha256.Sum256([]byte(normalized))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// CreatePayload 创建签名载荷
|
||||
func CreatePayload(name, version, source, keyID string) *PluginPayload {
|
||||
return &PluginPayload{
|
||||
Name: name,
|
||||
Version: version,
|
||||
SourceHash: HashSource(source),
|
||||
KeyID: keyID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// SignPlugin 签名插件
|
||||
func SignPlugin(priv ed25519.PrivateKey, payload *PluginPayload) (*SignedPlugin, error) {
|
||||
// 序列化载荷
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal payload: %w", err)
|
||||
}
|
||||
|
||||
// 签名
|
||||
sig := SignBase64(priv, data)
|
||||
|
||||
return &SignedPlugin{
|
||||
Payload: *payload,
|
||||
Signature: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyPlugin 验证插件签名
|
||||
func VerifyPlugin(pub ed25519.PublicKey, signed *SignedPlugin, source string) error {
|
||||
// 验证源码哈希
|
||||
expectedHash := HashSource(source)
|
||||
if signed.Payload.SourceHash != expectedHash {
|
||||
return fmt.Errorf("source hash mismatch")
|
||||
}
|
||||
|
||||
// 序列化载荷
|
||||
data, err := json.Marshal(signed.Payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal payload: %w", err)
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
return VerifyBase64(pub, data, signed.Signature)
|
||||
}
|
||||
|
||||
// EncodeSignedPlugin 编码已签名插件为 JSON
|
||||
func EncodeSignedPlugin(sp *SignedPlugin) (string, error) {
|
||||
data, err := json.Marshal(sp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// DecodeSignedPlugin 从 JSON 解码已签名插件
|
||||
func DecodeSignedPlugin(data string) (*SignedPlugin, error) {
|
||||
var sp SignedPlugin
|
||||
if err := json.Unmarshal([]byte(data), &sp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sp, nil
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package sign
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidSignature = errors.New("invalid signature")
|
||||
ErrInvalidPublicKey = errors.New("invalid public key")
|
||||
ErrInvalidPrivateKey = errors.New("invalid private key")
|
||||
)
|
||||
|
||||
// KeyPair Ed25519 密钥对
|
||||
type KeyPair struct {
|
||||
PublicKey ed25519.PublicKey
|
||||
PrivateKey ed25519.PrivateKey
|
||||
}
|
||||
|
||||
// GenerateKeyPair 生成新的密钥对
|
||||
func GenerateKeyPair() (*KeyPair, error) {
|
||||
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate key: %w", err)
|
||||
}
|
||||
return &KeyPair{PublicKey: pub, PrivateKey: priv}, nil
|
||||
}
|
||||
|
||||
// Sign 使用私钥签名数据
|
||||
func Sign(privateKey ed25519.PrivateKey, data []byte) []byte {
|
||||
return ed25519.Sign(privateKey, data)
|
||||
}
|
||||
|
||||
// Verify 使用公钥验证签名
|
||||
func Verify(publicKey ed25519.PublicKey, data, signature []byte) bool {
|
||||
return ed25519.Verify(publicKey, data, signature)
|
||||
}
|
||||
|
||||
// SignBase64 签名并返回 Base64 编码
|
||||
func SignBase64(privateKey ed25519.PrivateKey, data []byte) string {
|
||||
sig := Sign(privateKey, data)
|
||||
return base64.StdEncoding.EncodeToString(sig)
|
||||
}
|
||||
|
||||
// VerifyBase64 验证 Base64 编码的签名
|
||||
func VerifyBase64(publicKey ed25519.PublicKey, data []byte, sigB64 string) error {
|
||||
sig, err := base64.StdEncoding.DecodeString(sigB64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode signature: %w", err)
|
||||
}
|
||||
if !Verify(publicKey, data, sig) {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodePublicKey 编码公钥为 Base64
|
||||
func EncodePublicKey(pub ed25519.PublicKey) string {
|
||||
return base64.StdEncoding.EncodeToString(pub)
|
||||
}
|
||||
|
||||
// DecodePublicKey 从 Base64 解码公钥
|
||||
func DecodePublicKey(s string) (ed25519.PublicKey, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) != ed25519.PublicKeySize {
|
||||
return nil, ErrInvalidPublicKey
|
||||
}
|
||||
return ed25519.PublicKey(data), nil
|
||||
}
|
||||
|
||||
// EncodePrivateKey 编码私钥为 Base64
|
||||
func EncodePrivateKey(priv ed25519.PrivateKey) string {
|
||||
return base64.StdEncoding.EncodeToString(priv)
|
||||
}
|
||||
|
||||
// DecodePrivateKey 从 Base64 解码私钥
|
||||
func DecodePrivateKey(s string) (ed25519.PrivateKey, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) != ed25519.PrivateKeySize {
|
||||
return nil, ErrInvalidPrivateKey
|
||||
}
|
||||
return ed25519.PrivateKey(data), nil
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package sign
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CompareVersions 比较两个版本号
|
||||
// 返回: -1 (v1 < v2), 0 (v1 == v2), 1 (v1 > v2)
|
||||
func CompareVersions(v1, v2 string) int {
|
||||
parts1 := parseVersion(v1)
|
||||
parts2 := parseVersion(v2)
|
||||
|
||||
maxLen := len(parts1)
|
||||
if len(parts2) > maxLen {
|
||||
maxLen = len(parts2)
|
||||
}
|
||||
|
||||
for i := 0; i < maxLen; i++ {
|
||||
var p1, p2 int
|
||||
if i < len(parts1) {
|
||||
p1 = parts1[i]
|
||||
}
|
||||
if i < len(parts2) {
|
||||
p2 = parts2[i]
|
||||
}
|
||||
|
||||
if p1 < p2 {
|
||||
return -1
|
||||
}
|
||||
if p1 > p2 {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func parseVersion(v string) []int {
|
||||
v = strings.TrimPrefix(v, "v")
|
||||
parts := strings.Split(v, ".")
|
||||
result := make([]int, len(parts))
|
||||
for i, p := range parts {
|
||||
n, _ := strconv.Atoi(p)
|
||||
result[i] = n
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 基础类型
|
||||
// =============================================================================
|
||||
|
||||
// Side 运行侧
|
||||
type Side string
|
||||
|
||||
const (
|
||||
SideClient Side = "client"
|
||||
)
|
||||
|
||||
// PluginType 插件类别
|
||||
type PluginType string
|
||||
|
||||
const (
|
||||
PluginTypeProxy PluginType = "proxy" // 代理协议 (SOCKS5 等)
|
||||
PluginTypeApp PluginType = "app" // 应用服务 (VNC, Echo 等)
|
||||
)
|
||||
|
||||
// PluginSource 插件来源
|
||||
type PluginSource string
|
||||
|
||||
const (
|
||||
PluginSourceBuiltin PluginSource = "builtin" // 内置编译
|
||||
PluginSourceScript PluginSource = "script" // 脚本插件
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 配置相关
|
||||
// =============================================================================
|
||||
|
||||
// ConfigFieldType 配置字段类型
|
||||
type ConfigFieldType string
|
||||
|
||||
const (
|
||||
ConfigFieldString ConfigFieldType = "string"
|
||||
ConfigFieldNumber ConfigFieldType = "number"
|
||||
ConfigFieldBool ConfigFieldType = "bool"
|
||||
ConfigFieldSelect ConfigFieldType = "select"
|
||||
ConfigFieldPassword ConfigFieldType = "password"
|
||||
)
|
||||
|
||||
// ConfigField 配置字段定义
|
||||
type ConfigField struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Type ConfigFieldType `json:"type"`
|
||||
Default string `json:"default,omitempty"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
Options []string `json:"options,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// RuleSchema 规则表单模式
|
||||
type RuleSchema struct {
|
||||
NeedsLocalAddr bool `json:"needs_local_addr"`
|
||||
ExtraFields []ConfigField `json:"extra_fields,omitempty"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 元数据
|
||||
// =============================================================================
|
||||
|
||||
// Metadata 插件元数据
|
||||
type Metadata struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type PluginType `json:"type"`
|
||||
Source PluginSource `json:"source"`
|
||||
RunAt Side `json:"run_at"`
|
||||
Description string `json:"description"`
|
||||
Author string `json:"author,omitempty"`
|
||||
ConfigSchema []ConfigField `json:"config_schema,omitempty"`
|
||||
RuleSchema *RuleSchema `json:"rule_schema,omitempty"`
|
||||
}
|
||||
|
||||
// Info 插件运行时信息
|
||||
type Info struct {
|
||||
Metadata Metadata `json:"metadata"`
|
||||
Loaded bool `json:"loaded"`
|
||||
Enabled bool `json:"enabled"`
|
||||
LoadedAt time.Time `json:"loaded_at,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 核心接口
|
||||
// =============================================================================
|
||||
|
||||
// Dialer 网络拨号接口
|
||||
type Dialer interface {
|
||||
Dial(network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
// ClientPlugin 客户端插件接口
|
||||
// 运行在客户端,提供本地服务
|
||||
type ClientPlugin interface {
|
||||
Metadata() Metadata
|
||||
Init(config map[string]string) error
|
||||
Start() (localAddr string, err error)
|
||||
HandleConn(conn net.Conn) error
|
||||
Stop() error
|
||||
}
|
||||
@@ -26,34 +26,11 @@ const (
|
||||
MsgTypeProxyConnect uint8 = 9 // 代理连接请求 (SOCKS5/HTTP)
|
||||
MsgTypeProxyResult uint8 = 10 // 代理连接结果
|
||||
|
||||
// Plugin 相关消息
|
||||
MsgTypePluginList uint8 = 20 // 请求/响应可用 plugins
|
||||
MsgTypePluginDownload uint8 = 21 // 请求下载 plugin
|
||||
MsgTypePluginData uint8 = 22 // Plugin 二进制数据(分块)
|
||||
MsgTypePluginReady uint8 = 23 // Plugin 加载确认
|
||||
|
||||
// UDP 相关消息
|
||||
MsgTypeUDPData uint8 = 30 // UDP 数据包
|
||||
|
||||
// 插件安装消息
|
||||
MsgTypeInstallPlugins uint8 = 24 // 服务端推送安装插件列表
|
||||
MsgTypePluginConfig uint8 = 25 // 插件配置同步
|
||||
|
||||
// 客户端插件消息
|
||||
MsgTypeClientPluginStart uint8 = 40 // 启动客户端插件
|
||||
MsgTypeClientPluginStop uint8 = 41 // 停止客户端插件
|
||||
MsgTypeClientPluginStatus uint8 = 42 // 客户端插件状态
|
||||
MsgTypeClientPluginConn uint8 = 43 // 客户端插件连接请求
|
||||
MsgTypePluginStatusQuery uint8 = 44 // 查询所有插件状态
|
||||
MsgTypePluginStatusQueryResp uint8 = 45 // 插件状态查询响应
|
||||
|
||||
// JS 插件动态安装
|
||||
MsgTypeJSPluginInstall uint8 = 50 // 安装 JS 插件
|
||||
MsgTypeJSPluginResult uint8 = 51 // 安装结果
|
||||
|
||||
// 客户端控制消息
|
||||
MsgTypeClientRestart uint8 = 60 // 重启客户端
|
||||
MsgTypePluginConfigUpdate uint8 = 61 // 更新插件配置
|
||||
MsgTypeClientRestart uint8 = 60 // 重启客户端
|
||||
|
||||
// 更新相关消息
|
||||
MsgTypeUpdateCheck uint8 = 70 // 检查更新请求
|
||||
@@ -68,10 +45,6 @@ const (
|
||||
MsgTypeLogData uint8 = 81 // 日志数据
|
||||
MsgTypeLogStop uint8 = 82 // 停止日志流
|
||||
|
||||
// 插件 API 路由消息
|
||||
MsgTypePluginAPIRequest uint8 = 90 // 插件 API 请求
|
||||
MsgTypePluginAPIResponse uint8 = 91 // 插件 API 响应
|
||||
|
||||
// 系统状态消息
|
||||
MsgTypeSystemStatsRequest uint8 = 100 // 请求系统状态
|
||||
MsgTypeSystemStatsResponse uint8 = 101 // 系统状态响应
|
||||
@@ -111,22 +84,17 @@ type AuthResponse struct {
|
||||
// ProxyRule 代理规则
|
||||
type ProxyRule struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"` // 内置: tcp, udp, http, https, websocket; 插件: socks5 等
|
||||
Type string `json:"type" yaml:"type"` // tcp, udp, http, https, socks5
|
||||
LocalIP string `json:"local_ip" yaml:"local_ip"` // tcp/udp 模式使用
|
||||
LocalPort int `json:"local_port" yaml:"local_port"` // tcp/udp 模式使用
|
||||
RemotePort int `json:"remote_port" yaml:"remote_port"` // 服务端监听端口
|
||||
Enabled *bool `json:"enabled,omitempty" yaml:"enabled"` // 是否启用,默认为 true
|
||||
// Plugin 支持字段
|
||||
PluginID string `json:"plugin_id,omitempty" yaml:"plugin_id"` // 插件实例ID
|
||||
PluginName string `json:"plugin_name,omitempty" yaml:"plugin_name"`
|
||||
PluginVersion string `json:"plugin_version,omitempty" yaml:"plugin_version"`
|
||||
PluginConfig map[string]string `json:"plugin_config,omitempty" yaml:"plugin_config"`
|
||||
// HTTP Basic Auth 字段 (用于独立端口模式)
|
||||
// HTTP Basic Auth 字段
|
||||
AuthEnabled bool `json:"auth_enabled,omitempty" yaml:"auth_enabled"`
|
||||
AuthUsername string `json:"auth_username,omitempty" yaml:"auth_username"`
|
||||
AuthPassword string `json:"auth_password,omitempty" yaml:"auth_password"`
|
||||
// 插件管理标记 - 由插件自动创建的规则,不允许手动编辑/删除
|
||||
PluginManaged bool `json:"plugin_managed,omitempty" yaml:"plugin_managed"`
|
||||
// 端口状态: "listening", "failed: <error message>", ""
|
||||
PortStatus string `json:"port_status,omitempty" yaml:"-"`
|
||||
}
|
||||
|
||||
// IsEnabled 检查规则是否启用,默认为 true
|
||||
@@ -164,60 +132,6 @@ type ProxyConnectResult struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// PluginMetadata Plugin 元数据(协议层)
|
||||
type PluginMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Checksum string `json:"checksum"`
|
||||
Size int64 `json:"size"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// PluginListRequest 请求可用 plugins
|
||||
type PluginListRequest struct {
|
||||
ClientVersion string `json:"client_version"`
|
||||
}
|
||||
|
||||
// PluginListResponse 返回可用 plugins
|
||||
type PluginListResponse struct {
|
||||
Plugins []PluginMetadata `json:"plugins"`
|
||||
}
|
||||
|
||||
// PluginDownloadRequest 请求下载 plugin
|
||||
type PluginDownloadRequest struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// PluginDataChunk Plugin 二进制数据块
|
||||
type PluginDataChunk struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
ChunkIndex int `json:"chunk_index"`
|
||||
TotalChunks int `json:"total_chunks"`
|
||||
Data []byte `json:"data"`
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
}
|
||||
|
||||
// PluginReadyNotification Plugin 加载确认
|
||||
type PluginReadyNotification struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// InstallPluginsRequest 安装插件请求
|
||||
type InstallPluginsRequest struct {
|
||||
Plugins []string `json:"plugins"` // 要安装的插件名称列表
|
||||
}
|
||||
|
||||
// PluginConfigSync 插件配置同步
|
||||
type PluginConfigSync struct {
|
||||
PluginName string `json:"plugin_name"` // 插件名称
|
||||
Config map[string]string `json:"config"` // 配置内容
|
||||
}
|
||||
|
||||
// UDPPacket UDP 数据包
|
||||
type UDPPacket struct {
|
||||
RemotePort int `json:"remote_port"` // 服务端监听端口
|
||||
@@ -225,67 +139,6 @@ type UDPPacket struct {
|
||||
Data []byte `json:"data"` // UDP 数据
|
||||
}
|
||||
|
||||
// ClientPluginStartRequest 启动客户端插件请求
|
||||
type ClientPluginStartRequest struct {
|
||||
PluginName string `json:"plugin_name"` // 插件名称
|
||||
RuleName string `json:"rule_name"` // 规则名称
|
||||
RemotePort int `json:"remote_port"` // 服务端监听端口
|
||||
Config map[string]string `json:"config"` // 插件配置
|
||||
}
|
||||
|
||||
// ClientPluginStopRequest 停止客户端插件请求
|
||||
type ClientPluginStopRequest struct {
|
||||
PluginID string `json:"plugin_id,omitempty"` // 插件ID(优先使用)
|
||||
PluginName string `json:"plugin_name"` // 插件名称
|
||||
RuleName string `json:"rule_name"` // 规则名称
|
||||
}
|
||||
|
||||
// ClientPluginStatusResponse 客户端插件状态响应
|
||||
type ClientPluginStatusResponse struct {
|
||||
PluginName string `json:"plugin_name"` // 插件名称
|
||||
RuleName string `json:"rule_name"` // 规则名称
|
||||
Running bool `json:"running"` // 是否运行中
|
||||
LocalAddr string `json:"local_addr"` // 本地监听地址
|
||||
Error string `json:"error"` // 错误信息
|
||||
}
|
||||
|
||||
// ClientPluginConnRequest 客户端插件连接请求
|
||||
type ClientPluginConnRequest struct {
|
||||
PluginID string `json:"plugin_id,omitempty"` // 插件ID(优先使用)
|
||||
PluginName string `json:"plugin_name"` // 插件名称
|
||||
RuleName string `json:"rule_name"` // 规则名称
|
||||
}
|
||||
|
||||
// PluginStatusEntry 单个插件状态
|
||||
type PluginStatusEntry struct {
|
||||
PluginName string `json:"plugin_name"` // 插件名称
|
||||
Running bool `json:"running"` // 是否运行中
|
||||
}
|
||||
|
||||
// PluginStatusQueryResponse 插件状态查询响应
|
||||
type PluginStatusQueryResponse struct {
|
||||
Plugins []PluginStatusEntry `json:"plugins"` // 所有插件状态
|
||||
}
|
||||
|
||||
// JSPluginInstallRequest JS 插件安装请求
|
||||
type JSPluginInstallRequest struct {
|
||||
PluginID string `json:"plugin_id"` // 插件实例唯一 ID
|
||||
PluginName string `json:"plugin_name"` // 插件名称
|
||||
Source string `json:"source"` // JS 源码
|
||||
Signature string `json:"signature"` // 官方签名 (Base64)
|
||||
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"`
|
||||
}
|
||||
|
||||
// ClientRestartRequest 客户端重启请求
|
||||
type ClientRestartRequest struct {
|
||||
Reason string `json:"reason,omitempty"` // 重启原因
|
||||
@@ -297,23 +150,6 @@ type ClientRestartResponse struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// PluginConfigUpdateRequest 插件配置更新请求
|
||||
type PluginConfigUpdateRequest struct {
|
||||
PluginID string `json:"plugin_id,omitempty"` // 插件ID(优先使用)
|
||||
PluginName string `json:"plugin_name"` // 插件名称
|
||||
RuleName string `json:"rule_name"` // 规则名称
|
||||
Config map[string]string `json:"config"` // 新配置
|
||||
Restart bool `json:"restart"` // 是否重启插件
|
||||
}
|
||||
|
||||
// PluginConfigUpdateResponse 插件配置更新响应
|
||||
type PluginConfigUpdateResponse struct {
|
||||
PluginName string `json:"plugin_name"`
|
||||
RuleName string `json:"rule_name"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateCheckRequest 更新检查请求
|
||||
type UpdateCheckRequest struct {
|
||||
Component string `json:"component"` // "server" 或 "client"
|
||||
@@ -367,7 +203,7 @@ type LogEntry struct {
|
||||
Timestamp int64 `json:"ts"` // Unix 时间戳 (毫秒)
|
||||
Level string `json:"level"` // 日志级别: debug, info, warn, error
|
||||
Message string `json:"msg"` // 日志消息
|
||||
Source string `json:"src"` // 来源: client, plugin:<name>
|
||||
Source string `json:"src"` // 来源: client
|
||||
}
|
||||
|
||||
// LogData 日志数据
|
||||
@@ -382,25 +218,6 @@ type LogStopRequest struct {
|
||||
SessionID string `json:"session_id"` // 会话 ID
|
||||
}
|
||||
|
||||
// PluginAPIRequest 插件 API 请求
|
||||
type PluginAPIRequest struct {
|
||||
PluginID string `json:"plugin_id"` // 插件实例唯一 ID
|
||||
PluginName string `json:"plugin_name"` // 插件名称 (向后兼容)
|
||||
Method string `json:"method"` // HTTP 方法: GET, POST, PUT, DELETE
|
||||
Path string `json:"path"` // 路由路径
|
||||
Query string `json:"query"` // 查询参数
|
||||
Headers map[string]string `json:"headers"` // 请求头
|
||||
Body string `json:"body"` // 请求体
|
||||
}
|
||||
|
||||
// PluginAPIResponse 插件 API 响应
|
||||
type PluginAPIResponse struct {
|
||||
Status int `json:"status"` // HTTP 状态码
|
||||
Headers map[string]string `json:"headers"` // 响应头
|
||||
Body string `json:"body"` // 响应体
|
||||
Error string `json:"error"` // 错误信息
|
||||
}
|
||||
|
||||
// WriteMessage 写入消息到 writer
|
||||
func WriteMessage(w io.Writer, msg *Message) error {
|
||||
header := make([]byte, HeaderSize)
|
||||
|
||||
@@ -2,6 +2,8 @@ package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -12,13 +14,15 @@ import (
|
||||
|
||||
// HTTPServer HTTP 代理服务
|
||||
type HTTPServer struct {
|
||||
dialer Dialer
|
||||
onStats func(in, out int64) // 流量统计回调
|
||||
dialer Dialer
|
||||
onStats func(in, out int64) // 流量统计回调
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
// NewHTTPServer 创建 HTTP 代理服务
|
||||
func NewHTTPServer(dialer Dialer, onStats func(in, out int64)) *HTTPServer {
|
||||
return &HTTPServer{dialer: dialer, onStats: onStats}
|
||||
func NewHTTPServer(dialer Dialer, onStats func(in, out int64), username, password string) *HTTPServer {
|
||||
return &HTTPServer{dialer: dialer, onStats: onStats, username: username, password: password}
|
||||
}
|
||||
|
||||
// HandleConn 处理 HTTP 代理连接
|
||||
@@ -31,12 +35,45 @@ func (h *HTTPServer) HandleConn(conn net.Conn) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查认证
|
||||
if h.username != "" && h.password != "" {
|
||||
if !h.checkAuth(req) {
|
||||
conn.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\n\r\n"))
|
||||
return errors.New("authentication required")
|
||||
}
|
||||
}
|
||||
|
||||
if req.Method == http.MethodConnect {
|
||||
return h.handleConnect(conn, req)
|
||||
}
|
||||
return h.handleHTTP(conn, req, reader)
|
||||
}
|
||||
|
||||
// checkAuth 检查 Proxy-Authorization 头
|
||||
func (h *HTTPServer) checkAuth(req *http.Request) bool {
|
||||
auth := req.Header.Get("Proxy-Authorization")
|
||||
if auth == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
const prefix = "Basic "
|
||||
if !strings.HasPrefix(auth, prefix) {
|
||||
return false
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
credentials := strings.SplitN(string(decoded), ":", 2)
|
||||
if len(credentials) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return credentials[0] == h.username && credentials[1] == h.password
|
||||
}
|
||||
|
||||
// handleConnect 处理 CONNECT 方法 (HTTPS)
|
||||
func (h *HTTPServer) handleConnect(conn net.Conn, req *http.Request) error {
|
||||
target := req.Host
|
||||
|
||||
@@ -14,10 +14,10 @@ type Server struct {
|
||||
}
|
||||
|
||||
// NewServer 创建代理服务器
|
||||
func NewServer(typ string, dialer Dialer, onStats func(in, out int64)) *Server {
|
||||
func NewServer(typ string, dialer Dialer, onStats func(in, out int64), username, password string) *Server {
|
||||
return &Server{
|
||||
socks5: NewSOCKS5Server(dialer, onStats),
|
||||
http: NewHTTPServer(dialer, onStats),
|
||||
socks5: NewSOCKS5Server(dialer, onStats, username, password),
|
||||
http: NewHTTPServer(dialer, onStats, username, password),
|
||||
typ: typ,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
const (
|
||||
socks5Version = 0x05
|
||||
noAuth = 0x00
|
||||
userPassAuth = 0x02
|
||||
cmdConnect = 0x01
|
||||
atypIPv4 = 0x01
|
||||
atypDomain = 0x03
|
||||
@@ -21,8 +22,10 @@ const (
|
||||
|
||||
// SOCKS5Server SOCKS5 代理服务
|
||||
type SOCKS5Server struct {
|
||||
dialer Dialer
|
||||
onStats func(in, out int64) // 流量统计回调
|
||||
dialer Dialer
|
||||
onStats func(in, out int64) // 流量统计回调
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
// Dialer 连接拨号器接口
|
||||
@@ -31,8 +34,8 @@ type Dialer interface {
|
||||
}
|
||||
|
||||
// NewSOCKS5Server 创建 SOCKS5 服务
|
||||
func NewSOCKS5Server(dialer Dialer, onStats func(in, out int64)) *SOCKS5Server {
|
||||
return &SOCKS5Server{dialer: dialer, onStats: onStats}
|
||||
func NewSOCKS5Server(dialer Dialer, onStats func(in, out int64), username, password string) *SOCKS5Server {
|
||||
return &SOCKS5Server{dialer: dialer, onStats: onStats, username: username, password: password}
|
||||
}
|
||||
|
||||
// HandleConn 处理 SOCKS5 连接
|
||||
@@ -85,11 +88,54 @@ func (s *SOCKS5Server) handshake(conn net.Conn) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 响应:使用无认证
|
||||
// 如果配置了用户名密码,要求认证
|
||||
if s.username != "" && s.password != "" {
|
||||
_, err := conn.Write([]byte{socks5Version, userPassAuth})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.authenticate(conn)
|
||||
}
|
||||
|
||||
// 无认证
|
||||
_, err := conn.Write([]byte{socks5Version, noAuth})
|
||||
return err
|
||||
}
|
||||
|
||||
// authenticate 处理用户名密码认证
|
||||
func (s *SOCKS5Server) authenticate(conn net.Conn) error {
|
||||
buf := make([]byte, 2)
|
||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
if buf[0] != 0x01 {
|
||||
return errors.New("unsupported auth version")
|
||||
}
|
||||
|
||||
ulen := int(buf[1])
|
||||
username := make([]byte, ulen)
|
||||
if _, err := io.ReadFull(conn, username); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
plen := make([]byte, 1)
|
||||
if _, err := io.ReadFull(conn, plen); err != nil {
|
||||
return err
|
||||
}
|
||||
password := make([]byte, plen[0])
|
||||
if _, err := io.ReadFull(conn, password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if string(username) == s.username && string(password) == s.password {
|
||||
conn.Write([]byte{0x01, 0x00}) // 认证成功
|
||||
return nil
|
||||
}
|
||||
|
||||
conn.Write([]byte{0x01, 0x01}) // 认证失败
|
||||
return errors.New("authentication failed")
|
||||
}
|
||||
|
||||
// readRequest 读取请求
|
||||
func (s *SOCKS5Server) readRequest(conn net.Conn) (string, error) {
|
||||
buf := make([]byte, 4)
|
||||
|
||||
Reference in New Issue
Block a user