Skip to content

Commit db28074

Browse files
Rework rate-limiter
1 parent bbace84 commit db28074

File tree

5 files changed

+277
-87
lines changed

5 files changed

+277
-87
lines changed

adminforth/modules/utils.ts

Lines changed: 33 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import fs from 'fs';
44
import Fuse from 'fuse.js';
55
import crypto from 'crypto';
66
import AdminForth, { AdminForthConfig } from '../index.js';
7+
import { RateLimiterMemory, RateLimiterAbstract } from "rate-limiter-flexible";
78
// @ts-ignore-next-line
89

910

@@ -381,90 +382,50 @@ export function md5hash(str:string) {
381382
}
382383

383384
export class RateLimiter {
384-
static counterData = {};
385-
386-
/**
387-
* Very dirty version of ratelimiter for demo purposes (should not be considered as production ready)
388-
* Will be used as RateLimiter.checkRateLimit('key', '5/24h', clientIp)
389-
* Stores counter in this class, in RAM, resets limits on app restart.
390-
* Also it creates setTimeout for every call, so is not optimal for high load.
391-
* @param key - key to store rate limit for
392-
* @param limit - limit in format '5/24h' - 5 requests per 24 hours
393-
* @param clientIp
394-
*/
395-
static checkRateLimit(key: string, limit: string, clientIp: string) {
396-
397-
if (!limit) {
398-
throw new Error('Rate limit is not set');
399-
}
385+
// constructor, accepts string like 10/10m, or 20/10s, or 30/1d
400386

401-
if (!key) {
402-
throw new Error('Rate limit key is not set');
403-
}
404387

405-
if (!clientIp) {
406-
throw new Error('Client IP is not set');
407-
}
388+
rateLimiter: RateLimiterAbstract;
408389

409-
if (!limit.includes('/')) {
410-
throw new Error('Rate limit should be in format count/period, like 5/24h');
411-
}
412390

413-
// parse limit
414-
const [count, period] = limit.split('/');
415-
const [preiodAmount, periodType] = /(\d+)(\w+)/.exec(period).slice(1);
416-
const preiodAmountNumber = parseInt(preiodAmount);
417-
418-
// get current time
419-
const whenClear = new Date();
420-
if (periodType === 'h') {
421-
whenClear.setHours(whenClear.getHours() + preiodAmountNumber);
422-
} else if (periodType === 'd') {
423-
whenClear.setDate(whenClear.getDate() + preiodAmountNumber);
424-
} else if (periodType === 'm') {
425-
whenClear.setMinutes(whenClear.getMinutes() + preiodAmountNumber);
426-
} else if (periodType === 'y') {
427-
whenClear.setFullYear(whenClear.getFullYear() + preiodAmountNumber);
428-
} else if (periodType === 's') {
429-
whenClear.setSeconds(whenClear.getSeconds() + preiodAmountNumber);
430-
} else {
431-
throw new Error(`Unsupported period type for rate limiting: ${periodType}`);
391+
durStringToSeconds(rate: string): number {
392+
if (!rate) {
393+
throw new Error('Rate duration is required');
432394
}
433395

434-
435-
// get current counter
436-
const counter = this.counterData[key] && this.counterData[key][clientIp] || 0;
437-
if (counter >= count) {
438-
return { error: true };
396+
397+
const period = rate.slice(-1);
398+
const duration = parseInt(rate.slice(0, -1));
399+
if (period === 's') {
400+
return duration;
401+
} else if (period === 'm') {
402+
return duration * 60;
403+
} else if (period === 'h') {
404+
return duration * 60 * 60;
405+
} else if (period === 'd') {
406+
return duration * 60 * 60 * 24;
439407
}
440-
RateLimiter.incrementCounter(key, clientIp);
441-
setTimeout(() => {
442-
RateLimiter.decrementCounter(key, clientIp);
443-
}, whenClear.getTime() - Date.now());
408+
throw new Error(`Invalid rate duration period: ${period}`);
409+
}
444410

445-
return { error: false };
446411

412+
constructor(rate: string) {
413+
const [points, duration] = rate.split('/');
414+
const durationSeconds = this.durStringToSeconds(duration);
415+
const opts = {
416+
points: parseInt(points),
417+
duration: durationSeconds, // Per second
418+
};
419+
this.rateLimiter = new RateLimiterMemory(opts);
447420
}
448421

449-
static incrementCounter(key: string, ip: string) {
450-
if (!RateLimiter.counterData[key]) {
451-
RateLimiter.counterData[key] = {};
452-
}
453-
if (!RateLimiter.counterData[key][ip]) {
454-
RateLimiter.counterData[key][ip] = 0;
455-
}
456-
RateLimiter.counterData[key][ip]++;
457-
}
458422

459-
static decrementCounter(key: string, ip: string) {
460-
if (!RateLimiter.counterData[key]) {
461-
RateLimiter.counterData[key] = {};
462-
}
463-
if (!RateLimiter.counterData[key][ip]) {
464-
RateLimiter.counterData[key][ip] = 0;
465-
}
466-
if (RateLimiter.counterData[key][ip] > 0) {
467-
RateLimiter.counterData[key][ip]--;
423+
async consume(key: string) {
424+
try {
425+
await this.rateLimiter.consume(key);
426+
return true;
427+
} catch (rejRes) {
428+
return false;
468429
}
469430
}
470431

0 commit comments

Comments
 (0)