From f6633a66fd5b0e194f9f9782cdd0c2519cce77f6 Mon Sep 17 00:00:00 2001 From: bjarneo Date: Sun, 28 Jan 2024 21:32:48 +0100 Subject: [PATCH] feat: add the new rate limit This rate limit will use the method, path and ip as a key, and will be route based rate limiter. Also, this rate limit will be in memory for now using a Map as the store. --- package-lock.json | 57 ++++---------------------------- package.json | 3 +- server.js | 43 ++++++++++++++---------- src/server/plugins/rate-limit.js | 39 ++++++++++++++++++++++ 4 files changed, 72 insertions(+), 70 deletions(-) create mode 100644 src/server/plugins/rate-limit.js diff --git a/package-lock.json b/package-lock.json index 1d9de9d2..b5ab1355 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "@fastify/helmet": "^9.1.0", "@fastify/jwt": "^7.2.3", "@fastify/multipart": "^7.1.1", - "@fastify/rate-limit": "^8.0.0", "@fastify/static": "^6.5.0", "@mantine/core": "^6.0.6", "@mantine/form": "^6.0.6", @@ -29,7 +28,7 @@ "email-validator": "^2.0.4", "extract-domain": "^2.4.8", "fastify": "^4.9.1", - "fastify-plugin": "^3.0.0", + "fastify-plugin": "^3.0.1", "file-type": "^18.0.0", "generate-password-browser": "^1.1.0", "ip-range-check": "^0.2.0", @@ -1993,26 +1992,6 @@ "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.2.1.tgz", "integrity": "sha512-dlGKiwLzRBKkEf5J5ho0uAD/Jdv8GQVUbriB3tAX3ehRUXE4gTV3lRd5inEg9li1aLzb0EGj8y2K4/8g1TN06g==" }, - "node_modules/@fastify/rate-limit": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-8.0.0.tgz", - "integrity": "sha512-73pQFgpx6RMmY5nriYQW0AIATZpa/OAvWa5PQ9JHEqgjKMLNv9zLAphWIRh5914aN6sqtv8sLICr3Tp1NbXQIQ==", - "dependencies": { - "fastify-plugin": "^4.0.0", - "ms": "^2.1.3", - "tiny-lru": "^10.0.0" - } - }, - "node_modules/@fastify/rate-limit/node_modules/fastify-plugin": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.0.tgz", - "integrity": "sha512-79ak0JxddO0utAXAQ5ccKhvs6vX2MGyHHMMsmZkBANrq3hXc1CHzvNPHOcvTsVMEPl5I+NT+RO4YKMGehOfSIg==" - }, - "node_modules/@fastify/rate-limit/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/@fastify/static": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.5.0.tgz", @@ -4593,9 +4572,9 @@ } }, "node_modules/fastify-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz", - "integrity": "sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.1.tgz", + "integrity": "sha512-qKcDXmuZadJqdTm6vlCqioEbyewF60b/0LOFCcYN1B6BIZGlYJumWWOYs70SFYLDAH4YqdE1cxH/RKMG7rFxgA==" }, "node_modules/fastparallel": { "version": "2.4.1", @@ -10122,28 +10101,6 @@ } } }, - "@fastify/rate-limit": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-8.0.0.tgz", - "integrity": "sha512-73pQFgpx6RMmY5nriYQW0AIATZpa/OAvWa5PQ9JHEqgjKMLNv9zLAphWIRh5914aN6sqtv8sLICr3Tp1NbXQIQ==", - "requires": { - "fastify-plugin": "^4.0.0", - "ms": "^2.1.3", - "tiny-lru": "^10.0.0" - }, - "dependencies": { - "fastify-plugin": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.0.tgz", - "integrity": "sha512-79ak0JxddO0utAXAQ5ccKhvs6vX2MGyHHMMsmZkBANrq3hXc1CHzvNPHOcvTsVMEPl5I+NT+RO4YKMGehOfSIg==" - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, "@fastify/static": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.5.0.tgz", @@ -12111,9 +12068,9 @@ } }, "fastify-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz", - "integrity": "sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.1.tgz", + "integrity": "sha512-qKcDXmuZadJqdTm6vlCqioEbyewF60b/0LOFCcYN1B6BIZGlYJumWWOYs70SFYLDAH4YqdE1cxH/RKMG7rFxgA==" }, "fastparallel": { "version": "2.4.1", diff --git a/package.json b/package.json index 8689004d..ff521193 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "@fastify/helmet": "^9.1.0", "@fastify/jwt": "^7.2.3", "@fastify/multipart": "^7.1.1", - "@fastify/rate-limit": "^8.0.0", "@fastify/static": "^6.5.0", "@mantine/core": "^6.0.6", "@mantine/form": "^6.0.6", @@ -54,7 +53,7 @@ "email-validator": "^2.0.4", "extract-domain": "^2.4.8", "fastify": "^4.9.1", - "fastify-plugin": "^3.0.0", + "fastify-plugin": "^3.0.1", "file-type": "^18.0.0", "generate-password-browser": "^1.1.0", "ip-range-check": "^0.2.0", diff --git a/server.js b/server.js index a27d131b..6967c8fa 100644 --- a/server.js +++ b/server.js @@ -1,40 +1,41 @@ // Boot scripts import('./src/server/bootstrap.js'); +import cookie from '@fastify/cookie'; +import cors from '@fastify/cors'; +import helmet from '@fastify/helmet'; +import jwt from '@fastify/jwt'; +import fstatic from '@fastify/static'; import config from 'config'; -import path from 'path'; +import importFastify from 'fastify'; import fs from 'fs'; -import { fileURLToPath } from 'url'; import { JSDOM } from 'jsdom'; -import importFastify from 'fastify'; +import path from 'path'; +import { fileURLToPath } from 'url'; import template from 'y8'; -import helmet from '@fastify/helmet'; -import cors from '@fastify/cors'; -import fstatic from '@fastify/static'; -import cookie from '@fastify/cookie'; -import jwt from '@fastify/jwt'; -import rateLimit from '@fastify/rate-limit'; + +import rateLimit from './src/server/plugins/rate-limit.js'; import adminDecorator from './src/server/decorators/admin.js'; -import jwtDecorator from './src/server/decorators/jwt.js'; -import userFeatures from './src/server/decorators/user-features.js'; import allowedIp from './src/server/decorators/allowed-ip.js'; import attachment from './src/server/decorators/attachment-upload.js'; +import jwtDecorator from './src/server/decorators/jwt.js'; +import userFeatures from './src/server/decorators/user-features.js'; import readCookieAllRoutesHandler from './src/server/prehandlers/cookie-all-routes.js'; -import readOnlyHandler from './src/server/prehandlers/read-only.js'; -import disableUserHandler from './src/server/prehandlers/disable-users.js'; import disableUserAccountCreationHandler from './src/server/prehandlers/disable-user-account-creation.js'; +import disableUserHandler from './src/server/prehandlers/disable-users.js'; +import readOnlyHandler from './src/server/prehandlers/read-only.js'; import restrictOrganizationEmailHandler from './src/server/prehandlers/restrict-organization-email.js'; -import usersRoute from './src/server/controllers/admin/users.js'; +import accountRoute from './src/server/controllers/account.js'; import adminSettingsRoute from './src/server/controllers/admin/settings.js'; +import usersRoute from './src/server/controllers/admin/users.js'; import authenticationRoute from './src/server/controllers/authentication.js'; -import accountRoute from './src/server/controllers/account.js'; import downloadRoute from './src/server/controllers/download.js'; +import healthzRoute from './src/server/controllers/healthz.js'; import secretRoute from './src/server/controllers/secret.js'; import statsRoute from './src/server/controllers/stats.js'; -import healthzRoute from './src/server/controllers/healthz.js'; const isDev = process.env.NODE_ENV === 'development'; @@ -45,12 +46,18 @@ const fastify = importFastify({ bodyLimit: MAX_FILE_BYTES, }); -// https://github.com/fastify/fastify-rate-limit fastify.register(rateLimit, { + prefix: '/api/', + max: 100, + timeWindow: 60 * 1000, // 1 minute +}); + +// https://github.com/fastify/fastify-rate-limit +/*fastify.register(rateLimit, { prefix: '/api/', max: 10000, timeWindow: '1 minute', -}); +});*/ // https://github.com/fastify/fastify-helmet fastify.register(helmet, { diff --git a/src/server/plugins/rate-limit.js b/src/server/plugins/rate-limit.js new file mode 100644 index 00000000..f06429c0 --- /dev/null +++ b/src/server/plugins/rate-limit.js @@ -0,0 +1,39 @@ +import fp from 'fastify-plugin'; +import getClientIp from '../helpers/client-ip.js'; + +const store = new Map(); + +export default fp(function (fastify, opts, done) { + fastify.decorate('rateLimit', async (req, res) => { + if (!req.url.startsWith(opts.prefix)) { + done(); + } + + const ip = getClientIp(req.headers); + const key = `${req.method}${req.url}${ip}`; + + const currentTime = Date.now(); + const resetTime = currentTime + opts.timeWindow; + + if (!store.has(key) || currentTime > store.get(key)?.reset) { + store.set(key, { + count: 0, + reset: resetTime, + }); + } + + const current = store.get(key); + current.count += 1; + + if (current.count > opts.max && currentTime <= current.reset) { + return res.code(429).send({ message: 'Too many requests, please try again later.' }); + } + + res.header('X-RateLimit-Limit', opts.max); + res.header('X-RateLimit-Remaining', opts.max - current.count); + res.header('X-RateLimit-Reset', Math.floor((current.reset - currentTime) / 1000)); + }); + + fastify.addHook('onRequest', fastify.rateLimit); + done(); +});