update
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
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
This commit is contained in:
560
PLUGINS.md
Normal file
560
PLUGINS.md
Normal file
@@ -0,0 +1,560 @@
|
||||
# GoTunnel 插件开发指南
|
||||
|
||||
本文档介绍如何为 GoTunnel 开发 JS 插件。
|
||||
|
||||
## 目录
|
||||
|
||||
- [快速开始](#快速开始)
|
||||
- [插件结构](#插件结构)
|
||||
- [API 参考](#api-参考)
|
||||
- [示例插件](#示例插件)
|
||||
- [插件签名](#插件签名)
|
||||
- [发布到商店](#发布到商店)
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 最小插件示例
|
||||
|
||||
```javascript
|
||||
// 必须:定义插件元数据
|
||||
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()` | 否 | 插件停止时调用 |
|
||||
|
||||
### 元数据字段
|
||||
|
||||
```javascript
|
||||
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)`
|
||||
|
||||
输出日志信息。
|
||||
|
||||
```javascript
|
||||
log("Hello, World!");
|
||||
// 输出: [JS:plugin-name] Hello, World!
|
||||
```
|
||||
|
||||
#### `config(key)`
|
||||
|
||||
获取插件配置值。
|
||||
|
||||
```javascript
|
||||
var port = config("port");
|
||||
var host = config("host") || "127.0.0.1";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 连接 API (conn)
|
||||
|
||||
`handleConn` 函数接收的 `conn` 对象提供以下方法:
|
||||
|
||||
#### `conn.Read(size)`
|
||||
|
||||
读取数据,返回字节数组,失败返回 `null`。
|
||||
|
||||
```javascript
|
||||
var data = conn.Read(1024);
|
||||
if (data) {
|
||||
log("Received " + data.length + " bytes");
|
||||
}
|
||||
```
|
||||
|
||||
#### `conn.Write(data)`
|
||||
|
||||
写入数据,返回写入的字节数。
|
||||
|
||||
```javascript
|
||||
var written = conn.Write(data);
|
||||
log("Wrote " + written + " bytes");
|
||||
```
|
||||
|
||||
#### `conn.Close()`
|
||||
|
||||
关闭连接。
|
||||
|
||||
```javascript
|
||||
conn.Close();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 文件系统 API (fs)
|
||||
|
||||
所有文件操作都在沙箱中执行,有路径和大小限制。
|
||||
|
||||
#### `fs.readFile(path)`
|
||||
|
||||
读取文件内容。
|
||||
|
||||
```javascript
|
||||
var result = fs.readFile("/path/to/file.txt");
|
||||
if (result.error) {
|
||||
log("Error: " + result.error);
|
||||
} else {
|
||||
log("Content: " + result.data);
|
||||
}
|
||||
```
|
||||
|
||||
#### `fs.writeFile(path, content)`
|
||||
|
||||
写入文件内容。
|
||||
|
||||
```javascript
|
||||
var result = fs.writeFile("/path/to/file.txt", "Hello");
|
||||
if (result.ok) {
|
||||
log("File written");
|
||||
} else {
|
||||
log("Error: " + result.error);
|
||||
}
|
||||
```
|
||||
|
||||
#### `fs.readDir(path)`
|
||||
|
||||
读取目录内容。
|
||||
|
||||
```javascript
|
||||
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)`
|
||||
|
||||
获取文件信息。
|
||||
|
||||
```javascript
|
||||
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)`
|
||||
|
||||
检查文件是否存在。
|
||||
|
||||
```javascript
|
||||
var result = fs.exists("/path/to/file");
|
||||
if (result.exists) {
|
||||
log("File exists");
|
||||
}
|
||||
```
|
||||
|
||||
#### `fs.mkdir(path)`
|
||||
|
||||
创建目录。
|
||||
|
||||
```javascript
|
||||
var result = fs.mkdir("/path/to/new/dir");
|
||||
if (result.ok) {
|
||||
log("Directory created");
|
||||
}
|
||||
```
|
||||
|
||||
#### `fs.remove(path)`
|
||||
|
||||
删除文件或目录。
|
||||
|
||||
```javascript
|
||||
var result = fs.remove("/path/to/file");
|
||||
if (result.ok) {
|
||||
log("Removed");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### HTTP API (http)
|
||||
|
||||
用于构建简单的 HTTP 服务。
|
||||
|
||||
#### `http.serve(conn, handler)`
|
||||
|
||||
处理 HTTP 请求。
|
||||
|
||||
```javascript
|
||||
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 字符串。
|
||||
|
||||
```javascript
|
||||
var jsonStr = http.json({ name: "test", value: 123 });
|
||||
// 返回: '{"name":"test","value":123}'
|
||||
```
|
||||
|
||||
#### `http.sendFile(conn, filePath)`
|
||||
|
||||
发送文件作为 HTTP 响应。
|
||||
|
||||
```javascript
|
||||
function handleConn(conn) {
|
||||
http.sendFile(conn, "/path/to/index.html");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 示例插件
|
||||
|
||||
### Echo 服务
|
||||
|
||||
```javascript
|
||||
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 文件服务器
|
||||
|
||||
```javascript
|
||||
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 服务
|
||||
|
||||
```javascript
|
||||
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 编码的签名数据:
|
||||
|
||||
```json
|
||||
{
|
||||
"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` 文件索引所有插件:
|
||||
|
||||
```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()` 函数输出调试信息:
|
||||
|
||||
```javascript
|
||||
log("Debug: variable = " + JSON.stringify(variable));
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
始终检查 API 返回的错误:
|
||||
|
||||
```javascript
|
||||
var result = fs.readFile(path);
|
||||
if (result.error) {
|
||||
log("Error reading file: " + result.error);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### 配置测试
|
||||
|
||||
在服务端配置中测试插件:
|
||||
|
||||
```yaml
|
||||
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.*
|
||||
Reference in New Issue
Block a user