MessageWeave/cloudflare-worker/src/backends.js

122 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* origin 配置解析 + 校验(稳定 ID:C-HTTPS-INBOUND / SAF-LB-PASSTHRU / C-NO-DB)。
* 后端 origin 仅允许 https(C-HTTPS-INBOUND);不转发 Redis/JMAP 流量(C-NO-DB)。
* 仅 ES2022,零依赖,可 node 直接执行。
*/
/** 允许透传的入站路由 safelist(C-LB-SINGLE-REG-URL;/healthz 由 Worker 自身聚合,不在列):未知路径 → 404。
* SPA 静态资源为公开 GET;所有管理 API 仍由后端校验 Redis bootstrap 凭据或 admin session。 */
export const SAFE_ROUTES = Object.freeze([
"/",
"/assets/config.js",
"/assets/styles.css",
"/api/status",
"/api/config",
"/api/business-config",
"/api/admin/session",
"/api/admin/session/revoke",
"/webhook/tg",
"/push/jmap",
"/api/push/register",
"/api/push/disable",
"/reconcile",
"/ready",
]);
/** 需要透传到后端 origin 的路由(不含 Worker 聚合端点)。 */
export const LB_ROUTES = Object.freeze([...SAFE_ROUTES]);
export class BackendConfigError extends Error {
constructor(message) {
super(message);
this.name = "BackendConfigError";
}
}
const HTTP_STATUS = 80;
const HTTPS_STATUS = 443;
/**
* 校验并归一化单个 backend origin。
* 规则(C-HTTPS-INBOUND,对齐 §10 的 fail-closed):
* - 必须是 https;
* - 必须无内嵌凭据(禁 user:pass@host);
* - 必须无 query / fragment(origin 不应带这些);
* - 路径必须为空(origin 根),禁止 /path 形式(防止把路由写进 origin)。
* @param {string} raw
* @returns {{ url: string, host: string, port: number }}
*/
export function parseBackendOrigin(raw) {
if (typeof raw !== "string" || raw.trim() === "") {
throw new BackendConfigError("backend origin must be a non-empty https URL");
}
let parsed;
try {
parsed = new URL(raw.trim());
} catch {
throw new BackendConfigError(`backend origin is not a valid URL: ${raw}`);
}
if (parsed.protocol !== "https:") {
throw new BackendConfigError(
`backend origin must use https (C-HTTPS-INBOUND): ${raw}`,
);
}
if (parsed.username !== "" || parsed.password !== "") {
throw new BackendConfigError(
`backend origin must not embed credentials (C-HTTPS-INBOUND): ${parsed.host}`,
);
}
if (parsed.search !== "" || parsed.hash !== "") {
throw new BackendConfigError(
`backend origin must not contain query/fragment: ${parsed.host}`,
);
}
if (parsed.pathname !== "" && parsed.pathname !== "/") {
throw new BackendConfigError(
`backend origin must be a bare origin (no path): ${raw}`,
);
}
// 记录端口以便健康探测/请求拼 URL;缺省 443。
const port = parsed.port !== "" ? Number(parsed.port) : HTTPS_STATUS;
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
throw new BackendConfigError(`backend origin has an invalid port: ${raw}`);
}
return { url: parsed.origin, host: parsed.hostname, port };
}
/**
* 解析 `BACKEND_ORIGINS_JSON` 环境变量:JSON 字符串数组。
* 全部校验通过后返回去重(保持顺序)的 origins;无有效条目则报错。
* @param {unknown} raw 可能为 JSON 字符串或已解析数组
* @returns {Array<{url:string, host:string, port:number, source:string}>}
*/
export function parseBackendOrigins(raw) {
let list;
if (Array.isArray(raw)) {
list = raw;
} else if (typeof raw === "string") {
try {
list = JSON.parse(raw);
} catch {
throw new BackendConfigError("BACKEND_ORIGINS_JSON is not valid JSON");
}
} else {
throw new BackendConfigError("BACKEND_ORIGINS_JSON must be a JSON array of https origins");
}
if (!Array.isArray(list) || list.length === 0) {
throw new BackendConfigError("BACKEND_ORIGINS_JSON must be a non-empty array");
}
const seen = new Set();
const out = [];
for (const entry of list) {
const origin = parseBackendOrigin(entry);
if (!seen.has(origin.url)) {
seen.add(origin.url);
out.push(origin);
}
}
return out;
}
export { HTTP_STATUS, HTTPS_STATUS };