Files
GoTunnel/pkg/plugin/script/js.go
Flik 42e11e0aca
Some checks failed
Build Multi-Platform Binaries / build-frontend (push) Failing after 19s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Has been skipped
feat(plugin): 实现插件安全验证和审计日志功能
- 添加插件签名验证机制,支持远程证书吊销列表
- 增加插件安装时的安全检查和签名验证
- 实现插件版本存储的HMAC完整性校验
- 添加插件审计日志记录插件安装和验证事件
- 增加JS插件沙箱安全限制配置
- 添加插件商店API的签名URL字段支持
- 实现安全配置的自动刷新机制
2025-12-30 22:06:33 +08:00

475 lines
11 KiB
Go

package script
import (
"encoding/json"
"fmt"
"io"
"net"
"os"
"path/filepath"
"sync"
"github.com/dop251/goja"
"github.com/gotunnel/pkg/plugin"
)
// JSPlugin JavaScript 脚本插件
type JSPlugin struct {
name string
source string
vm *goja.Runtime
metadata plugin.Metadata
config map[string]string
sandbox *Sandbox
running bool
mu sync.Mutex
}
// NewJSPlugin 从 JS 源码创建插件
func NewJSPlugin(name, source string) (*JSPlugin, error) {
p := &JSPlugin{
name: name,
source: source,
vm: goja.New(),
sandbox: DefaultSandbox(),
}
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)
p.vm.Set("config", p.jsGetConfig)
// 注入文件 API
p.vm.Set("fs", p.createFsAPI())
// 注入 HTTP API
p.vm.Set("http", p.createHttpAPI())
// 执行脚本
_, err := p.vm.RunString(p.source)
if err != nil {
return fmt.Errorf("run script: %w", err)
}
// 获取元数据
if err := p.loadMetadata(); err != nil {
return err
}
return nil
}
// loadMetadata 从 JS 获取元数据
func (p *JSPlugin) loadMetadata() error {
fn, ok := goja.AssertFunction(p.vm.Get("metadata"))
if !ok {
// 使用默认元数据
p.metadata = plugin.Metadata{
Name: p.name,
Type: plugin.PluginTypeApp,
Source: plugin.PluginSourceScript,
RunAt: plugin.SideClient,
}
return nil
}
result, err := fn(goja.Undefined())
if err != nil {
return err
}
obj := result.ToObject(p.vm)
p.metadata = plugin.Metadata{
Name: getString(obj, "name", p.name),
Version: getString(obj, "version", "1.0.0"),
Type: plugin.PluginType(getString(obj, "type", "app")),
Source: plugin.PluginSourceScript,
RunAt: plugin.Side(getString(obj, "run_at", "client")),
Description: getString(obj, "description", ""),
Author: getString(obj, "author", ""),
}
return nil
}
// Metadata 返回插件元数据
func (p *JSPlugin) Metadata() plugin.Metadata {
return p.metadata
}
// Init 初始化插件配置
func (p *JSPlugin) Init(config map[string]string) error {
p.config = config
p.vm.Set("config", config)
return nil
}
// Start 启动插件
func (p *JSPlugin) Start() (string, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.running {
return "", nil
}
fn, ok := goja.AssertFunction(p.vm.Get("start"))
if ok {
_, err := fn(goja.Undefined())
if err != nil {
return "", err
}
}
p.running = true
return "script-plugin", nil
}
// HandleConn 处理连接
func (p *JSPlugin) HandleConn(conn net.Conn) error {
defer conn.Close()
// 创建连接包装器
jsConn := newJSConn(conn)
p.vm.Set("conn", jsConn)
fn, ok := goja.AssertFunction(p.vm.Get("handleConn"))
if !ok {
return fmt.Errorf("handleConn not defined")
}
_, err := fn(goja.Undefined(), p.vm.ToValue(jsConn))
return err
}
// Stop 停止插件
func (p *JSPlugin) Stop() error {
p.mu.Lock()
defer p.mu.Unlock()
if !p.running {
return nil
}
fn, ok := goja.AssertFunction(p.vm.Get("stop"))
if ok {
fn(goja.Undefined())
}
p.running = false
return nil
}
// jsLog JS 日志函数
func (p *JSPlugin) jsLog(msg string) {
fmt.Printf("[JS:%s] %s\n", p.name, msg)
}
// jsGetConfig 获取配置
func (p *JSPlugin) jsGetConfig(key string) string {
if p.config == nil {
return ""
}
return p.config[key]
}
// getString 从 JS 对象获取字符串
func getString(obj *goja.Object, key, def string) string {
v := obj.Get(key)
if v == nil || goja.IsUndefined(v) {
return def
}
return v.String()
}
// jsConn JS 连接包装器
type jsConn struct {
conn net.Conn
}
func newJSConn(conn net.Conn) *jsConn {
return &jsConn{conn: conn}
}
func (c *jsConn) Read(size int) []byte {
buf := make([]byte, size)
n, err := c.conn.Read(buf)
if err != nil {
return nil
}
return buf[:n]
}
func (c *jsConn) Write(data []byte) int {
n, _ := c.conn.Write(data)
return n
}
func (c *jsConn) Close() {
c.conn.Close()
}
// =============================================================================
// 文件系统 API
// =============================================================================
// createFsAPI 创建文件系统 API
func (p *JSPlugin) createFsAPI() map[string]interface{} {
return map[string]interface{}{
"readFile": p.fsReadFile,
"writeFile": p.fsWriteFile,
"readDir": p.fsReadDir,
"stat": p.fsStat,
"exists": p.fsExists,
"mkdir": p.fsMkdir,
"remove": p.fsRemove,
}
}
func (p *JSPlugin) fsReadFile(path string) 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(conn net.Conn, handler func(map[string]interface{}) map[string]interface{}) {
defer conn.Close()
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
return
}
req := parseHTTPRequest(buf[:n])
resp := handler(req)
writeHTTPResponse(conn, resp)
}
func (p *JSPlugin) httpJSON(data interface{}) string {
b, _ := json.Marshal(data)
return string(b)
}
func (p *JSPlugin) httpSendFile(conn net.Conn, filePath string) {
f, err := os.Open(filePath)
if err != nil {
conn.Write([]byte("HTTP/1.1 404 Not Found\r\n\r\n"))
return
}
defer f.Close()
info, _ := f.Stat()
contentType := getContentType(filePath)
header := fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n",
contentType, info.Size())
conn.Write([]byte(header))
io.Copy(conn, f)
}
// parseHTTPRequest 解析 HTTP 请求
func parseHTTPRequest(data []byte) map[string]interface{} {
lines := string(data)
req := map[string]interface{}{
"method": "GET",
"path": "/",
"body": "",
}
// 解析请求行
if idx := indexOf(lines, " "); idx > 0 {
req["method"] = lines[:idx]
rest := lines[idx+1:]
if idx2 := indexOf(rest, " "); idx2 > 0 {
req["path"] = rest[:idx2]
}
}
// 解析 body
if idx := indexOf(lines, "\r\n\r\n"); idx > 0 {
req["body"] = lines[idx+4:]
}
return req
}
// writeHTTPResponse 写入 HTTP 响应
func writeHTTPResponse(conn net.Conn, resp map[string]interface{}) {
status := 200
if s, ok := resp["status"].(int); ok {
status = s
}
body := ""
if b, ok := resp["body"].(string); ok {
body = b
}
contentType := "application/json"
if ct, ok := resp["contentType"].(string); ok {
contentType = ct
}
header := fmt.Sprintf("HTTP/1.1 %d OK\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n",
status, contentType, len(body))
conn.Write([]byte(header + body))
}
func indexOf(s, substr string) int {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}
func getContentType(path string) string {
ext := filepath.Ext(path)
types := map[string]string{
".html": "text/html",
".css": "text/css",
".js": "application/javascript",
".json": "application/json",
".png": "image/png",
".jpg": "image/jpeg",
".gif": "image/gif",
".txt": "text/plain",
}
if ct, ok := types[ext]; ok {
return ct
}
return "application/octet-stream"
}