mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2025-01-30 23:01:34 +00:00
feat: RateLimiter custom middleware class
this adds new production dependency rate-limiter-flexible this deprecates old rateLimits option in config to use the new rate limiters, the new option is named rateLimiters and rateLimitersWhitelist please consult config.sample.js rate limiters will also be now processed before any other middlewares, as only makes sense
This commit is contained in:
parent
26ae853362
commit
79631ce624
@ -206,42 +206,20 @@ module.exports = {
|
||||
trustProxy: true,
|
||||
|
||||
/*
|
||||
Rate limits.
|
||||
Please be aware that these apply to all users, including site owners.
|
||||
https://github.com/nfriedly/express-rate-limit/tree/v6.3.0#configuration
|
||||
Rate limiters.
|
||||
https://github.com/animir/node-rate-limiter-flexible/wiki/Memory
|
||||
*/
|
||||
rateLimits: [
|
||||
{
|
||||
// 10 requests in 1 second
|
||||
routes: [
|
||||
'/api/'
|
||||
],
|
||||
config: {
|
||||
windowMs: 1000,
|
||||
max: 10,
|
||||
legacyHeaders: true,
|
||||
standardHeaders: true,
|
||||
message: {
|
||||
success: false,
|
||||
description: 'Rate limit reached, please try again in a while.'
|
||||
}
|
||||
}
|
||||
},
|
||||
rateLimiters: [
|
||||
{
|
||||
// 2 requests in 5 seconds
|
||||
routes: [
|
||||
// If multiple routes, they will share the same points pool
|
||||
'/api/login',
|
||||
'/api/register'
|
||||
],
|
||||
config: {
|
||||
windowMs: 5 * 1000,
|
||||
max: 2,
|
||||
legacyHeaders: true,
|
||||
standardHeaders: true,
|
||||
message: {
|
||||
success: false,
|
||||
description: 'Rate limit reached, please try again in 5 seconds.'
|
||||
}
|
||||
options: {
|
||||
points: 2,
|
||||
duration: 5
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -249,11 +227,9 @@ module.exports = {
|
||||
routes: [
|
||||
'/api/album/zip'
|
||||
],
|
||||
config: {
|
||||
windowMs: 30 * 1000,
|
||||
max: 6,
|
||||
legacyHeaders: true,
|
||||
standardHeaders: true
|
||||
options: {
|
||||
points: 6,
|
||||
duration: 30
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -261,19 +237,35 @@ module.exports = {
|
||||
routes: [
|
||||
'/api/tokens/change'
|
||||
],
|
||||
config: {
|
||||
windowMs: 60 * 1000,
|
||||
max: 1,
|
||||
legacyHeaders: true,
|
||||
standardHeaders: true,
|
||||
message: {
|
||||
success: false,
|
||||
description: 'Rate limit reached, please try again in 60 seconds.'
|
||||
}
|
||||
options: {
|
||||
points: 1,
|
||||
duration: 60
|
||||
}
|
||||
},
|
||||
/*
|
||||
Routes, whose scope would have encompassed other routes that have their own rate limit pools,
|
||||
must only be set after said routes, otherwise their rate limit pools will never trigger.
|
||||
i.e. since /api/ encompass all other /api/* routes, it must be set last
|
||||
*/
|
||||
{
|
||||
// 10 requests in 1 second
|
||||
routes: [
|
||||
'/api/'
|
||||
],
|
||||
options: {
|
||||
points: 10,
|
||||
duration: 1
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
/*
|
||||
Whitelisted IP addresses for rate limiters.
|
||||
*/
|
||||
rateLimitersWhitelist: [
|
||||
'127.0.0.1'
|
||||
],
|
||||
|
||||
/*
|
||||
Uploads config.
|
||||
*/
|
||||
|
56
controllers/middlewares/rateLimiter.js
Normal file
56
controllers/middlewares/rateLimiter.js
Normal file
@ -0,0 +1,56 @@
|
||||
const { RateLimiterMemory } = require('rate-limiter-flexible')
|
||||
const ClientError = require('./../utils/ClientError')
|
||||
|
||||
class RateLimiter {
|
||||
#requestKey
|
||||
#whitelistedKeys
|
||||
rateLimiterMemory
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
async #middleware (req, res, next) {
|
||||
if (res.locals.rateLimit) return
|
||||
|
||||
// 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'
|
||||
return
|
||||
}
|
||||
|
||||
// Always consume only 1 point
|
||||
await this.rateLimiterMemory.consume(key, 1)
|
||||
.then(result => {
|
||||
res.locals.rateLimit = result
|
||||
res.set('Retry-After', String(result.msBeforeNext / 1000))
|
||||
res.set('X-RateLimit-Limit', String(this.rateLimiterMemory._points))
|
||||
res.set('X-RateLimit-Remaining', String(result.remainingPoints))
|
||||
res.set('X-RateLimit-Reset', String(new Date(Date.now() + result.msBeforeNext)))
|
||||
})
|
||||
.catch(reject => {
|
||||
// Re-throw with ClientError
|
||||
throw new ClientError('Rate limit reached, please try again in a while.', { statusCode: 429 })
|
||||
})
|
||||
}
|
||||
|
||||
get middleware () {
|
||||
return this.#middleware.bind(this)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RateLimiter
|
30
lolisafe.js
30
lolisafe.js
@ -14,7 +14,6 @@ const helmet = require('helmet')
|
||||
const HyperExpress = require('hyper-express')
|
||||
const LiveDirectory = require('live-directory')
|
||||
const NodeClam = require('clamscan')
|
||||
// const rateLimit = require('express-rate-limit') // FIXME: Find alternative
|
||||
const { accessSync, constants } = require('fs')
|
||||
|
||||
// Check required config files
|
||||
@ -46,6 +45,7 @@ const utils = require('./controllers/utilsController')
|
||||
|
||||
// Custom middlewares
|
||||
const NunjucksRenderer = require('./controllers/middlewares/nunjucksRenderer')
|
||||
const RateLimiter = require('./controllers/middlewares/rateLimiter')
|
||||
// const ServeStatic = require('./controllers/middlewares/serveStatic') // TODO
|
||||
|
||||
// Routes
|
||||
@ -57,6 +57,21 @@ const player = require('./routes/player')
|
||||
|
||||
const isDevMode = process.env.NODE_ENV === 'development'
|
||||
|
||||
// Rate limiters
|
||||
if (Array.isArray(config.rateLimiters)) {
|
||||
let whitelistedKeys
|
||||
if (Array.isArray(config.rateLimitersWhitelist)) {
|
||||
whitelistedKeys = new Set(config.rateLimitersWhitelist)
|
||||
}
|
||||
for (const rateLimit of config.rateLimiters) {
|
||||
// Init RateLimiter using Request.ip as key
|
||||
const rateLimiterInstance = new RateLimiter('ip', rateLimit.options, whitelistedKeys)
|
||||
for (const route of rateLimit.routes) {
|
||||
safe.use(route, rateLimiterInstance.middleware)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helmet security headers
|
||||
if (config.helmet instanceof Object) {
|
||||
// If an empty object, simply do not use Helmet
|
||||
@ -111,19 +126,6 @@ const initLiveDirectory = (options = {}) => {
|
||||
return new LiveDirectory(options)
|
||||
}
|
||||
|
||||
// Configure rate limits (disabled during development)
|
||||
// FIXME: express-rate-limit does not work with hyper-express, find alternative
|
||||
/*
|
||||
if (!isDevMode && Array.isArray(config.rateLimits) && config.rateLimits.length) {
|
||||
for (const _rateLimit of config.rateLimits) {
|
||||
const limiter = rateLimit(_rateLimit.config)
|
||||
for (const route of _rateLimit.routes) {
|
||||
safe.use(route, limiter)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const cdnPages = [...config.pages]
|
||||
|
||||
// Defaults to no-op
|
||||
|
@ -51,6 +51,7 @@
|
||||
"node-fetch": "~2.6.7",
|
||||
"nunjucks": "~3.2.3",
|
||||
"randomstring": "~1.2.2",
|
||||
"rate-limiter-flexible": "^2.3.7",
|
||||
"search-query-parser": "~1.6.0",
|
||||
"sharp": "~0.30.7",
|
||||
"systeminformation": "5.11.23"
|
||||
|
@ -5373,6 +5373,11 @@ range-parser@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||
|
||||
rate-limiter-flexible@^2.3.7:
|
||||
version "2.3.7"
|
||||
resolved "https://registry.yarnpkg.com/rate-limiter-flexible/-/rate-limiter-flexible-2.3.7.tgz#c23e1f818a1575f1de1fd173437f4072125e1615"
|
||||
integrity sha512-dmc+J/IffVBvHlqq5/XClsdLdkOdQV/tjrz00cwneHUbEDYVrf4aUDAyR4Jybcf2+Vpn4NwoVrnnAyt/D0ciWw==
|
||||
|
||||
rc@^1.2.7, rc@^1.2.8:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||
|
Loading…
Reference in New Issue
Block a user