"use strict"; /* ================= 工具:请求体 / 限流 / 输入校验 ================= */ /* 读取请求体(带大小上限,超限即毁连接) */ function readBody(req, limit = 16384) { return new Promise((resolve, reject) => { let size = 0; const chunks = []; req.on("data", (c) => { size += c.length; if (size > limit) { reject(Object.assign(new Error("body too large"), { status: 413 })); req.destroy(); } else chunks.push(c); }); req.on("end", () => resolve(Buffer.concat(chunks))); req.on("error", reject); }); } /* 读取 JSON:强制 content-type(配合 SameSite=Lax 防 CSRF),限长 */ async function readJson(req, limit) { const ct = String(req.headers["content-type"] || ""); if (!ct.includes("application/json")) throw Object.assign(new Error("Content-Type must be application/json"), { status: 415 }); const buf = await readBody(req, limit); try { return JSON.parse(buf.toString("utf8")); } catch (e) { throw Object.assign(new Error("invalid JSON"), { status: 400 }); } } /* ---------------- 限流(内存桶,按 key) ---------------- */ const buckets = new Map(); /** 返回 true=放行;false=超限 */ function rateLimit(key, max, winMs) { const now = Date.now(); let b = buckets.get(key); if (!b || now > b.reset) { b = { count: 0, reset: now + winMs }; buckets.set(key, b); } b.count++; return b.count <= max; } setInterval(() => { const now = Date.now(); for (const [k, b] of buckets) if (now > b.reset) buckets.delete(k); }, 60000).unref(); /* ---------------- 输入校验(白名单式) ---------------- */ const RE_USER = /^[A-Za-z0-9_一-龥]{2,16}$/; const RE_EMAIL = /^[^\s@]{1,40}@[^\s@]{1,40}\.[^\s@]{1,20}$/; const vUsername = (u) => typeof u === "string" && RE_USER.test(u); const vEmail = (e) => typeof e === "string" && e.length <= 64 && RE_EMAIL.test(e); const vPassword = (p) => typeof p === "string" && p.length >= 8 && p.length <= 72; const vInt = (n, min, max) => Number.isInteger(n) && n >= min && n <= max; /* 客户端 IP(反代后取 X-Forwarded-For 首段,仅用于限流) */ function clientIp(req) { const xf = req.headers["x-forwarded-for"]; if (xf) return String(xf).split(",")[0].trim().slice(0, 64); return (req.socket && req.socket.remoteAddress) || "unknown"; } /* 解析 Cookie 头 */ function parseCookies(req) { const out = {}; const raw = req.headers.cookie; if (!raw) return out; for (const part of String(raw).split(";")) { const i = part.indexOf("="); if (i < 0) continue; out[part.slice(0, i).trim()] = decodeURIComponent(part.slice(i + 1).trim()); } return out; } module.exports = { readJson, rateLimit, vUsername, vEmail, vPassword, vInt, clientIp, parseCookies };