diff --git a/docs/plugins.md b/docs/plugins.md index deaf260..c238730 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -377,7 +377,48 @@ plugin.http = { 路径,未认证会跳登录页。 - 未声明的路径不会因为落在允许命名空间而被放行——不在 handler 表里就是 404。 -### 5.6 在运行时安装/更新适配器(token 轮换) +### 5.6 前缀代理:把插件自起的服务挂到网关同端口 + +前节的自定义挂载点只能回答**由插件计算出来的内容**(`{status, headers, body/json}`)。 +若插件自己起了一个 TCP/HTTP 服务(典型:服务端浏览器栈 + noVNC),客户端就需要 +**字节级转发**,包括 **WebSocket 升级** —— 前者做不到。 + +为此声明 `prefix = true`,handler 每次请求返回一个**回环目标**: + +```lua +plugin.http = { + { path = "myplugin/proxy/:session", method = "GET", prefix = true, + roles = { "admin" }, handler = "proxy" }, +} + +function plugin.proxy(req) + -- 每次请求都问一遍:会话没了/过期了就返回 404,地址自然失效 + if not sessionAlive(req.params.session) then + return { status = 404, json = { error = "no such session" } } + end + return { proxy = "http://127.0.0.1:60100" } +end +``` + +规则: + +- **只能返回 `{ proxy = <目标> }`**,没有 `status` / `body`。写了 `status` 会被忽略。 +- 目标必须是 **带显式端口的字面回环 IP**(`http://127.0.0.1:60100`)。 + 拒绝 `https://`、`localhost` 这类主机名、无端口、带 userinfo/path/query 的地址。 + 理由:主机名可能被 hosts / DNS / 代理环境改写;通用能力只接受确定性地址。 +- 路由匹配同前缀下的**所有子路径**,`:param` 仍照常捕获,`req.path` 是完整 + 请求路径,子路径由插件自己从 `req.path` 里切。 +- **鉴权先于代理**:`roles` / `keys` / `public` 语义与普通端点一致。 +- **不转发网关凭据**:`Authorization`、`Cookie`、`api_key` 都不会传给后端服务。 +- **拒绝跨站**:`Origin` 与网关自身 host 不一致返回 403。WebSocket 由浏览器 + 发起且自动带 Cookie,否则第三方网页可借管理员会话开隧道。 +- **撤销点唯一**:handler 不再返回目标(或返回 404)即失效,内核不缓存目标。 + +客户端拿到的 URL 是**相对网关口**的绝对路径(如 +`/api/plugins/http/myplugin/proxy//vnc.html`),因此远程访问、HTTPS 入口、 +Cookie 鉴权都自然生效,无需暴露额外端口。 + +### 5.7 在运行时安装/更新适配器(token 轮换) 插件可以在运行时安装、改写**自己的**适配器 —— 这是"带自动轮换的认证"的实现 方式:拿着新 token 重写适配器源码,后续请求自然使用新逻辑(内核重建该适配器