Skip to content
This repository has been archived by the owner on Apr 19, 2023. It is now read-only.

Commit

Permalink
🔀 Merge #1497 (Add custom rate limiter)
Browse files Browse the repository at this point in the history
  • Loading branch information
FindingAnand authored Nov 17, 2020
2 parents d98b665 + 3905574 commit 047d9b7
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 206 deletions.
19 changes: 19 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ Alternately, if you want to use AWS SES, you should set these instead (note that

To generate an access/secret key pair, you can create an IAM user with the permission `AmazonSESFullAccess`. For more details, read the article [Creating an IAM user in your AWS account](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html#id_users_create_console) on the AWS website.

### Rate limiting

Staart API has three types of rate limits. When an endpoint is accessed, 1 point is consumed. There are also some endpoints that consume additional points (like logging in or creating an account consumes 10 points). The types of rate limits are:

1. "Public" for unauthenticated requests (250 points/hour)
2. "Authenticated" for requests with a user access token (5k points/hour)
3. "API key" for (automated) requests using an API key (10k points/hour)

You can set the rate limits for each of these categories. By default, the rate limit resets after one hour:

| Environment variable | Description | Default |
| ----------------------------------- | -------------------------------- | ------- |
| `RATE_LIMIT_PUBLIC_POINTS` | Maximum points for public | 250 |
| `RATE_LIMIT_PUBLIC_DURATION` | Reset duration for public | 3600 |
| `RATE_LIMIT_AUTHENTICATED_POINTS` | Maximum points for authenticated | 5000 |
| `RATE_LIMIT_AUTHENTICATED_DURATION` | Reset duration for authenticated | 3600 |
| `RATE_LIMIT_API_KEY_POINTS` | Maximum points for API key | 10000 |
| `RATE_LIMIT_API_KEY_DURATION` | Reset duration for API key | 3600 |

## Optional services

### ElasticSearch
Expand Down
141 changes: 5 additions & 136 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
"maxmind": "^4.3.1",
"mem": "^8.0.0",
"minimatch": "^3.0.4",
"nestjs-rate-limiter": "^2.5.6",
"nodemailer": "^6.4.15",
"normalize-email": "^1.1.1",
"object-literal-parse": "^2.1.0",
Expand All @@ -68,6 +67,7 @@
"qrcode": "^1.4.4",
"quick-lru": "^5.1.1",
"randomcolor": "^0.6.2",
"rate-limiter-flexible": "^2.1.13",
"reflect-metadata": "^0.1.13",
"request-ip": "^2.1.3",
"response-time": "^2.3.2",
Expand Down
8 changes: 2 additions & 6 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {
import { ConfigModule } from '@nestjs/config';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { ScheduleModule } from '@nestjs/schedule';
import { RateLimiterInterceptor, RateLimiterModule } from 'nestjs-rate-limiter';
import configuration from './config/configuration';
import { AuditLogger } from './interceptors/audit-log.interceptor';
import { RateLimitInterceptor } from './interceptors/rate-limit.interceptor';
import { ApiLoggerMiddleware } from './middleware/api-logger.middleware';
import { JsonBodyMiddleware } from './middleware/json-body.middleware';
import { RawBodyMiddleware } from './middleware/raw-body.middleware';
Expand Down Expand Up @@ -53,10 +53,6 @@ import { TasksModule } from './providers/tasks/tasks.module';
TasksModule,
UsersModule,
AuthModule,
RateLimiterModule.register({
points: 100,
duration: 60,
}),
MailModule,
SessionsModule,
EmailsModule,
Expand Down Expand Up @@ -84,7 +80,7 @@ import { TasksModule } from './providers/tasks/tasks.module';
providers: [
{
provide: APP_INTERCEPTOR,
useClass: RateLimiterInterceptor,
useClass: RateLimitInterceptor,
},
{
provide: APP_GUARD,
Expand Down
6 changes: 6 additions & 0 deletions src/config/configuration.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export interface Configuration {
apiKeyLruSize: number;
};

rateLimit: {
public: { points: number; duration: number };
authenticated: { points: number; duration: number };
apiKey: { points: number; duration: number };
};

security: {
saltRounds: number;
jwtSecret: string;
Expand Down
14 changes: 14 additions & 0 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ const configuration: Configuration = {
domainVerificationFile:
process.env.DOMAIN_VERIFICATION_FILE ?? 'staart-verify.txt',
},
rateLimit: {
public: {
points: int(process.env.RATE_LIMIT_PUBLIC_POINTS, 250),
duration: int(process.env.RATE_LIMIT_PUBLIC_DURATION, 3600),
},
authenticated: {
points: int(process.env.RATE_LIMIT_AUTHENTICATED_POINTS, 5000),
duration: int(process.env.RATE_LIMIT_AUTHENTICATED_DURATION, 3600),
},
apiKey: {
points: int(process.env.RATE_LIMIT_API_KEY_POINTS, 10000),
duration: int(process.env.RATE_LIMIT_API_KEY_DURATION, 3600),
},
},
caching: {
geolocationLruSize: int(process.env.GEOLOCATION_LRU_SIZE, 100),
apiKeyLruSize: int(process.env.API_KEY_LRU_SIZE, 100),
Expand Down
2 changes: 2 additions & 0 deletions src/errors/errors.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ export const BILLING_ACCOUNT_CREATED_CONFLICT =
export const MFA_ENABLED_CONFLICT =
'409004: Multi-factor authentication is already enabled';
export const MERGE_USER_CONFLICT = '409005: Cannot merge the same user';

export const RATE_LIMIT_EXCEEDED = '429000: Rate limit exceeded';
72 changes: 72 additions & 0 deletions src/interceptors/rate-limit.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
CallHandler,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { RateLimiterMemory } from 'rate-limiter-flexible';
import { getClientIp } from 'request-ip';
import { Observable } from 'rxjs';
import { Configuration } from '../config/configuration.interface';
import { RATE_LIMIT_EXCEEDED } from '../errors/errors.constants';
import { UserRequest } from '../modules/auth/auth.interface';

@Injectable()
export class RateLimitInterceptor implements NestInterceptor {
private rateLimiterPublic = new RateLimiterMemory(
this.configService.get<Configuration['rateLimit']['public']>(
'rateLimit.public',
),
);
private rateLimiterAuthenticated = new RateLimiterMemory(
this.configService.get<Configuration['rateLimit']['authenticated']>(
'rateLimit.authenticated',
),
);
private rateLimiterApiKey = new RateLimiterMemory(
this.configService.get<Configuration['rateLimit']['apiKey']>(
'rateLimit.apiKey',
),
);

constructor(
private readonly reflector: Reflector,
private configService: ConfigService,
) {}

async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const points =
this.reflector.get<number>('rateLimit', context.getHandler()) ?? 1;
const request = context.switchToHttp().getRequest() as UserRequest;
const response = context.switchToHttp().getResponse();
let limiter = this.rateLimiterPublic;
if (request.user.type === 'api-key') limiter = this.rateLimiterApiKey;
else if (request.user.type === 'user')
limiter = this.rateLimiterAuthenticated;
try {
const ip = getClientIp(request);
const result = await limiter.consume(ip.replace(/^.*:/, ''), points);
response.header('Retry-After', Math.ceil(result.msBeforeNext / 1000));
response.header('X-RateLimit-Limit', points);
response.header('X-Retry-Remaining', result.remainingPoints);
response.header(
'X-Retry-Reset',
new Date(Date.now() + result.msBeforeNext).toUTCString(),
);
} catch (result) {
response.header('Retry-After', Math.ceil(result.msBeforeNext / 1000));
throw new HttpException(
RATE_LIMIT_EXCEEDED,
HttpStatus.TOO_MANY_REQUESTS,
);
}
return next.handle();
}
}
Loading

0 comments on commit 047d9b7

Please sign in to comment.