From d301c12acd4f10e202a06241d997d46c55620833 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 6 Jul 2022 19:14:47 -0500 Subject: [PATCH] Remove dependency express-rate-limit --- package-lock.json | 5 - package.json | 1 - server/Server.js | 2 +- server/libs/expressRateLimit/LICENSE | 20 ++ server/libs/expressRateLimit/index.js | 196 +++++++++++++++++++ server/libs/expressRateLimit/memory-store.js | 47 +++++ 6 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 server/libs/expressRateLimit/LICENSE create mode 100644 server/libs/expressRateLimit/index.js create mode 100644 server/libs/expressRateLimit/memory-store.js diff --git a/package-lock.json b/package-lock.json index 01052b96..d3d3e5fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 61d31ef5..4f72599f 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/server/Server.js b/server/Server.js index f43d46c7..c2895de9 100644 --- a/server/Server.js +++ b/server/Server.js @@ -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') diff --git a/server/libs/expressRateLimit/LICENSE b/server/libs/expressRateLimit/LICENSE new file mode 100644 index 00000000..f4bb9cc3 --- /dev/null +++ b/server/libs/expressRateLimit/LICENSE @@ -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. \ No newline at end of file diff --git a/server/libs/expressRateLimit/index.js b/server/libs/expressRateLimit/index.js new file mode 100644 index 00000000..6df27ff5 --- /dev/null +++ b/server/libs/expressRateLimit/index.js @@ -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; diff --git a/server/libs/expressRateLimit/memory-store.js b/server/libs/expressRateLimit/memory-store.js new file mode 100644 index 00000000..60938dbc --- /dev/null +++ b/server/libs/expressRateLimit/memory-store.js @@ -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;