Skip to content
This repository was archived by the owner on Oct 11, 2022. It is now read-only.

Commit a1e941b

Browse files
authored
Merge pull request #4647 from withspectrum/node-rate-limits
Add request-level rate limiting
2 parents 8b55d30 + 6830083 commit a1e941b

File tree

8 files changed

+132
-1
lines changed

8 files changed

+132
-1
lines changed

api/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { init as initPassport } from './authentication.js';
1717
import apolloServer from './apollo-server';
1818
import { corsOptions } from 'shared/middlewares/cors';
1919
import errorHandler from 'shared/middlewares/error-handler';
20+
import rateLimiter from 'shared/middlewares/rate-limiter';
2021
import middlewares from './routes/middlewares';
2122
import authRoutes from './routes/auth';
2223
import apiRoutes from './routes/api';
@@ -43,6 +44,13 @@ app.use(statsd);
4344
// Trust the now proxy
4445
app.set('trust proxy', true);
4546
app.use(toobusy);
47+
// Allow bursts of up to 40 req for initial page loads, but block more than 40 / 10s
48+
app.use(
49+
rateLimiter({
50+
max: 40,
51+
duration: '10s',
52+
})
53+
);
4654

4755
// Security middleware.
4856
addSecurityMiddleware(app, { enableNonce: false, enableCSP: false });

api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"pre-commit": "^1.2.2",
9393
"prismjs": "^1.15.0",
9494
"query-string": "5.1.1",
95+
"ratelimiter": "^3.2.0",
9596
"raven": "^2.6.4",
9697
"react": "^15.4.1",
9798
"react-app-rewire-styled-components": "^3.0.2",

api/yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7791,6 +7791,11 @@ range-parser@~1.2.0:
77917791
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
77927792
integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=
77937793

7794+
ratelimiter@^3.2.0:
7795+
version "3.2.0"
7796+
resolved "https://registry.yarnpkg.com/ratelimiter/-/ratelimiter-3.2.0.tgz#ae74cf9629daae4cc8900ec126ab28d3794070f1"
7797+
integrity sha512-zMc9X4FNmOk3RBxV95lvp13sZRtf43UJJN1FficbYiusBB09zB6gcJtK2X18dKmH+Gq0C7W6qNCHE+UJx0YwVg==
7798+
77947799
raven@^2.6.4:
77957800
version "2.6.4"
77967801
resolved "https://registry.yarnpkg.com/raven/-/raven-2.6.4.tgz#458d4a380c8fbb59e0150c655625aaf60c167ea3"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// flow-typed signature: cfa8ede682b6042c752a254287cdd11e
2+
// flow-typed version: <<STUB>>/ratelimiter_vx.x.x/flow_v0.66.0
3+
4+
/**
5+
* This is an autogenerated libdef stub for:
6+
*
7+
* 'ratelimiter'
8+
*
9+
* Fill this stub out by replacing all the `any` types.
10+
*
11+
* Once filled out, we encourage you to share your work with the
12+
* community by sending a pull request to:
13+
* https://github.com/flowtype/flow-typed
14+
*/
15+
16+
declare module 'ratelimiter' {
17+
declare module.exports: any;
18+
}
19+
20+
/**
21+
* We include stubs for each file inside this npm package in case you need to
22+
* require those files directly. Feel free to delete any files that aren't
23+
* needed.
24+
*/
25+
declare module 'ratelimiter/microtime' {
26+
declare module.exports: any;
27+
}
28+
29+
declare module 'ratelimiter/test/index' {
30+
declare module.exports: any;
31+
}
32+
33+
// Filename aliases
34+
declare module 'ratelimiter/index' {
35+
declare module.exports: $Exports<'ratelimiter'>;
36+
}
37+
declare module 'ratelimiter/index.js' {
38+
declare module.exports: $Exports<'ratelimiter'>;
39+
}
40+
declare module 'ratelimiter/microtime.js' {
41+
declare module.exports: $Exports<'ratelimiter/microtime'>;
42+
}
43+
declare module 'ratelimiter/test/index.js' {
44+
declare module.exports: $Exports<'ratelimiter/test/index'>;
45+
}

hyperion/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import path from 'path';
1414
import { getUserById } from 'shared/db/queries/user';
1515
import Raven from 'shared/raven';
1616
import toobusy from 'shared/middlewares/toobusy';
17+
import rateLimiter from 'shared/middlewares/rate-limiter';
1718
import addSecurityMiddleware from 'shared/middlewares/security';
1819

1920
const PORT = process.env.PORT || 3006;
@@ -28,6 +29,12 @@ app.use(statsd);
2829
app.set('trust proxy', true);
2930

3031
app.use(toobusy);
32+
app.use(
33+
rateLimiter({
34+
max: 5,
35+
duration: '20s',
36+
})
37+
);
3138

