mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-19 15:48:04 +01:00
API v3 Rate Limiter (#12117)
* simplify ip address management by using the trust proxy express option * add setupExpress file * fix redirects middleware tests * fix lint * short circuit the ip blocking middleware * basic implementation with ip based limiting * improve logging * upgrade apidoc * apidoc: add introduction section * fix lint * fix tests * fix lint * add unit tests for rate limiter * do not send retry-after header when points are available * automatically fix lint * fix more lint issues * use userId as key for rate limit when available
This commit is contained in:
94
website/server/middlewares/rateLimiter.js
Normal file
94
website/server/middlewares/rateLimiter.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import nconf from 'nconf';
|
||||
import redis from 'redis';
|
||||
import {
|
||||
RateLimiterRedis,
|
||||
RateLimiterMemory,
|
||||
RateLimiterRes,
|
||||
} from 'rate-limiter-flexible';
|
||||
import {
|
||||
TooManyRequests,
|
||||
} from '../libs/errors';
|
||||
import logger from '../libs/logger';
|
||||
import apiError from '../libs/apiError';
|
||||
|
||||
// Middleware to rate limit requests to the API
|
||||
|
||||
// More info on the API rate limits can be found on the wiki at
|
||||
// https://habitica.fandom.com/wiki/Guidance_for_Comrades#Rules_for_Third-Party_Tools
|
||||
|
||||
const IS_TEST = nconf.get('IS_TEST');
|
||||
const RATE_LIMITER_ENABLED = nconf.get('RATE_LIMITER_ENABLED') === 'true';
|
||||
const REDIS_HOST = nconf.get('REDIS_HOST');
|
||||
const REDIS_PASSWORD = nconf.get('REDIS_PASSWORD');
|
||||
const REDIS_PORT = nconf.get('REDIS_PORT');
|
||||
|
||||
let redisClient;
|
||||
let rateLimiter;
|
||||
|
||||
const rateLimiterOpts = {
|
||||
keyPrefix: 'api-v3',
|
||||
points: 30, // 30 requests
|
||||
duration: 60, // per 1 minute by User ID or IP
|
||||
};
|
||||
|
||||
if (RATE_LIMITER_ENABLED) {
|
||||
if (IS_TEST) {
|
||||
rateLimiter = new RateLimiterMemory({
|
||||
...rateLimiterOpts,
|
||||
});
|
||||
} else {
|
||||
redisClient = redis.createClient({
|
||||
host: REDIS_HOST,
|
||||
password: REDIS_PASSWORD,
|
||||
port: REDIS_PORT,
|
||||
enable_offline_queue: false,
|
||||
});
|
||||
|
||||
redisClient.on('error', error => {
|
||||
logger.error(error, 'Redis Error');
|
||||
});
|
||||
|
||||
rateLimiter = new RateLimiterRedis({
|
||||
...rateLimiterOpts,
|
||||
storeClient: redisClient,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setResponseHeaders (res, rateLimiterRes) {
|
||||
const headers = {
|
||||
'X-RateLimit-Limit': rateLimiterOpts.points,
|
||||
'X-RateLimit-Remaining': rateLimiterRes.remainingPoints,
|
||||
'X-RateLimit-Reset': new Date(Date.now() + rateLimiterRes.msBeforeNext),
|
||||
};
|
||||
|
||||
if (rateLimiterRes.remainingPoints < 1) {
|
||||
headers['Retry-After'] = rateLimiterRes.msBeforeNext / 1000;
|
||||
}
|
||||
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
export default function rateLimiterMiddleware (req, res, next) {
|
||||
if (!RATE_LIMITER_ENABLED) return next();
|
||||
|
||||
const userId = req.header('x-api-user');
|
||||
|
||||
return rateLimiter.consume(userId || req.ip)
|
||||
.then(rateLimiterRes => {
|
||||
setResponseHeaders(res, rateLimiterRes);
|
||||
return next();
|
||||
})
|
||||
.catch(rateLimiterRes => {
|
||||
if (rateLimiterRes instanceof RateLimiterRes) {
|
||||
setResponseHeaders(res, rateLimiterRes);
|
||||
return next(new TooManyRequests(apiError('clientRateLimited')));
|
||||
}
|
||||
|
||||
// In case of an unhandled error we skip the middleware as it could mean
|
||||
// , for example, that the connection to the redis database is not working.
|
||||
// We do not want to block all requests in these cases.
|
||||
logger.error(rateLimiterRes, 'Rate Limiter Error');
|
||||
return next();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user