Remove dependency express-rate-limit

This commit is contained in:
advplyr 2022-07-06 19:14:47 -05:00
parent 7aa7e662b2
commit d301c12acd
6 changed files with 264 additions and 7 deletions

5
package-lock.json generated
View File

@ -433,11 +433,6 @@
"vary": "~1.1.2"
}
},
"express-rate-limit": {
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz",
"integrity": "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg=="
},
"finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",

View File

@ -34,7 +34,6 @@
"axios": "^0.26.1",
"date-and-time": "^2.3.1",
"express": "^4.17.1",
"express-rate-limit": "^5.3.0",
"htmlparser2": "^8.0.1",
"socket.io": "^4.4.1",
"xml2js": "^0.4.23"

View File

@ -4,7 +4,7 @@ const http = require('http')
const SocketIO = require('socket.io')
const fs = require('./libs/fsExtra')
const fileUpload = require('./libs/expressFileupload')
const rateLimit = require('express-rate-limit')
const rateLimit = require('./libs/expressRateLimit')
const { version } = require('../package.json')

View File

@ -0,0 +1,20 @@
# MIT License
Copyright 2021 Nathan Friedly
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,196 @@
"use strict";
//
// modified for use in audiobookshelf
// Source: https://github.com/nfriedly/express-rate-limit
//
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
// ability to manually decide if request was successful. Used when `skipSuccessfulRequests` and/or `skipFailedRequests` are set to `true`
requestWasSuccessful: function (req, res) {
return res.statusCode < 400;
},
skipFailedRequests: false, // Do not count failed requests
skipSuccessfulRequests: false, // Do not count successful requests
// allows to create custom keys (by default user IP is used)
keyGenerator: function (req /*, res*/) {
if (!req.ip) {
console.error(
"express-rate-limit: req.ip is undefined - you can avoid this by providing a custom keyGenerator function, but it may be indicative of a larger issue."
);
}
return req.ip;
},
skip: function (/*req, res*/) {
return false;
},
handler: function (req, res /*, next, optionsUsed*/) {
res.status(options.statusCode).send(options.message);
},
onLimitReached: function (/*req, res, optionsUsed*/) { },
requestPropertyName: "rateLimit", // Parameter name appended to req object
},
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) {
Promise.resolve(options.skip(req, res))
.then((skip) => {
if (skip) {
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[options.requestPropertyName] = {
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[options.requestPropertyName].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().toUTCString());
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[options.requestPropertyName].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 (!options.requestWasSuccessful(req, res)) {
decrementKey();
}
});
res.on("close", () => {
if (!res.finished) {
decrementKey();
}
});
res.on("error", () => decrementKey());
}
if (options.skipSuccessfulRequests) {
res.on("finish", function () {
if (options.requestWasSuccessful(req, res)) {
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, options);
}
next();
return null;
})
.catch(next);
});
return null;
})
.catch(next);
}
rateLimit.resetKey = options.store.resetKey.bind(options.store);
// Backward compatibility function
rateLimit.resetIp = rateLimit.resetKey;
return rateLimit;
}
module.exports = RateLimit;

View File

@ -0,0 +1,47 @@
"use strict";
function calculateNextResetTime(windowMs) {
const d = new Date();
d.setMilliseconds(d.getMilliseconds() + windowMs);
return d;
}
function MemoryStore(windowMs) {
let hits = {};
let resetTime = calculateNextResetTime(windowMs);
this.incr = function (key, cb) {
if (hits[key]) {
hits[key]++;
} else {
hits[key] = 1;
}
cb(null, hits[key], resetTime);
};
this.decrement = function (key) {
if (hits[key]) {
hits[key]--;
}
};
// export an API to allow hits all IPs to be reset
this.resetAll = function () {
hits = {};
resetTime = calculateNextResetTime(windowMs);
};
// export an API to allow hits from one IP to be reset
this.resetKey = function (key) {
delete hits[key];
};
// simply reset ALL hits every windowMs
const interval = setInterval(this.resetAll, windowMs);
if (interval.unref) {
interval.unref();
}
}
module.exports = MemoryStore;