Skip to content

Commit 50247f5

Browse files
fix: rate limits
1 parent 9309993 commit 50247f5

File tree

4 files changed

+34
-58
lines changed

4 files changed

+34
-58
lines changed

Diff for: src/lib/http/middleware/ratelimit.ts

-40
This file was deleted.

Diff for: src/lib/ratelimiter.ts

+25-7
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,39 @@
11
import config from 'config';
2+
import type { Context } from 'koa';
23
import { RateLimiterRedis } from 'rate-limiter-flexible';
4+
import requestIp from 'request-ip';
5+
import type { RateLimiterRes } from 'rate-limiter-flexible';
36
import { createRedisClient } from './redis/client.js';
7+
import createHttpError from 'http-errors';
48

59
const redisClient = await createRedisClient({ legacyMode: true });
610

7-
export const defaultState = {
8-
remainingPoints: config.get<number>('measurement.rateLimit'),
9-
msBeforeNext: config.get<number>('measurement.rateLimitReset') * 1000,
10-
consumedPoints: 0,
11-
isFirstInDuration: true,
12-
};
13-
1411
const rateLimiter = new RateLimiterRedis({
1512
storeClient: redisClient,
1613
keyPrefix: 'rate',
1714
points: config.get<number>('measurement.rateLimit'),
1815
duration: config.get<number>('measurement.rateLimitReset'),
1916
});
2017

18+
const setRateLimitHeaders = (ctx: Context, result: RateLimiterRes) => {
19+
ctx.set('X-RateLimit-Reset', `${Math.round(result.msBeforeNext / 1000)}`);
20+
ctx.set('X-RateLimit-Limit', `${rateLimiter.points}`);
21+
ctx.set('X-RateLimit-Remaining', `${result.remainingPoints}`);
22+
};
23+
24+
export const checkRateLimits = async (ctx: Context, numberOfProbes: number) => {
25+
if (ctx['isAdmin']) {
26+
return;
27+
}
28+
29+
try {
30+
const clientIp = requestIp.getClientIp(ctx.req) ?? '';
31+
const result = await rateLimiter.consume(clientIp, numberOfProbes);
32+
setRateLimitHeaders(ctx, result);
33+
} catch (error) {
34+
setRateLimitHeaders(ctx, error as RateLimiterRes);
35+
throw createHttpError(429, 'Too Many Probes Requested', { type: 'too_many_probes' });
36+
}
37+
};
38+
2139
export default rateLimiter;

Diff for: src/measurement/route/create-measurement.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,15 @@ import config from 'config';
22
import type { Context } from 'koa';
33
import type Router from '@koa/router';
44
import { getMeasurementRunner } from '../runner.js';
5-
import type { MeasurementRequest } from '../types.js';
65
import { bodyParser } from '../../lib/http/middleware/body-parser.js';
76
import { validate } from '../../lib/http/middleware/validate.js';
87
import { schema } from '../schema/global-schema.js';
9-
import { rateLimitHandler } from '../../lib/http/middleware/ratelimit.js';
108

119
const hostConfig = config.get<string>('server.host');
1210
const runner = getMeasurementRunner();
1311

1412
const handle = async (ctx: Context): Promise<void> => {
15-
const request = ctx.request.body as MeasurementRequest;
16-
const { measurementId, probesCount } = await runner.run(request);
13+
const { measurementId, probesCount } = await runner.run(ctx);
1714

1815
ctx.status = 202;
1916
ctx.set('Location', `${hostConfig}/v1/measurements/${measurementId}`);
@@ -25,5 +22,5 @@ const handle = async (ctx: Context): Promise<void> => {
2522
};
2623

2724
export const registerCreateMeasurementRoute = (router: Router): void => {
28-
router.post('/measurements', '/measurements', bodyParser(), validate(schema), rateLimitHandler(), handle);
25+
router.post('/measurements', '/measurements', bodyParser(), validate(schema), handle);
2926
};

Diff for: src/measurement/runner.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Context } from 'koa';
12
import config from 'config';
23
import type { Server } from 'socket.io';
34
import createHttpError from 'http-errors';
@@ -9,11 +10,8 @@ import type { Probe } from '../probe/types.js';
910
import { getMetricsAgent, type MetricsAgent } from '../lib/metrics.js';
1011
import type { MeasurementStore } from './store.js';
1112
import { getMeasurementStore } from './store.js';
12-
import type {
13-
MeasurementRequest,
14-
MeasurementResultMessage,
15-
MeasurementProgressMessage,
16-
} from './types.js';
13+
import type { MeasurementRequest, MeasurementResultMessage, MeasurementProgressMessage } from './types.js';
14+
import { checkRateLimits } from '../lib/ratelimiter.js';
1715

1816
export class MeasurementRunner {
1917
constructor (
@@ -24,13 +22,16 @@ export class MeasurementRunner {
2422
private readonly metrics: MetricsAgent,
2523
) {}
2624

27-
async run (request: MeasurementRequest): Promise<{measurementId: string; probesCount: number;}> {
25+
async run (ctx: Context): Promise<{measurementId: string; probesCount: number;}> {
26+
const request = ctx.request.body as MeasurementRequest;
2827
const probes = await this.router.findMatchingProbes(request.locations, request.limit);
2928

3029
if (probes.length === 0) {
3130
throw createHttpError(422, 'No suitable probes found.', { type: 'no_probes_found' });
3231
}
3332

33+
await checkRateLimits(ctx, probes.filter(probe => probe.status !== 'offline').length);
34+
3435
const measurementId = await this.store.createMeasurement(request, probes);
3536

3637
this.sendToProbes(measurementId, probes, request);

0 commit comments

Comments
 (0)