3239
// Security middleware.
3340
addSecurityMiddleware(app, { enableNonce: true, enableCSP: true });

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149
"prismjs": "^1.15.0",
150150
"query-string": "5.1.1",
151151
"raf": "^3.4.0",
152+
"ratelimiter": "^3.2.0",
152153
"raven": "^2.6.4",
153154
"react": "^16.7.0-alpha.2",
154155
"react-apollo": "^2.3.2",
@@ -220,7 +221,7 @@
220221
"start:analytics": "cross-env NODE_ENV=production node build-analytics/main.js",
221222
"start:api": "cross-env NODE_ENV=production node build-api/main.js",
222223
"dev:web": "cross-env NODE_PATH=./ react-app-rewired start",
223-
"dev:api": "cross-env FILE_STORAGE=local cross-env NODE_PATH=./ cross-env NODE_ENV=development cross-env DEBUG=build*,api*,shared:rethinkdb:db-query-cache,-api:resolvers cross-env DIR=api backpack",
224+
"dev:api": "cross-env FILE_STORAGE=local cross-env NODE_PATH=./ cross-env NODE_ENV=development cross-env DEBUG=build*,api*,shared:rethinkdb:db-query-cache,-api:resolvers,shared:middlewares:ratelimiter cross-env DIR=api backpack",
224225
"dev:api:s3": "cross-env FILE_STORAGE=s3 cross-env NODE_PATH=./ cross-env NODE_ENV=development cross-env DEBUG=build*,api*,shared:rethinkdb:db-query-cache,-api:resolvers cross-env DIR=api backpack",
225226
"dev:athena": "cross-env NODE_PATH=./ cross-env NODE_ENV=development cross-env DEBUG=build*,athena*,shared:middlewares*,-athena:resolvers cross-env DIR=athena backpack",
226227
"dev:hermes": "cross-env NODE_PATH=./ cross-env NODE_ENV=development cross-env DEBUG=build*,hermes*,shared:middlewares*,-hermes:resolvers cross-env DIR=hermes backpack",

shared/middlewares/rate-limiter.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// @flow
2+
const debug = require('debug')('shared:middlewares:ratelimiter');
3+
import requestIp from 'request-ip';
4+
import ms from 'ms';
5+
import Limiter from 'ratelimiter';
6+
import createRedis from '../bull/create-redis';
7+
import Raven from 'shared/raven';
8+
9+
const server = process.env.SENTRY_NAME || 'unnamed';
10+
const redis = createRedis({
11+
keyPrefix: 'request-rate-limit:',
12+
});
13+
14+
const rateLimiter = ({ max, duration }: { max: number, duration: string }) => {
15+
return (
16+
req: express$Request,
17+
res: express$Response,
18+
next: express$NextFunction
19+
) => {
20+
// if user is logged in than use their id, otherwise use their ip address
21+
const id =
22+
req.user && req.user.id ? req.user.id : requestIp.getClientIp(req);
23+
const limiter = new Limiter({
24+
// $FlowIssue
25+
id: `${server}:${id}`,
26+
db: redis,
27+
max,
28+
duration: ms(duration),
29+
});
30+
31+
limiter.get(function(err, limit) {
32+
if (err) return next(err);
33+
34+
const remaining = limit.remaining - 1;
35+
res.set('X-RateLimit-Limit', String(limit.total));
36+
res.set('X-RateLimit-Remaining', String(remaining));
37+
res.set('X-RateLimit-Reset', String(limit.reset));
38+
39+
const after = (limit.reset - Date.now() / 1000) | 0;
40+
const remainingTime = ms(after * 1000, { long: true });
41+
debug(
42+
'%s of %s requests remaining in the next %s, userId: %s',
43+
remaining,
44+
limit.total,
45+
remainingTime,
46+
id
47+
);
48+
if (limit.remaining) return next();
49+
50+
const delta = (limit.reset * 1000 - Date.now()) | 0;
51+
res.set('Retry-After', String(after));
52+
res
53+
.status(429)
54+
.send('Rate limit exceeded, retry in ' + ms(delta, { long: true }));
55+
});
56+
};
57+
};
58+
59+
export default rateLimiter;

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11773,6 +11773,11 @@ range-parser@^1.0.3, range-parser@~1.2.0:
1177311773
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
1177411774
integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=
1177511775

11776+
ratelimiter@^3.2.0:
11777+
version "3.2.0"
11778+
resolved "https://registry.yarnpkg.com/ratelimiter/-/ratelimiter-3.2.0.tgz#ae74cf9629daae4cc8900ec126ab28d3794070f1"
11779+
integrity sha512-zMc9X4FNmOk3RBxV95lvp13sZRtf43UJJN1FficbYiusBB09zB6gcJtK2X18dKmH+Gq0C7W6qNCHE+UJx0YwVg==
11780+
1177611781
raven@^2.6.4:
1177711782
version "2.6.4"
1177811783
resolved "https://registry.yarnpkg.com/raven/-/raven-2.6.4.tgz#458d4a380c8fbb59e0150c655625aaf60c167ea3"

0 commit comments

Comments
 (0)