added rate limiting
parent
87f476f29b
commit
3b2a262a72
11
index.js
11
index.js
|
@ -5,10 +5,21 @@ const JSDOM = require('jsdom').JSDOM;
|
|||
|
||||
service = new turndown();
|
||||
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const rateLimiter = rateLimit({
|
||||
windowMs: 30 * 1000,
|
||||
max: 5,
|
||||
message: 'Rate limit exceeded',
|
||||
headers: true
|
||||
});
|
||||
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
const port = process.env.PORT
|
||||
|
||||
app.use(rateLimiter)
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
url = req.query.url;
|
||||
res.header("Access-Control-Allow-Origin", '*');
|
||||
|
|
|
@ -406,6 +406,17 @@
|
|||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.0.5.tgz",
|
||||
"integrity": "sha512-EB1mRTrzyyPfEsQZIQFXocd8NKZoDZbEwrtbdgkc20Yed6oYg02Xfjza2HHPI/0orp54BrFeHeT92ICB9ydokw==",
|
||||
"engines": {
|
||||
"node": ">= 14.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to
|
||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [6.0.5](https://github.com/nfriedly/express-rate-limit/releases/tag/v6.0.5)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Use named imports for ExpressJS types so users do not need to enable the
|
||||
`esModuleInterop` flag in their Typescript compiler configuration.
|
||||
|
||||
## [6.0.4](https://github.com/nfriedly/express-rate-limit/releases/tag/v6.0.4)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Upload the built package as a `.tgz` to GitHub releases.
|
||||
|
||||
### Changed
|
||||
|
||||
- Add ` main` and `module` fields to `package.json`. This helps tools such as
|
||||
ESLint that do not yet support the `exports` field.
|
||||
- Bumped the minimum node.js version in `package-lock.json` to match
|
||||
`package.json`
|
||||
|
||||
## [6.0.3](https://github.com/nfriedly/express-rate-limit/releases/tag/v6.0.3)
|
||||
|
||||
### Changed
|
||||
|
||||
- Bumped minimum Node version from 12.9 to 14.5 in `package.json` because the
|
||||
transpiled output uses the nullish coalescing operator (`??`), which
|
||||
[isn't supported in node.js prior to 14.x](https://node.green/#ES2020-features--nullish-coalescing-operator-----).
|
||||
|
||||
## [6.0.2](https://github.com/nfriedly/express-rate-limit/releases/v6.0.2)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure CommonJS projects can import the module.
|
||||
|
||||
### Added
|
||||
|
||||
- Add additional tests that test:
|
||||
- importing the library in `js-cjs`, `js-esm`, `ts-cjs`, `ts-esm`
|
||||
environments.
|
||||
- usage of the library with external stores (`redis`, `mongo`, `memcached`,
|
||||
`precise`).
|
||||
|
||||
### Changed
|
||||
|
||||
- Use [`esbuild`](https://esbuild.github.io/) to generate ESM and CJS output.
|
||||
This reduces the size of the built package from 138 kb to 13kb and build time
|
||||
to 4 ms! :rocket:
|
||||
- Use [`dts-bundle-generator`](https://github.com/timocov/dts-bundle-generator)
|
||||
to generate a single Typescript declaration file.
|
||||
|
||||
## [6.0.1](https://github.com/nfriedly/express-rate-limit/releases/v6.0.1)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ensure CommonJS projects can import the module.
|
||||
|
||||
## [6.0.0](https://github.com/nfriedly/express-rate-limit/releases/v6.0.0)
|
||||
|
||||
### Added
|
||||
|
||||
- `express` 4.x as a peer dependency.
|
||||
- Better Typescript support (the library was rewritten in Typescript).
|
||||
- Export the package as both ESM and CJS.
|
||||
- Publish the built package (`.tgz` file) on GitHub releases as well as the npm
|
||||
registry.
|
||||
- Issue and PR templates.
|
||||
- A contributing guide.
|
||||
|
||||
### Changed
|
||||
|
||||
- Rename the `draft_polli_ratelimit_headers` option to `standardHeaders`.
|
||||
- Rename the `headers` option to `legacyHeaders`.
|
||||
- `Retry-After` header is now sent if either `legacyHeaders` or
|
||||
`standardHeaders` is set.
|
||||
- Allow `keyGenerator` to be an async function/return a promise.
|
||||
- Change the way custom stores are defined.
|
||||
- Add the `init` method for stores to set themselves up using options passed
|
||||
to the middleware.
|
||||
- Rename the `incr` method to `increment`.
|
||||
- Allow the `increment`, `decrement`, `resetKey` and `resetAll` methods to
|
||||
return a promise.
|
||||
- Old stores will automatically be promisified and used.
|
||||
- The package can now only be used with NodeJS version 12.9.0 or greater.
|
||||
- The `onLimitReached` configuration option is now deprecated. Replace it with a
|
||||
custom `handler` that checks the number of hits.
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove the deprecated `limiter.resetIp` method (use the `limiter.resetKey`
|
||||
method instead).
|
||||
- Remove the deprecated options `delayMs`, `delayAfter` (the delay functionality
|
||||
was moved to the
|
||||
[`express-slow-down`](https://github.com/nfriedly/express-slow-down) package)
|
||||
and `global` (use a key generator that returns a constant value).
|
||||
|
||||
## [5.x](https://github.com/nfriedly/express-rate-limit/releases/tag/v5.5.1)
|
||||
|
||||
### Added
|
||||
|
||||
- The middleware ~throws~ logs an error if `request.ip` is undefined.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removes typescript typings. (See
|
||||
[#138](https://github.com/nfriedly/express-rate-limit/issues/138))
|
||||
|
||||
## [4.x](https://github.com/nfriedly/express-rate-limit/releases/tag/v4.0.4)
|
||||
|
||||
### Changed
|
||||
|
||||
- The library no longer modifies the passed-in options object, it instead makes
|
||||
a clone of it.
|
||||
|
||||
## [3.x](https://github.com/nfriedly/express-rate-limit/releases/tag/v3.5.2)
|
||||
|
||||
### Added
|
||||
|
||||
- Simplifies the default `handler` function so that it no longer changes the
|
||||
response format. The default handler also uses
|
||||
[response.send](https://expressjs.com/en/4x/api.html#response.send).
|
||||
|
||||
### Changes
|
||||
|
||||
- `onLimitReached` now only triggers once for a client and window. However, the
|
||||
`handle` method is called for every blocked request.
|
||||
|
||||
### Removed
|
||||
|
||||
- The `delayAfter` and `delayMs` options; they were moved to the
|
||||
[express-slow-down](https://npmjs.org/package/express-slow-down) package.
|
||||
|
||||
## [2.x](https://github.com/nfriedly/express-rate-limit/releases/tag/v2.14.2)
|
||||
|
||||
### Added
|
||||
|
||||
- A `limiter.resetKey()` method to reset the hit counter for a particular client
|
||||
|
||||
### Changes
|
||||
|
||||
- The rate limiter now uses a less precise but less resource intensive method of
|
||||
tracking hits from a client.
|
||||
|
||||
### Removed
|
||||
|
||||
- The `global` option.
|
|
@ -0,0 +1,226 @@
|
|||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __markAsModule = (target) => __defProp(target, "__esModule", { value: true });
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __reExport = (target, module2, copyDefault, desc) => {
|
||||
if (module2 && typeof module2 === "object" || typeof module2 === "function") {
|
||||
for (let key of __getOwnPropNames(module2))
|
||||
if (!__hasOwnProp.call(target, key) && (copyDefault || key !== "default"))
|
||||
__defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable });
|
||||
}
|
||||
return target;
|
||||
};
|
||||
var __toCommonJS = /* @__PURE__ */ ((cache) => {
|
||||
return (module2, temp) => {
|
||||
return cache && cache.get(module2) || (temp = __reExport(__markAsModule({}), module2, 1), cache && cache.set(module2, temp), temp);
|
||||
};
|
||||
})(typeof WeakMap !== "undefined" ? /* @__PURE__ */ new WeakMap() : 0);
|
||||
|
||||
// source/index.ts
|
||||
var source_exports = {};
|
||||
__export(source_exports, {
|
||||
default: () => source_default
|
||||
});
|
||||
|
||||
// source/memory-store.ts
|
||||
var calculateNextResetTime = (windowMs) => {
|
||||
const resetTime = new Date();
|
||||
resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs);
|
||||
return resetTime;
|
||||
};
|
||||
var MemoryStore = class {
|
||||
init(options) {
|
||||
this.windowMs = options.windowMs;
|
||||
this.resetTime = calculateNextResetTime(this.windowMs);
|
||||
this.hits = {};
|
||||
const interval = setInterval(async () => {
|
||||
await this.resetAll();
|
||||
}, this.windowMs);
|
||||
if (interval.unref) {
|
||||
interval.unref();
|
||||
}
|
||||
}
|
||||
async increment(key) {
|
||||
const totalHits = (this.hits[key] ?? 0) + 1;
|
||||
this.hits[key] = totalHits;
|
||||
return {
|
||||
totalHits,
|
||||
resetTime: this.resetTime
|
||||
};
|
||||
}
|
||||
async decrement(key) {
|
||||
const current = this.hits[key];
|
||||
if (current) {
|
||||
this.hits[key] = current - 1;
|
||||
}
|
||||
}
|
||||
async resetKey(key) {
|
||||
delete this.hits[key];
|
||||
}
|
||||
async resetAll() {
|
||||
this.hits = {};
|
||||
this.resetTime = calculateNextResetTime(this.windowMs);
|
||||
}
|
||||
};
|
||||
|
||||
// source/lib.ts
|
||||
var isLegacyStore = (store) => typeof store.incr === "function" && typeof store.increment !== "function";
|
||||
var promisifyStore = (passedStore) => {
|
||||
if (!isLegacyStore(passedStore)) {
|
||||
return passedStore;
|
||||
}
|
||||
const legacyStore = passedStore;
|
||||
class PromisifiedStore {
|
||||
async increment(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
legacyStore.incr(key, (error, totalHits, resetTime) => {
|
||||
if (error)
|
||||
reject(error);
|
||||
resolve({ totalHits, resetTime });
|
||||
});
|
||||
});
|
||||
}
|
||||
async decrement(key) {
|
||||
return Promise.resolve(legacyStore.decrement(key));
|
||||
}
|
||||
async resetKey(key) {
|
||||
return Promise.resolve(legacyStore.resetKey(key));
|
||||
}
|
||||
async resetAll() {
|
||||
if (typeof legacyStore.resetAll === "function")
|
||||
return Promise.resolve(legacyStore.resetAll());
|
||||
}
|
||||
}
|
||||
return new PromisifiedStore();
|
||||
};
|
||||
var parseOptions = (passedOptions) => {
|
||||
const options = {
|
||||
windowMs: 60 * 1e3,
|
||||
store: new MemoryStore(),
|
||||
max: 5,
|
||||
message: "Too many requests, please try again later.",
|
||||
statusCode: 429,
|
||||
legacyHeaders: passedOptions.headers ?? true,
|
||||
standardHeaders: passedOptions.draft_polli_ratelimit_headers ?? false,
|
||||
requestPropertyName: "rateLimit",
|
||||
skipFailedRequests: false,
|
||||
skipSuccessfulRequests: false,
|
||||
requestWasSuccessful: (_request, response) => response.statusCode < 400,
|
||||
skip: (_request, _response) => false,
|
||||
keyGenerator: (request, _response) => {
|
||||
if (!request.ip) {
|
||||
console.error("WARN | `express-rate-limit` | `request.ip` is undefined. You can avoid this by providing a custom `keyGenerator` function, but it may be indicative of a larger issue.");
|
||||
}
|
||||
return request.ip;
|
||||
},
|
||||
handler: (_request, response, _next, _optionsUsed) => {
|
||||
response.status(options.statusCode).send(options.message);
|
||||
},
|
||||
onLimitReached: (_request, _response, _optionsUsed) => {
|
||||
},
|
||||
...passedOptions
|
||||
};
|
||||
if (typeof options.store.incr !== "function" && typeof options.store.increment !== "function" || typeof options.store.decrement !== "function" || typeof options.store.resetKey !== "function" || typeof options.store.resetAll !== "undefined" && typeof options.store.resetAll !== "function" || typeof options.store.init !== "undefined" && typeof options.store.init !== "function") {
|
||||
throw new TypeError("An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface.");
|
||||
}
|
||||
options.store = promisifyStore(options.store);
|
||||
return options;
|
||||
};
|
||||
var handleAsyncErrors = (fn) => async (request, response, next) => {
|
||||
try {
|
||||
await Promise.resolve(fn(request, response, next)).catch(next);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
var rateLimit = (passedOptions) => {
|
||||
const options = parseOptions(passedOptions ?? {});
|
||||
if (typeof options.store.init === "function")
|
||||
options.store.init(options);
|
||||
const middleware = handleAsyncErrors(async (request, response, next) => {
|
||||
const skip = await options.skip(request, response);
|
||||
if (skip) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const augmentedRequest = request;
|
||||
const key = await options.keyGenerator(request, response);
|
||||
const { totalHits, resetTime } = await options.store.increment(key);
|
||||
const retrieveQuota = typeof options.max === "function" ? options.max(request, response) : options.max;
|
||||
const maxHits = await retrieveQuota;
|
||||
augmentedRequest[options.requestPropertyName] = {
|
||||
limit: maxHits,
|
||||
current: totalHits,
|
||||
remaining: Math.max(maxHits - totalHits, 0),
|
||||
resetTime
|
||||
};
|
||||
if (options.legacyHeaders && !response.headersSent) {
|
||||
response.setHeader("X-RateLimit-Limit", maxHits);
|
||||
response.setHeader("X-RateLimit-Remaining", augmentedRequest[options.requestPropertyName].remaining);
|
||||
if (resetTime instanceof Date) {
|
||||
response.setHeader("Date", new Date().toUTCString());
|
||||
response.setHeader("X-RateLimit-Reset", Math.ceil(resetTime.getTime() / 1e3));
|
||||
}
|
||||
}
|
||||
if (options.standardHeaders && !response.headersSent) {
|
||||
response.setHeader("RateLimit-Limit", maxHits);
|
||||
response.setHeader("RateLimit-Remaining", augmentedRequest[options.requestPropertyName].remaining);
|
||||
if (resetTime) {
|
||||
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
|
||||
response.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
|
||||
}
|
||||
}
|
||||
if (options.skipFailedRequests || options.skipSuccessfulRequests) {
|
||||
let decremented = false;
|
||||
const decrementKey = async () => {
|
||||
if (!decremented) {
|
||||
await options.store.decrement(key);
|
||||
decremented = true;
|
||||
}
|
||||
};
|
||||
if (options.skipFailedRequests) {
|
||||
response.on("finish", async () => {
|
||||
if (!options.requestWasSuccessful(request, response))
|
||||
await decrementKey();
|
||||
});
|
||||
response.on("close", async () => {
|
||||
if (!response.writableEnded)
|
||||
await decrementKey();
|
||||
});
|
||||
response.on("error", async () => {
|
||||
await decrementKey();
|
||||
});
|
||||
}
|
||||
if (options.skipSuccessfulRequests) {
|
||||
response.on("finish", async () => {
|
||||
if (options.requestWasSuccessful(request, response))
|
||||
await decrementKey();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (maxHits && totalHits === maxHits + 1) {
|
||||
options.onLimitReached(request, response, options);
|
||||
}
|
||||
if (maxHits && totalHits > maxHits) {
|
||||
if ((options.legacyHeaders || options.standardHeaders) && !response.headersSent) {
|
||||
response.setHeader("Retry-After", Math.ceil(options.windowMs / 1e3));
|
||||
}
|
||||
options.handler(request, response, next, options);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
middleware.resetKey = options.store.resetKey.bind(options.store);
|
||||
return middleware;
|
||||
};
|
||||
var lib_default = rateLimit;
|
||||
|
||||
// source/index.ts
|
||||
var source_default = lib_default;
|
||||
module.exports = __toCommonJS(source_exports);
|
||||
module.exports = rateLimit;
|
|
@ -0,0 +1,248 @@
|
|||
// Generated by dts-bundle-generator v6.3.0
|
||||
|
||||
import { NextFunction, Request, RequestHandler, Response } from 'express';
|
||||
|
||||
/**
|
||||
* Callback that fires when a client's hit counter is incremented.
|
||||
*
|
||||
* @param error {Error | undefined} - The error that occurred, if any
|
||||
* @param totalHits {number} - The number of hits for that client so far
|
||||
* @param resetTime {Date | undefined} - The time when the counter resets
|
||||
*/
|
||||
export declare type IncrementCallback = (error: Error | undefined, totalHits: number, resetTime: Date | undefined) => void;
|
||||
/**
|
||||
* Method (in the form of middleware) to generate/retrieve a value based on the
|
||||
* incoming request
|
||||
*
|
||||
* @param request {Request} - The Express request object
|
||||
* @param response {Response} - The Express response object
|
||||
*
|
||||
* @returns {T} - The value needed
|
||||
*/
|
||||
export declare type ValueDeterminingMiddleware<T> = (request: Request, response: Response) => T | Promise<T>;
|
||||
/**
|
||||
* Express request handler that sends back a response when a client is
|
||||
* rate-limited.
|
||||
*
|
||||
* @param request {Request} - The Express request object
|
||||
* @param response {Response} - The Express response object
|
||||
* @param next {NextFunction} - The Express `next` function, can be called to skip responding
|
||||
* @param optionsUsed {Options} - The options used to set up the middleware
|
||||
*/
|
||||
export declare type RateLimitExceededEventHandler = (request: Request, response: Response, next: NextFunction, optionsUsed: Options) => void;
|
||||
/**
|
||||
* Event callback that is triggered on a client's first request that exceeds the limit
|
||||
* but not for subsequent requests. May be used for logging, etc. Should *not*
|
||||
* send a response.
|
||||
*
|
||||
* @param request {Request} - The Express request object
|
||||
* @param response {Response} - The Express response object
|
||||
* @param optionsUsed {Options} - The options used to set up the middleware
|
||||
*/
|
||||
export declare type RateLimitReachedEventHandler = (request: Request, response: Response, optionsUsed: Options) => void;
|
||||
/**
|
||||
* Data returned from the `Store` when a client's hit counter is incremented.
|
||||
*
|
||||
* @property totalHits {number} - The number of hits for that client so far
|
||||
* @property resetTime {Date | undefined} - The time when the counter resets
|
||||
*/
|
||||
export declare type IncrementResponse = {
|
||||
totalHits: number;
|
||||
resetTime: Date | undefined;
|
||||
};
|
||||
/**
|
||||
* A modified Express request handler with the rate limit functions.
|
||||
*/
|
||||
export declare type RateLimitRequestHandler = RequestHandler & {
|
||||
/**
|
||||
* Method to reset a client's hit counter.
|
||||
*
|
||||
* @param key {string} - The identifier for a client
|
||||
*/
|
||||
resetKey: (key: string) => void;
|
||||
};
|
||||
/**
|
||||
* An interface that all hit counter stores must implement.
|
||||
*
|
||||
* @deprecated 6.x - Implement the `Store` interface instead.
|
||||
*/
|
||||
export interface LegacyStore {
|
||||
/**
|
||||
* Method to increment a client's hit counter.
|
||||
*
|
||||
* @param key {string} - The identifier for a client
|
||||
* @param callback {IncrementCallback} - The callback to call once the counter is incremented
|
||||
*/
|
||||
incr: (key: string, callback: IncrementCallback) => void;
|
||||
/**
|
||||
* Method to decrement a client's hit counter.
|
||||
*
|
||||
* @param key {string} - The identifier for a client
|
||||
*/
|
||||
decrement: (key: string) => void;
|
||||
/**
|
||||
* Method to reset a client's hit counter.
|
||||
*
|
||||
* @param key {string} - The identifier for a client
|
||||
*/
|
||||
resetKey: (key: string) => void;
|
||||
/**
|
||||
* Method to reset everyone's hit counter.
|
||||
*/
|
||||
resetAll?: () => void;
|
||||
}
|
||||
/**
|
||||
* An interface that all hit counter stores must implement.
|
||||
*/
|
||||
export interface Store {
|
||||
/**
|
||||
* Method that initializes the store, and has access to the options passed to
|
||||
* the middleware too.
|
||||
*
|
||||
* @param options {Options} - The options used to setup the middleware
|
||||
*/
|
||||
init?: (options: Options) => void;
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
increment: (key: string) => Promise<IncrementResponse> | IncrementResponse;
|
||||
/**
|
||||
* Method to decrement a client's hit counter.
|
||||
*
|
||||
* @param key {string} - The identifier for a client
|
||||
*/
|
||||
decrement: (key: string) => Promise<void> | void;
|
||||
/**
|
||||
* Method to reset a client's hit counter.
|
||||
*
|
||||
* @param key {string} - The identifier for a client
|
||||
*/
|
||||
resetKey: (key: string) => Promise<void> | void;
|
||||
/**
|
||||
* Method to reset everyone's hit counter.
|
||||
*/
|
||||
resetAll?: () => Promise<void> | void;
|
||||
}
|
||||
/**
|
||||
* The configuration options for the rate limiter.
|
||||
*/
|
||||
export interface Options {
|
||||
/**
|
||||
* How long we should remember the requests.
|
||||
*/
|
||||
readonly windowMs: number;
|
||||
/**
|
||||
* The maximum number of connection to allow during the `window` before
|
||||
* rate limiting the client.
|
||||
*
|
||||
* Can be the limit itself as a number or express middleware that parses
|
||||
* the request and then figures out the limit.
|
||||
*/
|
||||
readonly max: number | ValueDeterminingMiddleware<number>;
|
||||
/**
|
||||
* The response body to send back when a client is rate limited.
|
||||
*/
|
||||
readonly message: any;
|
||||
/**
|
||||
* The HTTP status code to send back when a client is rate limited.
|
||||
*
|
||||
* Defaults to `HTTP 429 Too Many Requests` (RFC 6585).
|
||||
*/
|
||||
readonly statusCode: number;
|
||||
/**
|
||||
* Whether to send `X-RateLimit-*` headers with the rate limit and the number
|
||||
* of requests.
|
||||
*/
|
||||
readonly legacyHeaders: boolean;
|
||||
/**
|
||||
* Whether to enable support for the rate limit standardization headers (`RateLimit-*`).
|
||||
*/
|
||||
readonly standardHeaders: boolean;
|
||||
/**
|
||||
* The name of the property on the request object to store the rate limit info.
|
||||
*
|
||||
* Defaults to `rateLimit`.
|
||||
*/
|
||||
readonly requestPropertyName: string;
|
||||
/**
|
||||
* If `true`, the library will (by default) skip all requests that have a 4XX
|
||||
* or 5XX status.
|
||||
*/
|
||||
readonly skipFailedRequests: boolean;
|
||||
/**
|
||||
* If `true`, the library will (by default) skip all requests that have a
|
||||
* status code less than 400.
|
||||
*/
|
||||
readonly skipSuccessfulRequests: boolean;
|
||||
/**
|
||||
* Method to determine whether or not the request counts as 'succesful'. Used
|
||||
* when either `skipSuccessfulRequests` or `skipFailedRequests` is set to true.
|
||||
*/
|
||||
readonly requestWasSuccessful: ValueDeterminingMiddleware<boolean>;
|
||||
/**
|
||||
* Method to generate custom identifiers for clients.
|
||||
*
|
||||
* By default, the client's IP address is used.
|
||||
*/
|
||||
readonly keyGenerator: ValueDeterminingMiddleware<string>;
|
||||
/**
|
||||
* Method (in the form of middleware) to determine whether or not this request
|
||||
* counts towards a client's quota.
|
||||
*/
|
||||
readonly skip: ValueDeterminingMiddleware<boolean>;
|
||||
/**
|
||||
* Express request handler that sends back a response when a client is
|
||||
* rate-limited.
|
||||
*/
|
||||
readonly handler: RateLimitExceededEventHandler;
|
||||
/**
|
||||
* Express request handler that sends back a response when a client has
|
||||
* reached their rate limit, and will be rate limited on their next request.
|
||||
*/
|
||||
readonly onLimitReached: RateLimitReachedEventHandler;
|
||||
/**
|
||||
* The {@link Store} to use to store the hit count for each client.
|
||||
*/
|
||||
store: Store;
|
||||
/**
|
||||
* Whether to send `X-RateLimit-*` headers with the rate limit and the number
|
||||
* of requests.
|
||||
*
|
||||
* @deprecated 6.x - This option was renamed to `legacyHeaders`.
|
||||
*/
|
||||
headers?: boolean;
|
||||
/**
|
||||
* Whether to send `RateLimit-*` headers with the rate limit and the number
|
||||
* of requests.
|
||||
*
|
||||
* @deprecated 6.x - This option was renamed to `standardHeaders`.
|
||||
*/
|
||||
draft_polli_ratelimit_headers?: boolean;
|
||||
}
|
||||
/**
|
||||
* The extended request object that includes information about the client's
|
||||
* rate limit.
|
||||
*/
|
||||
export declare type AugmentedRequest = Request & {
|
||||
[key: string]: RateLimitInfo;
|
||||
};
|
||||
/**
|
||||
* The rate limit related information for each client included in the
|
||||
* Express request object.
|
||||
*/
|
||||
export interface RateLimitInfo {
|
||||
readonly limit: number;
|
||||
readonly current: number;
|
||||
readonly remaining: number;
|
||||
readonly resetTime: Date | undefined;
|
||||
}
|
||||
declare const rateLimit: (passedOptions?: (Omit<Partial<Options>, "store"> & {
|
||||
store?: LegacyStore | Store | undefined;
|
||||
}) | undefined) => RateLimitRequestHandler;
|
||||
export default rateLimit;
|
||||
|
||||
export {};
|
|
@ -0,0 +1,198 @@
|
|||
// source/memory-store.ts
|
||||
var calculateNextResetTime = (windowMs) => {
|
||||
const resetTime = new Date();
|
||||
resetTime.setMilliseconds(resetTime.getMilliseconds() + windowMs);
|
||||
return resetTime;
|
||||
};
|
||||
var MemoryStore = class {
|
||||
init(options) {
|
||||
this.windowMs = options.windowMs;
|
||||
this.resetTime = calculateNextResetTime(this.windowMs);
|
||||
this.hits = {};
|
||||
const interval = setInterval(async () => {
|
||||
await this.resetAll();
|
||||
}, this.windowMs);
|
||||
if (interval.unref) {
|
||||
interval.unref();
|
||||
}
|
||||
}
|
||||
async increment(key) {
|
||||
const totalHits = (this.hits[key] ?? 0) + 1;
|
||||
this.hits[key] = totalHits;
|
||||
return {
|
||||
totalHits,
|
||||
resetTime: this.resetTime
|
||||
};
|
||||
}
|
||||
async decrement(key) {
|
||||
const current = this.hits[key];
|
||||
if (current) {
|
||||
this.hits[key] = current - 1;
|
||||
}
|
||||
}
|
||||
async resetKey(key) {
|
||||
delete this.hits[key];
|
||||
}
|
||||
async resetAll() {
|
||||
this.hits = {};
|
||||
this.resetTime = calculateNextResetTime(this.windowMs);
|
||||
}
|
||||
};
|
||||
|
||||
// source/lib.ts
|
||||
var isLegacyStore = (store) => typeof store.incr === "function" && typeof store.increment !== "function";
|
||||
var promisifyStore = (passedStore) => {
|
||||
if (!isLegacyStore(passedStore)) {
|
||||
return passedStore;
|
||||
}
|
||||
const legacyStore = passedStore;
|
||||
class PromisifiedStore {
|
||||
async increment(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
legacyStore.incr(key, (error, totalHits, resetTime) => {
|
||||
if (error)
|
||||
reject(error);
|
||||
resolve({ totalHits, resetTime });
|
||||
});
|
||||
});
|
||||
}
|
||||
async decrement(key) {
|
||||
return Promise.resolve(legacyStore.decrement(key));
|
||||
}
|
||||
async resetKey(key) {
|
||||
return Promise.resolve(legacyStore.resetKey(key));
|
||||
}
|
||||
async resetAll() {
|
||||
if (typeof legacyStore.resetAll === "function")
|
||||
return Promise.resolve(legacyStore.resetAll());
|
||||
}
|
||||
}
|
||||
return new PromisifiedStore();
|
||||
};
|
||||
var parseOptions = (passedOptions) => {
|
||||
const options = {
|
||||
windowMs: 60 * 1e3,
|
||||
store: new MemoryStore(),
|
||||
max: 5,
|
||||
message: "Too many requests, please try again later.",
|
||||
statusCode: 429,
|
||||
legacyHeaders: passedOptions.headers ?? true,
|
||||
standardHeaders: passedOptions.draft_polli_ratelimit_headers ?? false,
|
||||
requestPropertyName: "rateLimit",
|
||||
skipFailedRequests: false,
|
||||
skipSuccessfulRequests: false,
|
||||
requestWasSuccessful: (_request, response) => response.statusCode < 400,
|
||||
skip: (_request, _response) => false,
|
||||
keyGenerator: (request, _response) => {
|
||||
if (!request.ip) {
|
||||
console.error("WARN | `express-rate-limit` | `request.ip` is undefined. You can avoid this by providing a custom `keyGenerator` function, but it may be indicative of a larger issue.");
|
||||
}
|
||||
return request.ip;
|
||||
},
|
||||
handler: (_request, response, _next, _optionsUsed) => {
|
||||
response.status(options.statusCode).send(options.message);
|
||||
},
|
||||
onLimitReached: (_request, _response, _optionsUsed) => {
|
||||
},
|
||||
...passedOptions
|
||||
};
|
||||
if (typeof options.store.incr !== "function" && typeof options.store.increment !== "function" || typeof options.store.decrement !== "function" || typeof options.store.resetKey !== "function" || typeof options.store.resetAll !== "undefined" && typeof options.store.resetAll !== "function" || typeof options.store.init !== "undefined" && typeof options.store.init !== "function") {
|
||||
throw new TypeError("An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface.");
|
||||
}
|
||||
options.store = promisifyStore(options.store);
|
||||
return options;
|
||||
};
|
||||
var handleAsyncErrors = (fn) => async (request, response, next) => {
|
||||
try {
|
||||
await Promise.resolve(fn(request, response, next)).catch(next);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
var rateLimit = (passedOptions) => {
|
||||
const options = parseOptions(passedOptions ?? {});
|
||||
if (typeof options.store.init === "function")
|
||||
options.store.init(options);
|
||||
const middleware = handleAsyncErrors(async (request, response, next) => {
|
||||
const skip = await options.skip(request, response);
|
||||
if (skip) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const augmentedRequest = request;
|
||||
const key = await options.keyGenerator(request, response);
|
||||
const { totalHits, resetTime } = await options.store.increment(key);
|
||||
const retrieveQuota = typeof options.max === "function" ? options.max(request, response) : options.max;
|
||||
const maxHits = await retrieveQuota;
|
||||
augmentedRequest[options.requestPropertyName] = {
|
||||
limit: maxHits,
|
||||
current: totalHits,
|
||||
remaining: Math.max(maxHits - totalHits, 0),
|
||||
resetTime
|
||||
};
|
||||
if (options.legacyHeaders && !response.headersSent) {
|
||||
response.setHeader("X-RateLimit-Limit", maxHits);
|
||||
response.setHeader("X-RateLimit-Remaining", augmentedRequest[options.requestPropertyName].remaining);
|
||||
if (resetTime instanceof Date) {
|
||||
response.setHeader("Date", new Date().toUTCString());
|
||||
response.setHeader("X-RateLimit-Reset", Math.ceil(resetTime.getTime() / 1e3));
|
||||
}
|
||||
}
|
||||
if (options.standardHeaders && !response.headersSent) {
|
||||
response.setHeader("RateLimit-Limit", maxHits);
|
||||
response.setHeader("RateLimit-Remaining", augmentedRequest[options.requestPropertyName].remaining);
|
||||
if (resetTime) {
|
||||
const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
|
||||
response.setHeader("RateLimit-Reset", Math.max(0, deltaSeconds));
|
||||
}
|
||||
}
|
||||
if (options.skipFailedRequests || options.skipSuccessfulRequests) {
|
||||
let decremented = false;
|
||||
const decrementKey = async () => {
|
||||
if (!decremented) {
|
||||
await options.store.decrement(key);
|
||||
decremented = true;
|
||||
}
|
||||
};
|
||||
if (options.skipFailedRequests) {
|
||||
response.on("finish", async () => {
|
||||
if (!options.requestWasSuccessful(request, response))
|
||||
await decrementKey();
|
||||
});
|
||||
response.on("close", async () => {
|
||||
if (!response.writableEnded)
|
||||
await decrementKey();
|
||||
});
|
||||
response.on("error", async () => {
|
||||
await decrementKey();
|
||||
});
|
||||
}
|
||||
if (options.skipSuccessfulRequests) {
|
||||
response.on("finish", async () => {
|
||||
if (options.requestWasSuccessful(request, response))
|
||||
await decrementKey();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (maxHits && totalHits === maxHits + 1) {
|
||||
options.onLimitReached(request, response, options);
|
||||
}
|
||||
if (maxHits && totalHits > maxHits) {
|
||||
if ((options.legacyHeaders || options.standardHeaders) && !response.headersSent) {
|
||||
response.setHeader("Retry-After", Math.ceil(options.windowMs / 1e3));
|
||||
}
|
||||
options.handler(request, response, next, options);
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
middleware.resetKey = options.store.resetKey.bind(options.store);
|
||||
return middleware;
|
||||
};
|
||||
var lib_default = rateLimit;
|
||||
|
||||
// source/index.ts
|
||||
var source_default = lib_default;
|
||||
export {
|
||||
source_default as default
|
||||
};
|
|
@ -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.
|
|
@ -0,0 +1,144 @@
|
|||
{
|
||||
"name": "express-rate-limit",
|
||||
"version": "6.0.5",
|
||||
"description": "Basic IP rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset.",
|
||||
"author": {
|
||||
"name": "Nathan Friedly",
|
||||
"url": "http://nfriedly.com/"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/nfriedly/express-rate-limit",
|
||||
"repository": "https://github.com/nfriedly/express-rate-limit",
|
||||
"keywords": [
|
||||
"express-rate-limit",
|
||||
"express",
|
||||
"rate",
|
||||
"limit",
|
||||
"ratelimit",
|
||||
"rate-limit",
|
||||
"middleware",
|
||||
"ip",
|
||||
"auth",
|
||||
"authorization",
|
||||
"security",
|
||||
"brute",
|
||||
"force",
|
||||
"bruteforce",
|
||||
"brute-force",
|
||||
"attack"
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/",
|
||||
"tsconfig.json",
|
||||
"package.json",
|
||||
"readme.md",
|
||||
"license.md",
|
||||
"changelog.md"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 14.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "del-cli dist/ coverage/ *.log *.tmp *.bak *.tgz",
|
||||
"build:cjs": "esbuild --bundle --format=cjs --outfile=dist/index.cjs --footer:js=\"module.exports = rateLimit;\" source/index.ts",
|
||||
"build:esm": "esbuild --bundle --format=esm --outfile=dist/index.mjs source/index.ts",
|
||||
"build:types": "dts-bundle-generator --out-file=dist/index.d.ts source/index.ts",
|
||||
"compile": "run-s clean build:*",
|
||||
"lint:code": "xo --ignore test/external/",
|
||||
"lint:rest": "prettier --ignore-path .gitignore --ignore-unknown --check .",
|
||||
"lint": "run-s lint:*",
|
||||
"autofix:code": "xo --ignore test/external/ --fix",
|
||||
"autofix:rest": "prettier --ignore-path .gitignore --ignore-unknown --write .",
|
||||
"autofix": "run-s autofix:*",
|
||||
"test:lib": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
|
||||
"test:ext": "npm pack && cd test/external/ && bash run-all-tests",
|
||||
"test": "run-s lint test:*",
|
||||
"pre-commit": "lint-staged",
|
||||
"prepare": "run-s compile && husky install config/husky"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": "^4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^27.4.6",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/node": "^16.11.17",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"cross-env": "^7.0.3",
|
||||
"del-cli": "^4.0.1",
|
||||
"dts-bundle-generator": "^6.3.0",
|
||||
"esbuild": "^0.14.10",
|
||||
"express": "^4.17.1",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^27.4.7",
|
||||
"lint-staged": "^12.1.5",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"supertest": "^6.1.6",
|
||||
"ts-jest": "^27.1.1",
|
||||
"ts-node": "^10.4.0",
|
||||
"typescript": "^4.5.2",
|
||||
"xo": "^0.47.0"
|
||||
},
|
||||
"xo": {
|
||||
"prettier": true,
|
||||
"rules": {
|
||||
"@typescript-eslint/no-empty-function": 0,
|
||||
"@typescript-eslint/no-dynamic-delete": 0,
|
||||
"@typescript-eslint/no-confusing-void-expression": 0,
|
||||
"@typescript-eslint/consistent-indexed-object-style": [
|
||||
"error",
|
||||
"index-signature"
|
||||
]
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"bracketSpacing": true,
|
||||
"trailingComma": "all",
|
||||
"proseWrap": "always"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest/presets/default-esm",
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"useESM": true
|
||||
}
|
||||
},
|
||||
"verbose": true,
|
||||
"collectCoverage": true,
|
||||
"collectCoverageFrom": [
|
||||
"source/**/*.ts"
|
||||
],
|
||||
"testTimeout": 30000,
|
||||
"testMatch": [
|
||||
"**/test/library/**/*-test.[jt]s?(x)"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"jsx",
|
||||
"json",
|
||||
"ts",
|
||||
"tsx"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^(\\.{1,2}/.*)\\.js$": "$1"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"{source,test}/**/*.ts": "xo --ignore test/external/ --fix",
|
||||
"**/*.{json,yaml,md}": "prettier --ignore-path .gitignore --ignore-unknown --write "
|
||||
}
|
||||
}
|
|
@ -0,0 +1,515 @@
|
|||
# <div align="center"> Express Rate Limit </div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Tests](https://github.com/nfriedly/express-rate-limit/workflows/Test/badge.svg)](https://github.com/nfriedly/express-rate-limit/actions)
|
||||
[![npm version](https://img.shields.io/npm/v/express-rate-limit.svg)](https://npmjs.org/package/express-rate-limit 'View this project on NPM')
|
||||
[![npm downloads](https://img.shields.io/npm/dm/express-rate-limit)](https://www.npmjs.com/package/express-rate-limit)
|
||||
|
||||
Basic rate-limiting middleware for Express. Use to limit repeated requests to
|
||||
public APIs and/or endpoints such as password reset. Plays nice with
|
||||
[express-slow-down](https://www.npmjs.com/package/express-slow-down).
|
||||
|
||||
</div>
|
||||
|
||||
### Alternate Rate Limiters
|
||||
|
||||
> This module does not share state with other processes/servers by default. If
|
||||
> you need a more robust solution, I recommend using an external store. See the
|
||||
> [`stores` section](#store) below for a list of external stores.
|
||||
|
||||
This module was designed to only handle the basics and didn't even support
|
||||
external stores initially. These other options all are excellent pieces of
|
||||
software and may be more appropriate for some situations:
|
||||
|
||||
- [rate-limiter-flexible](https://www.npmjs.com/package/rate-limiter-flexible)
|
||||
- [express-brute](https://www.npmjs.com/package/express-brute)
|
||||
- [rate-limiter](https://www.npmjs.com/package/express-limiter)
|
||||
|
||||
## Installation
|
||||
|
||||
From the npm registry:
|
||||
|
||||
```sh
|
||||
# Using npm
|
||||
> npm install express-rate-limit
|
||||
# Using yarn or pnpm
|
||||
> yarn/pnpm add express-rate-limit
|
||||
```
|
||||
|
||||
From Github Releases:
|
||||
|
||||
```sh
|
||||
# Using npm
|
||||
> npm install https://github.com/nfriedly/express-rate-limit/releases/download/v{version}/express-rate-limit.tgz
|
||||
# Using yarn or pnpm
|
||||
> yarn/pnpm add https://github.com/nfriedly/express-rate-limit/releases/download/v{version}/express-rate-limit.tgz
|
||||
```
|
||||
|
||||
Replace `{version}` with the version of the package that you want to your, e.g.:
|
||||
`6.0.0`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Importing
|
||||
|
||||
This library is provided in ESM as well as CJS forms, and works with both
|
||||
Javascript and Typescript projects.
|
||||
|
||||
**This package requires you to use Node 14 or above.**
|
||||
|
||||
Import it in a CommonJS project (`type: commonjs` or no `type` field in
|
||||
`package.json`) as follows:
|
||||
|
||||
```ts
|
||||
const rateLimit = require('express-rate-limit')
|
||||
```
|
||||
|
||||
Import it in a ESM project (`type: module` in `package.json`) as follows:
|
||||
|
||||
```ts
|
||||
import rateLimit from 'express-rate-limit'
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
To use it in an API-only server where the rate-limiter should be applied to all
|
||||
requests:
|
||||
|
||||
```ts
|
||||
import rateLimit from 'express-rate-limit'
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
})
|
||||
|
||||
// Apply the rate limiting middleware to all requests
|
||||
app.use(limiter)
|
||||
```
|
||||
|
||||
To use it in a 'regular' web server (e.g. anything that uses
|
||||
`express.static()`), where the rate-limiter should only apply to certain
|
||||
requests:
|
||||
|
||||
```ts
|
||||
import rateLimit from 'express-rate-limit'
|
||||
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
})
|
||||
|
||||
// Apply the rate limiting middleware to API calls only
|
||||
app.use('/api', apiLimiter)
|
||||
```
|
||||
|
||||
To create multiple instances to apply different rules to different endpoints:
|
||||
|
||||
```ts
|
||||
import rateLimit from 'express-rate-limit'
|
||||
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
})
|
||||
|
||||
app.use('/api/', apiLimiter)
|
||||
|
||||
const createAccountLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 5, // Limit each IP to 5 create account requests per `window` (here, per hour)
|
||||
message:
|
||||
'Too many accounts created from this IP, please try again after an hour',
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
})
|
||||
|
||||
app.post('/create-account', createAccountLimiter, (request, response) => {
|
||||
//...
|
||||
})
|
||||
```
|
||||
|
||||
To use a custom store:
|
||||
|
||||
```ts
|
||||
import rateLimit from 'express-rate-limit'
|
||||
import MemoryStore from 'express-rate-limit/memory-store.js'
|
||||
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
store: new MemoryStore(),
|
||||
})
|
||||
|
||||
// Apply the rate limiting middleware to API calls only
|
||||
app.use('/api', apiLimiter)
|
||||
```
|
||||
|
||||
> **Note:** most stores will require additional configuration, such as custom
|
||||
> prefixes, when using multiple instances. The default built-in memory store is
|
||||
> an exception to this rule.
|
||||
|
||||
### Troubleshooting Proxy Issues
|
||||
|
||||
If you are behind a proxy/load balancer (usually the case with most hosting
|
||||
services, e.g. Heroku, Bluemix, AWS ELB, Nginx, Cloudflare, Akamai, Fastly,
|
||||
Firebase Hosting, Rackspace LB, Riverbed Stingray, etc.), the IP address of the
|
||||
request might be the IP of the load balancer/reverse proxy (making the rate
|
||||
limiter effectively a global one and blocking all requests once the limit is
|
||||
reached) or `undefined`. To solve this issue, add the following line to your
|
||||
code (right after you create the express application):
|
||||
|
||||
```ts
|
||||
app.set('trust proxy', numberOfProxies)
|
||||
```
|
||||
|
||||
Where `numberOfProxies` is the number of proxies between the user and the
|
||||
server. To find the correct number, create a test endpoint that returns the
|
||||
client IP:
|
||||
|
||||
```ts
|
||||
app.set('trust proxy', 1)
|
||||
app.get('/ip', (request, response) => response.send(request.ip))
|
||||
```
|
||||
|
||||
Go to `/ip` and see the IP address returned in the response. If it matches your
|
||||
IP address (which you can get by going to http://ip.nfriedly.com/ or
|
||||
https://api.ipify.org/), then the number of proxies is correct and the rate
|
||||
limiter should now work correctly. If not, then keep increasing the number until
|
||||
it does.
|
||||
|
||||
For more information about the `trust proxy` setting, take a look at the
|
||||
[official Express documentation](https://expressjs.com/en/guide/behind-proxies.html).
|
||||
|
||||
## Request API
|
||||
|
||||
A `request.rateLimit` property is added to all requests with the `limit`,
|
||||
`current`, and `remaining` number of requests and, if the store provides it, a
|
||||
`resetTime` Date object. These may be used in your application code to take
|
||||
additional actions or inform the user of their status.
|
||||
|
||||
The property name can be configured with the configuration option
|
||||
`requestPropertyName`
|
||||
|
||||
## Configuration options
|
||||
|
||||
### `windowMs`
|
||||
|
||||
Time frame for which requests are checked/remembered. Also used in the
|
||||
`Retry-After` header when the limit is reached.
|
||||
|
||||
Note: with non-default stores, you may need to configure this value twice, once
|
||||
here and once on the store. In some cases the units also differ (e.g. seconds vs
|
||||
miliseconds)
|
||||
|
||||
Defaults to `60000` ms (= 1 minute).
|
||||
|
||||
### `max`
|
||||
|
||||
Max number of connections during `windowMs` milliseconds before sending a 429
|
||||
response.
|
||||
|
||||
May be a number, or a function that returns a number or a promise. If `max` is a
|
||||
function, it will be called with `request` and `response` params.
|
||||
|
||||
Defaults to `5`. Set to `0` to disable.
|
||||
|
||||
Example of using a function:
|
||||
|
||||
```ts
|
||||
import rateLimit from 'express-rate-limit'
|
||||
|
||||
const isPremium = (request) => {
|
||||
// ...
|
||||
}
|
||||
|
||||
const limiter = rateLimit({
|
||||
// `max` could also be an async function or return a promise
|
||||
max: (request, response) => {
|
||||
if (isPremium(request)) return 10
|
||||
else return 5
|
||||
},
|
||||
// ...
|
||||
})
|
||||
|
||||
// Apply the rate limiting middleware to all requests
|
||||
app.use(limiter)
|
||||
```
|
||||
|
||||
### `message`
|
||||
|
||||
Error message sent to user when `max` is exceeded.
|
||||
|
||||
May be a `string`, JSON object, or any other value that Express's
|
||||
[response.send](https://expressjs.com/en/4x/api.html#response.send) method
|
||||
supports.
|
||||
|
||||
Defaults to `'Too many requests, please try again later.'`
|
||||
|
||||
### `statusCode`
|
||||
|
||||
HTTP status code returned when `max` is exceeded.
|
||||
|
||||
Defaults to `429`.
|
||||
|
||||
### `legacyHeaders`
|
||||
|
||||
Enable headers for request limit (`X-RateLimit-Limit`) and current usage
|
||||
(`X-RateLimit-Remaining`) on all responses and time to wait before retrying
|
||||
(`Retry-After`) when `max` is exceeded.
|
||||
|
||||
Defaults to `true`.
|
||||
|
||||
> Renamed in `6.x` from `headers` to `legacyHeaders`.
|
||||
|
||||
### `standardHeaders`
|
||||
|
||||
Enable headers conforming to the
|
||||
[ratelimit standardization draft](https://github.com/ietf-wg-httpapi/ratelimit-headers/blob/main/draft-ietf-httpapi-ratelimit-headers.md)
|
||||
adopted by the IETF: `RateLimit-Limit`, `RateLimit-Remaining`, and, if the store
|
||||
supports it, `RateLimit-Reset`. May be used in conjunction with, or instead of
|
||||
the `legacyHeaders` option.
|
||||
|
||||
This setting also enables the `Retry-After` header when `max` is exceeded.
|
||||
|
||||
Defaults to `false` (for backward compatibility), but recommended to use.
|
||||
|
||||
> Renamed in `6.x` from `draft_polli_ratelimit_headers` to `standardHeaders`.
|
||||
|
||||
### `keyGenerator`
|
||||
|
||||
Function used to generate keys.
|
||||
|
||||
Defaults to `request.ip`, similar to this:
|
||||
|
||||
```ts
|
||||
const keyGenerator = (request /*, response*/) => request.ip
|
||||
```
|
||||
|
||||
### `handler`
|
||||
|
||||
The function to handle requests once the max limit is exceeded. It receives the
|
||||
`request` and the `response` objects. The `next` param is available if you need
|
||||
to pass to the next middleware/route. Finally, the `options` param has all of
|
||||
the options that originally passed in when creating the current limiter and the
|
||||
default values for other options.
|
||||
|
||||
The `request.rateLimit` object has `limit`, `current`, and `remaining` number of
|
||||
requests and, if the store provides it, a `resetTime` Date object.
|
||||
|
||||
Defaults to:
|
||||
|
||||
```ts
|
||||
const handler = (request, response, next, options) => {
|
||||
response.status(options.statusCode).send(options.message)
|
||||
}
|
||||
```
|
||||
|
||||
### `requestWasSuccessful`
|
||||
|
||||
Function that is called when `skipFailedRequests` and/or
|
||||
`skipSuccessfulRequests` are set to `true`. May be overridden if, for example, a
|
||||
service sends out a 200 status code on errors.
|
||||
|
||||
Defaults to
|
||||
|
||||
```ts
|
||||
const requestWasSuccessful = (request, response) => response.statusCode < 400
|
||||
```
|
||||
|
||||
### `skipFailedRequests`
|
||||
|
||||
When set to `true`, failed requests won't be counted. Request considered failed
|
||||
when:
|
||||
|
||||
- response status >= 400
|
||||
- requests that were cancelled before last chunk of data was sent (response
|
||||
`close` event triggered)
|
||||
- response `error` event was triggered by response
|
||||
|
||||
(Technically they are counted and then un-counted, so a large number of slow
|
||||
requests all at once could still trigger a rate-limit. This may be fixed in a
|
||||
future release.)
|
||||
|
||||
Defaults to `false`.
|
||||
|
||||
### `skipSuccessfulRequests`
|
||||
|
||||
When set to `true` successful requests (response status < 400) won't be counted.
|
||||
(Technically they are counted and then un-counted, so a large number of slow
|
||||
requests all at once could still trigger a rate-limit. This may be fixed in a
|
||||
future release.)
|
||||
|
||||
Defaults to `false`.
|
||||
|
||||
### `skip`
|
||||
|
||||
Function used to skip (whitelist) requests. Returning `true`, or a promise that
|
||||
resolves with `true`, from the function will skip limiting for that request.
|
||||
|
||||
Defaults to always `false` (count all requests):
|
||||
|
||||
```ts
|
||||
const skip = (/*request, response*/) => false
|
||||
```
|
||||
|
||||
### `requestPropertyName`
|
||||
|
||||
The name of the property that contains the rate limit information to add to the
|
||||
`request` object.
|
||||
|
||||
Defaults to `rateLimit`.
|
||||
|
||||
### `store`
|
||||
|
||||
The storage to use when persisting rate limit attempts.
|
||||
|
||||
By default, the [memory store](source/memory-store.ts) is used.
|
||||
|
||||
Available data stores are:
|
||||
|
||||
- [memory-store](source/memory-store.ts): _(default)_ Simple in-memory option.
|
||||
Does not share state when app has multiple processes or servers.
|
||||
- [rate-limit-redis](https://npmjs.com/package/rate-limit-redis): A
|
||||
[Redis](http://redis.io/)-backed store, more suitable for large or demanding
|
||||
deployments.
|
||||
- [rate-limit-memcached](https://npmjs.org/package/rate-limit-memcached): A
|
||||
[Memcached](https://memcached.org/)-backed store.
|
||||
- [rate-limit-mongo](https://www.npmjs.com/package/rate-limit-mongo): A
|
||||
[MongoDB](https://www.mongodb.com/)-backed store.
|
||||
- [precise-memory-rate-limit](https://www.npmjs.com/package/precise-memory-rate-limit) -
|
||||
A memory store similar to the built-in one, except that it stores a distinct
|
||||
timestamp for each IP rather than bucketing them together.
|
||||
|
||||
You may also create your own store. It must implement the `Store` interface as
|
||||
follows:
|
||||
|
||||
```ts
|
||||
import rateLimit, {
|
||||
Store,
|
||||
Options,
|
||||
IncrementResponse,
|
||||
} from 'express-rate-limit'
|
||||
|
||||
/**
|
||||
* A {@link Store} that stores the hit count for each client.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
class SomeStore implements Store {
|
||||
/**
|
||||
* Some store-specific parameter.
|
||||
*/
|
||||
customParam!: string
|
||||
/**
|
||||
* The duration of time before which all hit counts are reset (in milliseconds).
|
||||
*/
|
||||
windowMs!: number
|
||||
|
||||
/**
|
||||
* @constructor for {@link SomeStore}. Only required if the user needs to pass
|
||||
* some store specific parameters. For example, in a Mongo Store, the user will
|
||||
* need to pass the URI, username and password for the Mongo database.
|
||||
*
|
||||
* @param customParam {string} - Some store-specific parameter.
|
||||
*/
|
||||
constructor(customParam: string) {
|
||||
this.customParam = customParam
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that actually initializes the store. Must be synchronous.
|
||||
*
|
||||
* @param options {Options} - The options used to setup the middleware.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
init(options: Options): void {
|
||||
this.windowMs = options.windowMs
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: string): Promise<IncrementResponse> {
|
||||
// ...
|
||||
|
||||
return {
|
||||
totalHits,
|
||||
resetTime,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to decrement a client's hit counter.
|
||||
*
|
||||
* @param key {string} - The identifier for a client
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
async decrement(key: string): Promise<void> {
|
||||
// ...
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to reset a client's hit counter.
|
||||
*
|
||||
* @param key {string} - The identifier for a client
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
async resetKey(key: string): Promise<void> {
|
||||
// ...
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to reset everyone's hit counter.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
async resetAll(): Promise<void> {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
export default SomeStore
|
||||
```
|
||||
|
||||
## Instance API
|
||||
|
||||
### `resetKey(key)`
|
||||
|
||||
Resets the rate limiting for a given key. An example use case is to allow users
|
||||
to complete a captcha or whatever to reset their rate limit, then call this
|
||||
method.
|
||||
|
||||
## Issues and Contributing
|
||||
|
||||
If you encounter a bug or want to see something added/changed, please go ahead
|
||||
and [open an issue](https://github.com/nfriedly/express-rate-limit/issues/new)!
|
||||
If you need help with something, feel free to
|
||||
[start a discussion](https://github.com/nfriedly/express-rate-limit/discussions/new)!
|
||||
|
||||
If you wish to contribute to the library, thanks! First, please read
|
||||
[the contributing guide](contributing.md). Then you can pick up any issue and
|
||||
fix/implement it!
|
||||
|
||||
## License
|
||||
|
||||
MIT © [Nathan Friedly](http://nfriedly.com/)
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"include": ["source/"],
|
||||
"exclude": ["node_modules/"],
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@mozilla/readability": "^0.3.0",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^6.0.5",
|
||||
"jsdom": "^16.4.0",
|
||||
"turndown": "^7.0.0"
|
||||
},
|
||||
|
@ -418,6 +419,17 @@
|
|||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.0.5.tgz",
|
||||
"integrity": "sha512-EB1mRTrzyyPfEsQZIQFXocd8NKZoDZbEwrtbdgkc20Yed6oYg02Xfjza2HHPI/0orp54BrFeHeT92ICB9ydokw==",
|
||||
"engines": {
|
||||
"node": ">= 14.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": "^4"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||
|
@ -1433,6 +1445,12 @@
|
|||
"vary": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"express-rate-limit": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.0.5.tgz",
|
||||
"integrity": "sha512-EB1mRTrzyyPfEsQZIQFXocd8NKZoDZbEwrtbdgkc20Yed6oYg02Xfjza2HHPI/0orp54BrFeHeT92ICB9ydokw==",
|
||||
"requires": {}
|
||||
},
|
||||
"fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
"dependencies": {
|
||||
"@mozilla/readability": "^0.3.0",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^6.0.5",
|
||||
"jsdom": "^16.4.0",
|
||||
"turndown": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue