Skip to content

Commit

Permalink
Merge pull request #24 from icapps/feature/ratelimiter
Browse files Browse the repository at this point in the history
Get ratelimiter instance from express-brute
  • Loading branch information
knor-el-snor committed Apr 23, 2018
2 parents ad64dbf + 09add97 commit ce2dcbd
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 33 deletions.
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,24 +66,32 @@ treehouse.setBodyParser(app, '*', {

- [All available body parser options](https://github.com/expressjs/body-parser)

### setRateLimiter(app, route, options)
### getRateLimiter(options)

Set a rate limiter to prevent brute force attacks. At the moment there is support for a built in-memorystore or Redis. Both use the `express-brute` module.
Get a rate limiter instance to prevent brute force attacks. This can be used as a middleware in Express.
At the moment there is support for a built in-memorystore or Redis. Both use the `express-brute` module.

```javascript
const app = express();

// In memory store (development purposes)
treehouse.setRateLimiter(app, '*', {
freeRetries: 10,
})
const globalBruteforce = treehouse.getRateLimiter({
freeRetries: 1000,
attachResetToRequest: false,
refreshTimeoutOnRequest: false,
minWait: 25*60*60*1000, // 1 day 1 hour (should never reach this wait time)
maxWait: 25*60*60*1000, // 1 day 1 hour (should never reach this wait time)
lifetime: 24*60*60, // 1 day (seconds not milliseconds)
});

app.use('/login', globalBruteforce.prevent, ...);

// Using existing Redis client
treehouse.setRateLimiter(app, '*', {
treehouse.getRateLimiter({
redis: {
client: existingClient, // All Redis options or 'client' to use an existing client (see redis-express-brute)
},
})
});
```

- [All available Express-brute options](https://github.com/AdamPflug/express-brute)
Expand Down
8 changes: 3 additions & 5 deletions src/lib/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,10 @@ export function setBodyParser(app: Application, route: string, options: BodyPars


/**
* Set a rate limiter on a specific route
* Get a rate limiter instance
* Current support for: built-in memory and Redis
*/

// TODO: Research whether trust proxy for Heroku is required
export function setRateLimiter(app: Application, route: string, options: RateLimiterOptions = {}): void {
export function getRateLimiter(options: RateLimiterOptions = {}): ExpressBrute {
let store: ExpressBrute.MemoryStore;
const allOptions = Object.assign({}, defaults.rateLimiterOptions, options);

Expand All @@ -47,7 +45,7 @@ export function setRateLimiter(app: Application, route: string, options: RateLim
}

const { redis, ...bruteOptions } = allOptions; // Filter out unneeded properties
app.use(route, new ExpressBrute(store, bruteOptions).prevent);
return new ExpressBrute(store, bruteOptions);
}


Expand Down
39 changes: 22 additions & 17 deletions tests/express.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as request from 'supertest';
import * as express from 'express';
const redisMock = require('redis-mock');
import { setLocalHeaders, setBasicSecurity, setBodyParser, setRateLimiter } from '../src';
import { setBasicSecurity, setBodyParser, getRateLimiter } from '../src';

describe('Express', () => {
describe('#setBasicSecurity', () => {
Expand All @@ -10,7 +10,7 @@ describe('Express', () => {
app = express();
});

test('app should have security headers', async () => {
it('app should have security headers', async () => {
setBasicSecurity(app, '*');
app.use('/', (req, res) => res.status(200).send('Welcome'));

Expand All @@ -33,23 +33,23 @@ describe('Express', () => {
app = express();
});

test('app should have content-type header', async () => {
it('app should have content-type header', async () => {
setBodyParser(app, '/');
app.use('/', (req, res) => res.status(200).send('Welcome'));

const { headers } = await request(app).get('/');
expect(headers).toHaveProperty('content-type');
});

test('app should have content-type header (raw)', async () => {
it('app should have content-type header (raw)', async () => {
setBodyParser(app, '/', { raw: { limit: 500 } });
app.use('/', (req, res) => res.status(200).send('Welcome'));

const { headers } = await request(app).get('/');
expect(headers).toHaveProperty('content-type');
});

test('app should have content-type header (json)', async () => {
it('app should have content-type header (json)', async () => {
setBodyParser(app, '/', { json: { limit: 500 } });
app.use('/', (req, res) => res.status(200).json({ name: 'Welcome' }));

Expand All @@ -58,15 +58,15 @@ describe('Express', () => {
});


test('app should have content-type header (urlEncoded)', async () => {
it('app should have content-type header (urlEncoded)', async () => {
setBodyParser(app, '/', { json: { limit: 500 } });
app.use('/', (req, res) => res.status(200).send(encodeURI('Welcome')));

const { headers } = await request(app).get('/');
expect(headers).toHaveProperty('content-type');
});

test('app should have content-type header (text)', async () => {
it('app should have content-type header (text)', async () => {
setBodyParser(app, '/', { text: { limit: 500 } });
app.use('/', (req, res) => res.status(200).send('Welcome'));

Expand All @@ -75,32 +75,37 @@ describe('Express', () => {
});
});

describe('#setRateLimiter', () => {
describe('#getRateLimiter', () => {
let app;

beforeEach(() => {
app = express();
});

test('set default rateLimiter', async () => {
setRateLimiter(app, '/');
app.use('/', (req, res) => res.status(200).send('Welcome'));
it('set default rateLimiter', async () => {
const bruteForce = getRateLimiter();
app.use('/', bruteForce.prevent, (req, res) => res.status(200).send('Welcome'));
});
test('rateLimiter should return 429 on too many tries', async () => {
setRateLimiter(app, '/', { minWait: 5000, freeRetries: 1 });
app.use('/', (req, res) => res.status(200).send('Welcome'));

it('rateLimiter should return 429 on too many tries', async () => {
const bruteForce = getRateLimiter({ minWait: 5000, freeRetries: 1 });
app.use('/', bruteForce.prevent, (req, res) => res.status(200).send('Welcome'));

const { status } = await request(app).get('/');
expect(status).toEqual(200);

const { status: status2 } = await request(app).get('/');
expect(status2).toEqual(200);

const { status: status3 } = await request(app).get('/');
expect(status3).toEqual(429);
});
test('rateLimiter with custom redisStore', async () => {

it('rateLimiter with custom redisStore', async () => {
const redisClient = redisMock.createClient();

setRateLimiter(app, '/', { redis: { client: redisClient } });
app.use('/', (req, res) => res.status(200).send('Welcome'));
const bruteForce = getRateLimiter({ redis: { client: redisClient } });
app.use('/', bruteForce.prevent, (req, res) => res.status(200).send('Welcome'));

const { status } = await request(app).get('/');
expect(status).toEqual(200);
Expand Down
4 changes: 2 additions & 2 deletions tests/responder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('Responder', () => {
app.get('/hello', responder.handleAsyncFn(fn));
});

test('should catch an error', async () => {
it('should catch an error', async () => {
const { status, body } = await request(app).get('/hello');
expect(status).toEqual(500);
expect(body).toEqual('Something went wrong! 💩');
Expand All @@ -36,7 +36,7 @@ describe('Responder', () => {
app.get('/helloAsync', responder.handleAsyncFn(fn));
});

test('should catch an async error', async () => {
it('should catch an async error', async () => {
const { status, body, error } = await request(app).get('/helloAsync');
expect(status).toEqual(500);
expect(body).toEqual('Something went wrong! 💩💩');
Expand Down
4 changes: 2 additions & 2 deletions tests/swagger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ const app = express();

describe('Swagger', () => {
describe('#setSwagger', () => {
test('successfully open swagger', async () => {
it('successfully open swagger', async () => {
setSwagger(app, '/documentation', './tests/assets/docs.yml');
const { status } = await request(app).get('/documentation');
expect(status).toEqual(301);
});

test('throws error on invalid filepath', async () => {
it('throws error on invalid filepath', async () => {
expect.assertions(2);
try {
setSwagger(app, '/documentation', '../random/docs.yml');
Expand Down

0 comments on commit ce2dcbd

Please sign in to comment.