feat(worker): safelist 纳入 push/register 与 push/disable
网关白名单此前无条件 fail-closed,后端注册的 18 条路由中只有 POST /api/push/register 与 POST /api/push/disable 两条需要业务流量却未 被转发,导致 PWA 订阅/退订必须绕过网关直连后端。 - SAFE_ROUTES 12 → 14 条(backends.js:9-24) - ROUTE_METHODS 同步补两条 POST(index.js:40-55) - 头注释路由清单同步 - backends.test.js 转发集合断言补齐两条 - index.test.js 新增转发端到端用例(含鉴权头透传路径) 单入口边界:两条路由仍经 SAFE_ROUTES 无条件校验,未放宽白名单语义。
This commit is contained in:
parent
e35cc64ce3
commit
a39ebcf9d1
|
|
@ -17,6 +17,8 @@ export const SAFE_ROUTES = Object.freeze([
|
||||||
"/api/admin/session/revoke",
|
"/api/admin/session/revoke",
|
||||||
"/webhook/tg",
|
"/webhook/tg",
|
||||||
"/push/jmap",
|
"/push/jmap",
|
||||||
|
"/api/push/register",
|
||||||
|
"/api/push/disable",
|
||||||
"/reconcile",
|
"/reconcile",
|
||||||
"/ready",
|
"/ready",
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
* - GET|PUT /api/config、PUT /api/business-config、POST /api/admin/session[/revoke]
|
* - GET|PUT /api/config、PUT /api/business-config、POST /api/admin/session[/revoke]
|
||||||
* → 透传至后端;后端校验 bootstrap 凭据或短期 admin session
|
* → 透传至后端;后端校验 bootstrap 凭据或短期 admin session
|
||||||
* - GET /healthz → LB 聚合健康(MOD-HEALTH-AGG)
|
* - GET /healthz → LB 聚合健康(MOD-HEALTH-AGG)
|
||||||
* - POST /webhook/tg|/push/jmap|/reconcile、GET /ready → 透传 + 有界故障转移
|
* - POST /webhook/tg|/push/jmap|/api/push/register|/api/push/disable|/reconcile、GET /ready → 透传 + 有界故障转移
|
||||||
* - 其它 → 404 / 405
|
* - 其它 → 404 / 405
|
||||||
*
|
*
|
||||||
* 日志红线:仅打「方法 / 路径 / origin / 失败类别」,绝不打印 header/body/secret。
|
* 日志红线:仅打「方法 / 路径 / origin / 失败类别」,绝不打印 header/body/secret。
|
||||||
|
|
@ -48,6 +48,8 @@ const ROUTE_METHODS = Object.freeze({
|
||||||
"/api/admin/session/revoke": ["POST"],
|
"/api/admin/session/revoke": ["POST"],
|
||||||
"/webhook/tg": ["POST"],
|
"/webhook/tg": ["POST"],
|
||||||
"/push/jmap": ["POST"],
|
"/push/jmap": ["POST"],
|
||||||
|
"/api/push/register": ["POST"],
|
||||||
|
"/api/push/disable": ["POST"],
|
||||||
"/reconcile": ["POST"],
|
"/reconcile": ["POST"],
|
||||||
"/ready": ["GET"],
|
"/ready": ["GET"],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ test("BACKEND_ORIGINS_JSON parses, dedups, validates all entries", () => {
|
||||||
test("route safelist excludes /healthz and unknown paths (C-LB-SINGLE-REG-URL)", () => {
|
test("route safelist excludes /healthz and unknown paths (C-LB-SINGLE-REG-URL)", () => {
|
||||||
for (const r of [
|
for (const r of [
|
||||||
"/", "/assets/config.js", "/assets/styles.css", "/api/status", "/ready", "/webhook/tg", "/push/jmap", "/reconcile",
|
"/", "/assets/config.js", "/assets/styles.css", "/api/status", "/ready", "/webhook/tg", "/push/jmap", "/reconcile",
|
||||||
|
"/api/push/register", "/api/push/disable",
|
||||||
"/api/config", "/api/business-config", "/api/admin/session", "/api/admin/session/revoke",
|
"/api/config", "/api/business-config", "/api/admin/session", "/api/admin/session/revoke",
|
||||||
]) {
|
]) {
|
||||||
assert.ok(LB_ROUTES.includes(r), `forwarded: ${r}`);
|
assert.ok(LB_ROUTES.includes(r), `forwarded: ${r}`);
|
||||||
|
|
|
||||||
|
|
@ -131,3 +131,28 @@ test("Worker: valid env, GET /ready forwarded to backend with auth header intact
|
||||||
restore();
|
restore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Worker: POST /api/push/register and /api/push/disable are forwarded", async () => {
|
||||||
|
const seen = [];
|
||||||
|
const restore = stubFetch(async (url, init) => {
|
||||||
|
seen.push([url, init.method]);
|
||||||
|
return new Response("{\"error\":\"invalid_request\",\"request_id\":\"x\"}", {
|
||||||
|
status: 400,
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
for (const path of ["/api/push/register", "/api/push/disable"]) {
|
||||||
|
const res = await handleFetch(
|
||||||
|
new Request(`https://lb.example${path}`, { method: "POST", body: "{}" }),
|
||||||
|
makeEnv(),
|
||||||
|
);
|
||||||
|
assert.equal(res.status, 400);
|
||||||
|
assert.deepEqual(await res.json(), { error: "invalid_request", request_id: "x" });
|
||||||
|
}
|
||||||
|
assert.equal(seen.length, 2);
|
||||||
|
assert.ok(seen.every(([url, method]) => method === "POST"));
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue