summaryrefslogtreecommitdiff
path: root/src/limiter.js
blob: 2760ad3e06fa3af081f88fdd54e8cfcf466f4894 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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,
};