diff --git a/public/src/app.js b/public/src/app.js index 913360c615..421cf71943 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -439,6 +439,9 @@ app.cacheBuster = null; .replace('{pageTitle}', function () { return title; }) .replace('{browserTitle}', function () { return config.browserTitle; }); + // Allow translation strings in title on ajaxify (#5927) + title = translator.unescape(title); + translator.translate(title, function (translated) { titleObj.titles[0] = translated; app.alternatingTitle(''); diff --git a/src/controllers/errors.js b/src/controllers/errors.js index 177bcaa769..96bfda203c 100644 --- a/src/controllers/errors.js +++ b/src/controllers/errors.js @@ -3,6 +3,7 @@ var nconf = require('nconf'); var winston = require('winston'); var validator = require('validator'); +var plugins = require('../plugins'); exports.handleURIErrors = function (err, req, res, next) { // Handle cases where malformed URIs are passed in @@ -35,30 +36,50 @@ exports.handleURIErrors = function (err, req, res, next) { // this needs to have four arguments or express treats it as `(req, res, next)` // don't remove `next`! exports.handleErrors = function (err, req, res, next) { // eslint-disable-line no-unused-vars - switch (err.code) { - case 'EBADCSRFTOKEN': - winston.error(req.path + '\n', err.message); - return res.sendStatus(403); - case 'blacklisted-ip': - return res.status(403).type('text/plain').send(err.message); - } + var cases = { + EBADCSRFTOKEN: function () { + winston.error(req.path + '\n', err.message); + res.sendStatus(403); + }, + 'blacklisted-ip': function () { + res.status(403).type('text/plain').send(err.message); + }, + }; + var defaultHandler = function () { + // Display NodeBB error page + var status = parseInt(err.status, 10); + if ((status === 302 || status === 308) && err.path) { + return res.locals.isAPI ? res.set('X-Redirect', err.path).status(200).json(err.path) : res.redirect(err.path); + } - var status = parseInt(err.status, 10); - if ((status === 302 || status === 308) && err.path) { - return res.locals.isAPI ? res.set('X-Redirect', err.path).status(200).json(err.path) : res.redirect(err.path); - } + winston.error(req.path + '\n', err.stack); - winston.error(req.path + '\n', err.stack); + res.status(status || 500); - res.status(status || 500); + var path = String(req.path || ''); + if (res.locals.isAPI) { + res.json({ path: validator.escape(path), error: err.message }); + } else { + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function () { + res.render('500', { path: validator.escape(path), error: validator.escape(String(err.message)) }); + }); + } + }; - var path = String(req.path || ''); - if (res.locals.isAPI) { - res.json({ path: validator.escape(path), error: err.message }); - } else { - var middleware = require('../middleware'); - middleware.buildHeader(req, res, function () { - res.render('500', { path: validator.escape(path), error: validator.escape(String(err.message)) }); - }); - } + plugins.fireHook('filter:error.handle', { + cases: cases, + }, function (_err, data) { + if (_err) { + // Assume defaults + winston.warn('[errors/handle] Unable to retrieve plugin handlers for errors: ' + _err.message); + data.cases = cases; + } + + if (data.cases.hasOwnProperty(err.code)) { + data.cases[err.code](err, req, res, defaultHandler); + } else { + defaultHandler(); + } + }); }; diff --git a/src/meta/blacklist.js b/src/meta/blacklist.js index 31af5f9eee..d872f3ee07 100644 --- a/src/meta/blacklist.js +++ b/src/meta/blacklist.js @@ -20,7 +20,7 @@ Blacklist.load = function (callback) { Blacklist.get, Blacklist.validate, function (rules, next) { - winston.verbose('[meta/blacklist] Loading ' + rules.valid.length + ' blacklist rules'); + winston.verbose('[meta/blacklist] Loading ' + rules.valid.length + ' blacklist rule(s)' + (rules.duplicateCount > 0 ? ', ignored ' + rules.duplicateCount + ' duplicate(s)' : '')); if (rules.invalid.length) { winston.warn('[meta/blacklist] ' + rules.invalid.length + ' invalid blacklist rule(s) were ignored.'); } @@ -44,8 +44,8 @@ Blacklist.save = function (rules, callback) { db.setObject('ip-blacklist-rules', { rules: rules }, next); }, function (next) { - Blacklist.load(next); pubsub.publish('blacklist:reload'); + setImmediate(next); }, ], callback); }; @@ -111,6 +111,7 @@ Blacklist.validate = function (rules, callback) { var ipv6 = []; var cidr = []; var invalid = []; + var duplicateCount = 0; var inlineCommentMatch = /#.*$/; var whitelist = ['127.0.0.1', '::1', '::ffff:0:127.0.0.1']; @@ -122,6 +123,16 @@ Blacklist.validate = function (rules, callback) { return rule.length && !rule.startsWith('#') ? rule : null; }).filter(Boolean); + // Filter out duplicates + rules = rules.filter(function (rule, index) { + const pass = rules.indexOf(rule) === index; + if (!pass) { + duplicateCount += 1; + } + + return pass; + }); + // Filter out invalid rules rules = rules.filter(function (rule) { var addr; @@ -167,6 +178,7 @@ Blacklist.validate = function (rules, callback) { cidr: cidr, valid: rules, invalid: invalid, + duplicateCount: duplicateCount, }); }; diff --git a/src/middleware/index.js b/src/middleware/index.js index 507878dc0c..d5f7e5e6a3 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -9,6 +9,7 @@ var nconf = require('nconf'); var ensureLoggedIn = require('connect-ensure-login'); var toobusy = require('toobusy-js'); var Benchpress = require('benchpressjs'); +var LRU = require('lru-cache'); var plugins = require('../plugins'); var meta = require('../meta'); @@ -23,6 +24,10 @@ var controllers = { helpers: require('../controllers/helpers'), }; +var delayCache = LRU({ + maxAge: 1000 * 60, +}); + var middleware = module.exports; middleware.applyCSRF = csrf(); @@ -186,6 +191,14 @@ middleware.processTimeagoLocales = function (req, res, next) { middleware.delayLoading = function (req, res, next) { // Introduces an artificial delay during load so that brute force attacks are effectively mitigated + + // Add IP to cache so if too many requests are made, subsequent requests are blocked for a minute + var timesSeen = delayCache.get(req.ip) || 0; + if (timesSeen > 10) { + return res.sendStatus(429); + } + delayCache.set(req.ip, timesSeen += 1); + setTimeout(next, 1000); }; diff --git a/src/views/500-embed.tpl b/src/views/500-embed.tpl index f5258cb8db..d9d9f30d5d 100644 --- a/src/views/500-embed.tpl +++ b/src/views/500-embed.tpl @@ -1,12 +1,14 @@ \ No newline at end of file diff --git a/src/webserver.js b/src/webserver.js index 2918ea12f7..6f89ea8570 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -180,8 +180,15 @@ function setupExpressApp(app, callback) { setupAutoLocale(app, callback); } -function ping(req, res) { - res.status(200).send(req.path === '/sping' ? 'healthy' : '200'); +function ping(req, res, next) { + async.waterfall([ + function (next) { + db.getObject('config', next); + }, + function () { + res.status(200).send(req.path === '/sping' ? 'healthy' : '200'); + }, + ], next); } function setupFavicon(app) {