rate-limiter-flexible
Version:
Node.js rate limiter by key and protection from DDoS and Brute-Force attacks in process Memory, Redis, MongoDb, Memcached, MySQL, PostgreSQL, Cluster or PM
385 lines (350 loc) • 11.1 kB
JavaScript
const RateLimiterAbstract = require('./RateLimiterAbstract');
const BlockedKeys = require('./component/BlockedKeys');
const RateLimiterRes = require('./RateLimiterRes');
module.exports = class RateLimiterStoreAbstract extends RateLimiterAbstract {
/**
*
* @param opts Object Defaults {
* ... see other in RateLimiterAbstract
*
* inMemoryBlockOnConsumed: 40, // Number of points when key is blocked
* inMemoryBlockDuration: 10, // Block duration in seconds
* insuranceLimiter: RateLimiterAbstract
* }
*/
constructor(opts = {}) {
super(opts);
this.inMemoryBlockOnConsumed = opts.inMemoryBlockOnConsumed;
this.inMemoryBlockDuration = opts.inMemoryBlockDuration;
this.insuranceLimiter = opts.insuranceLimiter;
this._inMemoryBlockedKeys = new BlockedKeys();
}
get client() {
return this._client;
}
set client(value) {
if (typeof value === 'undefined') {
throw new Error('storeClient is not set');
}
this._client = value;
}
/**
* Have to be launched after consume
* It blocks key and execute evenly depending on result from store
*
* It uses _getRateLimiterRes function to prepare RateLimiterRes from store result
*
* @param resolve
* @param reject
* @param rlKey
* @param changedPoints
* @param storeResult
* @param {Object} options
* @private
*/
_afterConsume(resolve, reject, rlKey, changedPoints, storeResult, options = {}) {
const res = this._getRateLimiterRes(rlKey, changedPoints, storeResult);
if (this.inMemoryBlockOnConsumed > 0 && !(this.inMemoryBlockDuration > 0)
&& res.consumedPoints >= this.inMemoryBlockOnConsumed
) {
this._inMemoryBlockedKeys.addMs(rlKey, res.msBeforeNext);
if (res.consumedPoints > this.points) {
return reject(res);
} else {
return resolve(res)
}
} else if (res.consumedPoints > this.points) {
let blockPromise = Promise.resolve();
// Block only first time when consumed more than points
if (this.blockDuration > 0 && res.consumedPoints <= (this.points + changedPoints)) {
res.msBeforeNext = this.msBlockDuration;
blockPromise = this._block(rlKey, res.consumedPoints, this.msBlockDuration, options);
}
if (this.inMemoryBlockOnConsumed > 0 && res.consumedPoints >= this.inMemoryBlockOnConsumed) {
// Block key for this.inMemoryBlockDuration seconds
this._inMemoryBlockedKeys.add(rlKey, this.inMemoryBlockDuration);
res.msBeforeNext = this.msInMemoryBlockDuration;
}
blockPromise
.then(() => {
reject(res);
})
.catch((err) => {
reject(err);
});
} else if (this.execEvenly && res.msBeforeNext > 0 && !res.isFirstInDuration) {
let delay = Math.ceil(res.msBeforeNext / (res.remainingPoints + 2));
if (delay < this.execEvenlyMinDelayMs) {
delay = res.consumedPoints * this.execEvenlyMinDelayMs;
}
setTimeout(resolve, delay, res);
} else {
resolve(res);
}
}
_handleError(err, funcName, resolve, reject, key, data = false, options = {}) {
if (!(this.insuranceLimiter instanceof RateLimiterAbstract)) {
reject(err);
} else {
this.insuranceLimiter[funcName](key, data, options)
.then((res) => {
resolve(res);
})
.catch((res) => {
reject(res);
});
}
}
getInMemoryBlockMsBeforeExpire(rlKey) {
if (this.inMemoryBlockOnConsumed > 0) {
return this._inMemoryBlockedKeys.msBeforeExpire(rlKey);
}
return 0;
}
get inMemoryBlockOnConsumed() {
return this._inMemoryBlockOnConsumed;
}
set inMemoryBlockOnConsumed(value) {
this._inMemoryBlockOnConsumed = value ? parseInt(value) : 0;
if (this.inMemoryBlockOnConsumed > 0 && this.points > this.inMemoryBlockOnConsumed) {
throw new Error('inMemoryBlockOnConsumed option must be greater or equal "points" option');
}
}
get inMemoryBlockDuration() {
return this._inMemoryBlockDuration;
}
set inMemoryBlockDuration(value) {
this._inMemoryBlockDuration = value ? parseInt(value) : 0;
if (this.inMemoryBlockDuration > 0 && this.inMemoryBlockOnConsumed === 0) {
throw new Error('inMemoryBlockOnConsumed option must be set up');
}
}
get msInMemoryBlockDuration() {
return this._inMemoryBlockDuration * 1000;
}
get insuranceLimiter() {
return this._insuranceLimiter;
}
set insuranceLimiter(value) {
if (typeof value !== 'undefined' && !(value instanceof RateLimiterAbstract)) {
throw new Error('insuranceLimiter must be instance of RateLimiterAbstract');
}
this._insuranceLimiter = value;
if (this._insuranceLimiter) {
this._insuranceLimiter.blockDuration = this.blockDuration;
this._insuranceLimiter.execEvenly = this.execEvenly;
}
}
/**
* Block any key for secDuration seconds
*
* @param key
* @param secDuration
* @param {Object} options
*
* @return Promise<RateLimiterRes>
*/
block(key, secDuration, options = {}) {
const msDuration = secDuration * 1000;
return this._block(this.getKey(key), this.points + 1, msDuration, options);
}
/**
* Set points by key for any duration
*
* @param key
* @param points
* @param secDuration
* @param {Object} options
*
* @return Promise<RateLimiterRes>
*/
set(key, points, secDuration, options = {}) {
const msDuration = (secDuration >= 0 ? secDuration : this.duration) * 1000;
return this._block(this.getKey(key), points, msDuration, options);
}
/**
*
* @param key
* @param pointsToConsume
* @param {Object} options
* @returns Promise<RateLimiterRes>
*/
consume(key, pointsToConsume = 1, options = {}) {
return new Promise((resolve, reject) => {
const rlKey = this.getKey(key);
const inMemoryBlockMsBeforeExpire = this.getInMemoryBlockMsBeforeExpire(rlKey);
if (inMemoryBlockMsBeforeExpire > 0) {
return reject(new RateLimiterRes(0, inMemoryBlockMsBeforeExpire));
}
this._upsert(rlKey, pointsToConsume, this._getKeySecDuration(options) * 1000, false, options)
.then((res) => {
this._afterConsume(resolve, reject, rlKey, pointsToConsume, res);
})
.catch((err) => {
this._handleError(err, 'consume', resolve, reject, key, pointsToConsume, options);
});
});
}
/**
*
* @param key
* @param points
* @param {Object} options
* @returns Promise<RateLimiterRes>
*/
penalty(key, points = 1, options = {}) {
const rlKey = this.getKey(key);
return new Promise((resolve, reject) => {
this._upsert(rlKey, points, this._getKeySecDuration(options) * 1000, false, options)
.then((res) => {
resolve(this._getRateLimiterRes(rlKey, points, res));
})
.catch((err) => {
this._handleError(err, 'penalty', resolve, reject, key, points, options);
});
});
}
/**
*
* @param key
* @param points
* @param {Object} options
* @returns Promise<RateLimiterRes>
*/
reward(key, points = 1, options = {}) {
const rlKey = this.getKey(key);
return new Promise((resolve, reject) => {
this._upsert(rlKey, -points, this._getKeySecDuration(options) * 1000, false, options)
.then((res) => {
resolve(this._getRateLimiterRes(rlKey, -points, res));
})
.catch((err) => {
this._handleError(err, 'reward', resolve, reject, key, points, options);
});
});
}
/**
*
* @param key
* @param {Object} options
* @returns Promise<RateLimiterRes>|null
*/
get(key, options = {}) {
const rlKey = this.getKey(key);
return new Promise((resolve, reject) => {
this._get(rlKey, options)
.then((res) => {
if (res === null || typeof res === 'undefined') {
resolve(null);
} else {
resolve(this._getRateLimiterRes(rlKey, 0, res));
}
})
.catch((err) => {
this._handleError(err, 'get', resolve, reject, key, options);
});
});
}
/**
*
* @param key
* @param {Object} options
* @returns Promise<boolean>
*/
delete(key, options = {}) {
const rlKey = this.getKey(key);
return new Promise((resolve, reject) => {
this._delete(rlKey, options)
.then((res) => {
this._inMemoryBlockedKeys.delete(rlKey);
resolve(res);
})
.catch((err) => {
this._handleError(err, 'delete', resolve, reject, key, options);
});
});
}
/**
* Cleanup keys no-matter expired or not.
*/
deleteInMemoryBlockedAll() {
this._inMemoryBlockedKeys.delete();
}
/**
* Get RateLimiterRes object filled depending on storeResult, which specific for exact store
*
* @param rlKey
* @param changedPoints
* @param storeResult
* @private
*/
_getRateLimiterRes(rlKey, changedPoints, storeResult) { // eslint-disable-line no-unused-vars
throw new Error("You have to implement the method '_getRateLimiterRes'!");
}
/**
* Block key for this.msBlockDuration milliseconds
* Usually, it just prolongs lifetime of key
*
* @param rlKey
* @param initPoints
* @param msDuration
* @param {Object} options
*
* @return Promise<any>
*/
_block(rlKey, initPoints, msDuration, options = {}) {
return new Promise((resolve, reject) => {
this._upsert(rlKey, initPoints, msDuration, true, options)
.then(() => {
resolve(new RateLimiterRes(0, msDuration > 0 ? msDuration : -1, initPoints));
})
.catch((err) => {
this._handleError(err, 'block', resolve, reject, this.parseKey(rlKey), msDuration / 1000, options);
});
});
}
/**
* Have to be implemented in every limiter
* Resolve with raw result from Store OR null if rlKey is not set
* or Reject with error
*
* @param rlKey
* @param {Object} options
* @private
*
* @return Promise<any>
*/
_get(rlKey, options = {}) { // eslint-disable-line no-unused-vars
throw new Error("You have to implement the method '_get'!");
}
/**
* Have to be implemented
* Resolve with true OR false if rlKey doesn't exist
* or Reject with error
*
* @param rlKey
* @param {Object} options
* @private
*
* @return Promise<any>
*/
_delete(rlKey, options = {}) { // eslint-disable-line no-unused-vars
throw new Error("You have to implement the method '_delete'!");
}
/**
* Have to be implemented
* Resolve with object used for {@link _getRateLimiterRes} to generate {@link RateLimiterRes}
*
* @param {string} rlKey
* @param {number} points
* @param {number} msDuration
* @param {boolean} forceExpire
* @param {Object} options
* @abstract
*
* @return Promise<Object>
*/
_upsert(rlKey, points, msDuration, forceExpire = false, options = {}) {
throw new Error("You have to implement the method '_upsert'!");
}
};