@routup/rate-limit
Version:
Routup rate limiter.
232 lines (221 loc) • 7.66 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var routup = require('routup');
const symbol = Symbol.for('ReqRateLimit');
function useRequestRateLimitInfo(req, key) {
if (symbol in req) {
if (typeof key === 'string') {
return req[symbol][key];
}
return req[symbol];
}
return {};
}
function setRequestRateLimitInfo(req, key, value) {
if (symbol in req) {
if (typeof key === 'object') {
req[symbol] = key;
} else {
req[symbol][key] = value;
}
return;
}
if (typeof key === 'object') {
req[symbol] = key;
return;
}
req[symbol] = {
[key]: value
};
}
const RETRY_AGAIN_MESSAGE = 'Too many requests, please try again later.';
function calculateNextResetTime(windowMs) {
const resetTime = new Date();
resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs);
return resetTime;
}
class MemoryStore {
/**
* Method that initializes the store.
*
* @param options {Options} - The options used to setup the middleware.
*/ init(options) {
// Get the duration of a window from the options.
this.windowMs = options.windowMs;
// Then calculate the reset time using that.
this.resetTime = calculateNextResetTime(this.windowMs);
// Initialise the hit counter map.
this.hits = {};
// Reset hit counts for ALL clients every `windowMs` - this will also
// re-calculate the `resetTime`
this.interval = setInterval(async ()=>{
await this.resetAll();
}, this.windowMs);
// Cleaning up the interval will be taken care of by the `shutdown` method.
if (this.interval.unref) this.interval.unref();
}
/**
* Method to increment a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @returns {IncrementResponse} - The number of hits and reset time for that client.
*
* @public
*/ async increment(key) {
const totalHits = (this.hits[key] ?? 0) + 1;
this.hits[key] = totalHits;
return {
totalHits,
resetTime: this.resetTime
};
}
/**
* Method to decrement a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @public
*/ async decrement(key) {
const current = this.hits[key];
if (current) this.hits[key] = current - 1;
}
/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client.
*
* @public
*/ async reset(key) {
delete this.hits[key];
}
/**
* Method to reset everyone's hit counter.
*
* @public
*/ /* istanbul ignore next */ async resetAll() {
this.hits = {};
this.resetTime = calculateNextResetTime(this.windowMs);
}
}
function buildHandlerOptions(input) {
input = input || {};
const options = {
windowMs: 60 * 1000,
max: 5,
message: RETRY_AGAIN_MESSAGE,
statusCode: 429,
skipFailedRequest: false,
skipSuccessfulRequest: false,
requestWasSuccessful: (request, response)=>response.statusCode < 400,
skip: (_request, _response)=>false,
keyGenerator: (request, _response)=>routup.getRequestIP(request, {
trustProxy: true
}),
async handler (request, response, _next, _optionsUsed) {
// Set the response status code
response.statusCode = options.statusCode;
// Call the `message` if it is a function.
const message = typeof options.message === 'function' ? await options.message(request, response) : options.message;
// Send the response if writable.
if (!response.writableEnded) {
routup.send(response, message ?? 'Too many requests, please try again later.');
}
},
...input,
store: input.store || new MemoryStore()
};
return options;
}
function createHandler(input) {
const options = buildHandlerOptions({
...input || {}
});
if (typeof options.store.init === 'function') {
options.store.init(options);
}
return routup.coreHandler(async (req, res, next)=>{
const skip = await options.skip(req, res);
if (skip) {
next();
return;
}
const key = await options.keyGenerator(req, res);
const { totalHits, resetTime } = await options.store.increment(key);
const retrieveQuota = typeof options.max === 'function' ? options.max(req, res) : options.max;
const maxHits = await retrieveQuota;
setRequestRateLimitInfo(req, {
limit: maxHits,
current: totalHits,
remaining: Math.max(maxHits - totalHits, 0),
resetTime
});
if (!res.headersSent) {
res.setHeader(routup.HeaderName.RATE_LIMIT_LIMIT, maxHits);
res.setHeader(routup.HeaderName.RATE_LIMIT_REMAINING, useRequestRateLimitInfo(req, 'remaining'));
if (resetTime) {
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1000);
res.setHeader(routup.HeaderName.RATE_LIMIT_RESET, Math.max(0, deltaSeconds));
}
}
if (options.skipFailedRequest || options.skipSuccessfulRequest) {
let decremented = false;
const decrementKey = async ()=>{
if (!decremented) {
await options.store.decrement(key);
decremented = true;
setRequestRateLimitInfo(req, 'remaining', Math.max(maxHits - totalHits - 1, 0));
}
};
if (options.skipFailedRequest) {
res.on('finish', async ()=>{
if (!options.requestWasSuccessful(req, res)) {
await decrementKey();
}
});
res.on('close', async ()=>{
if (!res.writableEnded) {
await decrementKey();
}
});
res.on('error', async ()=>{
await decrementKey();
});
}
if (options.skipSuccessfulRequest) {
res.on('finish', async ()=>{
if (options.requestWasSuccessful(req, res)) {
await decrementKey();
}
});
}
}
if (maxHits && totalHits > maxHits) {
if (!res.headersSent) {
res.setHeader(routup.HeaderName.RETRY_AFTER, Math.ceil(options.windowMs / 1000));
}
options.handler(req, res, next, options);
return;
}
next();
});
}
function rateLimit(options) {
return {
name: 'rateLimit',
install: (router)=>{
router.use(createHandler(options));
}
};
}
exports.MemoryStore = MemoryStore;
exports.RETRY_AGAIN_MESSAGE = RETRY_AGAIN_MESSAGE;
exports.buildHandlerOptions = buildHandlerOptions;
exports.calculateNextResetTime = calculateNextResetTime;
exports.createHandler = createHandler;
exports.default = rateLimit;
exports.rateLimit = rateLimit;
exports.setRequestRateLimitInfo = setRequestRateLimitInfo;
exports.useRequestRateLimitInfo = useRequestRateLimitInfo;
module.exports = Object.assign(exports.default, exports);
//# sourceMappingURL=index.cjs.map