1 |
|
2 | var getResetSeconds = (resetTime, windowMs) => {
|
3 | let resetSeconds = void 0;
|
4 | if (resetTime) {
|
5 | const deltaSeconds = Math.ceil((resetTime.getTime() - Date.now()) / 1e3);
|
6 | resetSeconds = Math.max(0, deltaSeconds);
|
7 | } else if (windowMs) {
|
8 | resetSeconds = Math.ceil(windowMs / 1e3);
|
9 | }
|
10 | return resetSeconds;
|
11 | };
|
12 | var setLegacyHeaders = (response, info) => {
|
13 | if (response.headersSent)
|
14 | return;
|
15 | response.setHeader("X-RateLimit-Limit", info.limit);
|
16 | response.setHeader("X-RateLimit-Remaining", info.remaining);
|
17 | if (info.resetTime instanceof Date) {
|
18 | response.setHeader("Date", ( new Date()).toUTCString());
|
19 | response.setHeader(
|
20 | "X-RateLimit-Reset",
|
21 | Math.ceil(info.resetTime.getTime() / 1e3)
|
22 | );
|
23 | }
|
24 | };
|
25 | var setDraft6Headers = (response, info, windowMs) => {
|
26 | if (response.headersSent)
|
27 | return;
|
28 | const windowSeconds = Math.ceil(windowMs / 1e3);
|
29 | const resetSeconds = getResetSeconds(info.resetTime);
|
30 | response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
31 | response.setHeader("RateLimit-Limit", info.limit);
|
32 | response.setHeader("RateLimit-Remaining", info.remaining);
|
33 | if (resetSeconds)
|
34 | response.setHeader("RateLimit-Reset", resetSeconds);
|
35 | };
|
36 | var setDraft7Headers = (response, info, windowMs) => {
|
37 | if (response.headersSent)
|
38 | return;
|
39 | const windowSeconds = Math.ceil(windowMs / 1e3);
|
40 | const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
41 | response.setHeader("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
42 | response.setHeader(
|
43 | "RateLimit",
|
44 | `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
|
45 | );
|
46 | };
|
47 | var setRetryAfterHeader = (response, info, windowMs) => {
|
48 | if (response.headersSent)
|
49 | return;
|
50 | const resetSeconds = getResetSeconds(info.resetTime, windowMs);
|
51 | response.setHeader("Retry-After", resetSeconds);
|
52 | };
|
53 |
|
54 |
|
55 | import { isIP } from "net";
|
56 | var ValidationError = class extends Error {
|
57 | |
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 | constructor(code, message) {
|
65 | const url = `https://express-rate-limit.github.io/${code}/`;
|
66 | super(`${message} See ${url} for more information.`);
|
67 | this.name = this.constructor.name;
|
68 | this.code = code;
|
69 | this.help = url;
|
70 | }
|
71 | };
|
72 | var ChangeWarning = class extends ValidationError {
|
73 | };
|
74 | var singleCountKeys = new WeakMap();
|
75 | var validations = {
|
76 |
|
77 | enabled: {
|
78 | default: true
|
79 | },
|
80 |
|
81 | disable() {
|
82 | for (const k of Object.keys(this.enabled))
|
83 | this.enabled[k] = false;
|
84 | },
|
85 | |
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 | ip(ip) {
|
96 | if (ip === void 0) {
|
97 | throw new ValidationError(
|
98 | "ERR_ERL_UNDEFINED_IP_ADDRESS",
|
99 | `An undefined 'request.ip' was detected. This might indicate a misconfiguration or the connection being destroyed prematurely.`
|
100 | );
|
101 | }
|
102 | if (!isIP(ip)) {
|
103 | throw new ValidationError(
|
104 | "ERR_ERL_INVALID_IP_ADDRESS",
|
105 | `An invalid 'request.ip' (${ip}) was detected. Consider passing a custom 'keyGenerator' function to the rate limiter.`
|
106 | );
|
107 | }
|
108 | },
|
109 | |
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 | trustProxy(request) {
|
119 | if (request.app.get("trust proxy") === true) {
|
120 | throw new ValidationError(
|
121 | "ERR_ERL_PERMISSIVE_TRUST_PROXY",
|
122 | `The Express 'trust proxy' setting is true, which allows anyone to trivially bypass IP-based rate limiting.`
|
123 | );
|
124 | }
|
125 | },
|
126 | |
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 | xForwardedForHeader(request) {
|
137 | if (request.headers["x-forwarded-for"] && request.app.get("trust proxy") === false) {
|
138 | throw new ValidationError(
|
139 | "ERR_ERL_UNEXPECTED_X_FORWARDED_FOR",
|
140 | `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.`
|
141 | );
|
142 | }
|
143 | },
|
144 | |
145 |
|
146 |
|
147 |
|
148 |
|
149 | positiveHits(hits) {
|
150 | if (typeof hits !== "number" || hits < 1 || hits !== Math.round(hits)) {
|
151 | throw new ValidationError(
|
152 | "ERR_ERL_INVALID_HITS",
|
153 | `The totalHits value returned from the store must be a positive integer, got ${hits}`
|
154 | );
|
155 | }
|
156 | },
|
157 | |
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 | singleCount(request, store, key) {
|
167 | let storeKeys = singleCountKeys.get(request);
|
168 | if (!storeKeys) {
|
169 | storeKeys = new Map();
|
170 | singleCountKeys.set(request, storeKeys);
|
171 | }
|
172 | const storeKey = store.localKeys ? store : store.constructor.name;
|
173 | let keys = storeKeys.get(storeKey);
|
174 | if (!keys) {
|
175 | keys = [];
|
176 | storeKeys.set(storeKey, keys);
|
177 | }
|
178 | const prefixedKey = `${store.prefix ?? ""}${key}`;
|
179 | if (keys.includes(prefixedKey)) {
|
180 | throw new ValidationError(
|
181 | "ERR_ERL_DOUBLE_COUNT",
|
182 | `The hit count for ${key} was incremented more than once for a single request.`
|
183 | );
|
184 | }
|
185 | keys.push(prefixedKey);
|
186 | },
|
187 | |
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 | limit(limit) {
|
196 | if (limit === 0) {
|
197 | throw new ChangeWarning(
|
198 | "WRN_ERL_MAX_ZERO",
|
199 | `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`
|
200 | );
|
201 | }
|
202 | },
|
203 | |
204 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 | draftPolliHeaders(draft_polli_ratelimit_headers) {
|
212 | if (draft_polli_ratelimit_headers) {
|
213 | throw new ChangeWarning(
|
214 | "WRN_ERL_DEPRECATED_DRAFT_POLLI_HEADERS",
|
215 | `The draft_polli_ratelimit_headers configuration option is deprecated and has been removed in express-rate-limit v7, please set standardHeaders: 'draft-6' instead.`
|
216 | );
|
217 | }
|
218 | },
|
219 | |
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 | onLimitReached(onLimitReached) {
|
228 | if (onLimitReached) {
|
229 | throw new ChangeWarning(
|
230 | "WRN_ERL_DEPRECATED_ON_LIMIT_REACHED",
|
231 | `The onLimitReached configuration option is deprecated and has been removed in express-rate-limit v7.`
|
232 | );
|
233 | }
|
234 | },
|
235 | |
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 | headersResetTime(resetTime) {
|
244 | if (!resetTime) {
|
245 | throw new ValidationError(
|
246 | "ERR_ERL_HEADERS_NO_RESET",
|
247 | `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.`
|
248 | );
|
249 | }
|
250 | },
|
251 | |
252 |
|
253 |
|
254 |
|
255 |
|
256 | validationsConfig() {
|
257 | const supportedValidations = Object.keys(this).filter(
|
258 | (k) => !["enabled", "disable"].includes(k)
|
259 | );
|
260 | supportedValidations.push("default");
|
261 | for (const key of Object.keys(this.enabled)) {
|
262 | if (!supportedValidations.includes(key)) {
|
263 | throw new ValidationError(
|
264 | "ERR_ERL_UNKNOWN_VALIDATION",
|
265 | `options.validate.${key} is not recognized. Supported validate options are: ${supportedValidations.join(
|
266 | ", "
|
267 | )}.`
|
268 | );
|
269 | }
|
270 | }
|
271 | }
|
272 | };
|
273 | var getValidations = (_enabled) => {
|
274 | let enabled;
|
275 | if (typeof _enabled === "boolean") {
|
276 | enabled = {
|
277 | default: _enabled
|
278 | };
|
279 | } else {
|
280 | enabled = {
|
281 | default: true,
|
282 | ..._enabled
|
283 | };
|
284 | }
|
285 | const wrappedValidations = {
|
286 | enabled
|
287 | };
|
288 | for (const [name, validation] of Object.entries(validations)) {
|
289 | if (typeof validation === "function")
|
290 | wrappedValidations[name] = (...args) => {
|
291 | if (!(enabled[name] ?? enabled.default)) {
|
292 | return;
|
293 | }
|
294 | try {
|
295 | ;
|
296 | validation.apply(
|
297 | wrappedValidations,
|
298 | args
|
299 | );
|
300 | } catch (error) {
|
301 | if (error instanceof ChangeWarning)
|
302 | console.warn(error);
|
303 | else
|
304 | console.error(error);
|
305 | }
|
306 | };
|
307 | }
|
308 | return wrappedValidations;
|
309 | };
|
310 |
|
311 |
|
312 | var MemoryStore = class {
|
313 | constructor() {
|
314 | |
315 |
|
316 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 | this.previous = new Map();
|
325 | this.current = new Map();
|
326 | |
327 |
|
328 |
|
329 |
|
330 | this.localKeys = true;
|
331 | }
|
332 | |
333 |
|
334 |
|
335 |
|
336 |
|
337 | init(options) {
|
338 | this.windowMs = options.windowMs;
|
339 | if (this.interval)
|
340 | clearInterval(this.interval);
|
341 | this.interval = setInterval(() => {
|
342 | this.clearExpired();
|
343 | }, this.windowMs);
|
344 | if (this.interval.unref)
|
345 | this.interval.unref();
|
346 | }
|
347 | |
348 |
|
349 |
|
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 | async get(key) {
|
357 | return this.current.get(key) ?? this.previous.get(key);
|
358 | }
|
359 | |
360 |
|
361 |
|
362 |
|
363 |
|
364 |
|
365 |
|
366 |
|
367 |
|
368 | async increment(key) {
|
369 | const client = this.getClient(key);
|
370 | const now = Date.now();
|
371 | if (client.resetTime.getTime() <= now) {
|
372 | this.resetClient(client, now);
|
373 | }
|
374 | client.totalHits++;
|
375 | return client;
|
376 | }
|
377 | |
378 |
|
379 |
|
380 |
|
381 |
|
382 |
|
383 |
|
384 | async decrement(key) {
|
385 | const client = this.getClient(key);
|
386 | if (client.totalHits > 0)
|
387 | client.totalHits--;
|
388 | }
|
389 | |
390 |
|
391 |
|
392 |
|
393 |
|
394 |
|
395 |
|
396 | async resetKey(key) {
|
397 | this.current.delete(key);
|
398 | this.previous.delete(key);
|
399 | }
|
400 | |
401 |
|
402 |
|
403 |
|
404 |
|
405 | async resetAll() {
|
406 | this.current.clear();
|
407 | this.previous.clear();
|
408 | }
|
409 | |
410 |
|
411 |
|
412 |
|
413 |
|
414 |
|
415 | shutdown() {
|
416 | clearInterval(this.interval);
|
417 | void this.resetAll();
|
418 | }
|
419 | |
420 |
|
421 |
|
422 |
|
423 |
|
424 |
|
425 |
|
426 |
|
427 |
|
428 |
|
429 |
|
430 |
|
431 | resetClient(client, now = Date.now()) {
|
432 | client.totalHits = 0;
|
433 | client.resetTime.setTime(now + this.windowMs);
|
434 | return client;
|
435 | }
|
436 | |
437 |
|
438 |
|
439 |
|
440 |
|
441 |
|
442 |
|
443 |
|
444 | getClient(key) {
|
445 | if (this.current.has(key))
|
446 | return this.current.get(key);
|
447 | let client;
|
448 | if (this.previous.has(key)) {
|
449 | client = this.previous.get(key);
|
450 | this.previous.delete(key);
|
451 | } else {
|
452 | client = { totalHits: 0, resetTime: new Date() };
|
453 | this.resetClient(client);
|
454 | }
|
455 | this.current.set(key, client);
|
456 | return client;
|
457 | }
|
458 | |
459 |
|
460 |
|
461 |
|
462 |
|
463 | clearExpired() {
|
464 | this.previous = this.current;
|
465 | this.current = new Map();
|
466 | }
|
467 | };
|
468 |
|
469 |
|
470 | var isLegacyStore = (store) => (
|
471 |
|
472 |
|
473 | typeof store.incr === "function" && typeof store.increment !== "function"
|
474 | );
|
475 | var promisifyStore = (passedStore) => {
|
476 | if (!isLegacyStore(passedStore)) {
|
477 | return passedStore;
|
478 | }
|
479 | const legacyStore = passedStore;
|
480 | class PromisifiedStore {
|
481 | async increment(key) {
|
482 | return new Promise((resolve, reject) => {
|
483 | legacyStore.incr(
|
484 | key,
|
485 | (error, totalHits, resetTime) => {
|
486 | if (error)
|
487 | reject(error);
|
488 | resolve({ totalHits, resetTime });
|
489 | }
|
490 | );
|
491 | });
|
492 | }
|
493 | async decrement(key) {
|
494 | return legacyStore.decrement(key);
|
495 | }
|
496 | async resetKey(key) {
|
497 | return legacyStore.resetKey(key);
|
498 | }
|
499 |
|
500 | async resetAll() {
|
501 | if (typeof legacyStore.resetAll === "function")
|
502 | return legacyStore.resetAll();
|
503 | }
|
504 | }
|
505 | return new PromisifiedStore();
|
506 | };
|
507 | var getOptionsFromConfig = (config) => {
|
508 | const { validations: validations2, ...directlyPassableEntries } = config;
|
509 | return {
|
510 | ...directlyPassableEntries,
|
511 | validate: validations2.enabled
|
512 | };
|
513 | };
|
514 | var omitUndefinedOptions = (passedOptions) => {
|
515 | const omittedOptions = {};
|
516 | for (const k of Object.keys(passedOptions)) {
|
517 | const key = k;
|
518 | if (passedOptions[key] !== void 0) {
|
519 | omittedOptions[key] = passedOptions[key];
|
520 | }
|
521 | }
|
522 | return omittedOptions;
|
523 | };
|
524 | var parseOptions = (passedOptions) => {
|
525 | const notUndefinedOptions = omitUndefinedOptions(passedOptions);
|
526 | const validations2 = getValidations(notUndefinedOptions?.validate ?? true);
|
527 | validations2.validationsConfig();
|
528 | validations2.draftPolliHeaders(
|
529 |
|
530 | notUndefinedOptions.draft_polli_ratelimit_headers
|
531 | );
|
532 | validations2.onLimitReached(notUndefinedOptions.onLimitReached);
|
533 | let standardHeaders = notUndefinedOptions.standardHeaders ?? false;
|
534 | if (standardHeaders === true)
|
535 | standardHeaders = "draft-6";
|
536 | const config = {
|
537 | windowMs: 60 * 1e3,
|
538 | limit: passedOptions.max ?? 5,
|
539 |
|
540 | message: "Too many requests, please try again later.",
|
541 | statusCode: 429,
|
542 | legacyHeaders: passedOptions.headers ?? true,
|
543 | requestPropertyName: "rateLimit",
|
544 | skipFailedRequests: false,
|
545 | skipSuccessfulRequests: false,
|
546 | requestWasSuccessful: (_request, response) => response.statusCode < 400,
|
547 | skip: (_request, _response) => false,
|
548 | keyGenerator(request, _response) {
|
549 | validations2.ip(request.ip);
|
550 | validations2.trustProxy(request);
|
551 | validations2.xForwardedForHeader(request);
|
552 | return request.ip;
|
553 | },
|
554 | async handler(request, response, _next, _optionsUsed) {
|
555 | response.status(config.statusCode);
|
556 | const message = typeof config.message === "function" ? await config.message(
|
557 | request,
|
558 | response
|
559 | ) : config.message;
|
560 | if (!response.writableEnded) {
|
561 | response.send(message);
|
562 | }
|
563 | },
|
564 |
|
565 | ...notUndefinedOptions,
|
566 |
|
567 | standardHeaders,
|
568 |
|
569 |
|
570 | store: promisifyStore(notUndefinedOptions.store ?? new MemoryStore()),
|
571 |
|
572 | validations: validations2
|
573 | };
|
574 | 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") {
|
575 | throw new TypeError(
|
576 | "An invalid store was passed. Please ensure that the store is a class that implements the `Store` interface."
|
577 | );
|
578 | }
|
579 | return config;
|
580 | };
|
581 | var handleAsyncErrors = (fn) => async (request, response, next) => {
|
582 | try {
|
583 | await Promise.resolve(fn(request, response, next)).catch(next);
|
584 | } catch (error) {
|
585 | next(error);
|
586 | }
|
587 | };
|
588 | var rateLimit = (passedOptions) => {
|
589 | const config = parseOptions(passedOptions ?? {});
|
590 | const options = getOptionsFromConfig(config);
|
591 | if (typeof config.store.init === "function")
|
592 | config.store.init(options);
|
593 | const middleware = handleAsyncErrors(
|
594 | async (request, response, next) => {
|
595 | const skip = await config.skip(request, response);
|
596 | if (skip) {
|
597 | next();
|
598 | return;
|
599 | }
|
600 | const augmentedRequest = request;
|
601 | const key = await config.keyGenerator(request, response);
|
602 | const { totalHits, resetTime } = await config.store.increment(key);
|
603 | config.validations.positiveHits(totalHits);
|
604 | config.validations.singleCount(request, config.store, key);
|
605 | const retrieveLimit = typeof config.limit === "function" ? config.limit(request, response) : config.limit;
|
606 | const limit = await retrieveLimit;
|
607 | config.validations.limit(limit);
|
608 | const info = {
|
609 | limit,
|
610 | used: totalHits,
|
611 | remaining: Math.max(limit - totalHits, 0),
|
612 | resetTime
|
613 | };
|
614 | Object.defineProperty(info, "current", {
|
615 | configurable: false,
|
616 | enumerable: false,
|
617 | value: totalHits
|
618 | });
|
619 | augmentedRequest[config.requestPropertyName] = info;
|
620 | if (config.legacyHeaders && !response.headersSent) {
|
621 | setLegacyHeaders(response, info);
|
622 | }
|
623 | if (config.standardHeaders && !response.headersSent) {
|
624 | if (config.standardHeaders === "draft-6") {
|
625 | setDraft6Headers(response, info, config.windowMs);
|
626 | } else if (config.standardHeaders === "draft-7") {
|
627 | config.validations.headersResetTime(info.resetTime);
|
628 | setDraft7Headers(response, info, config.windowMs);
|
629 | }
|
630 | }
|
631 | if (config.skipFailedRequests || config.skipSuccessfulRequests) {
|
632 | let decremented = false;
|
633 | const decrementKey = async () => {
|
634 | if (!decremented) {
|
635 | await config.store.decrement(key);
|
636 | decremented = true;
|
637 | }
|
638 | };
|
639 | if (config.skipFailedRequests) {
|
640 | response.on("finish", async () => {
|
641 | if (!config.requestWasSuccessful(request, response))
|
642 | await decrementKey();
|
643 | });
|
644 | response.on("close", async () => {
|
645 | if (!response.writableEnded)
|
646 | await decrementKey();
|
647 | });
|
648 | response.on("error", async () => {
|
649 | await decrementKey();
|
650 | });
|
651 | }
|
652 | if (config.skipSuccessfulRequests) {
|
653 | response.on("finish", async () => {
|
654 | if (config.requestWasSuccessful(request, response))
|
655 | await decrementKey();
|
656 | });
|
657 | }
|
658 | }
|
659 | config.validations.disable();
|
660 | if (totalHits > limit) {
|
661 | if (config.legacyHeaders || config.standardHeaders) {
|
662 | setRetryAfterHeader(response, info, config.windowMs);
|
663 | }
|
664 | config.handler(request, response, next, options);
|
665 | return;
|
666 | }
|
667 | next();
|
668 | }
|
669 | );
|
670 | const getThrowFn = () => {
|
671 | throw new Error("The current store does not support the get/getKey method");
|
672 | };
|
673 | middleware.resetKey = config.store.resetKey.bind(config.store);
|
674 | middleware.getKey = typeof config.store.get === "function" ? config.store.get.bind(config.store) : getThrowFn;
|
675 | return middleware;
|
676 | };
|
677 | var lib_default = rateLimit;
|
678 | export {
|
679 | MemoryStore,
|
680 | lib_default as default,
|
681 | lib_default as rateLimit
|
682 | };
|