From a9c9fd38a535be36e9794821fad51622475cedba Mon Sep 17 00:00:00 2001 From: lesion Date: Thu, 26 Jan 2023 23:30:53 +0100 Subject: [PATCH] introduce a new instance api rate limiter --- server/api/controller/event.js | 2 +- server/api/controller/geocoding.js | 10 ---------- server/api/controller/place.js | 2 -- server/api/index.js | 23 ++++++++++++--------- server/api/limiter.js | 32 ++++++++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 22 deletions(-) create mode 100644 server/api/limiter.js diff --git a/server/api/controller/event.js b/server/api/controller/event.js index e84f03dc..8bc5fe24 100644 --- a/server/api/controller/event.js +++ b/server/api/controller/event.js @@ -689,7 +689,7 @@ const eventController = { ...pagination, replacements }).catch(e => { - log.error('[EVENT]', e) + log.error('[EVENT]' + String(e)) return [] }) diff --git a/server/api/controller/geocoding.js b/server/api/controller/geocoding.js index a86b1ca3..b30d166b 100644 --- a/server/api/controller/geocoding.js +++ b/server/api/controller/geocoding.js @@ -1,4 +1,3 @@ -const rateLimit = require('express-rate-limit'); const log = require('../../log') const nominatim = require('../../services/geocoding/nominatim') const photon = require('../../services/geocoding/photon') @@ -8,15 +7,6 @@ let d = 0 // departure time let h = 0 // hit geocoding provider time (aka Latency) const geocodingController = { - /** - * TODO: replace/merge with a general 'instance rate-limiter' or 'instance api-related rate-limiter' when this will be defined - */ - instanceApiRateLimiter: rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes) - standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers - legacyHeaders: false, // Disable the `X-RateLimit-*` headers - }), /** * Limit provider api usage. diff --git a/server/api/controller/place.js b/server/api/controller/place.js index 47f71b34..cf217bf7 100644 --- a/server/api/controller/place.js +++ b/server/api/controller/place.js @@ -3,8 +3,6 @@ const { Place, Event } = require('../models/models') const eventController = require('./event') const exportController = require('./export') -const { version } = require('../../../package.json') - const log = require('../../log') const { Op, where, col, fn, cast } = require('sequelize') diff --git a/server/api/index.js b/server/api/index.js index f6c8bf6b..a44f231d 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -20,6 +20,7 @@ const oauthController = require('./controller/oauth') const announceController = require('./controller/announce') const pluginController = require('./controller/plugins') const geocodingController = require('./controller/geocoding') +const { DDOSProtectionApiRateLimiter, SPAMProtectionApiRateLimiter } = require('./limiter') const helpers = require('../helpers') const storage = require('./storage') @@ -30,6 +31,10 @@ module.exports = () => { api.use(express.urlencoded({ extended: false })) api.use(express.json()) + if (process.env.NODE_ENV !== 'test') { + api.use(DDOSProtectionApiRateLimiter) + } + if (config.status !== 'READY') { @@ -66,12 +71,12 @@ module.exports = () => { api.get('/ping', (_req, res) => res.sendStatus(200)) api.get('/user', isAuth, (req, res) => res.json(req.user)) - api.post('/user/recover', userController.forgotPassword) + api.post('/user/recover', SPAMProtectionApiRateLimiter, userController.forgotPassword) api.post('/user/check_recover_code', userController.checkRecoverCode) - api.post('/user/recover_password', userController.updatePasswordWithRecoverCode) + api.post('/user/recover_password', SPAMProtectionApiRateLimiter, userController.updatePasswordWithRecoverCode) // register and add users - api.post('/user/register', userController.register) + api.post('/user/register', SPAMProtectionApiRateLimiter, userController.register) api.post('/user', isAdmin, userController.create) // update user @@ -127,7 +132,7 @@ module.exports = () => { */ // allow anyone to add an event (anon event has to be confirmed, TODO: flood protection) - api.post('/event', eventController.isAnonEventAllowed, upload.single('image'), eventController.add) + api.post('/event', eventController.isAnonEventAllowed, SPAMProtectionApiRateLimiter, upload.single('image'), eventController.add) // api.get('/event/search', eventController.search) @@ -141,8 +146,8 @@ module.exports = () => { api.get('/event/meta', eventController.searchMeta) // add event notification TODO - api.post('/event/notification', eventController.addNotification) - api.delete('/event/notification/:code', eventController.delNotification) + // api.post('/event/notification', eventController.addNotification) + // api.delete('/event/notification/:code', eventController.delNotification) api.post('/settings', isAdmin, settingsController.setRequest) api.get('/settings', isAdmin, settingsController.getAll) @@ -173,8 +178,8 @@ module.exports = () => { api.put('/place', isAdmin, placeController.updatePlace) // - GEOCODING - api.get('/placeOSM/Nominatim/:place_details', helpers.isGeocodingEnabled, geocodingController.instanceApiRateLimiter, geocodingController.nominatimRateLimit, geocodingController._nominatim) - api.get('/placeOSM/Photon/:place_details', helpers.isGeocodingEnabled, geocodingController.instanceApiRateLimiter, geocodingController.photonRateLimit, geocodingController._photon) + api.get('/placeOSM/Nominatim/:place_details', helpers.isGeocodingEnabled, geocodingController.nominatimRateLimit, geocodingController._nominatim) + api.get('/placeOSM/Photon/:place_details', helpers.isGeocodingEnabled, geocodingController.photonRateLimit, geocodingController._photon) // - TAGS api.get('/tags', isAdmin, tagController.getAll) @@ -215,7 +220,7 @@ module.exports = () => { // OAUTH api.get('/clients', isAuth, oauthController.getClients) api.get('/client/:client_id', isAuth, oauthController.getClient) - api.post('/client', oauthController.createClient) + api.post('/client', SPAMProtectionApiRateLimiter, oauthController.createClient) } api.use((_req, res) => res.sendStatus(404)) diff --git a/server/api/limiter.js b/server/api/limiter.js new file mode 100644 index 00000000..1f417402 --- /dev/null +++ b/server/api/limiter.js @@ -0,0 +1,32 @@ +const rateLimit = require('express-rate-limit') +const log = require('../log') + +const next = (req, res, next) => next() + +const instanceApiRateLimiter = { + + DDOSProtectionApiRateLimiter: (process.env.NODE_ENV === 'test' ? next : rateLimit({ + windowMs: 60 * 1000, // 5 minutes + max: 100, // Limit each IP to 100 requests per `window` (here, per 5 minutes) + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + handler: (request, response, next, options) => { + log.warn(`DDOS protection api rate limiter: > 100req/minute/ip ${request.ip}`) + return response.status(options.statusCode).send(options.message) + } + })), + + SPAMProtectionApiRateLimiter: (process.env.NODE_ENV === 'test' ? next : rateLimit({ + windowMs: 5 * 60 * 1000, // 10 minutes + max: 3, // Limit each IP to 3 requests per `window` (here, per 15 minutes) + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + handler: (request, response, next, options) => { + log.warn(`SPAM protection api rate limiter: 3req/5min/ip ${request.ip}`) + return response.status(options.statusCode).send(options.message) + } + })) + +} + +module.exports = instanceApiRateLimiter \ No newline at end of file