block-ddos
Version:
This package provide a middleware to block multiple request
254 lines (253 loc) • 10.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.blockDDoS = void 0;
const defaultMsg = 'Blocked by proxy. Try again in a moment!';
/**
* @description Data to store and control the request flow.
*/
class Info {
constructor(hash, expiresAt, attempts, ttl) {
this.hash = hash;
this.expiresAt = expiresAt;
this.attempts = attempts;
this.ttl = ttl;
Object.freeze(this);
}
/**
* @description Get unique hash from request.
* @param request user Request.
* @returns hash as string.
*/
static GetHash(request) {
const ip = this.GetIP(request);
const hash = `[${ip}][${request.method}][${request.path}]`;
return hash;
}
/**
* @description Create a new instance with incremented attempts.
* @returns instance of Data.
*/
Increment() {
const attempts = this.attempts + 1;
const expiresAt = Date.now() + this.ttl;
return new Info(this.hash, expiresAt, attempts, this.ttl);
}
/**
* @description Create an instance of Data from request to store.
* @param request user Request.
* @param ttl time to expires in milliseconds.
* @returns instance of Data as readonly.
* @default ttl 10000 as milliseconds
*/
static Create(request, ttl = 10000) {
const ip = this.GetIP(request);
const hash = `[${ip}][${request.method}][${request.path}]`;
const createdAt = Date.now();
const expiresAt = createdAt + ttl;
const attempts = 1;
return new Info(hash, expiresAt, attempts, ttl);
}
/**
* @description Get ip from user request.
* @param request user Request.
* @returns ip as string.
*/
static GetIP(request) {
var _a, _b, _c, _d, _e, _f, _g;
const headersIp = (_b = (_a = request === null || request === void 0 ? void 0 : request.headers) === null || _a === void 0 ? void 0 : _a['x-forwarded-for']) !== null && _b !== void 0 ? _b : '';
const ipStr = Array.isArray(headersIp) ? headersIp.toString() : headersIp;
const ip = (_f = (_e = (_d = (_c = ipStr === null || ipStr === void 0 ? void 0 : ipStr.replace(/\s/g, '')) === null || _c === void 0 ? void 0 : _c.split(',')) === null || _d === void 0 ? void 0 : _d[0]) !== null && _e !== void 0 ? _e : request === null || request === void 0 ? void 0 : request.ip) !== null && _f !== void 0 ? _f : (_g = request.socket) === null || _g === void 0 ? void 0 : _g.remoteAddress;
return (ip === '::1') ? '127.0.0.1' : ip !== null && ip !== void 0 ? ip : '0.0.0.0';
}
}
/**
* @description Local instance in memory to store request info.
* @summary all data are updated for each 5000 milliseconds or 5 seconds
*/
class MemoryStore {
constructor(attempts) {
this.Db = [];
this.interval = 10000;
this.attempts = attempts;
this.timer = null;
this.StartTimerCaseData();
}
/**
* @description Create a singleton instance of MemoryStore.
* @param interval time in milliseconds to expires data in store.
* @returns instance of MemoryStore.
*/
static Create(attempts = 2) {
if (MemoryStore.instance)
return MemoryStore.instance;
MemoryStore.instance = new MemoryStore(attempts);
return MemoryStore.instance;
}
/**
* @description Save data to store
* @param data instance of Data
* @returns instance of MemoryStore.
*/
Save(data) {
const alreadyExists = this.Exists(data.hash);
if (alreadyExists) {
this.Increment(data);
this.StartTimerCaseData();
return this;
}
this.Db.push(data);
this.StartTimerCaseData();
return this;
}
/**
* @description Check if exists data for hash.
* @param hash string as `[ip][method][path]`
* @returns true if exist store for hash and returns false if do not exists.
*/
Exists(hash) {
const exists = this.GetByHash(hash);
return !!exists;
}
/**
* @description Increment attempts to info.
* @param data instance of Data.
*/
Increment(data) {
const lastInfo = this.GetByHash(data.hash);
this.Db = this.Db.filter((dt) => dt.hash !== data.hash);
if (lastInfo)
this.Db.push(lastInfo.Increment());
}
/**
* @description Check total attempts to a route.
* @param data instance of Data.
* @returns true if has the limit attempts and return false if do not.
*/
HasMaxAttempts(data) {
return this.attempts <= data.attempts;
}
/**
* @description Check if can user can access the route.
* @param hash string as `[ip][method][path]`
* @returns true if user can access the route and returns false if user do not.
*/
CanAccess(hash) {
const exists = this.GetByHash(hash);
if (!exists)
return true;
const hasMaxAttempts = this.HasMaxAttempts(exists);
if (hasMaxAttempts)
return false;
return true;
}
/**
* @description Get store by hash.
* @param hash string as `[ip][method][path]`
* @returns Data as store or null.
*/
GetByHash(hash) {
const data = this.Db.find((data) => data.hash === hash);
if (!data)
return null;
return data;
}
/**
* @description Remove expired data from store.
* @returns instance of MemoryStore.
*/
RemoveExpired() {
const now = Date.now();
this.Db = this.Db.filter((data) => data.expiresAt > now);
this.StopTimerCaseEmpty();
return this;
}
/**
* @description Check if exists data in store.
* @returns true if exist data in store and false if do not.
*/
HasData() {
return this.Db.length > 0;
}
/**
* @description Stops Event Loop if do not has data in store.
* @returns void.
*/
StopTimerCaseEmpty() {
if (this.HasData() || !this.timer)
return;
clearInterval(this.timer);
this.timer = null;
}
/**
* @description Event Loop to delete expired register.
* @returns instance of timer or null.
* @summary Running as single event and only if exist data.
*/
StartTimerCaseData() {
if (!this.HasData() || this.timer)
return null;
const timer = setInterval(() => { this.RemoveExpired(); }, this.interval);
this.timer = timer;
return timer;
}
}
/**
* @description Validate params.
* @param params throws if some value is invalid.
*/
const ValidateParams = (params) => {
if (params && (params === null || params === void 0 ? void 0 : params.attempts) && typeof params.attempts !== 'number')
throw new Error('The attempts param must be a positive number');
if (params && (params === null || params === void 0 ? void 0 : params.interval) && typeof params.interval !== 'number')
throw new Error('The time interval must be a number');
if (params && (params === null || params === void 0 ? void 0 : params.interval) && params.interval < 10000)
throw new Error('The time interval must be greater than or equal to 10000ms');
if (typeof (params === null || params === void 0 ? void 0 : params.attempts) === 'number' && (params.attempts < 1 || params.attempts > 7))
throw new Error('The attempts param must be between 0 and 8');
};
/**
* @description Middleware to block multiple request to a same route from a same ip.
* @param param is Object:
* @param interval time interval between requests in milliseconds.
* @param error Object or String. data to be sent to user in response when error.
* @param attempts number of attempts allowed before blocking next request. 1 - 7.
* @returns Middleware function.
*
* @default interval `10000` = `10 sec`
* @default error { message: `Blocked by proxy. Try again in a moment!` }
* @default attempts 2
* @throws if provide interval as not a number
* @throws if provide interval less than 10000ms or 10 sec
* @throws if provide attempts less than 1 or greater than 7.
*/
const blockDDoS = (params) => {
ValidateParams(params);
return (req, res, next) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
if ((req === null || req === void 0 ? void 0 : req.path) === '/favicon.ico' || !req.protocol.includes('http'))
return next();
const maybeTotal = (_e = (_d = (_c = (_b = (_a = req.headers) === null || _a === void 0 ? void 0 : _a.cookie) === null || _b === void 0 ? void 0 : _b.split(';')) === null || _c === void 0 ? void 0 : _c.find((c) => c === null || c === void 0 ? void 0 : c.includes('ddos-blocked-times='))) === null || _d === void 0 ? void 0 : _d.split("=")) === null || _e === void 0 ? void 0 : _e[1];
if (maybeTotal && !isNaN(+String(maybeTotal)) && +String(maybeTotal) >= 20) {
return res.status(429).json({ error: (_f = params === null || params === void 0 ? void 0 : params.error) !== null && _f !== void 0 ? _f : { message: defaultMsg } });
}
const store = MemoryStore.Create(params === null || params === void 0 ? void 0 : params.attempts);
const info = Info.Create(req, params === null || params === void 0 ? void 0 : params.interval);
const hash = Info.GetHash(req);
const canAccess = store.CanAccess(hash);
if (!canAccess) {
const hasProtocol = (_h = (_g = req === null || req === void 0 ? void 0 : req.headers) === null || _g === void 0 ? void 0 : _g['x-forwarded-proto']) !== null && _h !== void 0 ? _h : '';
const isHTTPS = hasProtocol.includes('https');
const TEN_MIN = 1000 * 60 * 10;
const tries = (maybeTotal && !isNaN(+String(maybeTotal))) ? +String(maybeTotal) + 1 : 1;
const secure = isHTTPS && hasProtocol.includes('https');
const maxAge = TEN_MIN;
const options = { maxAge, httpOnly: true, domain: req.hostname, secure, path: req.path };
res.cookie('ddos-blocked-times', tries, options);
return res.status(429).json({ error: (_j = params === null || params === void 0 ? void 0 : params.error) !== null && _j !== void 0 ? _j : { message: defaultMsg } });
}
store.Save(info);
return next();
};
};
exports.blockDDoS = blockDDoS;
exports.default = exports.blockDDoS;