mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2025-02-07 13:59:01 +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,
|
trustProxy: true,
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Rate limits.
|
Rate limiters.
|
||||||
Please be aware that these apply to all users, including site owners.
|
https://github.com/animir/node-rate-limiter-flexible/wiki/Memory
|
||||||
https://github.com/nfriedly/express-rate-limit/tree/v6.3.0#configuration
|
|
||||||
*/
|
*/
|
||||||
rateLimits: [
|
rateLimiters: [
|
||||||
{
|
|
||||||
// 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.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// 2 requests in 5 seconds
|
// 2 requests in 5 seconds
|
||||||
routes: [
|
routes: [
|
||||||
|
// If multiple routes, they will share the same points pool
|
||||||
'/api/login',
|
'/api/login',
|
||||||
'/api/register'
|
'/api/register'
|
||||||
],
|
],
|
||||||
config: {
|
options: {
|
||||||
windowMs: 5 * 1000,
|
points: 2,
|
||||||
max: 2,
|
duration: 5
|
||||||
legacyHeaders: true,
|
|
||||||
standardHeaders: true,
|
|
||||||
message: {
|
|
||||||
success: false,
|
|
||||||
description: 'Rate limit reached, please try again in 5 seconds.'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -249,11 +227,9 @@ module.exports = {
|
|||||||
routes: [
|
routes: [
|
||||||
'/api/album/zip'
|
'/api/album/zip'
|
||||||
],
|
],
|
||||||
config: {
|
options: {
|
||||||
windowMs: 30 * 1000,
|
points: 6,
|
||||||
max: 6,
|
duration: 30
|
||||||
legacyHeaders: true,
|
|
||||||
standardHeaders: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -261,19 +237,35 @@ module.exports = {
|
|||||||
routes: [
|
routes: [
|
||||||
'/api/tokens/change'
|
'/api/tokens/change'
|
||||||
],
|
],
|
||||||
config: {
|
options: {
|
||||||
windowMs: 60 * 1000,
|
points: 1,
|
||||||
max: 1,
|
duration: 60
|
||||||
legacyHeaders: true,
|
}
|
||||||
standardHeaders: true,
|
},
|
||||||
message: {
|
/*
|
||||||
success: false,
|
Routes, whose scope would have encompassed other routes that have their own rate limit pools,
|
||||||
description: 'Rate limit reached, please try again in 60 seconds.'
|
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.
|
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 HyperExpress = require('hyper-express')
|
||||||
const LiveDirectory = require('live-directory')
|
const LiveDirectory = require('live-directory')
|
||||||
const NodeClam = require('clamscan')
|
const NodeClam = require('clamscan')
|
||||||
// const rateLimit = require('express-rate-limit') // FIXME: Find alternative
|
|
||||||
const { accessSync, constants } = require('fs')
|
const { accessSync, constants } = require('fs')
|
||||||
|
|
||||||
// Check required config files
|
// Check required config files
|
||||||
@ -46,6 +45,7 @@ const utils = require('./controllers/utilsController')
|
|||||||
|
|
||||||
// Custom middlewares
|
// Custom middlewares
|
||||||
const NunjucksRenderer = require('./controllers/middlewares/nunjucksRenderer')
|
const NunjucksRenderer = require('./controllers/middlewares/nunjucksRenderer')
|
||||||
|
const RateLimiter = require('./controllers/middlewares/rateLimiter')
|
||||||
// const ServeStatic = require('./controllers/middlewares/serveStatic') // TODO
|
// const ServeStatic = require('./controllers/middlewares/serveStatic') // TODO
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
@ -57,6 +57,21 @@ const player = require('./routes/player')
|
|||||||
|
|
||||||
const isDevMode = process.env.NODE_ENV === 'development'
|
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
|
// Helmet security headers
|
||||||
if (config.helmet instanceof Object) {
|
if (config.helmet instanceof Object) {
|
||||||
// If an empty object, simply do not use Helmet
|
// If an empty object, simply do not use Helmet
|
||||||
@ -111,19 +126,6 @@ const initLiveDirectory = (options = {}) => {
|
|||||||
return new LiveDirectory(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]
|
const cdnPages = [...config.pages]
|
||||||
|
|
||||||
// Defaults to no-op
|
// Defaults to no-op
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
"node-fetch": "~2.6.7",
|
"node-fetch": "~2.6.7",
|
||||||
"nunjucks": "~3.2.3",
|
"nunjucks": "~3.2.3",
|
||||||
"randomstring": "~1.2.2",
|
"randomstring": "~1.2.2",
|
||||||
|
"rate-limiter-flexible": "^2.3.7",
|
||||||
"search-query-parser": "~1.6.0",
|
"search-query-parser": "~1.6.0",
|
||||||
"sharp": "~0.30.7",
|
"sharp": "~0.30.7",
|
||||||
"systeminformation": "5.11.23"
|
"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"
|
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
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:
|
rc@^1.2.7, rc@^1.2.8:
|
||||||
version "1.2.8"
|
version "1.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||||
|
Loading…
Reference in New Issue
Block a user