diff options
Diffstat (limited to 'src/limiter.js')
-rw-r--r-- | src/limiter.js | 68 |
1 files changed, 68 insertions, 0 deletions
diff --git a/src/limiter.js b/src/limiter.js new file mode 100644 index 0000000..2760ad3 --- /dev/null +++ b/src/limiter.js @@ -0,0 +1,68 @@ +const limiters = new Map(); // Map<route, Map<ip, {Timeout}>> +const bad_actors = new Map(); // Map<ip, badness> + +const MAX_DANGER = 5; // ban after X bad things +const BAN_HOURS = 6; // ban X hours on max danger + +const setLimiter = (req, cooldown = 1e3) => { + const route = req.method + req.baseUrl + req.path; + let route_map = limiters.get(route); + if (route_map === undefined || route_map === null) { + route_map = limiters.set(route, new Map()).get(route); + } + + const active_timer = route_map.get(req.ip); + if (active_timer) { + clearTimeout(active_timer.timer); + } + + // if cooldown is above 5min, assume they did something bad + if (cooldown >= 3e5) { + const bad_level = (bad_actors.get(req.ip) || 0) + 1; + bad_actors.set(req.ip, bad_level); + + setTimeout(() => { + bad_actors.set(req.ip, (bad_actors.get(req.ip) || 1) - 1); + console.info(`Limiter: decreasing threat level of IP [${req.ip}]`); + }, BAN_HOURS * 3.6e6); // decrease danger level every X hours + + if (bad_level >= MAX_DANGER) { + req.app.locals.logger.warn(`Limiter: banning IP for ${BAN_HOURS} hours [${req.ip}]`); + } else { + console.warn(`Limiter: bad actor [${req.ip}]`); + } + } + + route_map.set(req.ip, { + timer: setTimeout(() => limiters.get(route).delete(req.ip), cooldown), + expires: Date.now() + cooldown, + }); +}; + +const checkRateLimiter = (req, res, next) => { + const route = req.method + req.path; + const route_map = limiters.get(route); + let timer; + if (route_map && (timer = route_map.get(req.ip))) { + const left = Math.ceil((timer.expires - Date.now()) / 1000); + res.append("Retry-After", left); + res.status(429).json({ + status: "error", + error: "too many requests", + retry: left, + }); + } else if ((bad_actors.get(req.ip) || 0) >= MAX_DANGER) { + // refuse to process request + res.status(418).json({ + status: "GTFO", + }); + } else { + next(); + } + return; +}; + +module.exports = { + cooldown: setLimiter, + check: checkRateLimiter, +}; |