This commit is contained in:
starified
2026-04-21 22:03:19 -04:00
parent 36e2d11f2e
commit 08bf320b57
4681 changed files with 566542 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
const crypto = require("crypto");
const { sendError } = require("../utils/http");
function normalizeIp(req) {
return String(req.socket?.remoteAddress || req.ip || "unknown").trim();
}
function findUserIdentifier(req) {
const candidates = [
req.params?.username,
req.params?.gamemode,
req.body?.username,
req.body?.ingame_name,
req.query?.q,
];
for (const candidate of candidates) {
if (candidate == null) {
continue;
}
const normalized = String(candidate).trim().toLowerCase();
if (normalized) {
return normalized.slice(0, 64);
}
}
return "anonymous";
}
function buildBucketKey(raw) {
return crypto.createHash("sha256").update(String(raw)).digest("hex");
}
function createRateLimiter({ name, windowMs, max, keyFn }) {
const buckets = new Map();
function cleanup(now) {
for (const [key, bucket] of buckets.entries()) {
if (bucket.resetAt <= now) {
buckets.delete(key);
}
}
}
return function rateLimitMiddleware(req, res, next) {
const now = Date.now();
const rawKey = keyFn(req);
const key = buildBucketKey(`${name}:${rawKey}`);
const current = buckets.get(key);
if (!current || current.resetAt <= now) {
buckets.set(key, { count: 1, resetAt: now + windowMs });
if (buckets.size > 5000) {
cleanup(now);
}
return next();
}
if (current.count >= max) {
const retryAfterSeconds = Math.max(Math.ceil((current.resetAt - now) / 1000), 1);
res.setHeader("Retry-After", String(retryAfterSeconds));
res.setHeader("X-RateLimit-Limit", String(max));
res.setHeader("X-RateLimit-Remaining", "0");
res.setHeader("X-RateLimit-Reset", String(Math.ceil(current.resetAt / 1000)));
return sendError(
res,
429,
"RATE_LIMITED",
"Too many requests. Please retry later.",
{
limiter: name,
retryAfterSeconds,
}
);
}
current.count += 1;
const remaining = Math.max(max - current.count, 0);
res.setHeader("X-RateLimit-Limit", String(max));
res.setHeader("X-RateLimit-Remaining", String(remaining));
res.setHeader("X-RateLimit-Reset", String(Math.ceil(current.resetAt / 1000)));
return next();
};
}
function createApiRateLimiters({ ipMax = 60, userMax = 60, windowMs = 60 * 1000 }) {
const byIp = createRateLimiter({
name: "ip",
windowMs,
max: ipMax,
keyFn: (req) => normalizeIp(req),
});
const byUser = createRateLimiter({
name: "user",
windowMs,
max: userMax,
keyFn: (req) => `${normalizeIp(req)}:${findUserIdentifier(req)}`,
});
return [byIp, byUser];
}
module.exports = {
createApiRateLimiters,
};