-
Notifications
You must be signed in to change notification settings - Fork 13
/
index.ts
103 lines (84 loc) · 3.57 KB
/
index.ts
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import type { MiddlewareHandler } from 'hono';
import { type IRateLimiterPostgresOptions, RateLimiterPostgres, RateLimiterRes } from 'rate-limiter-flexible';
import { errorResponse } from '../../lib/errors';
import { queryClient } from '../../db/db';
import type { Env } from '../../types/common';
type RateLimiterMode = 'success' | 'fail' | 'limit';
/*
* This file contains the implementation of a rate limiter middleware.
* It uses the `rate-limiter-flexible` library to limit the number of requests per user or IP address.
* https://github.com/animir/node-rate-limiter-flexible#readme
* The rate limiter is implemented as a class `RateLimiter` that extends `RateLimiterPostgres`.
*
* The 'success' mode decreases the available points for the user or IP address on successful requests.
* The 'fail' (default mode does the same but for failed requests.
* The 'limit' mode consumes points for each request without blocking.
*
* Additionally, there is a separate rate limiter for sign-in requests that limits the number of failed attempts per IP address and username.
*/
const getUsernameIPkey = (username?: string, ip?: string) => `${username}_${ip}`;
class RateLimiter extends RateLimiterPostgres {
public middleware(mode: RateLimiterMode = 'fail'): MiddlewareHandler<Env> {
if (mode === 'success' || mode === 'fail') {
this.points = this.points - 1;
}
return async (ctx, next) => {
const ipAddr = ctx.req.header('x-forwarded-for');
// biome-ignore lint/suspicious/noExplicitAny: it's required to use `any` here
const body = ctx.req.header('content-type') === 'application/json' ? ((await ctx.req.raw.clone().json()) as any) : undefined;
const user = ctx.get('user');
const username = body?.email || user?.id;
if (!ipAddr && !username) {
return next();
}
const usernameIPkey = getUsernameIPkey(username, ipAddr);
const res = await this.get(usernameIPkey);
let retrySecs = 0;
// Check if IP or Username + IP is already blocked
if (res !== null && res.consumedPoints > this.points) {
retrySecs = Math.round(res.msBeforeNext / 1000) || 1;
}
if (retrySecs > 0) {
ctx.header('Retry-After', String(retrySecs));
return errorResponse(ctx, 429, 'too_many_requests', 'warn', undefined, { usernameIPkey });
}
if (mode === 'limit') {
try {
await this.consume(usernameIPkey);
} catch (rlRejected) {
if (rlRejected instanceof RateLimiterRes) {
ctx.header('Retry-After', String(Math.round(rlRejected.msBeforeNext / 1000) || 1));
return errorResponse(ctx, 429, 'too_many_requests', 'warn', undefined, { usernameIPkey });
}
throw rlRejected;
}
}
await next();
if (ctx.res.status === 200) {
if (mode === 'success') {
try {
await this.consume(usernameIPkey);
} catch {}
} else if (mode === 'fail') {
await this.delete(usernameIPkey);
}
} else if (mode === 'fail') {
try {
await this.consume(usernameIPkey);
} catch {}
}
};
}
}
// Default options to limit fail requests ('fail' mode)
const defaultOptions = {
points: 5, // 5 requests
duration: 60 * 60, // within 1 hour
blockDuration: 60 * 10, // Block for 10 minutes
};
export const rateLimiter = (options: Omit<IRateLimiterPostgresOptions, 'storeClient'> = defaultOptions, mode: RateLimiterMode = 'fail') =>
new RateLimiter({
...options,
tableName: 'rate_limits',
storeClient: queryClient,
}).middleware(mode);