Files
GoTunnel/pkg/plugin/script/js.go
Flik 2f98e1ac7d
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m16s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m4s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 2m29s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 54s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 2m35s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 55s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 2m21s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m35s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 1m1s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m55s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 1m39s
feat(logging): implement client log streaming and management
- Added log streaming functionality for clients, allowing real-time log access via SSE.
- Introduced LogHandler to manage log streaming requests and responses.
- Implemented LogSessionManager to handle active log sessions and listeners.
- Enhanced protocol with log-related message types and structures.
- Created Logger for client-side logging, supporting various log levels and file output.
- Developed LogViewer component for the web interface to display and filter logs.
- Updated API to support log stream creation and management.
- Added support for querying logs by level and searching through log messages.
2026-01-03 16:19:52 +08:00

672 lines
16 KiB
Go

package script
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"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
}
// 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"),
}
// 确保存储目录存在
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())
// 执行脚本
_, 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) // Do not overwrite the config API
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"
}
// =============================================================================
// 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)
},
}
}