summaryrefslogtreecommitdiff
path: root/src/limiter.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/limiter.js')
-rw-r--r--src/limiter.js68
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,
+};