Files
GoTunnel/PLUGINS.md
Flik 4d2a2a7117
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 58s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 48s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m23s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 56s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 52s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m42s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m19s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 54s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 2m3s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 1m1s
update
2025-12-29 23:08:15 +08:00

10 KiB
Raw Permalink Blame History

GoTunnel 插件开发指南

本文档介绍如何为 GoTunnel 开发 JS 插件。

目录


快速开始

最小插件示例

// 必须:定义插件元数据
function metadata() {
    return {
        name: "my-plugin",
        version: "1.0.0",
        type: "app",
        description: "My first plugin",
        author: "Your Name"
    };
}

// 可选:插件启动时调用
function start() {
    log("Plugin started");
}

// 必须:处理连接
function handleConn(conn) {
    // 处理连接逻辑
    conn.Close();
}

// 可选:插件停止时调用
function stop() {
    log("Plugin stopped");
}

插件结构

生命周期函数

函数 必须 说明
metadata() 返回插件元数据,不定义则使用默认值
start() 插件启动时调用
handleConn(conn) 处理每个连接
stop() 插件停止时调用

元数据字段

function metadata() {
    return {
        name: "plugin-name",      // 插件名称
        version: "1.0.0",         // 版本号
        type: "app",              // 类型: "app" 或 "proxy"
        run_at: "client",         // 运行位置: "client" 或 "server"
        description: "描述",      // 插件描述
        author: "作者"            // 作者名称
    };
}

API 参考

基础 API

log(message)

输出日志信息。

log("Hello, World!");
// 输出: [JS:plugin-name] Hello, World!

config(key)

获取插件配置值。

var port = config("port");
var host = config("host") || "127.0.0.1";

连接 API (conn)

handleConn 函数接收的 conn 对象提供以下方法:

conn.Read(size)

读取数据,返回字节数组,失败返回 null

var data = conn.Read(1024);
if (data) {
    log("Received " + data.length + " bytes");
}

conn.Write(data)

写入数据,返回写入的字节数。

var written = conn.Write(data);
log("Wrote " + written + " bytes");

conn.Close()

关闭连接。

conn.Close();

文件系统 API (fs)

所有文件操作都在沙箱中执行,有路径和大小限制。

fs.readFile(path)

读取文件内容。

var result = fs.readFile("/path/to/file.txt");
if (result.error) {
    log("Error: " + result.error);
} else {
    log("Content: " + result.data);
}

fs.writeFile(path, content)

写入文件内容。

var result = fs.writeFile("/path/to/file.txt", "Hello");
if (result.ok) {
    log("File written");
} else {
    log("Error: " + result.error);
}

fs.readDir(path)

读取目录内容。

var result = fs.readDir("/path/to/dir");
if (!result.error) {
    for (var i = 0; i < result.entries.length; i++) {
        var entry = result.entries[i];
        log(entry.name + " - " + (entry.isDir ? "DIR" : entry.size + " bytes"));
    }
}

fs.stat(path)

获取文件信息。

var result = fs.stat("/path/to/file");
if (!result.error) {
    log("Name: " + result.name);
    log("Size: " + result.size);
    log("IsDir: " + result.isDir);
    log("ModTime: " + result.modTime);
}

fs.exists(path)

检查文件是否存在。

var result = fs.exists("/path/to/file");
if (result.exists) {
    log("File exists");
}

fs.mkdir(path)

创建目录。

var result = fs.mkdir("/path/to/new/dir");
if (result.ok) {
    log("Directory created");
}

fs.remove(path)

删除文件或目录。

var result = fs.remove("/path/to/file");
if (result.ok) {
    log("Removed");
}

HTTP API (http)

用于构建简单的 HTTP 服务。

http.serve(conn, handler)

处理 HTTP 请求。

function handleConn(conn) {
    http.serve(conn, function(req) {
        return {
            status: 200,
            contentType: "application/json",
            body: http.json({ message: "Hello", path: req.path })
        };
    });
}

请求对象 (req):

字段 类型 说明
method string HTTP 方法 (GET, POST, etc.)
path string 请求路径
body string 请求体

响应对象:

字段 类型 说明
status number HTTP 状态码 (默认 200)
contentType string Content-Type (默认 application/json)
body string 响应体

http.json(data)

将对象序列化为 JSON 字符串。

var jsonStr = http.json({ name: "test", value: 123 });
// 返回: '{"name":"test","value":123}'

http.sendFile(conn, filePath)

发送文件作为 HTTP 响应。

function handleConn(conn) {
    http.sendFile(conn, "/path/to/index.html");
}

示例插件

Echo 服务

