UNPKG

block-ddos

Version:

This package provide a middleware to block multiple request

254 lines (253 loc) 10.5 kB
"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;