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, };