function metadata() {
    return {
        name: "echo",
        version: "1.0.0",
        type: "app",
        description: "Echo back received data"
    };
}

function handleConn(conn) {
    while (true) {
        var data = conn.Read(4096);
        if (!data || data.length === 0) {
            break;
        }
        conn.Write(data);
    }
    conn.Close();
}

HTTP 文件服务器

function metadata() {
    return {
        name: "file-server",
        version: "1.0.0",
        type: "app",
        description: "Simple HTTP file server"
    };
}

var rootDir = "";

function start() {
    rootDir = config("root") || "/tmp";
    log("Serving files from: " + rootDir);
}

function handleConn(conn) {
    http.serve(conn, function(req) {
        if (req.method === "GET") {
            var filePath = rootDir + req.path;
            if (req.path === "/") {
                filePath = rootDir + "/index.html";
            }

            var stat = fs.stat(filePath);
            if (stat.error) {
                return { status: 404, body: "Not Found" };
            }

            if (stat.isDir) {
                return listDirectory(filePath);
            }

            var file = fs.readFile(filePath);
            if (file.error) {
                return { status: 500, body: file.error };
            }

            return {
                status: 200,
                contentType: "text/html",
                body: file.data
            };
        }
        return { status: 405, body: "Method Not Allowed" };
    });
}

function listDirectory(path) {
    var result = fs.readDir(path);
    if (result.error) {
        return { status: 500, body: result.error };
    }

    var html = "<html><body><h1>Directory Listing</h1><ul>";
    for (var i = 0; i < result.entries.length; i++) {
        var e = result.entries[i];
        html += "<li><a href='" + e.name + "'>" + e.name + "</a></li>";
    }
    html += "</ul></body></html>";

    return { status: 200, contentType: "text/html", body: html };
}

JSON API 服务

function metadata() {
    return {
        name: "api-server",
        version: "1.0.0",
        type: "app",
        description: "JSON API server"
    };
}

var counter = 0;

function handleConn(conn) {
    http.serve(conn, function(req) {
        if (req.path === "/api/status") {
            return {
                status: 200,
                body: http.json({
                    status: "ok",
                    counter: counter++,
                    timestamp: Date.now()
                })
            };
        }

        if (req.path === "/api/echo" && req.method === "POST") {
            return {
                status: 200,
                body: http.json({
                    received: req.body
                })
            };
        }

        return {
            status: 404,
            body: http.json({ error: "Not Found" })
        };
    });
}

插件签名

为了安全JS 插件需要官方签名才能运行。

签名格式

签名文件 (.sig) 包含 Base64 编码的签名数据:

{
  "payload": {
    "name": "plugin-name",
    "version": "1.0.0",
    "checksum": "sha256-hash",
    "key_id": "official-v1"
  },
  "signature": "base64-signature"
}

获取签名

  1. 提交插件到官方仓库
  2. 通过审核后获得签名
  3. .js.sig 文件一起分发

发布到商店

商店 JSON 格式

插件商店使用 store.json 文件索引所有插件:

[
  {
    "name": "echo",
    "version": "1.0.0",
    "type": "app",
    "description": "Echo service plugin",
    "author": "GoTunnel",
    "icon": "https://example.com/icon.png",
    "download_url": "https://example.com/plugins/echo.js"
  }
]

提交流程

  1. Fork 官方插件仓库
  2. 添加插件文件到 plugins/ 目录
  3. 更新 store.json
  4. 提交 Pull Request
  5. 等待审核和签名

沙箱限制

为了安全JS 插件运行在沙箱环境中:

限制项 默认值
最大读取文件大小 10 MB
最大写入文件大小 10 MB
允许读取路径 插件数据目录
允许写入路径 插件数据目录

调试技巧

日志输出

使用 log() 函数输出调试信息:

log("Debug: variable = " + JSON.stringify(variable));

错误处理

始终检查 API 返回的错误:

var result = fs.readFile(path);
if (result.error) {
    log("Error reading file: " + result.error);
    return;
}

配置测试

在服务端配置中测试插件:

js_plugins:
  - name: my-plugin
    path: /path/to/my-plugin.js
    sig_path: /path/to/my-plugin.js.sig
    config:
      debug: "true"
      port: "8080"

常见问题

Q: 插件无法加载?

A: 检查签名文件是否存在且有效。

Q: 文件操作失败?

A: 确认路径在沙箱允许范围内。

Q: 如何获取客户端 IP

A: 目前 API 不支持,计划在后续版本添加。


更新日志

v1.0.0

  • 初始版本
  • 支持基础 API: log, config
  • 支持连接 API: Read, Write, Close
  • 支持文件系统 API: fs.*
  • 支持 HTTP API: http.*