Skip to content

Commit

Permalink
feat: add support for ignoreUserAgents option
Browse files Browse the repository at this point in the history
  • Loading branch information
kkoomen authored and jmcdo29 committed Jun 9, 2020
1 parent 4ffefd2 commit 1ab5e17
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 13 deletions.
53 changes: 48 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ For an overview of the community storage providers, see [Community Storage Provi

This package comes with a couple of goodies that should be mentioned, first is the `ThrottlerModule`.

# Table of Contents

- [NestJS Throttler Package](#nestjs-throttler-package)
- [Table of Contents](#table-of-contents)
- [Usage](#usage)
- [ThrottlerModule](#throttlermodule)
- [Decorators](#decorators)
- [@Throttle()](#throttle)
- [@SkipThrottle()](#skipthrottle)
- [Ignoring specific user agents](#ignoring-specific-user-agents)
- [ThrottlerStorage](#throttlerstorage)
- [Community Storage Providers](#community-storage-providers)

# Usage

## ThrottlerModule

The `ThrottleModule` is the main entry point for this package, and can be used
Expand Down Expand Up @@ -88,8 +103,6 @@ export class AppController {
}
```



## Decorators

### @Throttle()
Expand All @@ -108,7 +121,8 @@ and routes.
@SkipThrottle(skip = true)
```

This decorator can be used to skip a route or a class **or** to negate the skipping of a route in a class that is skipped.
This decorator can be used to skip a route or a class **or** to negate the
skipping of a route in a class that is skipped.

```ts
@SkipThrottle()
Expand All @@ -121,13 +135,42 @@ export class AppController {
}
```

In the above controller, `dontSkip` would be counted against and rate-limited while `doSkip` would not be limited in any way.
In the above controller, `dontSkip` would be counted against and rate-limited
while `doSkip` would not be limited in any way.

## Ignoring specific user agents

You can use the `ignoreUserAgents` key to ignore specific user agents.

```ts
@Module({
imports: [
ThrottlerModule.forRoot({
ttl: 60,
limit: 10,
ignoreUserAgents: [
// Don't throttle request that have 'googlebot' defined in them.
// Example user agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
/googlebot/gi,

// Don't throttle request that have 'bingbot' defined in them.
// Example user agent: Mozilla/5.0 (compatible; Bingbot/2.0; +http://www.bing.com/bingbot.htm)
new RegExp('Bingbot', 'gi'),
],
}),
],
})
export class AppModule {}
```

## ThrottlerStorage

Interface to define the methods to handle the details when it comes to keeping track of the requests.

Currently the key is seen as an `MD5` hash of the `IP` the `ClassName` and the `MethodName`, to ensure that no unsafe characters are used and to ensure that the package works for contexts that don't have explicit routes (like Websockets and GraphQL).
Currently the key is seen as an `MD5` hash of the `IP` the `ClassName` and the
`MethodName`, to ensure that no unsafe characters are used and to ensure that
the package works for contexts that don't have explicit routes (like Websockets
and GraphQL).

The interface looks like this:

Expand Down
11 changes: 10 additions & 1 deletion src/throttler.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class ThrottlerGuard implements CanActivate {
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const handler = context.getHandler();
const classRef = context.getClass();
const headerPrefix = 'X-RateLimit';
Expand All @@ -29,6 +30,15 @@ export class ThrottlerGuard implements CanActivate {
return true;
}

// Return early if the current route should be ignored.
if (Array.isArray(this.options.ignoreUserAgents)) {
for (const pattern of this.options.ignoreUserAgents) {
if (pattern.test(req.headers['user-agent'])) {
return true;
}
}
}

// Return early when we have no limit or ttl data.
const routeOrClassLimit = this.reflector.getAllAndOverride<number>(THROTTLER_LIMIT, [
handler,
Expand All @@ -44,7 +54,6 @@ export class ThrottlerGuard implements CanActivate {
const ttl = routeOrClassTtl || this.options.ttl;

// Here we start to check the amount of requests being done against the ttl.
const req = context.switchToHttp().getRequest();
const res = context.switchToHttp().getResponse();
const key = md5(`${req.ip}-${classRef.name}-${handler.name}`);
const ttls = await this.storageService.getRecord(key);
Expand Down
1 change: 1 addition & 0 deletions src/throttler.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { ThrottlerStorage } from './throttler-storage.interface';
export interface ThrottlerOptions {
limit: number;
ttl: number;
ignoreUserAgents?: RegExp[];
storage?: ThrottlerStorage;
}
6 changes: 3 additions & 3 deletions test/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerGuard } from '../../src';
import { ControllerModule } from './controllers/controller.module';
import { GatewayModule } from './gateways/gateway.module';
import { ResolverModule } from './resolvers/resolver.module';
// import { GatewayModule } from './gateways/gateway.module';
// import { ResolverModule } from './resolvers/resolver.module';

@Module({
imports: [ControllerModule, GatewayModule, ResolverModule],
imports: [ControllerModule /* , GatewayModule, ResolverModule */],
providers: [
{
provide: APP_GUARD,
Expand Down
5 changes: 5 additions & 0 deletions test/app/controllers/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ export class AppController {
async ignored() {
return this.appService.ignored();
}

@Get('ignore-user-agents')
async ignoreUserAgents() {
return this.appService.ignored();
}
}
1 change: 1 addition & 0 deletions test/app/controllers/controller.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { LimitController } from './limit.controller';
ThrottlerModule.forRoot({
limit: 5,
ttl: 60,
ignoreUserAgents: [/throttler-test/g],
}),
],
controllers: [AppController, DefaultController, LimitController],
Expand Down
9 changes: 6 additions & 3 deletions test/app/main.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter } from '@nestjs/platform-fastify';
// import { FastifyAdapter } from '@nestjs/platform-fastify';
import { ExpressAdapter } from '@nestjs/platform-express';
// import { WsAdapter } from '@nestjs/platform-ws';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(
AppModule,
// new ExpressAdapter(),
new FastifyAdapter(),
new ExpressAdapter(),
// new FastifyAdapter(),
);
// app.useWebSocketAdapter(new WsAdapter(app))
await app.listen(3000);
}
bootstrap();
13 changes: 12 additions & 1 deletion test/controller.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { AbstractHttpAdapter, APP_GUARD } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { FastifyAdapter } from '@nestjs/platform-fastify';
import { Test, TestingModule } from '@nestjs/testing';
import { ThrottlerGuard } from '../src';
import { ControllerModule } from './app/controllers/controller.module';
import { httPromise } from './utility/httpromise';
import { ThrottlerGuard } from '../src';

describe.each`
adapter | adapterName
Expand Down Expand Up @@ -52,6 +52,17 @@ describe.each`
'x-ratelimit-reset': /\d+/,
});
});
it('GET /ignore-user-agents', async () => {
const response = await httPromise(appUrl + '/ignore-user-agents', 'GET', {
'user-agent': 'throttler-test/0.0.0',
});
expect(response.data).toEqual({ ignored: true });
expect(response.headers).not.toMatchObject({
'x-ratelimit-limit': '2',
'x-ratelimit-remaining': '1',
'x-ratelimit-reset': /\d+/,
});
});
it('GET /', async () => {
const response = await httPromise(appUrl + '/');
expect(response.data).toEqual({ success: true });
Expand Down
6 changes: 6 additions & 0 deletions test/utility/httpromise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type HttpMethods = 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT';
export function httPromise(
url: string,
method: HttpMethods = 'GET',
headers: Record<string, any> = {},
body?: Record<string, any>,
): Promise<{ data: any; headers: Record<string, any>; status: number }> {
return new Promise((resolve, reject) => {
Expand All @@ -30,6 +31,11 @@ export function httPromise(
});
});
req.method = method;

Object.keys(headers).forEach((key) => {
req.setHeader(key, headers[key]);
});

switch (method) {
case 'GET':
break;
Expand Down

0 comments on commit 1ab5e17

Please sign in to comment.