- Console:React 19 + antd 6.6.5 前端(登录 admin/admin)+ Hono 管理 API (Nodes/Domains/Mailboxes/Emails/Webhooks/Keys/OAuth/Admin) - Node:Bun 转发节点(HMAC 签名、有限重试、心跳、结构化日志、优雅退出) - Email Worker:Cloudflare email handler + 本地联调入口 - shared:HMAC 协议(docs/04)、wmh_ Key(32 位)、AES-GCM 加密、SSRF 防护 - 邮件管线:postal-mime 解析、附件、catch-all、限额、malformed 降级、TTL 1 天 - Webhook:签名投递、指数退避重试、SSRF 校验、投递日志 - API v1 + MCP(JSON-RPC,六工具,凭据 kind 隔离) - Cloudflare OAuth:真实 + 本地 sim 模式,自动安装管线(幂等,无需手工命令) - 测试:bun test 41 例(协议安全/管线/TTL/Webhook/SSRF/MCP/OAuth/鉴权)+ Playwright UI 冒烟 - LOGO/favicon 由 codex CLI 生成
155 lines
6.6 KiB
TypeScript
155 lines
6.6 KiB
TypeScript
/** Cloudflare OAuth(本地 sim 模式)测试:state CSRF、自动安装幂等、连接存储 */
|
|
import { describe, expect, test } from "bun:test";
|
|
import { authedFetch, loginCookie, makeTestEnv } from "./helpers.ts";
|
|
|
|
describe("Cloudflare OAuth(sim 模式)", () => {
|
|
test("start 生成 state;sim approve 完成自动安装(OAuth→identity→discover→worker→routing→domains→mailboxes)", async () => {
|
|
const t = await makeTestEnv();
|
|
const cookie = await loginCookie(t);
|
|
|
|
// Add Node
|
|
const created = (await (
|
|
await authedFetch(t, cookie, "http://localhost/api/nodes", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name: "Sim Node", ingress_url: "http://127.0.0.1:18788/ingress/email" }),
|
|
})
|
|
).json()) as { node_id: string; enrollment_token: string };
|
|
expect(created.enrollment_token.startsWith("wenr_")).toBe(true);
|
|
|
|
// OAuth start
|
|
const start = (await (
|
|
await authedFetch(t, cookie, `http://localhost/api/oauth/cloudflare/start?node_id=${created.node_id}`)
|
|
).json()) as { url: string; state: string };
|
|
expect(start.url).toContain("/oauth/sim/authorize?state=");
|
|
|
|
// sim approve(等价于用户在 Cloudflare 授权页点击同意)
|
|
const approve = (await (
|
|
await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/sim/approve", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ state: start.state }),
|
|
})
|
|
).json()) as { ok: boolean; error?: string };
|
|
expect(approve.ok).toBe(true);
|
|
|
|
// 安装结果
|
|
const detail = (await (
|
|
await authedFetch(t, cookie, `http://localhost/api/nodes/${created.node_id}`)
|
|
).json()) as {
|
|
node: { status: string; cloudflare_account_id: string | null };
|
|
steps: Record<string, { status: string }>;
|
|
domains: { name: string }[];
|
|
};
|
|
expect(detail.node.status).toBe("offline"); // 等待 Node 心跳上线
|
|
expect(detail.node.cloudflare_account_id).toBe("sim-account-0001");
|
|
for (const key of ["oauth", "identity", "discover", "email_worker", "email_routing", "domains", "mailboxes"]) {
|
|
expect(detail.steps[key].status).toBe("done");
|
|
}
|
|
expect(detail.domains.map((d) => d.name)).toContain("example.test");
|
|
|
|
// catch-all mailbox 已创建
|
|
const mb = await t.env.DB.prepare("SELECT address FROM mailboxes WHERE is_catch_all = 1").all<{ address: string }>();
|
|
expect(mb.results.map((r) => r.address)).toContain("*@example.test");
|
|
|
|
// sim 资源记录(Email Worker / Routing rule)
|
|
const sim = await t.env.DB.prepare("SELECT kind FROM cf_sim_resources").all<{ kind: string }>();
|
|
const kinds = sim.results.map((r) => r.kind);
|
|
expect(kinds).toContain("email_worker");
|
|
expect(kinds).toContain("routing_rule");
|
|
|
|
// OAuth token 已加密入库(不含明文 token)
|
|
const conn = await t.env.DB.prepare("SELECT encrypted_access_token FROM oauth_connections").first<{
|
|
encrypted_access_token: string;
|
|
}>();
|
|
expect(conn?.encrypted_access_token).toBeTruthy();
|
|
expect(conn!.encrypted_access_token.startsWith("sim_")).toBe(false);
|
|
});
|
|
|
|
test("state 一次性:重放回调 state 被拒(OAuth CSRF 防护)", async () => {
|
|
const t = await makeTestEnv();
|
|
const cookie = await loginCookie(t);
|
|
const created = (await (
|
|
await authedFetch(t, cookie, "http://localhost/api/nodes", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name: "n" }),
|
|
})
|
|
).json()) as { node_id: string };
|
|
const { state } = (await (
|
|
await authedFetch(t, cookie, `http://localhost/api/oauth/cloudflare/start?node_id=${created.node_id}`)
|
|
).json()) as { state: string };
|
|
|
|
const first = await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/sim/approve", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ state }),
|
|
});
|
|
expect(first.status).toBe(200);
|
|
// state 已消费,重放被拒
|
|
const second = await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/sim/approve", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ state }),
|
|
});
|
|
expect(second.status).toBe(400);
|
|
});
|
|
|
|
test("伪造/过期 state 被拒;真实 callback 端点同样拒绝", async () => {
|
|
const t = await makeTestEnv();
|
|
const cookie = await loginCookie(t);
|
|
const bad = await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/sim/approve", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ state: "cfstate_forged" }),
|
|
});
|
|
expect(bad.status).toBe(400);
|
|
const cb = await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/callback?code=x&state=forged");
|
|
expect(cb.status).toBe(400);
|
|
});
|
|
|
|
test("enrollment:token 换取身份,凭证明文仅返回一次且库中只存摘要/密文", async () => {
|
|
const t = await makeTestEnv();
|
|
const cookie = await loginCookie(t);
|
|
const created = (await (
|
|
await authedFetch(t, cookie, "http://localhost/api/nodes", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name: "n2" }),
|
|
})
|
|
).json()) as { node_id: string; enrollment_token: string };
|
|
|
|
const enroll = (await (
|
|
await t.app.fetch(
|
|
new Request("http://localhost/api/internal/enroll", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ enrollment_token: created.enrollment_token }),
|
|
}),
|
|
t.env,
|
|
)
|
|
).json()) as { node_id: string; credential_secret: string; ingress_secret: string };
|
|
expect(enroll.node_id).toBe(created.node_id);
|
|
expect(enroll.credential_secret.startsWith("wns_")).toBe(true);
|
|
|
|
// 库中不存明文 secret
|
|
const cred = await t.env.DB.prepare(
|
|
"SELECT secret_hash, secret_enc FROM node_credentials WHERE node_id = ? AND status='active'",
|
|
)
|
|
.bind(created.node_id)
|
|
.first<{ secret_hash: string; secret_enc: string }>();
|
|
expect(cred?.secret_hash).not.toBe(enroll.credential_secret);
|
|
expect(cred?.secret_enc).not.toContain(enroll.credential_secret);
|
|
// 无效 token 被拒
|
|
const badEnroll = await t.app.fetch(
|
|
new Request("http://localhost/api/internal/enroll", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ enrollment_token: "wenr_invalid" }),
|
|
}),
|
|
t.env,
|
|
);
|
|
expect(badEnroll.status).toBe(401);
|
|
});
|
|
});
|