summaryrefslogtreecommitdiff
path: root/src/limiter.js
blob: 9239fb739ed4f9f9612a1ebfb025bcefca43c9aa (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
69
70
71
72
73
74
75
76
77
78
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;

        if (bad_level > MAX_DANGER) {
            console.warn(`Limiter: bad actor above max danger level [${req.ip}]`);
        } else {
            bad_actors.set(req.ip, bad_level);
            setTimeout(() => {
                const current_level = bad_actors.get(req.ip) || 1;
                bad_actors.set(req.ip, current_level - 1);

                if (current_level === MAX_DANGER) {
                    req.app.locals.logger.info(`Limiter: unbanning IP (ban expired) [${req.ip}]`);
                } else {
                    console.info(`Limiter: decreasing threat level of IP (was level ${current_level}) [${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 (threat level ${bad_level}) [${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,
};