UNPKG

@yveskaufmann/koa2-ratelimit

Version:

IP rate-limiting middleware for Koajs 2. Use to limit repeated requests to APIs and/or endpoints such as password reset.

224 lines 8.13 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.defaultOptions = exports.middleware = exports.RateLimit = exports.DEFAULT_OPTIONS = void 0; const stores_1 = require("./stores"); const Time_1 = require("./Time"); exports.DEFAULT_OPTIONS = { // window, delay, and max apply per-key unless global is set to true interval: { min: 1 }, delayAfter: 0, timeWait: { sec: 1 }, max: 5, message: "Too many requests, please try again later.", statusCode: 429, headers: true, skipFailedRequests: false, prefixKey: "global", prefixKeySeparator: "::", store: new stores_1.MemoryStore(), // redefin fonction keyGenerator: undefined, getUserIdFromKey: undefined, skip: undefined, getUserId: undefined, handler: undefined, onLimitReached: undefined, weight: undefined, whitelist: [], }; const toFinds = ["id", "userId", "user_id", "idUser", "id_user"]; class RateLimit { constructor(options) { this.options = Object.assign({}, exports.DEFAULT_OPTIONS, options); this.options.interval = Time_1.Time.toMs(this.options.interval); this.options.timeWait = Time_1.Time.toMs(this.options.timeWait); // store to use for persisting rate limit data this.store = this.options.store; // ensure that the store extends Store class if (!(this.store instanceof stores_1.Store)) { throw new Error("The store is not valid."); } } static timeToMs(time) { return Time_1.Time.toMs(time); } keyGenerator(ctx) { return __awaiter(this, void 0, void 0, function* () { if (this.options.keyGenerator) { return this.options.keyGenerator(ctx); } const userId = yield this.getUserId(ctx); if (userId) { return `${this.options.prefixKey}|${userId}`; } return `${this.options.prefixKey}|${ctx.request.ip}`; }); } weight(ctx) { return __awaiter(this, void 0, void 0, function* () { if (this.options.weight) { return this.options.weight(ctx); } return 1; }); } skip(ctx) { return __awaiter(this, void 0, void 0, function* () { // eslint-disable-line if (this.options.skip) { return this.options.skip(ctx); } return false; }); } getUserId(ctx) { return __awaiter(this, void 0, void 0, function* () { if (this.options.getUserId) { return this.options.getUserId(ctx); } const whereFinds = [ ctx.state.user, ctx.user, ctx.state.User, ctx.User, ctx.state, ctx, ]; for (const whereFind of whereFinds) { if (whereFind) { for (const toFind of toFinds) { if (whereFind[toFind]) { return whereFind[toFind]; } } } } return null; }); } handler(ctx, next) { return __awaiter(this, void 0, void 0, function* () { if (this.options.handler) { this.options.handler(ctx); } else { ctx.status = this.options.statusCode; ctx.body = { message: this.options.message }; if (this.options.headers) { ctx.set("Retry-After", Math.ceil(Time_1.Time.toMs(this.options.interval) / 1000).toString(10)); } } }); } onLimitReached(ctx) { return __awaiter(this, void 0, void 0, function* () { if (this.options.onLimitReached) { this.options.onLimitReached(ctx); } else { this.store.saveAbuse(Object.assign({}, this.options, { key: yield this.keyGenerator(ctx), ip: ctx.request.ip, user_id: yield this.getUserId(ctx), })); } }); } get middleware() { return this._rateLimit.bind(this); } _rateLimit(ctx, next) { var _a; return __awaiter(this, void 0, void 0, function* () { const skip = yield this.skip(ctx); if (skip) { return next(); } const key = yield this.keyGenerator(ctx); if (this._isWhitelisted(key)) { return next(); } const weight = yield this.weight(ctx); const { counter, dateEnd } = yield this.store.incr(key, this.options, weight); const reset = new Date(dateEnd).getTime(); ctx.state.rateLimit = { limit: this.options.max, current: counter, remaining: Math.max(this.options.max - counter, 0), reset: Math.ceil(reset / 1000), }; if (this.options.headers) { ctx.set("X-RateLimit-Limit", (_a = this.options.max) === null || _a === void 0 ? void 0 : _a.toString(10)); ctx.set("X-RateLimit-Remaining", ctx.state.rateLimit.remaining); ctx.set("X-RateLimit-Reset", ctx.state.rateLimit.reset); } if (this.options.max && counter > this.options.max) { yield this.onLimitReached(ctx); return this.handler(ctx, next); } if (this.options.skipFailedRequests) { ctx.res.on("finish", () => { if (ctx.status >= 400) { this.store.decrement(key, this.options, weight); } }); } if (this.options.delayAfter && this.options.timeWait && counter > this.options.delayAfter) { const delay = (counter - this.options.delayAfter) * Time_1.Time.toMs(this.options.timeWait); yield this.wait(delay); return next(); } return next(); }); } _isWhitelisted(key) { const { whitelist } = this.options; if (whitelist == null || whitelist.length === 0) { return false; } const userId = this.getUserIdFromKey(key); if (userId) { return whitelist.includes(userId); } return false; } getUserIdFromKey(key) { if (this.options.getUserIdFromKey) { return this.options.getUserIdFromKey(key); } Time_1.Time.toMs; const [, userId] = key.split(this.options.prefixKeySeparator); return userId; } wait(ms) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve) => setTimeout(resolve, ms)); }); } } exports.RateLimit = RateLimit; function middleware(options = {}) { return new RateLimit(options).middleware; } exports.middleware = middleware; function defaultOptions(options = {}) { Object.assign(exports.DEFAULT_OPTIONS, options); } exports.defaultOptions = defaultOptions; exports.default = { RateLimit, middleware, defaultOptions, }; //# sourceMappingURL=RateLimit.js.map