187 lines
4.9 KiB
JavaScript
187 lines
4.9 KiB
JavaScript
'use strict';
|
|
|
|
const REDIS_DB = 8;
|
|
|
|
const RATE_LIMIT_ENDPOINTS_GROUPS = {
|
|
ANONYMOUS: 'anonymous',
|
|
STATIC: 'static',
|
|
STATIC_NAMED: 'static_named',
|
|
DATAVIEW: 'dataview',
|
|
DATAVIEW_SEARCH: 'dataview_search',
|
|
ANALYSIS: 'analysis',
|
|
TILE: 'tile',
|
|
ATTRIBUTES: 'attributes',
|
|
NAMED_LIST: 'named_list',
|
|
NAMED_CREATE: 'named_create',
|
|
NAMED_GET: 'named_get',
|
|
NAMED: 'named',
|
|
NAMED_UPDATE: 'named_update',
|
|
NAMED_DELETE: 'named_delete',
|
|
NAMED_TILES: 'named_tiles'
|
|
};
|
|
|
|
function rateLimitFn(metadataBackend, endpointGroup = null) {
|
|
return function rateLimitMiddleware(req, res, next) {
|
|
if (!global.environment.enabledFeatures.rateLimitsEnabled) {
|
|
return next();
|
|
}
|
|
|
|
const user = res.locals.user;
|
|
|
|
// TODO: remove comments to move it to global
|
|
// if (!endpointGroup) {
|
|
// endpointGroup = getEndpointGroup();
|
|
// }
|
|
|
|
if (!endpointGroup || !isRateLimitEnabledByEndpoint(endpointGroup)) {
|
|
return next();
|
|
}
|
|
|
|
const redisParams = [
|
|
getLuaScript(),
|
|
2,
|
|
getStoreKey(user, endpointGroup), // KEY[1]
|
|
getStatusKey(user, endpointGroup) // KEY[2]
|
|
];
|
|
|
|
metadataBackend.redisCmd(REDIS_DB, 'EVAL', redisParams, function (err, rateLimits) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
const rateLimit = getLowerRateLimit(rateLimits);
|
|
|
|
if (!rateLimit) {
|
|
return next();
|
|
}
|
|
|
|
const [isBlocked, limit, remaining, retry, reset] = rateLimit;
|
|
|
|
res.set({
|
|
'X-Rate-Limit-Limit': limit,
|
|
'X-Rate-Limit-Remaining': remaining,
|
|
'X-Rate-Limit-Retry-After': retry,
|
|
'X-Rate-Limit-Reset': reset
|
|
});
|
|
|
|
if (isBlocked) {
|
|
const err = new Error('You are over the limits.');
|
|
err.http_status = 429;
|
|
return next(err);
|
|
}
|
|
|
|
return next();
|
|
});
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the endpoint key in Redis
|
|
*/
|
|
// function getEndpointGroup() {
|
|
// // TODO: get endpoint from route path
|
|
// return null;
|
|
// }
|
|
|
|
/**
|
|
* Returns Redis key where the limits are saved by user and endpoint
|
|
* The value is a Redis hash:
|
|
* maxBurst (b): Integer (as string)
|
|
* countPerPeriod (c): Integer (as string)
|
|
* period (p): Integer (as string)
|
|
* @param {string} user
|
|
* @param {string} endpointGroup
|
|
*/
|
|
function getStoreKey(user, endpointGroup) {
|
|
return `limits:rate:store:${user}:${endpointGroup}`;
|
|
}
|
|
|
|
/**
|
|
* Returns Redis key where the current state of the limit by user and endpoint
|
|
* This key is managed by redis-cell (CL.THROTTLE command)
|
|
* @param {string} user
|
|
* @param {string} endpointGroup
|
|
*/
|
|
function getStatusKey(user, endpointGroup) {
|
|
return `limits:rate:status:${user}:${endpointGroup}`;
|
|
}
|
|
|
|
function getLuaScript() {
|
|
return `
|
|
local results = {}
|
|
local resultsCounter = 0
|
|
|
|
local limits = {}
|
|
local limitsArray = redis.call("LRANGE", KEYS[1], 0, -1)
|
|
|
|
for i, v in ipairs(limitsArray) do
|
|
local rest = i % 3
|
|
if rest ~= 0 then
|
|
limits[rest] = v
|
|
else
|
|
resultsCounter = resultsCounter + 1
|
|
results[resultsCounter] = redis.call("CL.THROTTLE", KEYS[2], limits[1], limits[2], v)
|
|
end
|
|
end
|
|
|
|
return results
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Returns the inner rateLimit what is the strictest one
|
|
* @param {Array} rateLimits Each inner array has 5 integers indicating:
|
|
* isBloqued, limit, remaining, retry, reset
|
|
*/
|
|
function getLowerRateLimit(rateLimits) {
|
|
/*jshint maxcomplexity:10 */
|
|
if (!rateLimits || !Array.isArray(rateLimits) || !rateLimits.length) {
|
|
return;
|
|
}
|
|
|
|
let minIndex = 0;
|
|
let minRemainingValue;
|
|
let currentIndex = 0;
|
|
for (let rateLimit of rateLimits) {
|
|
if (!validRatelimit(rateLimit)) {
|
|
currentIndex++;
|
|
continue;
|
|
}
|
|
|
|
const [isBlocked, , remaining] = rateLimit;
|
|
|
|
if (isBlocked === 1) {
|
|
minIndex = currentIndex;
|
|
break;
|
|
}
|
|
|
|
if (minRemainingValue === undefined || remaining < minRemainingValue) {
|
|
minIndex = currentIndex;
|
|
minRemainingValue = remaining;
|
|
}
|
|
|
|
currentIndex++;
|
|
}
|
|
|
|
if (validRatelimit(rateLimits[minIndex])) {
|
|
return rateLimits[minIndex];
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
function validRatelimit(rateLimit) {
|
|
return rateLimit.length === 5;
|
|
}
|
|
|
|
function isRateLimitEnabledByEndpoint(endpointGroup) {
|
|
return global.environment.enabledFeatures.rateLimitsByEndpoint[endpointGroup] === true;
|
|
}
|
|
|
|
|
|
module.exports = rateLimitFn;
|
|
module.exports.RATE_LIMIT_ENDPOINTS_GROUPS = RATE_LIMIT_ENDPOINTS_GROUPS;
|
|
module.exports.getStoreKey = getStoreKey;
|
|
module.exports.getLowerRateLimit = getLowerRateLimit;
|