uploaded
This commit is contained in:
111
server/middleware/rate-limit.js
Normal file
111
server/middleware/rate-limit.js
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user