后端补上流式透传和两个写操作,给后面几个页面铺路
原以为流量和日志要写 WebSocket 转发,实测不用:内核在没有 WebSocket 升级时把
traffic、memory、logs 退化成分块的 HTTP 流,一行一个 JSON(server.go:380-383)。
现有的透传本来就能流式传,唯一的问题是那个 15 秒超时会每隔一会儿掐断一次,
给这三个路径去掉超时即可。
写操作照旧是白名单,逐条列出:断开连接(DELETE /connections 和 /connections/{id})、
刷新订阅(PUT /providers/proxies/{名字})。两者都只改内核运行状态、不碰配置文件,
所以不走落盘那条保护链路。方法和路径都要匹配才放行,测试里有反例。
This commit is contained in:
+32
-2
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user