index.mjs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  1. // source/ip-key-generator.ts
  2. import { isIPv6 } from "node:net";
  3. import { Address6 } from "ip-address";
  4. function ipKeyGenerator(ip, ipv6Subnet = 56) {
  5. if (isIPv6(ip)) {
  6. const address = new Address6(ip);
  7. if (address.is4()) return address.to4().correctForm();
  8. if (ipv6Subnet) {
  9. const subnet = new Address6(`${ip}/${ipv6Subnet}`);
  10. return `${subnet.startAddress().correctForm()}/${ipv6Subnet}`;
  11. }
  12. }
  13. return ip;
  14. }
  15. // source/memory-store.ts
  16. var MemoryStore = class {
  17. constructor(validations2) {
  18. this.validations = validations2;
  19. /**
  20. * These two maps store usage (requests) and reset time by key (for example, IP
  21. * addresses or API keys).
  22. *
  23. * They are split into two to avoid having to iterate through the entire set to
  24. * determine which ones need reset. Instead, `Client`s are moved from `previous`
  25. * to `current` as they hit the endpoint. Once `windowMs` has elapsed, all clients
  26. * left in `previous`, i.e., those that have not made any recent requests, are
  27. * known to be expired and can be deleted in bulk.
  28. */
  29. this.previous = /* @__PURE__ */ new Map();
  30. this.current = /* @__PURE__ */ new Map();
  31. /**
  32. * Confirmation that the keys incremented in once instance of MemoryStore
  33. * cannot affect other instances.
  34. */
  35. this.localKeys = true;
  36. }
  37. /**
  38. * Method that initializes the store.
  39. *
  40. * @param options {Options} - The options used to setup the middleware.
  41. */
  42. init(options) {
  43. this.windowMs = options.windowMs;
  44. this.validations?.windowMs(this.windowMs);
  45. if (this.interval) clearInterval(this.interval);
  46. this.interval = setInterval(() => {
  47. this.clearExpired();
  48. }, this.windowMs);
  49. this.interval.unref?.();
  50. }
  51. /**
  52. * Method to fetch a client's hit count and reset time.
  53. *
  54. * @param key {string} - The identifier for a client.
  55. *
  56. * @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
  57. *
  58. * @public
  59. */
  60. async get(key) {
  61. return this.current.get(key) ?? this.previous.get(key);
  62. }
  63. /**
  64. * Method to increment a client's hit counter.
  65. *
  66. * @param key {string} - The identifier for a client.
  67. *
  68. * @returns {ClientRateLimitInfo} - The number of hits and reset time for that client.
  69. *
  70. * @public
  71. */
  72. async increment(key) {
  73. const client = this.getClient(key);
  74. const now = Date.now();
  75. if (client.resetTime.getTime() <= now) {
  76. this.resetClient(client, now);
  77. }
  78. client.totalHits++;
  79. return client;
  80. }
  81. /**
  82. * Method to decrement a client's hit counter.
  83. *
  84. * @param key {string} - The identifier for a client.
  85. *
  86. * @public
  87. */
  88. async decrement(key) {
  89. const client = this.getClient(key);
  90. if (client.totalHits > 0) client.totalHits--;
  91. }
  92. /**
  93. * Method to reset a client's hit counter.
  94. *
  95. * @param key {string} - The identifier for a client.
  96. *
  97. * @public
  98. */
  99. async resetKey(key) {
  100. this.current.delete(key);
  101. this.previous.delete(key);
  102. }
  103. /**
  104. * Method to reset everyone's hit counter.
  105. *
  106. * @public
  107. */
  108. async resetAll() {
  109. this.current.clear();
  110. this.previous.clear();
  111. }
  112. /**
  113. * Method to stop the timer (if currently running) and prevent any memory
  114. * leaks.
  115. *
  116. * @public
  117. */
  118. shutdown() {
  119. clearInterval(this.interval);
  120. void this.resetAll();
  121. }
  122. /**
  123. * Recycles a client by setting its hit count to zero, and reset time to
  124. * `windowMs` milliseconds from now.
  125. *
  126. * NOT to be confused with `#resetKey()`, which removes a client from both the
  127. * `current` and `previous` maps.
  128. *
  129. * @param client {Client} - The client to recycle.
  130. * @param now {number} - The current time, to which the `windowMs` is added to get the `resetTime` for the client.
  131. *
  132. * @return {Client} - The modified client that was passed in, to allow for chaining.
  133. */
  134. resetClient(client, now = Date.now()) {
  135. client.totalHits = 0;
  136. client.resetTime.setTime(now + this.windowMs);
  137. return client;
  138. }
  139. /**
  140. * Retrieves or creates a client, given a key. Also ensures that the client being
  141. * returned is in the `current` map.
  142. *
  143. * @param key {string} - The key under which the client is (or is to be) stored.
  144. *
  145. * @returns {Client} - The requested client.
  146. */
  147. getClient(key) {
  148. if (this.current.has(key)) return this.current.get(key);
  149. let client;
  150. if (this.previous.has(key)) {
  151. client = this.previous.get(key);
  152. this.previous.delete(key);
  153. } else {
  154. client = { totalHits: 0, resetTime: /* @__PURE__ */ new Date() };
  155. this.resetClient(client);
  156. }
  157. this.current.set(key, client);
  158. return client;
  159. }
  160. /**
  161. * Move current clients to previous, create a new map for current.
  162. *
  163. * This function is called every `windowMs`.
  164. */
  165. clearExpired() {
  166. this.previous = this.current;
  167. this.current = /* @__PURE__ */ new Map();
  168. }
  169. };
  170. // source/rate-limit.ts
  171. import { isIPv6 as isIPv62 } from "node:net";
  172. // source/headers.ts
  173. import { Buffer } from "node:buffer";
  174. import { createHash } from "node:crypto";
  175. var SUPPORTED_DRAFT_VERSIONS = [
  176. "draft-6",
  177. "draft-7",
  178. "draft-8"
  179. ];
  180. var getResetSeconds = (windowMs, resetTime) => {
  181. let resetSeconds;
  182. if (resetTime) {
  183. const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
  184. resetSeconds = Math.max(0, deltaSeconds);
  185. } else {
  186. resetSeconds = Math.ceil(windowMs / 1e3);
  187. }
  188. return resetSeconds;
  189. };
  190. var getPartitionKey = (key) => {
  191. const hash = createHash("sha256");
  192. hash.update(key);
  193. const partitionKey = hash.digest("hex").slice(0, 12);
  194. return Buffer.from(partitionKey).toString("base64");
  195. };
  196. var setLegacyHeaders = (response, info) => {
  197. if (response.headersSent) return;
  198. response.setHeader("X-RateLimit-Limit", info.limit.toString());
  199. response.setHeader("X-RateLimit-Remaining", info.remaining.toString());
  200. if (info.resetTime instanceof Date) {
  201. response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
  202. response.setHeader(
  203. "X-RateLimit-Reset",
  204. Math.ceil(info.resetTime.getTime() / 1e3).toString()
  205. );
  206. }
  207. };
  208. var setDraft6Headers = (response, info, windowMs) => {
  209. if (response.headersSent) return;
  210. const windowSeconds = Math.ceil(windowMs / 1e3);
  211. const resetSeconds = getResetSeconds(windowMs, info.resetTime);
  212. response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
  213. response.setHeader("RateLimit-Limit", info.limit.toString());
  214. response.setHeader("RateLimit-Remaining", info.remaining.toString());
  215. if (typeof resetSeconds === "number")
  216. response.setHeader("RateLimit-Reset", resetSeconds.toString());
  217. };
  218. var setDraft7Headers = (response, info, windowMs) => {
  219. if (response.headersSent) return;
  220. const windowSeconds = Math.ceil(windowMs / 1e3);
  221. const resetSeconds = getResetSeconds(windowMs, info.resetTime);
  222. response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
  223. response.setHeader(
  224. "RateLimit",
  225. `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
  226. );
  227. };
  228. var setDraft8Headers = (response, info, windowMs, name, key) => {
  229. if (response.headersSent) return;
  230. const windowSeconds = Math.ceil(windowMs / 1e3);
  231. const resetSeconds = getResetSeconds(windowMs, info.resetTime);
  232. const partitionKey = getPartitionKey(key);
  233. const header = `r=${info.remaining}; t=${resetSeconds}`;
  234. const policy = `q=${info.limit}; w=${windowSeconds}; pk=:${partitionKey}:`;
  235. response.append("RateLimit", `"${name}"; ${header}`);
  236. response.append("RateLimit-Policy", `"${name}"; ${policy}`);
  237. };
  238. var setRetryAfterHeader = (response, info, windowMs) => {
  239. if (response.headersSent) return;
  240. const resetSeconds = getResetSeconds(windowMs, info.resetTime);
  241. response.setHeader("Retry-After", resetSeconds.toString());
  242. };
  243. // source/utils.ts
  244. var omitUndefinedProperties = (passedOptions) => {
  245. const omittedOptions = {};
  246. for (const k of Object.keys(passedOptions)) {
  247. const key = k;
  248. if (passedOptions[key] !== void 0) {
  249. omittedOptions[key] = passedOptions[key];
  250. }
  251. }
  252. return omittedOptions;
  253. };
  254. // source/validations.ts
  255. import { isIP } from "node:net";
  256. var ValidationError = class extends Error {
  257. /**
  258. * The code must be a string, in snake case and all capital, that starts with
  259. * the substring `ERR_ERL_`.
  260. *
  261. * The message must be a string, starting with an uppercase character,
  262. * describing the issue in detail.
  263. */
  264. constructor(code, message) {
  265. const url = `https://express-rate-limit.github.io/${code}/`;
  266. super(`${message} See ${url} for more information.`);
  267. this.name = this.constructor.name;
  268. this.code = code;
  269. this.help = url;
  270. }
  271. };
  272. var ChangeWarning = class extends ValidationError {
  273. };
  274. var usedStores = /* @__PURE__ */ new Set();
  275. var singleCountKeys = /* @__PURE__ */ new WeakMap();
  276. var validations = {
  277. enabled: {
  278. default: true
  279. },
  280. // Should be EnabledValidations type, but that's a circular reference
  281. disable() {
  282. for (const k of Object.keys(this.enabled)) this.enabled[k] = false;
  283. },
  284. /**
  285. * Checks whether the IP address is valid, and that it does not have a port
  286. * number in it.
  287. *
  288. * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_invalid_ip_address.
  289. *
  290. * @param ip {string | undefined} - The IP address provided by Express as request.ip.
  291. *
  292. * @returns {void}
  293. */
  294. ip(ip) {
  295. if (ip === void 0) {
  296. throw new ValidationError(
  297. "ERR_ERL_UNDEFINED_IP_ADDRESS",
  298. `An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.`
  299. );
  300. }
  301. if (!isIP(ip)) {
  302. throw new ValidationError(
  303. "ERR_ERL_INVALID_IP_ADDRESS",
  304. `An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.`
  305. );
  306. }
  307. },
  308. /**
  309. * Makes sure the trust proxy setting is not set to `true`.
  310. *
  311. * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_permissive_trust_proxy.
  312. *
  313. * @param request {Request} - The Express request object.
  314. *
  315. * @returns {void}
  316. */
  317. trustProxy(request) {
  318. if (request.app.get("trust proxy") === true) {
  319. throw new ValidationError(
  320. "ERR_ERL_PERMISSIVE_TRUST_PROXY",
  321. `The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting.`
  322. );
  323. }
  324. },
  325. /**
  326. * Makes sure the trust proxy setting is set in case the `X-Forwarded-For`
  327. * header is present.
  328. *
  329. * See https://github.com/express-rate-limit/express-rate-limit/wiki/Error-Codes#err_erl_unset_trust_proxy.
  330. *
  331. * @param request {Request} - The Express request object.
  332. *
  333. * @returns {void}
  334. */
  335. xForwardedForHeader(request) {
  336. if (request.headers["x-forwarded-for"] && request.app.get("trust proxy") === false) {
  337. throw new ValidationError(
  338. "ERR_ERL_UNEXPECTED_X_FORWARDED_FOR",
  339. `The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default). This could indicate a misconfiguration which would prevent express-rate-limit from accurately identifying users.`
  340. );
  341. }
  342. },
  343. /**
  344. * Alert the user if the Forwarded header is set (standardized version of X-Forwarded-For - not supported by express as of version 5.1.0)
  345. *
  346. * @param request {Request} - The Express request object.
  347. *
  348. * @returns {void}
  349. */
  350. forwardedHeader(request) {
  351. if (request.headers.forwarded && request.ip === request.socket?.remoteAddress) {
  352. throw new ValidationError(
  353. "ERR_ERL_FORWARDED_HEADER",
  354. `The 'Forwarded' header (standardized X-Forwarded-For) is set but currently being ignored. Add a custom keyGenerator to use a value from this header.`
  355. );
  356. }
  357. },
  358. /**
  359. * Ensures totalHits value from store is a positive integer.
  360. *
  361. * @param hits {any} - The `totalHits` returned by the store.
  362. */
  363. positiveHits(hits) {
  364. if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) {
  365. throw new ValidationError(
  366. "ERR_ERL_INVALID_HITS",
  367. `The totalHits value returned from the store must be a positive integer, got ${hits}`
  368. );
  369. }
  370. },
  371. /**
  372. * Ensures a single store instance is not used with multiple express-rate-limit instances
  373. */
  374. unsharedStore(store) {
  375. if (usedStores.has(store)) {
  376. const maybeUniquePrefix = store?.localKeys ? "" : " (with a unique prefix)";
  377. throw new ValidationError(
  378. "ERR_ERL_STORE_REUSE",
  379. `A Store instance must not be shared across multiple rate limiters. Create a new instance of ${store.constructor.name}${maybeUniquePrefix} for each limiter instead.`
  380. );
  381. }
  382. usedStores.add(store);
  383. },
  384. /**
  385. * Ensures a given key is incremented only once per request.
  386. *
  387. * @param request {Request} - The Express request object.
  388. * @param store {Store} - The store class.
  389. * @param key {string} - The key used to store the client's hit count.
  390. *
  391. * @returns {void}
  392. */
  393. singleCount(request, store, key) {
  394. let storeKeys = singleCountKeys.get(request);
  395. if (!storeKeys) {
  396. storeKeys = /* @__PURE__ */ new Map();
  397. singleCountKeys.set(request, storeKeys);
  398. }
  399. const storeKey = store.localKeys ? store : store.constructor.name;
  400. let keys = storeKeys.get(storeKey);
  401. if (!keys) {
  402. keys = [];
  403. storeKeys.set(storeKey, keys);
  404. }
  405. const prefixedKey = `${store.prefix ?? ""}${key}`;
  406. if (keys.includes(prefixedKey)) {
  407. throw new ValidationError(
  408. "ERR_ERL_DOUBLE_COUNT",
  409. `The hit count for ${key} was incremented more than once for a single request.`
  410. );
  411. }
  412. keys.push(prefixedKey);
  413. },
  414. /**
  415. * Warns the user that the behaviour for `max: 0` / `limit: 0` is
  416. * changing in the next major release.
  417. *
  418. * @param limit {number} - The maximum number of hits per client.
  419. *
  420. * @returns {void}
  421. */
  422. limit(limit) {
  423. if (limit === 0) {
  424. throw new ChangeWarning(
  425. "WRN_ERL_MAX_ZERO",
  426. "Setting limit or max to 0 disables rate limiting in express-rate-limit v6 and older, but will cause all requests to be blocked in v7"
  427. );
  428. }
  429. },
  430. /**
  431. * Warns the user that the `draft_polli_ratelimit_headers` option is deprecated
  432. * and will be removed in the next major release.
  433. *
  434. * @param draft_polli_ratelimit_headers {any | undefined} - The now-deprecated setting that was used to enable standard headers.
  435. *
  436. * @returns {void}
  437. */
  438. draftPolliHeaders(draft_polli_ratelimit_headers) {
  439. if (draft_polli_ratelimit_headers) {
  440. throw new ChangeWarning(
  441. "WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS",
  442. `The draft_polli_ratelimit_headers configuration option is deprecated and has been removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.`
  443. );
  444. }
  445. },
  446. /**
  447. * Warns the user that the `onLimitReached` option is deprecated and
  448. * will be removed in the next major release.
  449. *
  450. * @param onLimitReached {any | undefined} - The maximum number of hits per client.
  451. *
  452. * @returns {void}
  453. */
  454. onLimitReached(onLimitReached) {
  455. if (onLimitReached) {
  456. throw new ChangeWarning(
  457. "WRN_ERL_DEPRECATED_ON_LIMIT_REACHED",
  458. "The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7."
  459. );
  460. }
  461. },
  462. /**
  463. * Warns the user when an invalid/unsupported version of the draft spec is passed.
  464. *
  465. * @param version {any | undefined} - The version passed by the user.
  466. *
  467. * @returns {void}
  468. */
  469. headersDraftVersion(version) {
  470. if (typeof version !== "string" || // @ts-expect-error This is fine. If version is not in the array, it will just return false.
  471. !SUPPORTED_DRAFT_VERSIONS.includes(version)) {
  472. const versionString = SUPPORTED_DRAFT_VERSIONS.join(", ");
  473. throw new ValidationError(
  474. "ERR_ERL_HEADERS_UNSUPPORTED_DRAFT_VERSION",
  475. `standardHeaders: only the following versions of the IETF draft specification are supported: ${versionString}.`
  476. );
  477. }
  478. },
  479. /**
  480. * Warns the user when the selected headers option requires a reset time but
  481. * the store does not provide one.
  482. *
  483. * @param resetTime {Date | undefined} - The timestamp when the client's hit count will be reset.
  484. *
  485. * @returns {void}
  486. */
  487. headersResetTime(resetTime) {
  488. if (!resetTime) {
  489. throw new ValidationError(
  490. "ERR_ERL_HEADERS_NO_RESET",
  491. `standardHeaders: 'draft-7' requires a 'resetTime', but the store did not provide one. The 'windowMs' value will be used instead, which may cause clients to wait longer than necessary.`
  492. );
  493. }
  494. },
  495. knownOptions(passedOptions) {
  496. if (!passedOptions) return;
  497. const optionsMap = {
  498. windowMs: true,
  499. limit: true,
  500. message: true,
  501. statusCode: true,
  502. legacyHeaders: true,
  503. standardHeaders: true,
  504. identifier: true,
  505. requestPropertyName: true,
  506. skipFailedRequests: true,
  507. skipSuccessfulRequests: true,
  508. keyGenerator: true,
  509. ipv6Subnet: true,
  510. handler: true,
  511. skip: true,
  512. requestWasSuccessful: true,
  513. store: true,
  514. validate: true,
  515. headers: true,
  516. max: true,
  517. passOnStoreError: true
  518. };
  519. const validOptions = Object.keys(optionsMap).concat(
  520. "draft_polli_ratelimit_headers",
  521. // not a valid option anymore, but we have a more specific check for this one, so don't warn for it here
  522. // from express-slow-down - https://github.com/express-rate-limit/express-slow-down/blob/main/source/types.ts#L65
  523. "delayAfter",
  524. "delayMs",
  525. "maxDelayMs"
  526. );
  527. for (const key of Object.keys(passedOptions)) {
  528. if (!validOptions.includes(key)) {
  529. throw new ValidationError(
  530. "ERR_ERL_UNKNOWN_OPTION",
  531. `Unexpected configuration option: ${key}`
  532. // todo: suggest a valid option with a short levenstein distance?
  533. );
  534. }
  535. }
  536. },
  537. /**
  538. * Checks the options.validate setting to ensure that only recognized
  539. * validations are enabled or disabled.
  540. *
  541. * If any unrecognized values are found, an error is logged that
  542. * includes the list of supported validations.
  543. */
  544. validationsConfig() {
  545. const supportedValidations = Object.keys(this).filter(
  546. (k) => !["enabled", "disable"].includes(k)
  547. );
  548. supportedValidations.push("default");
  549. for (const key of Object.keys(this.enabled)) {
  550. if (!supportedValidations.includes(key)) {
  551. throw new ValidationError(
  552. "ERR_ERL_UNKNOWN_VALIDATION",
  553. `options.validate.${key} is not recognized. Supported validate options are: ${supportedValidations.join(
  554. ", "
  555. )}.`
  556. );
  557. }
  558. }
  559. },
  560. /**
  561. * Checks to see if the instance was created inside of a request handler,
  562. * which would prevent it from working correctly, with the default memory
  563. * store (or any other store with localKeys.)
  564. */
  565. creationStack(store) {
  566. const { stack } = new Error(
  567. "express-rate-limit validation check (set options.validate.creationStack=false to disable)"
  568. );
  569. if (stack?.includes("Layer.handle [as handle_request]") || // express v4
  570. stack?.includes("Layer.handleRequest")) {
  571. if (!store.localKeys) {
  572. throw new ValidationError(
  573. "ERR_ERL_CREATED_IN_REQUEST_HANDLER",
  574. "express-rate-limit instance should *usually* be created at app initialization, not when responding to a request."
  575. );
  576. }
  577. throw new ValidationError(
  578. "ERR_ERL_CREATED_IN_REQUEST_HANDLER",
  579. "express-rate-limit instance should be created at app initialization, not when responding to a request."
  580. );
  581. }
  582. },
  583. ipv6Subnet(ipv6Subnet) {
  584. if (ipv6Subnet === false) {
  585. return;
  586. }
  587. if (!Number.isInteger(ipv6Subnet) || ipv6Subnet < 32 || ipv6Subnet > 64) {
  588. throw new ValidationError(
  589. "ERR_ERL_IPV6_SUBNET",
  590. `Unexpected ipv6Subnet value: ${ipv6Subnet}. Expected an integer between 32 and 64 (usually 48-64).`
  591. );
  592. }
  593. },
  594. ipv6SubnetOrKeyGenerator(options) {
  595. if (options.ipv6Subnet !== void 0 && options.keyGenerator) {
  596. throw new ValidationError(
  597. "ERR_ERL_IPV6SUBNET_OR_KEYGENERATOR",
  598. `Incompatible options: the 'ipv6Subnet' option is ignored when a custom 'keyGenerator' function is also set.`
  599. );
  600. }
  601. },
  602. keyGeneratorIpFallback(keyGenerator) {
  603. if (!keyGenerator) {
  604. return;
  605. }
  606. const src = keyGenerator.toString();
  607. if ((src.includes("req.ip") || src.includes("request.ip")) && !src.includes("ipKeyGenerator")) {
  608. throw new ValidationError(
  609. "ERR_ERL_KEY_GEN_IPV6",
  610. "Custom keyGenerator appears to use request IP without calling the ipKeyGenerator helper function for IPv6 addresses. This could allow IPv6 users to bypass limits."
  611. );
  612. }
  613. },
  614. /**
  615. * Checks to see if the window duration is greater than 2^32 - 1. This is only
  616. * called by the default MemoryStore, since it uses Node's setInterval method.
  617. *
  618. * See https://nodejs.org/api/timers.html#setintervalcallback-delay-args.
  619. */
  620. windowMs(windowMs) {
  621. const SET_TIMEOUT_MAX = 2 ** 31 - 1;
  622. if (typeof windowMs !== "number" || Number.isNaN(windowMs) || windowMs < 1 || windowMs > SET_TIMEOUT_MAX) {
  623. throw new ValidationError(
  624. "ERR_ERL_WINDOW_MS",
  625. `Invalid windowMs value: ${windowMs}${typeof windowMs !== "number" ? ` (${typeof windowMs})` : ""}, must be a number between 1 and ${SET_TIMEOUT_MAX} when using the default MemoryStore`
  626. );
  627. }
  628. }
  629. };
  630. var getValidations = (_enabled) => {
  631. let enabled;
  632. if (typeof _enabled === "boolean") {
  633. enabled = {
  634. default: _enabled
  635. };
  636. } else {
  637. enabled = {
  638. default: true,
  639. ..._enabled
  640. };
  641. }
  642. const wrappedValidations = { enabled };
  643. for (const [name, validation] of Object.entries(validations)) {
  644. if (typeof validation === "function")
  645. wrappedValidations[name] = (...args) => {
  646. if (!(enabled[name] ?? enabled.default)) {
  647. return;
  648. }
  649. try {
  650. ;
  651. validation.apply(
  652. wrappedValidations,
  653. args
  654. );
  655. } catch (error) {
  656. if (error instanceof ChangeWarning) console.warn(error);
  657. else console.error(error);
  658. }
  659. };
  660. }
  661. return wrappedValidations;
  662. };
  663. // source/rate-limit.ts
  664. var isLegacyStore = (store) => (
  665. // Check that `incr` exists but `increment` does not - store authors might want
  666. // to keep both around for backwards compatibility.
  667. typeof store.incr === "function" && typeof store.increment !== "function"
  668. );
  669. var promisifyStore = (passedStore) => {
  670. if (!isLegacyStore(passedStore)) {
  671. return passedStore;
  672. }
  673. const legacyStore = passedStore;
  674. class PromisifiedStore {
  675. async increment(key) {
  676. return new Promise((resolve, reject) => {
  677. legacyStore.incr(
  678. key,
  679. (error, totalHits, resetTime) => {
  680. if (error) reject(error);
  681. resolve({ totalHits, resetTime });
  682. }
  683. );
  684. });
  685. }
  686. async decrement(key) {
  687. return legacyStore.decrement(key);
  688. }
  689. async resetKey(key) {
  690. return legacyStore.resetKey(key);
  691. }
  692. /* istanbul ignore next */
  693. async resetAll() {
  694. if (typeof legacyStore.resetAll === "function")
  695. return legacyStore.resetAll();
  696. }
  697. }
  698. return new PromisifiedStore();
  699. };
  700. var getOptionsFromConfig = (config) => {
  701. const { validations: validations2, ...directlyPassableEntries } = config;
  702. return {
  703. ...directlyPassableEntries,
  704. validate: validations2.enabled
  705. };
  706. };
  707. var parseOptions = (passedOptions) => {
  708. const notUndefinedOptions = omitUndefinedProperties(passedOptions);
  709. const validations2 = getValidations(notUndefinedOptions?.validate ?? true);
  710. validations2.validationsConfig();
  711. validations2.knownOptions(passedOptions);
  712. validations2.draftPolliHeaders(
  713. // @ts-expect-error see the note above.
  714. notUndefinedOptions.draft_polli_ratelimit_headers
  715. );
  716. validations2.onLimitReached(notUndefinedOptions.onLimitReached);
  717. if (notUndefinedOptions.ipv6Subnet !== void 0 && typeof notUndefinedOptions.ipv6Subnet !== "function") {
  718. validations2.ipv6Subnet(notUndefinedOptions.ipv6Subnet);
  719. }
  720. validations2.keyGeneratorIpFallback(notUndefinedOptions.keyGenerator);
  721. validations2.ipv6SubnetOrKeyGenerator(notUndefinedOptions);
  722. let standardHeaders = notUndefinedOptions.standardHeaders ?? false;
  723. if (standardHeaders === true) standardHeaders = "draft-6";
  724. const config = {
  725. windowMs: 60 * 1e3,
  726. limit: passedOptions.max ?? 5,
  727. // `max` is deprecated, but support it anyways.
  728. message: "Too many requests, please try again later.",
  729. statusCode: 429,
  730. legacyHeaders: passedOptions.headers ?? true,
  731. identifier(request, _response) {
  732. let duration = "";
  733. const property = config.requestPropertyName;
  734. const { limit } = request[property];
  735. const seconds = config.windowMs / 1e3;
  736. const minutes = config.windowMs / (1e3 * 60);
  737. const hours = config.windowMs / (1e3 * 60 * 60);
  738. const days = config.windowMs / (1e3 * 60 * 60 * 24);
  739. if (seconds < 60) duration = `${seconds}sec`;
  740. else if (minutes < 60) duration = `${minutes}min`;
  741. else if (hours < 24) duration = `${hours}hr${hours > 1 ? "s" : ""}`;
  742. else duration = `${days}day${days > 1 ? "s" : ""}`;
  743. return `${limit}-in-${duration}`;
  744. },
  745. requestPropertyName: "rateLimit",
  746. skipFailedRequests: false,
  747. skipSuccessfulRequests: false,
  748. requestWasSuccessful: (_request, response) => response.statusCode < 400,
  749. skip: (_request, _response) => false,
  750. async keyGenerator(request, response) {
  751. validations2.ip(request.ip);
  752. validations2.trustProxy(request);
  753. validations2.xForwardedForHeader(request);
  754. validations2.forwardedHeader(request);
  755. const ip = request.ip;
  756. let subnet = 56;
  757. if (isIPv62(ip)) {
  758. subnet = typeof config.ipv6Subnet === "function" ? await config.ipv6Subnet(request, response) : config.ipv6Subnet;
  759. if (typeof config.ipv6Subnet === "function")
  760. validations2.ipv6Subnet(subnet);
  761. }
  762. return ipKeyGenerator(ip, subnet);
  763. },
  764. ipv6Subnet: 56,
  765. async handler(request, response, _next, _optionsUsed) {
  766. response.status(config.statusCode);
  767. const message = typeof config.message === "function" ? await config.message(
  768. request,
  769. response
  770. ) : config.message;
  771. if (!response.writableEnded) response.send(message);
  772. },
  773. passOnStoreError: false,
  774. // Allow the default options to be overridden by the passed options.
  775. ...notUndefinedOptions,
  776. // `standardHeaders` is resolved into a draft version above, use that.
  777. standardHeaders,
  778. // Note that this field is declared after the user's options are spread in,
  779. // so that this field doesn't get overridden with an un-promisified store!
  780. store: promisifyStore(
  781. notUndefinedOptions.store ?? new MemoryStore(validations2)
  782. ),
  783. // Print an error to the console if a few known misconfigurations are detected.
  784. validations: validations2
  785. };
  786. if (typeof config.store.increment !== "function" || typeof config.store.decrement !== "function" || typeof config.store.resetKey !== "function" || config.store.resetAll !== void 0 && typeof config.store.resetAll !== "function" || config.store.init !== void 0 && typeof config.store.init !== "function") {
  787. throw new TypeError(
  788. "An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface."
  789. );
  790. }
  791. return config;
  792. };
  793. var handleAsyncErrors = (fn) => async (request, response, next) => {
  794. try {
  795. await Promise.resolve(fn(request, response, next)).catch(next);
  796. } catch (error) {
  797. next(error);
  798. }
  799. };
  800. var rateLimit = (passedOptions) => {
  801. const config = parseOptions(passedOptions ?? {});
  802. const options = getOptionsFromConfig(config);
  803. config.validations.creationStack(config.store);
  804. config.validations.unsharedStore(config.store);
  805. if (typeof config.store.init === "function") config.store.init(options);
  806. const middleware = handleAsyncErrors(
  807. async (request, response, next) => {
  808. const closePromise = config.skipFailedRequests && new Promise((resolve) => response.once("close", resolve));
  809. const finishPromise = (config.skipFailedRequests || config.skipSuccessfulRequests) && new Promise((resolve) => response.once("finish", resolve));
  810. const errorPromise = config.skipFailedRequests && new Promise((resolve) => response.once("error", resolve));
  811. const skip = await config.skip(request, response);
  812. if (skip) {
  813. next();
  814. return;
  815. }
  816. const augmentedRequest = request;
  817. const key = await config.keyGenerator(request, response);
  818. let totalHits = 0;
  819. let resetTime;
  820. try {
  821. const incrementResult = await config.store.increment(key);
  822. totalHits = incrementResult.totalHits;
  823. resetTime = incrementResult.resetTime;
  824. } catch (error) {
  825. if (config.passOnStoreError) {
  826. console.error(
  827. "express-rate-limit: error from store, allowing request without rate-limiting.",
  828. error
  829. );
  830. next();
  831. return;
  832. }
  833. throw error;
  834. }
  835. config.validations.positiveHits(totalHits);
  836. config.validations.singleCount(request, config.store, key);
  837. const retrieveLimit = typeof config.limit === "function" ? config.limit(request, response) : config.limit;
  838. const limit = await retrieveLimit;
  839. config.validations.limit(limit);
  840. const info = {
  841. limit,
  842. used: totalHits,
  843. remaining: Math.max(limit - totalHits, 0),
  844. resetTime,
  845. key
  846. };
  847. Object.defineProperty(info, "current", {
  848. configurable: false,
  849. enumerable: false,
  850. value: totalHits
  851. });
  852. augmentedRequest[config.requestPropertyName] = info;
  853. if (config.legacyHeaders && !response.headersSent) {
  854. setLegacyHeaders(response, info);
  855. }
  856. if (config.standardHeaders && !response.headersSent) {
  857. switch (config.standardHeaders) {
  858. case "draft-6": {
  859. setDraft6Headers(response, info, config.windowMs);
  860. break;
  861. }
  862. case "draft-7": {
  863. config.validations.headersResetTime(info.resetTime);
  864. setDraft7Headers(response, info, config.windowMs);
  865. break;
  866. }
  867. case "draft-8": {
  868. const retrieveName = typeof config.identifier === "function" ? config.identifier(request, response) : config.identifier;
  869. const name = await retrieveName;
  870. config.validations.headersResetTime(info.resetTime);
  871. setDraft8Headers(response, info, config.windowMs, name, key);
  872. break;
  873. }
  874. default: {
  875. config.validations.headersDraftVersion(config.standardHeaders);
  876. break;
  877. }
  878. }
  879. }
  880. if (config.skipFailedRequests || config.skipSuccessfulRequests) {
  881. let decremented = false;
  882. const decrementKey = async () => {
  883. if (!decremented) {
  884. await config.store.decrement(key);
  885. decremented = true;
  886. }
  887. };
  888. if (config.skipFailedRequests) {
  889. if (finishPromise) {
  890. void finishPromise.then(async () => {
  891. if (!await config.requestWasSuccessful(request, response))
  892. await decrementKey();
  893. });
  894. }
  895. if (closePromise) {
  896. void closePromise.then(async () => {
  897. if (!response.writableEnded) await decrementKey();
  898. });
  899. }
  900. if (errorPromise) {
  901. void errorPromise.then(async () => {
  902. await decrementKey();
  903. });
  904. }
  905. }
  906. if (config.skipSuccessfulRequests) {
  907. if (finishPromise) {
  908. void finishPromise.then(async () => {
  909. if (await config.requestWasSuccessful(request, response))
  910. await decrementKey();
  911. });
  912. }
  913. }
  914. }
  915. config.validations.disable();
  916. if (totalHits > limit) {
  917. if (config.legacyHeaders || config.standardHeaders) {
  918. setRetryAfterHeader(response, info, config.windowMs);
  919. }
  920. config.handler(request, response, next, options);
  921. return;
  922. }
  923. next();
  924. }
  925. );
  926. const getThrowFn = () => {
  927. throw new Error("The current store does not support the get/getKey method");
  928. };
  929. middleware.resetKey = config.store.resetKey.bind(config.store);
  930. middleware.getKey = typeof config.store.get === "function" ? config.store.get.bind(config.store) : getThrowFn;
  931. return middleware;
  932. };
  933. var rate_limit_default = rateLimit;
  934. export {
  935. MemoryStore,
  936. rate_limit_default as default,
  937. ipKeyGenerator,
  938. rate_limit_default as rateLimit
  939. };