Skip to content

Commit b1caa5f

Browse files
Merge pull request #6 from agbilotia1998/master
Adds throttling and corresponding tests
2 parents ca6d6bd + 21c7a44 commit b1caa5f

File tree

5 files changed

+119
-15
lines changed

5 files changed

+119
-15
lines changed

.env.sample

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ BACK_END_URL="http://localhost:3000"
33
PORT="4000"
44
EXPIRE_IN_SECONDS=15
55
REQUEST_LIMIT=99
6-
# LIMIT_BY_IP=0
6+
LIMIT_BY_IP=0
77
REDIS_URL="redis://host.docker.internal:6379"

package-lock.json

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@
2727
"express": "^4.16.2",
2828
"express-http-proxy": "^1.5.0",
2929
"express-limiter": "^1.6.1",
30+
"express-rate-limit": "^3.3.2",
31+
"express-slow-down": "^1.3.1",
3032
"helmet": "^3.12.0",
3133
"nodemon": "^1.18.9",
3234
"prom-client": "^11.2.0",
35+
"rate-limit-redis": "^1.6.0",
3336
"redis": "^2.8.0",
3437
"response-time": "^2.3.2"
3538
},

src/server.js

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ const http = require('http');
66
const proxy = require('express-http-proxy');
77
const redis = require('redis');
88
const Prometheus = require('./prometheus');
9+
const RateLimit = require('express-rate-limit');
10+
const RedisStore = require('rate-limit-redis');
11+
const slowDown = require('express-slow-down');
12+
13+
const redisServer = process.env.REDIS_URL || '';
14+
const client = redis.createClient(redisServer);
915

1016
app.use(helmet({
1117
hsts: false,
@@ -30,21 +36,50 @@ app.use((req, res, next) => {
3036
next();
3137
});
3238

33-
const redisServer = process.env.REDIS_URL || '';
34-
const client = redis.createClient(redisServer);
35-
const limiter = require('express-limiter')(app, client);
36-
3739
const expireInSeconds = process.env.EXPIRE_IN_SECONDS || 15;
3840
const requestLimit = process.env.REQUEST_LIMIT || 99;
39-
const limitByIP = process.env.LIMIT_BY_IP ? 'connection.remoteAddress' : 'hostname';
40-
limiter({
41-
path: '*',
42-
method: 'all',
43-
lookup: limitByIP,
44-
total: requestLimit,
45-
expire: 1000 * expireInSeconds,
41+
const delayTimeInSeconds = process.env.DELAY_IN_SECONDS || 0.5;
42+
const limitByIP = process.env.LIMIT_BY_IP || 1;
43+
44+
const redisKeyGenerator = function keyGenerator(req) {
45+
if (parseInt(limitByIP, 10) === 1) {
46+
return req.ip;
47+
}
48+
49+
return 'ALL_IPS';
50+
};
51+
52+
const speedLimiter = slowDown({
53+
keyGenerator: redisKeyGenerator,
54+
store: new RedisStore({
55+
client,
56+
// setting prefix to avoid collision between delaying and rate limiting requests
57+
prefix: 'sd: ',
58+
expiry: expireInSeconds,
59+
}),
60+
windowMs: 1000 * expireInSeconds,
61+
// allow specified requests per window time
62+
delayAfter: requestLimit,
63+
// begin adding specified delay time per request above maximum limit:
64+
delayMs: 1000 * delayTimeInSeconds,
4665
});
4766

67+
const rateLimiter = new RateLimit({
68+
keyGenerator: redisKeyGenerator,
69+
store: new RedisStore({
70+
client,
71+
// setting prefix to avoid collision between delaying and rate limiting requests
72+
prefix: 'rl: ',
73+
expiry: expireInSeconds,
74+
}),
75+
// setting window size in ms(eg: 2000)
76+
windowMs: 1000 * expireInSeconds,
77+
// limit each IP to specified requests per windowMs (e.g: 1)
78+
max: requestLimit,
79+
});
80+
81+
app.use(speedLimiter);
82+
app.use(rateLimiter);
4883
const target = process.env.BACK_END_URL || 'http://localhost:3000';
4984
const port = 4000 || process.env.PORT;
5085
app.use('/', proxy(target)).listen(port, () => {
@@ -62,3 +97,4 @@ if (!process.env.BACK_END_URL) {
6297
res.end();
6398
}).listen(3000);
6499
}
100+

test/api/proxy.spec.js

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,41 @@
11
const chai = require('chai');
22
const chaiHttp = require('chai-http');
3-
require('../../src/server');
4-
53
const { expect } = chai;
64
chai.use(chaiHttp);
75
const remoteServer = 'http://127.0.0.1';
86
const port = process.env.PORT || 4000;
7+
process.env.DELAY_IN_SECONDS = 1.6;
8+
process.env.EXPIRE_IN_SECONDS = 3;
9+
process.env.REQUEST_LIMIT = 1;
10+
process.env.BACK_END_URL = '';
11+
require('../../src/server');
912

1013
describe('reli-proxy', function module1() {
1114
this.timeout(10000);
15+
/*
16+
* Test the throttling
17+
*/
18+
describe('/Testing the throttling of requests', () => {
19+
it('it should rate limit second request', (done) => {
20+
setTimeout(() => {
21+
let requester = chai.request(`${remoteServer}:${port}`).keepOpen();
22+
Promise.all([
23+
requester.get('/test'),
24+
requester.get('/test'),
25+
requester.get('/test')
26+
]).then(responses => {
27+
expect(responses[0].status).to.eql(200);
28+
expect(responses[0].text).to.include('Response from back-end!');
29+
// Rate limit exceeds but request is queued, since the window size is 3 seconds the request still fails
30+
expect(responses[1].status).to.eql(429);
31+
// Rate limit exceeds but request is queued, since the window size is 3 seconds and delay induced is 1.6*2 = 3.2 seconds the request is completed successfully
32+
expect(responses[2].status).to.eql(200); expect(responses[0].text).to.include('Response from back-end!');
33+
done();
34+
}).then(requester.close());
35+
}, 50);
36+
});
37+
});
38+
1239
/*
1340
* Test the proxy
1441
*/
@@ -22,7 +49,7 @@ describe('reli-proxy', function module1() {
2249
expect(res.text).to.include('Response from back-end!');
2350
done();
2451
});
25-
}, 50);
52+
}, 3000);
2653
});
2754
});
2855

0 commit comments

Comments
 (0)