Skip to content

Commit

Permalink
feat: Implement ignoreRoutes functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
kkoomen authored and jmcdo29 committed May 31, 2020
1 parent 328c0a3 commit 7b8ab42
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 15 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
lib
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@nestjs/core": "^7.0.0",
"@nestjs/platform-express": "^7.0.0",
"md5": "^2.2.1",
"path-to-regexp": "^6.1.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^6.5.4"
Expand Down
6 changes: 1 addition & 5 deletions src/throttler.exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ import { HttpException } from '@nestjs/common';

export class ThrottlerException extends HttpException {
constructor() {
const statusCode = 429;
super({
status: statusCode,
error: 'Too Many Requests',
}, statusCode);
super('ThrottlerException: Too Many Requests', 429);
}
}
39 changes: 36 additions & 3 deletions src/throttler.guard.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { CanActivate, ExecutionContext, Inject, Injectable, RequestMethod } from '@nestjs/common';
import { RouteInfo } from '@nestjs/common/interfaces/middleware';
import { Reflector } from '@nestjs/core';
import * as md5 from 'md5';
import { THROTTLER_LIMIT, THROTTLER_TTL } from './throttler.constants';
import { pathToRegexp } from 'path-to-regexp';
import { THROTTLER_LIMIT, THROTTLER_OPTIONS, THROTTLER_TTL } from './throttler.constants';
import { ThrottlerException } from './throttler.exception';
import { ThrottlerOptions } from './throttler.interface';
import { ThrottlerStorageService } from './throttler.service';

type RouteInfoRegex = RouteInfo & { regex: RegExp };

@Injectable()
export class ThrottlerGuard implements CanActivate {
constructor(
@Inject(THROTTLER_OPTIONS) private readonly options: ThrottlerOptions,
private readonly reflector: Reflector,
private readonly storageService: ThrottlerStorageService,
) {}
Expand All @@ -17,16 +23,30 @@ export class ThrottlerGuard implements CanActivate {
const handler = context.getHandler();
const headerPrefix = 'X-RateLimit';

// Return early when we have no limit or ttl data.
const limit = this.reflector.get<number>(THROTTLER_LIMIT, handler);
const ttl = this.reflector.get<number>(THROTTLER_TTL, handler);
if (typeof limit === 'undefined' || typeof ttl === 'undefined') {
return true;
}

// Return early if the current route is ignored.
const req = context.switchToHttp().getRequest();
const routes = this.normalizeRoutes(this.options.ignoreRoutes);
for (const route of routes) {
const currentRoutePath = req.url.replace(/^\/+/, '');
const currentRouteMethod = this.reflector.get<RequestMethod>('method', handler);

const ignored = (
route.path === currentRoutePath &&
[RequestMethod.ALL, currentRouteMethod].indexOf(route.method) !== -1
) || route.regex.exec(currentRoutePath);

if (ignored) return true;
}

const res = context.switchToHttp().getResponse();
const key = md5(`${req.ip}-${context.getClass().name}-${handler.name}`)

const record = this.storageService.getRecord(key);
const nearestExpiryTime = record.length > 0
? Math.ceil((record[0].getTime() - new Date().getTime()) / 1000)
Expand All @@ -45,4 +65,17 @@ export class ThrottlerGuard implements CanActivate {
this.storageService.addRecord(key, ttl);
return true;
}

normalizeRoutes(routes: Array<string | RouteInfo>): RouteInfoRegex[] {
if (!Array.isArray(routes)) return [];

return routes.map((routeObj: string | RouteInfo): RouteInfoRegex => {
const route = typeof routeObj === 'string' ? ({
path: routeObj,
method: RequestMethod.ALL,
}) : routeObj;

return { ...route, regex: pathToRegexp(route.path) };
});
}
}
4 changes: 3 additions & 1 deletion src/throttler.interface.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { RouteInfo } from '@nestjs/common/interfaces/middleware';
import { ThrottlerStorage } from './throttler-storage.interface';
import { Type } from './type';


export interface ThrottlerOptions {
ignoreRoutes?: string[];
ignoreRoutes?: Array<string | RouteInfo>;
limit?: number;
ttl?: number;
storage?: Type<ThrottlerStorage>
Expand Down
2 changes: 1 addition & 1 deletion src/throttler.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AsyncModuleConfig } from '@golevelup/nestjs-modules';
import { Global, Module } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { ThrottlerCoreModule } from './throttler-core.module';
import { ThrottlerOptions } from './throttler.interface';

Expand Down
36 changes: 33 additions & 3 deletions test/app/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,41 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Throttle, ThrottlerGuard } from '../../src';
import { All, Controller, Get, Post } from '@nestjs/common';
import { Throttle } from '../../src';

@Controller()
export class AppController {
@Throttle(2, 10)
@Get()
@Throttle(2, 10)
async test() {
return 'test';
}

// Route that are defined in the `ignoreRoutes` property.

// ignoreRoutes: ['ignored']
@Get('ignored')
@Throttle(2, 10)
async ignored() {
return 'ignored';
}

// ignoreRoutes: [{ path: 'ignored-2', method: RequestMethod.POST }]
@Post('ignored-2')
@Throttle(2, 10)
async ignored2() {
return 'ignored';
}

// ignoreRoutes: [{ path: 'ignored-3', method: RequestMethod.ALL }]
@All('ignored-3')
@Throttle(2, 10)
async ignored3() {
return 'ignored';
}

// ignoreRoutes: [{ path: 'ignored/:foo', method: RequestMethod.GET }]
@Get('ignored/:foo')
@Throttle(2, 10)
async ignored4() {
return 'ignored';
}
}
11 changes: 9 additions & 2 deletions test/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { Module } from '@nestjs/common';
import { Module, RequestMethod } from '@nestjs/common';
import { ThrottlerModule } from '../../src';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
imports: [ThrottlerModule.forRoot()],
imports: [ThrottlerModule.forRoot({
ignoreRoutes: [
'ignored',
{ path: 'ignored-2', method: RequestMethod.POST },
{ path: 'ignored-3', method: RequestMethod.ALL },
{ path: 'ignored/:foo', method: RequestMethod.GET },
],
})],
controllers: [AppController],
providers: [AppService],
})
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5413,6 +5413,11 @@ path-to-regexp@3.2.0:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f"
integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==

path-to-regexp@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.1.0.tgz#0b18f88b7a0ce0bfae6a25990c909ab86f512427"
integrity sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==

path-type@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
Expand Down

0 comments on commit 7b8ab42

Please sign in to comment.