48 lines
1.4 KiB
JavaScript
48 lines
1.4 KiB
JavaScript
/*
|
|
* 极简内存限流 · 给 vault Express 用
|
|
* 守护: 铸渊 · ICE-GL-ZY001
|
|
*
|
|
* 设计:
|
|
* - 滑动窗口 (固定 60 秒桶)
|
|
* - per-IP 计数, 超阈值返 429 + Retry-After
|
|
* - 内存占用小 (单 Map · key=ip · 每分钟自清)
|
|
*
|
|
* 注: nginx 在前层已经 allow 127.0.0.1; deny all + basic-auth, 这里是
|
|
* 纵深防御 (CodeQL js/missing-rate-limiting).
|
|
*/
|
|
"use strict";
|
|
|
|
function createRateLimit({ windowMs = 60_000, max = 120, message } = {}) {
|
|
const buckets = new Map(); // ip → { count, resetAt }
|
|
const msg = message || "请求太频繁, 请 1 分钟后再试.";
|
|
|
|
return function rateLimit(req, res, next) {
|
|
const ip = req.ip || (req.socket && req.socket.remoteAddress) || "unknown";
|
|
const now = Date.now();
|
|
let b = buckets.get(ip);
|
|
if (!b || b.resetAt < now) {
|
|
b = { count: 0, resetAt: now + windowMs };
|
|
buckets.set(ip, b);
|
|
}
|
|
b.count++;
|
|
if (b.count > max) {
|
|
const retry = Math.max(1, Math.ceil((b.resetAt - now) / 1000));
|
|
res.set("Retry-After", String(retry));
|
|
return res.status(429).json({
|
|
error: true,
|
|
code: "rate_limited",
|
|
message: msg + " (Retry-After=" + retry + "s)"
|
|
});
|
|
}
|
|
// 偶尔扫垃圾 (轻量, 不阻塞主路径)
|
|
if (buckets.size > 1024) {
|
|
for (const [k, v] of buckets) {
|
|
if (v.resetAt < now) buckets.delete(k);
|
|
}
|
|
}
|
|
next();
|
|
};
|
|
}
|
|
|
|
module.exports = { createRateLimit };
|