后端补上流式透传和两个写操作,给后面几个页面铺路

原以为流量和日志要写 WebSocket 转发,实测不用:内核在没有 WebSocket 升级时把
traffic、memory、logs 退化成分块的 HTTP 流,一行一个 JSON(server.go:380-383)。
现有的透传本来就能流式传,唯一的问题是那个 15 秒超时会每隔一会儿掐断一次,
给这三个路径去掉超时即可。

写操作照旧是白名单,逐条列出:断开连接(DELETE /connections 和 /connections/{id})、
刷新订阅(PUT /providers/proxies/{名字})。两者都只改内核运行状态、不碰配置文件,
所以不走落盘那条保护链路。方法和路径都要匹配才放行,测试里有反例。
This commit is contained in:
im0o
2026-08-10 19:45:31 +08:00
parent cb8effe05b
commit afe53b9790
3 changed files with 79 additions and 5 deletions
+32 -2
View File
@@ -11,7 +11,7 @@
*/
import listenerFields from '../shared/listener-fields.json' with { type: 'json' }
import { applyConfig, listBackups, readConfig, restoreBackup } from './config-store.ts'
import { kernelVersion, proxyGet, selectProxy } from './kernel.ts'
import { kernelVersion, proxyGet, proxyWrite, selectProxy } from './kernel.ts'
import { tokenFromRequest, tokenMatches } from './auth.ts'
import type { ConfigObject } from '../vendor/config-editor/index.ts'
import type { Env } from './env.ts'
@@ -30,6 +30,25 @@ const DELAY_PATH = /^\/(proxies|group)\/.+\/delay$/
*/
const HEALTHCHECK_PATH = /^\/providers\/proxies\/[^/]+(\/[^/]+)?\/healthcheck$/
/**
* 会持续吐数据的接口。透传它们时不能设超时。
* 内核在没有 WebSocket 升级时把这三个退化成分块的 HTTP 流,一行一个 JSON。
*/
const STREAM_PATHS = new Set(['/traffic', '/memory', '/logs'])
/**
* 允许透传的写操作。
*
* 每一条都是「只改内核运行状态、不碰配置文件」的动作,所以不走落盘那条保护链路。
* 仍然是白名单:能写的东西必须一条条列出来。
*/
const WRITABLE: Array<{ method: 'PUT' | 'DELETE'; pattern: RegExp }> = [
// 断开某条连接,或者全部断开
{ method: 'DELETE', pattern: /^\/connections(\/[^/]+)?$/ },
// 让内核重新拉取某个订阅
{ method: 'PUT', pattern: /^\/providers\/proxies\/[^/]+$/ },
]
/** 允许透传的管理接口路径。白名单而不是黑名单——多开一个口子要有意为之。 */
const READABLE_KERNEL_PATHS = new Set([
'/version',
@@ -175,6 +194,17 @@ function makeRoute(env: Env, token: string) {
: json({ message: upstream.message || '内核拒绝了这次切换' }, 422)
}
if (path.startsWith('/api/kernel/') && (req.method === 'PUT' || req.method === 'DELETE')) {
const target = path.slice('/api/kernel'.length)
const rule = WRITABLE.find((w) => w.method === req.method && w.pattern.test(target))
if (!rule) return json({ message: `不允许写入该路径:${target}` }, 403)
const upstream = await proxyWrite(env, rule.method, target + url.search)
return new Response(upstream.body, {
status: upstream.status,
headers: { 'Content-Type': upstream.headers.get('Content-Type') ?? 'application/json' },
})
}
if (path.startsWith('/api/kernel/') && req.method === 'GET') {
const target = path.slice('/api/kernel'.length)
const allowed =
@@ -182,7 +212,7 @@ function makeRoute(env: Env, token: string) {
if (!allowed) {
return json({ message: `不允许访问该路径:${target}` }, 403)
}
const upstream = await proxyGet(env, target + url.search)
const upstream = await proxyGet(env, target + url.search, STREAM_PATHS.has(target))
return new Response(upstream.body, {
status: upstream.status,
headers: { 'Content-Type': upstream.headers.get('Content-Type') ?? 'application/json' },