Files
Andrei 58f8093689 Rebrand from 'Redirect Intelligence v2' to 'URL Tracker Tool V2' throughout UI
- Updated all component headers and documentation
- Changed navbar and footer branding
- Updated homepage hero badge
- Modified page title in index.html
- Simplified footer text to 'Built with ❤️'
- Consistent V2 capitalization across all references
2025-08-19 19:12:23 +00:00

348 lines
11 KiB
JavaScript

const {
LIMITER_TYPES,
ERR_UNKNOWN_LIMITER_TYPE_MESSAGE,
} = require('./constants');
const crypto = require('crypto');
const {
RateLimiterMemory,
RateLimiterCluster,
RateLimiterMemcache,
RateLimiterMongo,
RateLimiterMySQL,
RateLimiterPostgres,
RateLimiterRedis,
} = require('../index');
function getDelayMs(count, delays, maxWait) {
let msDelay = maxWait;
const delayIndex = count - 1;
if (delayIndex >= 0 && delayIndex < delays.length) {
msDelay = delays[delayIndex];
}
return msDelay;
}
const ExpressBruteFlexible = function (limiterType, options) {
ExpressBruteFlexible.instanceCount++;
this.name = `brute${ExpressBruteFlexible.instanceCount}`;
this.options = Object.assign({}, ExpressBruteFlexible.defaults, options);
if (this.options.minWait < 1) {
this.options.minWait = 1;
}
const validLimiterTypes = Object.keys(ExpressBruteFlexible.LIMITER_TYPES).map(k => ExpressBruteFlexible.LIMITER_TYPES[k]);
if (!validLimiterTypes.includes(limiterType)) {
throw new Error(ERR_UNKNOWN_LIMITER_TYPE_MESSAGE);
}
this.limiterType = limiterType;
this.delays = [this.options.minWait];
while (this.delays[this.delays.length - 1] < this.options.maxWait) {
const nextNum = this.delays[this.delays.length - 1] + (this.delays.length > 1 ? this.delays[this.delays.length - 2] : 0);
this.delays.push(nextNum);
}
this.delays[this.delays.length - 1] = this.options.maxWait;
// set default lifetime
if (typeof this.options.lifetime === 'undefined') {
this.options.lifetime = Math.ceil((this.options.maxWait / 1000) * (this.delays.length + this.options.freeRetries));
}
this.prevent = this.getMiddleware({
prefix: this.options.prefix,
});
};
ExpressBruteFlexible.prototype.getMiddleware = function (options) {
const opts = Object.assign({}, options);
const commonKeyPrefix = opts.prefix || '';
const freeLimiterOptions = {
storeClient: this.options.storeClient,
storeType: this.options.storeType,
keyPrefix: `${commonKeyPrefix}free`,
dbName: this.options.dbName,
tableName: this.options.tableName,
points: this.options.freeRetries > 0 ? this.options.freeRetries - 1 : 0,
duration: this.options.lifetime,
};
const blockLimiterOptions = {
storeClient: this.options.storeClient,
storeType: this.options.storeType,
keyPrefix: `${commonKeyPrefix}block`,
dbName: this.options.dbName,
tableName: this.options.tableName,
points: 1,
duration: Math.min(this.options.lifetime, Math.ceil((this.options.maxWait / 1000))),
};
const counterLimiterOptions = {
storeClient: this.options.storeClient,
storeType: this.options.storeType,
keyPrefix: `${commonKeyPrefix}counter`,
dbName: this.options.dbName,
tableName: this.options.tableName,
points: 1,
duration: this.options.lifetime,
};
switch (this.limiterType) {
case 'memory':
this.freeLimiter = new RateLimiterMemory(freeLimiterOptions);
this.blockLimiter = new RateLimiterMemory(blockLimiterOptions);
this.counterLimiter = new RateLimiterMemory(counterLimiterOptions);
break;
case 'cluster':
this.freeLimiter = new RateLimiterCluster(freeLimiterOptions);
this.blockLimiter = new RateLimiterCluster(blockLimiterOptions);
this.counterLimiter = new RateLimiterCluster(counterLimiterOptions);
break;
case 'memcache':
this.freeLimiter = new RateLimiterMemcache(freeLimiterOptions);
this.blockLimiter = new RateLimiterMemcache(blockLimiterOptions);
this.counterLimiter = new RateLimiterMemcache(counterLimiterOptions);
break;
case 'mongo':
this.freeLimiter = new RateLimiterMongo(freeLimiterOptions);
this.blockLimiter = new RateLimiterMongo(blockLimiterOptions);
this.counterLimiter = new RateLimiterMongo(counterLimiterOptions);
break;
case 'mysql':
this.freeLimiter = new RateLimiterMySQL(freeLimiterOptions);
this.blockLimiter = new RateLimiterMySQL(blockLimiterOptions);
this.counterLimiter = new RateLimiterMySQL(counterLimiterOptions);
break;
case 'postgres':
this.freeLimiter = new RateLimiterPostgres(freeLimiterOptions);
this.blockLimiter = new RateLimiterPostgres(blockLimiterOptions);
this.counterLimiter = new RateLimiterPostgres(counterLimiterOptions);
break;
case 'redis':
this.freeLimiter = new RateLimiterRedis(freeLimiterOptions);
this.blockLimiter = new RateLimiterRedis(blockLimiterOptions);
this.counterLimiter = new RateLimiterRedis(counterLimiterOptions);
break;
default:
throw new Error(ERR_UNKNOWN_LIMITER_TYPE_MESSAGE);
}
let keyFunc = opts.key;
if (typeof keyFunc !== 'function') {
keyFunc = function (req, res, next) {
next(opts.key);
};
}
const getFailCallback = (() => (typeof opts.failCallback === 'undefined' ? this.options.failCallback : opts.failCallback));
return (req, res, next) => {
const cannotIncrementErrorObjectBase = {
req,
res,
next,
message: 'Cannot increment request count',
};
keyFunc(req, res, (key) => {
if (!opts.ignoreIP) {
key = ExpressBruteFlexible._getKey([req.ip, this.name, key]);
} else {
key = ExpressBruteFlexible._getKey([this.name, key]);
}
// attach a simpler "reset" function to req.brute.reset
if (this.options.attachResetToRequest) {
let reset = ((callback) => {
Promise.all([
this.freeLimiter.delete(key),
this.blockLimiter.delete(key),
this.counterLimiter.delete(key),
]).then(() => {
if (typeof callback === 'function') {
process.nextTick(() => {
callback();
});
}
}).catch((err) => {
if (typeof callback === 'function') {
process.nextTick(() => {
callback(err);
});
}
});
});
if (req.brute && req.brute.reset) {
// wrap existing reset if one exists
const oldReset = req.brute.reset;
const newReset = reset;
reset = function (callback) {
oldReset(() => {
newReset(callback);
});
};
}
req.brute = {
reset,
};
}
this.freeLimiter.consume(key)
.then(() => {
if (typeof next === 'function') {
next();
}
})
.catch(() => {
Promise.all([
this.blockLimiter.get(key),
this.counterLimiter.get(key),
])
.then((allRes) => {
const [blockRes, counterRes] = allRes;
if (blockRes === null) {
const msDelay = getDelayMs(
counterRes ? counterRes.consumedPoints + 1 : 1,
this.delays,
// eslint-disable-next-line
this.options.maxWait
);
this.blockLimiter.penalty(key, 1, { customDuration: Math.ceil(msDelay / 1000) })
.then((blockPenaltyRes) => {
if (blockPenaltyRes.consumedPoints === 1) {
this.counterLimiter.penalty(key)
.then(() => {
if (typeof next === 'function') {
next();
}
})
.catch((err) => {
this.options.handleStoreError(Object.assign({}, cannotIncrementErrorObjectBase, { parent: err }));
});
} else {
const nextValidDate = new Date(Date.now() + blockPenaltyRes.msBeforeNext);
const failCallback = getFailCallback();
if (typeof failCallback === 'function') {
failCallback(req, res, next, nextValidDate);
}
}
})
.catch((err) => {
this.options.handleStoreError(Object.assign({}, cannotIncrementErrorObjectBase, { parent: err }));
});
} else {
const nextValidDate = new Date(Date.now() + blockRes.msBeforeNext);
const failCallback = getFailCallback();
if (typeof failCallback === 'function') {
failCallback(req, res, next, nextValidDate);
}
}
})
.catch((err) => {
this.options.handleStoreError(Object.assign({}, cannotIncrementErrorObjectBase, { parent: err }));
});
});
});
};
};
ExpressBruteFlexible.prototype.reset = function (ip, key, callback) {
let keyArgs = [];
if (ip) {
keyArgs.push(ip)
}
keyArgs.push(this.name);
keyArgs.push(key);
const ebKey = ExpressBruteFlexible._getKey(keyArgs);
Promise.all([
this.freeLimiter.delete(ebKey),
this.blockLimiter.delete(ebKey),
this.counterLimiter.delete(ebKey),
]).then(() => {
if (typeof callback === 'function') {
process.nextTick(() => {
callback();
});
}
}).catch((err) => {
this.options.handleStoreError({
message: 'Cannot reset request count',
parent: err,
key,
ip,
});
});
};
ExpressBruteFlexible._getKey = function (arr) {
let key = '';
arr.forEach((part) => {
if (part) {
key += crypto.createHash('sha256').update(part).digest('base64');
}
});
return crypto.createHash('sha256').update(key).digest('base64');
};
const setRetryAfter = function (res, nextValidRequestDate) {
const secondUntilNextRequest = Math.ceil((nextValidRequestDate.getTime() - Date.now()) / 1000);
res.header('Retry-After', secondUntilNextRequest);
};
ExpressBruteFlexible.FailTooManyRequests = function (req, res, next, nextValidRequestDate) {
setRetryAfter(res, nextValidRequestDate);
res.status(429);
res.send({
error: {
text: 'Too many requests in this time frame.',
nextValidRequestDate,
},
});
};
ExpressBruteFlexible.FailForbidden = function (req, res, next, nextValidRequestDate) {
setRetryAfter(res, nextValidRequestDate);
res.status(403);
res.send({
error: {
text: 'Too many requests in this time frame.',
nextValidRequestDate,
},
});
};
ExpressBruteFlexible.FailMark = function (req, res, next, nextValidRequestDate) {
res.status(429);
setRetryAfter(res, nextValidRequestDate);
res.nextValidRequestDate = nextValidRequestDate;
next();
};
ExpressBruteFlexible.defaults = {
freeRetries: 2,
attachResetToRequest: true,
minWait: 500,
maxWait: 1000 * 60 * 15,
failCallback: ExpressBruteFlexible.FailTooManyRequests,
handleStoreError(err) {
// eslint-disable-next-line
throw {
message: err.message,
parent: err.parent,
};
},
};
ExpressBruteFlexible.LIMITER_TYPES = LIMITER_TYPES;
ExpressBruteFlexible.instanceCount = 0;
module.exports = ExpressBruteFlexible;