diff --git a/services/web/app/src/Features/Security/RateLimiterMiddleware.js b/services/web/app/src/Features/Security/RateLimiterMiddleware.js index 4212543684..5fb9a1040e 100644 --- a/services/web/app/src/Features/Security/RateLimiterMiddleware.js +++ b/services/web/app/src/Features/Security/RateLimiterMiddleware.js @@ -1,73 +1,85 @@ -/* eslint-disable - camelcase, - max-len, - no-unused-vars, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -let RateLimiterMiddleware const RateLimiter = require('../../infrastructure/RateLimiter') const logger = require('logger-sharelatex') const AuthenticationController = require('../Authentication/AuthenticationController') +const LoginRateLimiter = require('./LoginRateLimiter') const settings = require('settings-sharelatex') -module.exports = RateLimiterMiddleware = { - /* - Do not allow more than opts.maxRequests from a single client in - opts.timeInterval. Pass an array of opts.params to segment this based on - parameters in the request URL, e.g.: +/* + Do not allow more than opts.maxRequests from a single client in + opts.timeInterval. Pass an array of opts.params to segment this based on + parameters in the request URL, e.g.: - app.get "/project/:project_id", RateLimiterMiddleware.rateLimit(endpointName: "open-editor", params: ["project_id"]) + app.get "/project/:project_id", RateLimiterMiddleware.rateLimit(endpointName: "open-editor", params: ["project_id"]) - will rate limit each project_id separately. + will rate limit each project_id separately. - Unique clients are identified by user_id if logged in, and IP address if not. - */ - rateLimit(opts) { - return function(req, res, next) { - const user_id = AuthenticationController.getLoggedInUserId(req) || req.ip - if ( - settings.smokeTest && - settings.smokeTest.userId && - settings.smokeTest.userId.toString() === user_id.toString() - ) { - // ignore smoke test user - return next() - } - const params = (opts.params || []).map(p => req.params[p]) - params.push(user_id) - let subjectName = params.join(':') - if (opts.ipOnly) { - subjectName = req.ip - } - if (opts.endpointName == null) { - throw new Error('no endpointName provided') - } - const options = { - endpointName: opts.endpointName, - timeInterval: opts.timeInterval || 60, - subjectName, - throttle: opts.maxRequests || 6 - } - return RateLimiter.addCount(options, function(error, canContinue) { - if (error != null) { - return next(error) - } - if (canContinue) { - return next() - } else { - logger.warn(options, 'rate limit exceeded') - res.status(429) // Too many requests - res.write('Rate limit reached, please try again later') - return res.end() - } - }) + Unique clients are identified by user_id if logged in, and IP address if not. +*/ +function rateLimit(opts) { + return function(req, res, next) { + const userId = AuthenticationController.getLoggedInUserId(req) || req.ip + if ( + settings.smokeTest && + settings.smokeTest.userId && + settings.smokeTest.userId.toString() === userId.toString() + ) { + // ignore smoke test user + return next() } + const params = (opts.params || []).map(p => req.params[p]) + params.push(userId) + let subjectName = params.join(':') + if (opts.ipOnly) { + subjectName = req.ip + } + if (opts.endpointName == null) { + throw new Error('no endpointName provided') + } + const options = { + endpointName: opts.endpointName, + timeInterval: opts.timeInterval || 60, + subjectName, + throttle: opts.maxRequests || 6 + } + return RateLimiter.addCount(options, function(error, canContinue) { + if (error != null) { + return next(error) + } + if (canContinue) { + return next() + } else { + logger.warn(options, 'rate limit exceeded') + res.status(429) // Too many requests + res.write('Rate limit reached, please try again later') + return res.end() + } + }) } } + +function loginRateLimit(req, res, next) { + const { email } = req.body + if (!email) { + return next() + } + LoginRateLimiter.processLoginRequest(email, function(err, isAllowed) { + if (err) { + return next(err) + } + if (isAllowed) { + return next() + } else { + logger.warn({ email }, 'rate limit exceeded') + res.status(429) // Too many requests + res.write('Rate limit reached, please try again later') + return res.end() + } + }) +} + +const RateLimiterMiddleware = { + rateLimit, + loginRateLimit +} + +module.exports = RateLimiterMiddleware diff --git a/services/web/test/acceptance/src/helpers/UserHelper.js b/services/web/test/acceptance/src/helpers/UserHelper.js index 71da9cc2c0..822b93efcf 100644 --- a/services/web/test/acceptance/src/helpers/UserHelper.js +++ b/services/web/test/acceptance/src/helpers/UserHelper.js @@ -139,7 +139,9 @@ module.exports = class UserHelper { json: userData }) if (response.statusCode !== 200 || response.body.redir !== '/project') { - throw new Error('login failed') + const error = new Error('login failed') + error.response = response + throw error } userHelper.user = await UserGetter.promises.getUser({ email: userData.email