159 lines
5.1 KiB
JavaScript
159 lines
5.1 KiB
JavaScript
"use strict";
|
|
const MemoryStore = require("./memory-store");
|
|
|
|
function RateLimit(options) {
|
|
options = Object.assign(
|
|
{
|
|
windowMs: 60 * 1000, // milliseconds - how long to keep records of requests in memory
|
|
max: 5, // max number of recent connections during `window` milliseconds before sending a 429 response
|
|
message: "Too many requests, please try again later.",
|
|
statusCode: 429, // 429 status = Too Many Requests (RFC 6585)
|
|
headers: true, //Send custom rate limit header with limit and remaining
|
|
draft_polli_ratelimit_headers: false, //Support for the new RateLimit standardization headers
|
|
skipFailedRequests: false, // Do not count failed requests (status >= 400)
|
|
skipSuccessfulRequests: false, // Do not count successful requests (status < 400)
|
|
// allows to create custom keys (by default user IP is used)
|
|
keyGenerator: function (req /*, res*/) {
|
|
return req.ip;
|
|
},
|
|
skip: function (/*req, res*/) {
|
|
return false;
|
|
},
|
|
handler: function (req, res /*, next*/) {
|
|
res.status(options.statusCode).send(options.message);
|
|
},
|
|
onLimitReached: function (/*req, res, optionsUsed*/) {},
|
|
},
|
|
options
|
|
);
|
|
|
|
// store to use for persisting rate limit data
|
|
options.store = options.store || new MemoryStore(options.windowMs);
|
|
|
|
// ensure that the store has the incr method
|
|
if (
|
|
typeof options.store.incr !== "function" ||
|
|
typeof options.store.resetKey !== "function" ||
|
|
(options.skipFailedRequests &&
|
|
typeof options.store.decrement !== "function")
|
|
) {
|
|
throw new Error("The store is not valid.");
|
|
}
|
|
|
|
["global", "delayMs", "delayAfter"].forEach((key) => {
|
|
// note: this doesn't trigger if delayMs or delayAfter are set to 0, because that essentially disables them
|
|
if (options[key]) {
|
|
throw new Error(
|
|
`The ${key} option was removed from express-rate-limit v3.`
|
|
);
|
|
}
|
|
});
|
|
|
|
function rateLimit(req, res, next) {
|
|
if (options.skip(req, res)) {
|
|
return next();
|
|
}
|
|
|
|
const key = options.keyGenerator(req, res);
|
|
|
|
options.store.incr(key, function (err, current, resetTime) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
const maxResult =
|
|
typeof options.max === "function" ? options.max(req, res) : options.max;
|
|
|
|
Promise.resolve(maxResult)
|
|
.then((max) => {
|
|
req.rateLimit = {
|
|
limit: max,
|
|
current: current,
|
|
remaining: Math.max(max - current, 0),
|
|
resetTime: resetTime,
|
|
};
|
|
|
|
if (options.headers && !res.headersSent) {
|
|
res.setHeader("X-RateLimit-Limit", max);
|
|
res.setHeader("X-RateLimit-Remaining", req.rateLimit.remaining);
|
|
if (resetTime instanceof Date) {
|
|
// if we have a resetTime, also provide the current date to help avoid issues with incorrect clocks
|
|
res.setHeader("Date", new Date().toGMTString());
|
|
res.setHeader(
|
|
"X-RateLimit-Reset",
|
|
Math.ceil(resetTime.getTime() / 1000)
|
|
);
|
|
}
|
|
}
|
|
if (options.draft_polli_ratelimit_headers && !res.headersSent) {
|
|
res.setHeader("RateLimit-Limit", max);
|
|
res.setHeader("RateLimit-Remaining", req.rateLimit.remaining);
|
|
if (resetTime) {
|
|
const deltaSeconds = Math.ceil(
|
|
(resetTime.getTime() - Date.now()) / 1000
|
|
);
|
|
res.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
|
|
}
|
|
}
|
|
|
|
if (options.skipFailedRequests || options.skipSuccessfulRequests) {
|
|
let decremented = false;
|
|
const decrementKey = () => {
|
|
if (!decremented) {
|
|
options.store.decrement(key);
|
|
decremented = true;
|
|
}
|
|
};
|
|
|
|
if (options.skipFailedRequests) {
|
|
res.on("finish", function () {
|
|
if (res.statusCode >= 400) {
|
|
decrementKey();
|
|
}
|
|
});
|
|
|
|
res.on("close", () => {
|
|
if (!res.finished) {
|
|
decrementKey();
|
|
}
|
|
});
|
|
|
|
res.on("error", () => decrementKey());
|
|
}
|
|
|
|
if (options.skipSuccessfulRequests) {
|
|
res.on("finish", function () {
|
|
if (res.statusCode < 400) {
|
|
options.store.decrement(key);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
if (max && current === max + 1) {
|
|
options.onLimitReached(req, res, options);
|
|
}
|
|
|
|
if (max && current > max) {
|
|
if (options.headers && !res.headersSent) {
|
|
res.setHeader("Retry-After", Math.ceil(options.windowMs / 1000));
|
|
}
|
|
return options.handler(req, res, next);
|
|
}
|
|
|
|
next();
|
|
})
|
|
.catch(next);
|
|
});
|
|
}
|
|
|
|
rateLimit.resetKey = options.store.resetKey.bind(options.store);
|
|
|
|
// Backward compatibility function
|
|
rateLimit.resetIp = rateLimit.resetKey;
|
|
|
|
return rateLimit;
|
|
}
|
|
|
|
module.exports = RateLimit;
|