2022-07-12 01:48:09 +00:00
|
|
|
const { RateLimiterMemory } = require('rate-limiter-flexible')
|
|
|
|
const ClientError = require('./../utils/ClientError')
|
|
|
|
|
|
|
|
class RateLimiter {
|
2022-07-12 03:30:36 +00:00
|
|
|
rateLimiterMemory
|
|
|
|
|
2022-07-12 01:48:09 +00:00
|
|
|
#requestKey
|
|
|
|
#whitelistedKeys
|
|
|
|
|
|
|
|
constructor (requestKey, options = {}, whitelistedKeys) {
|
|
|
|
if (typeof options.points !== 'number' || typeof options.duration !== 'number') {
|
|
|
|
throw new Error('Points and Duration must be set with numbers in options')
|
|
|
|
}
|
|
|
|
|
|
|
|
if (whitelistedKeys && typeof whitelistedKeys instanceof Set) {
|
|
|
|
throw new TypeError('Whitelisted keys must be a Set')
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#requestKey = requestKey
|
|
|
|
this.#whitelistedKeys = new Set(whitelistedKeys)
|
|
|
|
|
|
|
|
this.rateLimiterMemory = new RateLimiterMemory(options)
|
|
|
|
}
|
|
|
|
|
2022-07-21 17:57:57 +00:00
|
|
|
#middleware (req, res, next) {
|
|
|
|
if (res.locals.rateLimit) {
|
|
|
|
return next()
|
|
|
|
}
|
2022-07-12 01:48:09 +00:00
|
|
|
|
|
|
|
// If unset, assume points pool is shared to all visitors of each route
|
|
|
|
const key = this.#requestKey ? req[this.#requestKey] : req.path
|
|
|
|
|
|
|
|
if (this.#whitelistedKeys.has(key)) {
|
|
|
|
// Set the Response local variable for earlier bypass in any subsequent RateLimit middlewares
|
|
|
|
res.locals.rateLimit = 'BYPASS'
|
2022-07-21 17:57:57 +00:00
|
|
|
return next()
|
2022-07-12 01:48:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Always consume only 1 point
|
2022-07-21 17:57:57 +00:00
|
|
|
this.rateLimiterMemory.consume(key, 1)
|
2022-07-12 01:48:09 +00:00
|
|
|
.then(result => {
|
|
|
|
res.locals.rateLimit = result
|
2022-07-21 13:28:10 +00:00
|
|
|
res.header('Retry-After', String(result.msBeforeNext / 1000))
|
|
|
|
res.header('X-RateLimit-Limit', String(this.rateLimiterMemory._points))
|
|
|
|
res.header('X-RateLimit-Remaining', String(result.remainingPoints))
|
|
|
|
res.header('X-RateLimit-Reset', String(new Date(Date.now() + result.msBeforeNext)))
|
2022-07-21 17:57:57 +00:00
|
|
|
return next()
|
2022-07-12 01:48:09 +00:00
|
|
|
})
|
|
|
|
.catch(reject => {
|
|
|
|
// Re-throw with ClientError
|
2022-07-21 17:57:57 +00:00
|
|
|
return next(new ClientError('Rate limit reached, please try again in a while.', { statusCode: 429 }))
|
2022-07-12 01:48:09 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
get middleware () {
|
|
|
|
return this.#middleware.bind(this)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = RateLimiter
|