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

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

View File

@@ -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,
})
}

View File

@@ -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]
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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)