@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
JavaScript
"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