feat: WmailHub v1.0.0 —— Cloudflare Email Workers 邮件接收与转发平台(本地可用版本)

- 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 生成
This commit is contained in:
BigCode
2026-09-30 03:34:37 +08:00
commit d4c1334143
87 changed files with 9268 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
/**
* Webhook endpoint SSRF 防护(docs/07、docs/08)
*
* 至少阻止:localhost、private IPv4/IPv6、link-local、云 metadata endpoint、
* 非法 scheme、以及 DNS 解析出私网 IP 的情况(DNS rebinding 的一半防御;
* 另一半由"解析后连接同一 IP"的策略约束,本地运行时直接按解析结果拦截)。
*/
/** # 成员变量: 允许的 URL scheme **/
const ALLOWED_SCHEMES = new Set(["http", "https"]);
/** # 函数
* 判断 IPv4 是否为私网/保留地址
*/
function isPrivateIPv4(ip: string): boolean {
const parts = ip.split(".").map(Number);
if (parts.length !== 4 || parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return true;
const [a, b] = parts;
if (a === 0 || a === 10 || a === 127) return true; // 本网段 / 私网 / loopback
if (a === 169 && b === 254) return true; // link-local + 云 metadata 169.254.169.254
if (a === 172 && b >= 16 && b <= 31) return true;
if (a === 192 && b === 168) return true;
if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT
if (a === 192 && b === 0) return true; // 192.0.0.x / 192.0.2.x
if (a === 198 && (b === 18 || b === 19)) return true; // benchmark
if (a >= 224) return true; // 组播 + 保留
return false;
}
/** # 函数
* 判断 IPv6 是否为 loopback/私网/link-local/IPv4 映射私网
*/
function isPrivateIPv6(raw: string): boolean {
const ip = raw.toLowerCase().replace(/^\[|\]$/g, "");
if (ip === "::" || ip === "::1") return true;
if (ip.startsWith("fe8") || ip.startsWith("fe9") || ip.startsWith("fea") || ip.startsWith("feb")) {
return true; // link-local fe80::/10
}
if (ip.startsWith("fc") || ip.startsWith("fd")) return true; // ULA fc00::/7
if (ip.startsWith("::ffff:")) {
const v4 = ip.slice(7);
if (v4.includes(".")) return isPrivateIPv4(v4);
}
if (ip.startsWith("64:ff9b:")) return true; // NAT64
if (ip.startsWith("2002:")) {
// 6to4:内嵌 IPv4
const seg = ip.slice(5);
const v4 = seg
.split(":")[0]
.match(/.{2}/g)
?.map((x) => Number.parseInt(x, 16))
.join(".");
if (v4) return isPrivateIPv4(v4);
}
return false;
}
/** # 函数
* 判断字符串是否为 IP 字面量(v4 或 v6)
*/
export function isIpLiteral(host: string): boolean {
const h = host.replace(/^\[|\]$/g, "");
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(h)) return true;
if (h.includes(":")) return true; // 含冒号即视为 IPv6
return false;
}
/** # 函数
* 校验 IP 是否允许外发
*/
export function isPublicIp(ip: string): boolean {
if (ip.includes(".") && !ip.startsWith("::ffff:") && !ip.includes(":")) return !isPrivateIPv4(ip);
return !isPrivateIPv6(ip);
}
/** # 函数
* 静态校验 webhook URL(不含 DNS 解析)
* 参数: rawUrl - 用户配置的 endpoint
* 返回值: null 表示通过;否则返回拒绝原因
*/
export function checkWebhookUrl(rawUrl: string, allowPrivateHosts = false): string | null {
let u: URL;
try {
u = new URL(rawUrl);
} catch {
return "invalid url";
}
if (!ALLOWED_SCHEMES.has(u.protocol.slice(0, -1))) return "scheme not allowed";
if (allowPrivateHosts) return null; // 本地联调开关(WMAILHUB_ALLOW_PRIVATE_WEBHOOKS=1)
const host = u.hostname.toLowerCase();
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) {
return "localhost-like host not allowed";
}
if (host === "metadata.google.internal" || host === "metadata" || host.endsWith(".internal")) {
return "metadata/internal host not allowed";
}
if (isIpLiteral(host) && !isPublicIp(host)) {
return "private/link-local IP not allowed";
}
return null;
}
/** # 成员变量: DNS 解析函数类型(返回该主机名解析到的全部 IP) **/
export type HostResolver = (host: string) => Promise<string[]>;
/** # 函数
* 完整 SSRF 校验:静态检查 + (可选)DNS 解析结果检查
* 参数: rawUrl - endpoint;resolve - DNS 解析器(本地 Bun/Node 运行时注入,Workers 环境可省略)
* 返回值: null 表示通过;否则返回拒绝原因
*/
export async function assertWebhookUrlSafe(
rawUrl: string,
resolve?: HostResolver,
allowPrivateHosts = false,
): Promise<string | null> {
const staticError = checkWebhookUrl(rawUrl, allowPrivateHosts);
if (staticError) return staticError;
if (allowPrivateHosts) return null;
if (resolve) {
const host = new URL(rawUrl).hostname;
if (!isIpLiteral(host)) {
let ips: string[];
try {
ips = await resolve(host);
} catch {
return "dns lookup failed";
}
if (ips.length === 0) return "dns lookup failed";
for (const ip of ips) {
if (!isPublicIp(ip)) return `resolves to private address (${ip})`;
}
}
}
return null;
}