2018-02-20 17:57:29 +08:00
|
|
|
require('../support/test_helper');
|
|
|
|
|
|
|
|
const assert = require('../support/assert');
|
|
|
|
const redis = require('redis');
|
2018-02-26 23:23:42 +08:00
|
|
|
const RedisPool = require('redis-mpool');
|
|
|
|
const cartodbRedis = require('cartodb-redis');
|
|
|
|
const TestClient = require('../support/test-client');
|
2018-02-27 23:52:27 +08:00
|
|
|
const rateLimitMiddleware = require('../../lib/cartodb/middleware/rate-limit');
|
|
|
|
const { RATE_LIMIT_ENDPOINTS_GROUPS, getStoreKey } = rateLimitMiddleware;
|
2018-02-20 17:57:29 +08:00
|
|
|
|
2018-02-26 23:23:42 +08:00
|
|
|
let rateLimit;
|
2018-02-20 17:57:29 +08:00
|
|
|
let redisClient;
|
|
|
|
let testClient;
|
|
|
|
let keysToDelete = ['user:localhost:mapviews:global'];
|
2018-02-26 18:20:31 +08:00
|
|
|
const user = 'localhost';
|
2018-02-20 17:57:29 +08:00
|
|
|
|
|
|
|
const query = `
|
|
|
|
SELECT
|
|
|
|
ST_Transform('SRID=4326;POINT(-180 85.05112877)'::geometry, 3857) the_geom_webmercator,
|
|
|
|
1 cartodb_id,
|
|
|
|
2 val
|
|
|
|
`;
|
|
|
|
|
|
|
|
const createMapConfig = ({
|
|
|
|
version = '1.6.0',
|
|
|
|
type = 'cartodb',
|
|
|
|
sql = query,
|
|
|
|
cartocss = TestClient.CARTOCSS.POINTS,
|
|
|
|
cartocss_version = '2.3.0',
|
|
|
|
interactivity = 'cartodb_id',
|
|
|
|
countBy = 'cartodb_id'
|
|
|
|
} = {}) => ({
|
|
|
|
version,
|
|
|
|
layers: [{
|
|
|
|
type,
|
|
|
|
options: {
|
|
|
|
source: {
|
|
|
|
id: 'a0'
|
|
|
|
},
|
|
|
|
cartocss,
|
|
|
|
cartocss_version,
|
|
|
|
interactivity
|
|
|
|
}
|
|
|
|
}],
|
|
|
|
analyses: [
|
|
|
|
{
|
|
|
|
id: 'a0',
|
|
|
|
type: 'source',
|
|
|
|
params: {
|
|
|
|
query: sql
|
|
|
|
}
|
|
|
|
}
|
|
|
|
],
|
|
|
|
dataviews: {
|
|
|
|
count: {
|
|
|
|
source: {
|
|
|
|
id: 'a0'
|
|
|
|
},
|
|
|
|
type: 'formula',
|
|
|
|
options: {
|
|
|
|
column: countBy,
|
|
|
|
operation: 'count'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function setLimit(count, period, burst) {
|
2018-02-20 18:38:44 +08:00
|
|
|
redisClient.SELECT(8, err => {
|
2018-02-20 17:57:29 +08:00
|
|
|
if (err) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-02-21 00:19:50 +08:00
|
|
|
const key = getStoreKey(user, RATE_LIMIT_ENDPOINTS_GROUPS.ENDPOINT_1);
|
2018-02-23 23:23:59 +08:00
|
|
|
redisClient.rpush(key, burst);
|
|
|
|
redisClient.rpush(key, count);
|
|
|
|
redisClient.rpush(key, period);
|
|
|
|
keysToDelete.push(key);
|
2018-02-20 17:57:29 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-02-26 23:23:42 +08:00
|
|
|
function getReqAndRes() {
|
|
|
|
return {
|
|
|
|
req: {},
|
|
|
|
res: {
|
|
|
|
headers: {},
|
|
|
|
set(headers) {
|
|
|
|
this.headers = headers;
|
|
|
|
},
|
|
|
|
locals: {
|
|
|
|
user: 'localhost'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
describe('rate limit', function() {
|
2018-02-20 17:57:29 +08:00
|
|
|
before(function() {
|
2018-02-21 00:58:08 +08:00
|
|
|
global.environment.enabledFeatures.rateLimitsEnabled = true;
|
2018-02-21 01:17:25 +08:00
|
|
|
global.environment.enabledFeatures.rateLimitsByEndpoint.anonymous = true;
|
2018-02-21 00:58:08 +08:00
|
|
|
|
2018-02-20 17:57:29 +08:00
|
|
|
redisClient = redis.createClient(global.environment.redis.port);
|
|
|
|
testClient = new TestClient(createMapConfig(), 1234);
|
|
|
|
});
|
|
|
|
|
2018-02-21 00:58:08 +08:00
|
|
|
after(function() {
|
|
|
|
global.environment.enabledFeatures.rateLimitsEnabled = false;
|
2018-02-21 01:17:25 +08:00
|
|
|
global.environment.enabledFeatures.rateLimitsByEndpoint.anonymous = false;
|
2018-02-21 00:58:08 +08:00
|
|
|
});
|
|
|
|
|
2018-02-20 17:57:29 +08:00
|
|
|
afterEach(function(done) {
|
|
|
|
keysToDelete.forEach( key => {
|
|
|
|
redisClient.del(key);
|
|
|
|
});
|
|
|
|
|
|
|
|
redisClient.SELECT(0, () => {
|
|
|
|
redisClient.del('user:localhost:mapviews:global');
|
|
|
|
|
|
|
|
redisClient.SELECT(5, () => {
|
|
|
|
redisClient.del('user:localhost:mapviews:global');
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not be rate limited', function (done) {
|
|
|
|
const count = 1;
|
|
|
|
const period = 1;
|
|
|
|
const burst = 1;
|
|
|
|
setLimit(count, period, burst);
|
|
|
|
|
|
|
|
let response = {
|
|
|
|
status: 200,
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
|
|
'X-Rate-Limit-Limit': '2',
|
|
|
|
'X-Rate-Limit-Remaining': '1',
|
|
|
|
'X-Rate-Limit-Reset': '1',
|
|
|
|
'X-Rate-Limit-Retry-After': '-1'
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-02-20 18:38:44 +08:00
|
|
|
testClient.getLayergroup({ response }, err => {
|
2018-02-20 17:57:29 +08:00
|
|
|
assert.ifError(err);
|
|
|
|
setTimeout(done, period * 1000);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2018-02-27 00:03:56 +08:00
|
|
|
it("1 req/sec: 2 req/seg should be limited", function(done) {
|
2018-02-20 17:57:29 +08:00
|
|
|
const count = 1;
|
|
|
|
const period = 1;
|
|
|
|
const burst = 1;
|
|
|
|
setLimit(count, period, burst);
|
|
|
|
|
|
|
|
let response = {
|
|
|
|
status: 200,
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
|
|
'X-Rate-Limit-Limit': '2',
|
|
|
|
'X-Rate-Limit-Remaining': '1',
|
|
|
|
'X-Rate-Limit-Reset': '1',
|
|
|
|
'X-Rate-Limit-Retry-After': '-1'
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-02-20 18:38:44 +08:00
|
|
|
testClient.getLayergroup({ response }, err => {
|
2018-02-20 17:57:29 +08:00
|
|
|
assert.ifError(err);
|
|
|
|
});
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
function() {
|
|
|
|
let response = {
|
|
|
|
status: 200,
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
|
|
'X-Rate-Limit-Limit': '2',
|
|
|
|
'X-Rate-Limit-Remaining': '0',
|
|
|
|
'X-Rate-Limit-Reset': '1',
|
|
|
|
'X-Rate-Limit-Retry-After': '-1'
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-02-20 18:38:44 +08:00
|
|
|
testClient.getLayergroup({ response }, err => {
|
2018-02-20 17:57:29 +08:00
|
|
|
assert.ifError(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
250
|
|
|
|
);
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
function() {
|
|
|
|
let response = {
|
2018-02-27 00:03:56 +08:00
|
|
|
status: 429,
|
2018-02-20 17:57:29 +08:00
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
|
|
'X-Rate-Limit-Limit': '2',
|
|
|
|
'X-Rate-Limit-Remaining': '0',
|
2018-02-27 00:03:56 +08:00
|
|
|
'X-Rate-Limit-Reset': '1',
|
|
|
|
'X-Rate-Limit-Retry-After': '0'
|
2018-02-20 17:57:29 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-02-20 18:38:44 +08:00
|
|
|
testClient.getLayergroup({ response }, err => {
|
2018-02-20 17:57:29 +08:00
|
|
|
assert.ifError(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
500
|
|
|
|
);
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
function() {
|
|
|
|
let response = {
|
|
|
|
status: 429,
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
|
|
'X-Rate-Limit-Limit': '2',
|
|
|
|
'X-Rate-Limit-Remaining': '0',
|
2018-02-27 00:03:56 +08:00
|
|
|
'X-Rate-Limit-Reset': '1',
|
|
|
|
'X-Rate-Limit-Retry-After': '0'
|
2018-02-20 17:57:29 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-02-20 18:38:44 +08:00
|
|
|
testClient.getLayergroup({ response }, err => {
|
2018-02-20 17:57:29 +08:00
|
|
|
assert.ifError(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
750
|
|
|
|
);
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
function() {
|
|
|
|
let response = {
|
|
|
|
status: 429,
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
|
|
'X-Rate-Limit-Limit': '2',
|
|
|
|
'X-Rate-Limit-Remaining': '0',
|
2018-02-27 00:03:56 +08:00
|
|
|
'X-Rate-Limit-Reset': '1',
|
|
|
|
'X-Rate-Limit-Retry-After': '0'
|
2018-02-20 17:57:29 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-02-20 18:38:44 +08:00
|
|
|
testClient.getLayergroup({ response }, err => {
|
2018-02-20 17:57:29 +08:00
|
|
|
assert.ifError(err);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
950
|
|
|
|
);
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
function() {
|
|
|
|
let response = {
|
|
|
|
status: 200,
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
|
|
'X-Rate-Limit-Limit': '2',
|
|
|
|
'X-Rate-Limit-Remaining': '0',
|
2018-02-27 00:03:56 +08:00
|
|
|
'X-Rate-Limit-Reset': '1',
|
2018-02-20 17:57:29 +08:00
|
|
|
'X-Rate-Limit-Retry-After': '-1'
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-02-20 18:38:44 +08:00
|
|
|
testClient.getLayergroup({ response }, err => {
|
2018-02-20 17:57:29 +08:00
|
|
|
assert.ifError(err);
|
2018-02-26 23:23:42 +08:00
|
|
|
setTimeout(done, period * 2 * 1000);
|
2018-02-20 17:57:29 +08:00
|
|
|
});
|
|
|
|
},
|
|
|
|
1050
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2018-02-26 23:23:42 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('rate limit middleware', function () {
|
|
|
|
before(function () {
|
|
|
|
global.environment.enabledFeatures.rateLimitsEnabled = true;
|
|
|
|
global.environment.enabledFeatures.rateLimitsByEndpoint.anonymous = true;
|
|
|
|
|
|
|
|
const redisPool = new RedisPool(global.environment.redis);
|
|
|
|
const metadataBackend = cartodbRedis({ pool: redisPool });
|
|
|
|
rateLimit = rateLimitMiddleware(metadataBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ENDPOINT_1);
|
|
|
|
|
|
|
|
redisClient = redis.createClient(global.environment.redis.port);
|
|
|
|
testClient = new TestClient(createMapConfig(), 1234);
|
|
|
|
|
|
|
|
|
|
|
|
const count = 1;
|
|
|
|
const period = 1;
|
|
|
|
const burst = 0;
|
|
|
|
setLimit(count, period, burst);
|
|
|
|
});
|
|
|
|
|
|
|
|
after(function () {
|
|
|
|
global.environment.enabledFeatures.rateLimitsEnabled = false;
|
|
|
|
global.environment.enabledFeatures.rateLimitsByEndpoint.anonymous = false;
|
|
|
|
|
|
|
|
keysToDelete.forEach(key => {
|
|
|
|
redisClient.del(key);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it("3 request (1 per second) should not be rate limited", function (done) {
|
|
|
|
let { req, res } = getReqAndRes();
|
|
|
|
rateLimit(req, res, function (err) {
|
|
|
|
assert.ifError(err);
|
|
|
|
assert.deepEqual(res.headers, {
|
|
|
|
"X-Rate-Limit-Limit": 1,
|
|
|
|
"X-Rate-Limit-Remaining": 0,
|
|
|
|
"X-Rate-Limit-Reset": 1,
|
|
|
|
"X-Rate-Limit-Retry-After": -1
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
function () {
|
|
|
|
let { req, res } = getReqAndRes();
|
|
|
|
rateLimit(req, res, function (err) {
|
|
|
|
assert.ifError(err);
|
|
|
|
assert.deepEqual(res.headers, {
|
|
|
|
"X-Rate-Limit-Limit": 1,
|
|
|
|
"X-Rate-Limit-Remaining": 0,
|
|
|
|
"X-Rate-Limit-Reset": 1,
|
|
|
|
"X-Rate-Limit-Retry-After": -1
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
1100
|
|
|
|
);
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
function () {
|
|
|
|
let { req, res } = getReqAndRes();
|
|
|
|
rateLimit(req, res, function (err) {
|
|
|
|
assert.ifError(err);
|
|
|
|
assert.deepEqual(res.headers, {
|
|
|
|
"X-Rate-Limit-Limit": 1,
|
|
|
|
"X-Rate-Limit-Remaining": 0,
|
|
|
|
"X-Rate-Limit-Reset": 1,
|
|
|
|
"X-Rate-Limit-Retry-After": -1
|
|
|
|
});
|
|
|
|
|
|
|
|
setTimeout(done, 1000);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
2 * 1100
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2018-02-27 00:03:56 +08:00
|
|
|
it("1 req/sec: 2 req/seg should be limited", function (done) {
|
2018-02-26 23:23:42 +08:00
|
|
|
let { req, res } = getReqAndRes();
|
|
|
|
rateLimit(req, res, function (err) {
|
|
|
|
assert.ifError(err);
|
|
|
|
assert.deepEqual(res.headers, {
|
|
|
|
"X-Rate-Limit-Limit": 1,
|
|
|
|
"X-Rate-Limit-Remaining": 0,
|
|
|
|
"X-Rate-Limit-Reset": 1,
|
|
|
|
"X-Rate-Limit-Retry-After": -1
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
function () {
|
|
|
|
let { req, res } = getReqAndRes();
|
|
|
|
rateLimit(req, res, function (err) {
|
2018-02-27 00:03:56 +08:00
|
|
|
assert.ok(err);
|
2018-02-26 23:23:42 +08:00
|
|
|
assert.deepEqual(res.headers, {
|
|
|
|
"X-Rate-Limit-Limit": 1,
|
|
|
|
"X-Rate-Limit-Remaining": 0,
|
2018-02-27 00:03:56 +08:00
|
|
|
"X-Rate-Limit-Reset": 0,
|
|
|
|
"X-Rate-Limit-Retry-After": 0
|
2018-02-26 23:23:42 +08:00
|
|
|
});
|
2018-02-27 00:03:56 +08:00
|
|
|
assert.equal(err.message, 'You are over the limits.');
|
|
|
|
assert.equal(err.http_status, 429);
|
2018-02-26 23:23:42 +08:00
|
|
|
});
|
|
|
|
},
|
|
|
|
250
|
|
|
|
);
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
function () {
|
|
|
|
let { req, res } = getReqAndRes();
|
|
|
|
rateLimit(req, res, function (err) {
|
|
|
|
assert.ok(err);
|
|
|
|
assert.deepEqual(res.headers, {
|
|
|
|
"X-Rate-Limit-Limit": 1,
|
|
|
|
"X-Rate-Limit-Remaining": 0,
|
2018-02-27 00:03:56 +08:00
|
|
|
"X-Rate-Limit-Reset": 0,
|
|
|
|
"X-Rate-Limit-Retry-After": 0
|
2018-02-26 23:23:42 +08:00
|
|
|
});
|
|
|
|
assert.equal(err.message, 'You are over the limits.');
|
|
|
|
assert.equal(err.http_status, 429);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
500
|
|
|
|
);
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
function () {
|
|
|
|
let { req, res } = getReqAndRes();
|
|
|
|
rateLimit(req, res, function (err) {
|
|
|
|
assert.ok(err);
|
|
|
|
assert.deepEqual(res.headers, {
|
|
|
|
"X-Rate-Limit-Limit": 1,
|
|
|
|
"X-Rate-Limit-Remaining": 0,
|
2018-02-27 00:03:56 +08:00
|
|
|
"X-Rate-Limit-Reset": 0,
|
|
|
|
"X-Rate-Limit-Retry-After": 0
|
2018-02-26 23:23:42 +08:00
|
|
|
});
|
|
|
|
assert.equal(err.message, 'You are over the limits.');
|
|
|
|
assert.equal(err.http_status, 429);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
750
|
|
|
|
);
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
function () {
|
|
|
|
let { req, res } = getReqAndRes();
|
|
|
|
rateLimit(req, res, function (err) {
|
|
|
|
assert.ok(err);
|
|
|
|
assert.deepEqual(res.headers, {
|
|
|
|
"X-Rate-Limit-Limit": 1,
|
|
|
|
"X-Rate-Limit-Remaining": 0,
|
2018-02-27 00:03:56 +08:00
|
|
|
"X-Rate-Limit-Reset": 0,
|
|
|
|
"X-Rate-Limit-Retry-After": 0
|
2018-02-26 23:23:42 +08:00
|
|
|
});
|
|
|
|
assert.equal(err.message, 'You are over the limits.');
|
|
|
|
assert.equal(err.http_status, 429);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
950
|
|
|
|
);
|
|
|
|
|
|
|
|
setTimeout(
|
|
|
|
function () {
|
|
|
|
let { req, res } = getReqAndRes();
|
|
|
|
rateLimit(req, res, function (err) {
|
|
|
|
assert.ifError(err);
|
|
|
|
assert.deepEqual(res.headers, {
|
|
|
|
"X-Rate-Limit-Limit": 1,
|
|
|
|
"X-Rate-Limit-Remaining": 0,
|
|
|
|
"X-Rate-Limit-Reset": 1,
|
|
|
|
"X-Rate-Limit-Retry-After": -1
|
|
|
|
});
|
|
|
|
setTimeout(done, 1000);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
1050
|
|
|
|
);
|
|
|
|
});
|
2018-02-27 00:03:56 +08:00
|
|
|
});
|