diff --git a/src/controllers/index.js b/src/controllers/index.js index 975dca54ae..d39352e6d0 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -376,8 +376,8 @@ Controllers.handle404 = function(req, res) { if (res.locals.isAPI) { return res.json({path: validator.escape(path.replace(/^\/api/, '')), title: '[[global:404.title]]'}); } - - req.app.locals.middleware.buildHeader(req, res, function() { + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function() { res.render('404', {path: validator.escape(path), title: '[[global:404.title]]'}); }); } else { @@ -402,7 +402,8 @@ Controllers.handleURIErrors = function(err, req, res, next) { error: '[[global:400.title]]' }); } else { - req.app.locals.middleware.buildHeader(req, res, function() { + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function() { res.render('400', { error: validator.escape(String(err.message)) }); }); } @@ -435,7 +436,8 @@ Controllers.handleErrors = function(err, req, res, next) { if (res.locals.isAPI) { res.json({path: validator.escape(path), error: err.message}); } else { - req.app.locals.middleware.buildHeader(req, res, function() { + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function() { res.render('500', { path: validator.escape(path), error: validator.escape(String(err.message)) }); }); } diff --git a/src/meta/configs.js b/src/meta/configs.js index 69cc375b85..61464c232a 100644 --- a/src/meta/configs.js +++ b/src/meta/configs.js @@ -1,11 +1,12 @@ 'use strict'; -var winston = require('winston'), - db = require('../database'), - pubsub = require('../pubsub'), - nconf = require('nconf'), - utils = require('../../public/src/utils'); +var winston = require('winston'); +var nconf = require('nconf'); + +var db = require('../database'); +var pubsub = require('../pubsub'); +var utils = require('../../public/src/utils'); module.exports = function(Meta) { diff --git a/src/middleware/admin.js b/src/middleware/admin.js index 2ee0f7fd80..5e4399f226 100644 --- a/src/middleware/admin.js +++ b/src/middleware/admin.js @@ -1,130 +1,125 @@ "use strict"; -var app, - middleware = {}, - nconf = require('nconf'), - async = require('async'), - winston = require('winston'), - user = require('../user'), - meta = require('../meta'), - plugins = require('../plugins'), - - controllers = { - api: require('../controllers/api'), - helpers: require('../controllers/helpers') - }; - -middleware.isAdmin = function(req, res, next) { - winston.warn('[middleware.admin.isAdmin] deprecation warning, no need to use this from plugins!'); - - if (!req.user) { - return controllers.helpers.notAllowed(req, res); - } - - user.isAdministrator(req.user.uid, function (err, isAdmin) { - if (err || isAdmin) { - return next(err); - } - - controllers.helpers.notAllowed(req, res); - }); +var async = require('async'); +var winston = require('winston'); +var user = require('../user'); +var meta = require('../meta'); +var plugins = require('../plugins'); + +var controllers = { + api: require('../controllers/api'), + helpers: require('../controllers/helpers') }; -middleware.buildHeader = function(req, res, next) { - res.locals.renderAdminHeader = true; +module.exports = function(middleware) { + middleware.admin = {}; + middleware.admin.isAdmin = function(req, res, next) { + winston.warn('[middleware.admin.isAdmin] deprecation warning, no need to use this from plugins!'); - async.parallel({ - config: function(next) { - controllers.api.getConfig(req, res, next); - }, - footer: function(next) { - app.render('admin/footer', {}, next); - } - }, function(err, results) { - if (err) { - return next(err); + if (!req.user) { + return controllers.helpers.notAllowed(req, res); } - res.locals.config = results.config; - res.locals.adminFooter = results.footer; - next(); - }); -}; + user.isAdministrator(req.user.uid, function (err, isAdmin) { + if (err || isAdmin) { + return next(err); + } -middleware.renderHeader = function(req, res, data, next) { - var custom_header = { - 'plugins': [], - 'authentication': [] + controllers.helpers.notAllowed(req, res); + }); }; - user.getUserFields(req.uid, ['username', 'userslug', 'email', 'picture', 'email:confirmed'], function(err, userData) { - if (err) { - return next(err); - } - - userData.uid = req.uid; - userData['email:confirmed'] = parseInt(userData['email:confirmed'], 10) === 1; + middleware.admin.buildHeader = function(req, res, next) { + res.locals.renderAdminHeader = true; async.parallel({ - scripts: function(next) { - plugins.fireHook('filter:admin.scripts.get', [], function(err, scripts) { - if (err) { - return next(err); - } - var arr = []; - scripts.forEach(function(script) { - arr.push({src: script}); - }); - - next(null, arr); - }); - }, - custom_header: function(next) { - plugins.fireHook('filter:admin.header.build', custom_header, next); - }, config: function(next) { controllers.api.getConfig(req, res, next); }, - configs: function(next) { - meta.configs.list(next); + footer: function(next) { + req.app.render('admin/footer', {}, next); } }, function(err, results) { if (err) { return next(err); } + res.locals.config = results.config; + res.locals.adminFooter = results.footer; + next(); + }); + }; + + middleware.admin.renderHeader = function(req, res, data, next) { + var custom_header = { + 'plugins': [], + 'authentication': [] + }; - var acpPath = req.path.slice(1).split('/'); - acpPath.forEach(function(path, i) { - acpPath[i] = path.charAt(0).toUpperCase() + path.slice(1); + user.getUserFields(req.uid, ['username', 'userslug', 'email', 'picture', 'email:confirmed'], function(err, userData) { + if (err) { + return next(err); + } + + userData.uid = req.uid; + userData['email:confirmed'] = parseInt(userData['email:confirmed'], 10) === 1; + + async.parallel({ + scripts: function(next) { + plugins.fireHook('filter:admin.scripts.get', [], function(err, scripts) { + if (err) { + return next(err); + } + var arr = []; + scripts.forEach(function(script) { + arr.push({src: script}); + }); + + next(null, arr); + }); + }, + custom_header: function(next) { + plugins.fireHook('filter:admin.header.build', custom_header, next); + }, + config: function(next) { + controllers.api.getConfig(req, res, next); + }, + configs: function(next) { + meta.configs.list(next); + } + }, function(err, results) { + if (err) { + return next(err); + } + res.locals.config = results.config; + + var acpPath = req.path.slice(1).split('/'); + acpPath.forEach(function(path, i) { + acpPath[i] = path.charAt(0).toUpperCase() + path.slice(1); + }); + acpPath = acpPath.join(' > '); + + var templateValues = { + config: results.config, + configJSON: JSON.stringify(results.config), + relative_path: results.config.relative_path, + adminConfigJSON: encodeURIComponent(JSON.stringify(results.configs)), + user: userData, + userJSON: JSON.stringify(userData).replace(/'/g, "\\'"), + plugins: results.custom_header.plugins, + authentication: results.custom_header.authentication, + scripts: results.scripts, + 'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '', + env: process.env.NODE_ENV ? true : false, + title: (acpPath || 'Dashboard') + ' | NodeBB Admin Control Panel', + bodyClass: data.bodyClass + }; + + templateValues.template = {name: res.locals.template}; + templateValues.template[res.locals.template] = true; + + req.app.render('admin/header', templateValues, next); }); - acpPath = acpPath.join(' > '); - - var templateValues = { - config: results.config, - configJSON: JSON.stringify(results.config), - relative_path: results.config.relative_path, - adminConfigJSON: encodeURIComponent(JSON.stringify(results.configs)), - user: userData, - userJSON: JSON.stringify(userData).replace(/'/g, "\\'"), - plugins: results.custom_header.plugins, - authentication: results.custom_header.authentication, - scripts: results.scripts, - 'cache-buster': meta.config['cache-buster'] ? 'v=' + meta.config['cache-buster'] : '', - env: process.env.NODE_ENV ? true : false, - title: (acpPath || 'Dashboard') + ' | NodeBB Admin Control Panel', - bodyClass: data.bodyClass - }; - - templateValues.template = {name: res.locals.template}; - templateValues.template[res.locals.template] = true; - - app.render('admin/header', templateValues, next); }); - }); -}; - -module.exports = function(webserver) { - app = webserver; - return middleware; + }; }; diff --git a/src/middleware/header.js b/src/middleware/header.js index 6425c17d74..b4013d1481 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -16,7 +16,7 @@ var controllers = { helpers: require('../controllers/helpers') }; -module.exports = function(app, middleware) { +module.exports = function(middleware) { middleware.buildHeader = function(req, res, next) { res.locals.renderHeader = true; @@ -28,7 +28,7 @@ module.exports = function(app, middleware) { controllers.api.getConfig(req, res, next); }, footer: function(next) { - app.render('footer', { + req.app.render('footer', { loggedIn: !!req.uid, title: validator.escape(meta.config.title || meta.config.browserTitle || 'NodeBB') }, next); @@ -160,7 +160,7 @@ module.exports = function(app, middleware) { return callback(err); } - app.render('header', data.templateValues, callback); + req.app.render('header', data.templateValues, callback); }); }); }; diff --git a/src/middleware/headers.js b/src/middleware/headers.js new file mode 100644 index 0000000000..39c9520e59 --- /dev/null +++ b/src/middleware/headers.js @@ -0,0 +1,51 @@ +'use strict'; + + +var meta = require('../meta'); +var _ = require('underscore'); + + +module.exports = function(middleware) { + + middleware.addHeaders = function (req, res, next) { + var defaults = { + 'X-Powered-By': 'NodeBB', + 'X-Frame-Options': 'SAMEORIGIN', + 'Access-Control-Allow-Origin': 'null' // yes, string null. + }; + var headers = { + 'X-Powered-By': meta.config['powered-by'], + 'X-Frame-Options': meta.config['allow-from-uri'] ? 'ALLOW-FROM ' + meta.config['allow-from-uri'] : undefined, + 'Access-Control-Allow-Origin': meta.config['access-control-allow-origin'], + 'Access-Control-Allow-Methods': meta.config['access-control-allow-methods'], + 'Access-Control-Allow-Headers': meta.config['access-control-allow-headers'] + }; + + _.defaults(headers, defaults); + headers = _.pick(headers, Boolean); // Remove falsy headers + + for(var key in headers) { + if (headers.hasOwnProperty(key)) { + res.setHeader(key, headers[key]); + } + } + + next(); + }; + + middleware.addExpiresHeaders = function(req, res, next) { + if (req.app.enabled('cache')) { + res.setHeader("Cache-Control", "public, max-age=5184000"); + res.setHeader("Expires", new Date(Date.now() + 5184000000).toUTCString()); + } else { + res.setHeader("Cache-Control", "public, max-age=0"); + res.setHeader("Expires", new Date().toUTCString()); + } + + next(); + }; + +}; + + + diff --git a/src/middleware/index.js b/src/middleware/index.js index 867af6fed2..266f2b1488 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -1,83 +1,202 @@ "use strict"; -var meta = require('../meta'), - db = require('../database'), - file = require('../file'), - auth = require('../routes/authentication'), - - path = require('path'), - nconf = require('nconf'), - flash = require('connect-flash'), - templates = require('templates.js'), - bodyParser = require('body-parser'), - cookieParser = require('cookie-parser'), - compression = require('compression'), - favicon = require('serve-favicon'), - session = require('express-session'), - useragent = require('express-useragent'); - +var async = require('async'); +var fs = require('fs'); +var path = require('path'); +var csrf = require('csurf'); +var validator = require('validator'); +var nconf = require('nconf'); +var ensureLoggedIn = require('connect-ensure-login'); +var toobusy = require('toobusy-js'); + +var plugins = require('../plugins'); +var languages = require('../languages'); +var meta = require('../meta'); +var user = require('../user'); +var groups = require('../groups'); + +var analytics = require('../analytics'); + +var controllers = { + api: require('./../controllers/api'), + helpers: require('../controllers/helpers') +}; var middleware = {}; -function setupFavicon(app) { - var faviconPath = path.join(__dirname, '../../', 'public', meta.config['brand:favicon'] ? meta.config['brand:favicon'] : 'favicon.ico'); - if (file.existsSync(faviconPath)) { - app.use(nconf.get('relative_path'), favicon(faviconPath)); +middleware.applyCSRF = csrf(); + +middleware.ensureLoggedIn = ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login'); + +require('./admin')(middleware); +require('./header')(middleware); +require('./render')(middleware); +require('./maintenance')(middleware); +require('./user')(middleware); +require('./headers')(middleware); + +middleware.authenticate = function(req, res, next) { + if (req.user) { + return next(); + } else if (plugins.hasListeners('action:middleware.authenticate')) { + return plugins.fireHook('action:middleware.authenticate', { + req: req, + res: res, + next: next + }); } -} -module.exports = function(app) { - var relativePath = nconf.get('relative_path'); + controllers.helpers.notAllowed(req, res); +}; - middleware = require('./middleware')(app); +middleware.pageView = function(req, res, next) { + analytics.pageView({ + ip: req.ip, + path: req.path, + uid: req.hasOwnProperty('user') && req.user.hasOwnProperty('uid') ? parseInt(req.user.uid, 10) : 0 + }); + + plugins.fireHook('action:middleware.pageView', {req: req}); + + if (req.user) { + user.updateLastOnlineTime(req.user.uid); + if (req.path.startsWith('/api/users') || req.path.startsWith('/users')) { + user.updateOnlineUsers(req.user.uid, next); + } else { + user.updateOnlineUsers(req.user.uid); + next(); + } + } else { + next(); + } +}; - app.engine('tpl', templates.__express); - app.set('view engine', 'tpl'); - app.set('views', nconf.get('views_dir')); - app.set('json spaces', process.env.NODE_ENV === 'development' ? 4 : 0); - app.use(flash()); - app.enable('view cache'); +middleware.pluginHooks = function(req, res, next) { + async.each(plugins.loadedHooks['filter:router.page'] || [], function(hookObj, next) { + hookObj.method(req, res, next); + }, function() { + // If it got here, then none of the subscribed hooks did anything, or there were no hooks + next(); + }); +}; - app.use(compression()); +middleware.validateFiles = function(req, res, next) { + if (!Array.isArray(req.files.files) || !req.files.files.length) { + return next(new Error(['[[error:invalid-files]]'])); + } - setupFavicon(app); + next(); +}; - app.use(relativePath + '/apple-touch-icon', middleware.routeTouchIcon); +middleware.prepareAPI = function(req, res, next) { + res.locals.isAPI = true; + next(); +}; + +middleware.routeTouchIcon = function(req, res) { + if (meta.config['brand:touchIcon'] && validator.isURL(meta.config['brand:touchIcon'])) { + return res.redirect(meta.config['brand:touchIcon']); + } else { + return res.sendFile(path.join(__dirname, '../../public', meta.config['brand:touchIcon'] || '/logo.png'), { + maxAge: req.app.enabled('cache') ? 5184000000 : 0 + }); + } +}; - app.use(bodyParser.urlencoded({extended: true})); - app.use(bodyParser.json()); - app.use(cookieParser()); - app.use(useragent.express()); +middleware.privateTagListing = function(req, res, next) { + if (!req.user && parseInt(meta.config.privateTagListing, 10) === 1) { + controllers.helpers.notAllowed(req, res); + } else { + next(); + } +}; - var cookie = { - maxAge: 1000 * 60 * 60 * 24 * (parseInt(meta.config.loginDays, 10) || 14) - }; +middleware.exposeGroupName = function(req, res, next) { + expose('groupName', groups.getGroupNameByGroupSlug, 'slug', req, res, next); +}; - if (meta.config.cookieDomain) { - cookie.domain = meta.config.cookieDomain; +middleware.exposeUid = function(req, res, next) { + expose('uid', user.getUidByUserslug, 'userslug', req, res, next); +}; + +function expose(exposedField, method, field, req, res, next) { + if (!req.params.hasOwnProperty(field)) { + return next(); + } + method(req.params[field], function(err, id) { + if (err) { + return next(err); + } + + res.locals[exposedField] = id; + next(); + }); +} + +middleware.privateUploads = function(req, res, next) { + if (req.user || parseInt(meta.config.privateUploads, 10) !== 1) { + return next(); } + if (req.path.startsWith('/uploads/files')) { + return res.status(403).json('not-allowed'); + } + next(); +}; - if (nconf.get('secure')) { - cookie.secure = true; +middleware.busyCheck = function(req, res, next) { + if (global.env === 'production' && (!meta.config.hasOwnProperty('eventLoopCheckEnabled') || parseInt(meta.config.eventLoopCheckEnabled, 10) === 1) && toobusy()) { + analytics.increment('errors:503'); + res.status(503).type('text/html').sendFile(path.join(__dirname, '../../public/503.html')); + } else { + next(); } +}; + +middleware.applyBlacklist = function(req, res, next) { + meta.blacklist.test(req.ip, function(err) { + next(err); + }); +}; + +middleware.processLanguages = function(req, res, next) { + var code = req.params.code; + var key = req.path.match(/[\w]+\.json/); - if (relativePath !== '') { - cookie.path = relativePath; + if (code && key) { + languages.get(code, key[0], function(err, language) { + if (err) { + return next(err); + } + + res.status(200).json(language); + }); + } else { + res.status(404).json('{}'); } +}; - app.use(session({ - store: db.sessionStore, - secret: nconf.get('secret'), - key: 'express.sid', - cookie: cookie, - resave: true, - saveUninitialized: true - })); +middleware.processTimeagoLocales = function(req, res, next) { + var fallback = req.path.indexOf('-short') === -1 ? 'jquery.timeago.en.js' : 'jquery.timeago.en-short.js', + localPath = path.join(__dirname, '../../public/vendor/jquery/timeago/locales', req.path), + exists; - app.use(middleware.addHeaders); - app.use(middleware.processRender); - auth.initialize(app, middleware); + try { + exists = fs.accessSync(localPath, fs.F_OK | fs.R_OK); + } catch(e) { + exists = false; + } - return middleware; + if (exists) { + res.status(200).sendFile(localPath, { + maxAge: req.app.enabled('cache') ? 5184000000 : 0 + }); + } else { + res.status(200).sendFile(path.join(__dirname, '../../public/vendor/jquery/timeago/locales', fallback), { + maxAge: req.app.enabled('cache') ? 5184000000 : 0 + }); + } }; + + +module.exports = middleware; diff --git a/src/middleware/middleware.js b/src/middleware/middleware.js deleted file mode 100644 index e5caf9ba00..0000000000 --- a/src/middleware/middleware.js +++ /dev/null @@ -1,384 +0,0 @@ -"use strict"; - -var app; -var middleware = { - admin: {} -}; -var async = require('async'); -var fs = require('fs'); -var path = require('path'); -var csrf = require('csurf'); -var _ = require('underscore'); - -var validator = require('validator'); -var nconf = require('nconf'); -var ensureLoggedIn = require('connect-ensure-login'); -var toobusy = require('toobusy-js'); - -var plugins = require('../plugins'); -var languages = require('../languages'); -var meta = require('../meta'); -var user = require('../user'); -var groups = require('../groups'); - -var analytics = require('../analytics'); - -var controllers = { - api: require('./../controllers/api'), - helpers: require('../controllers/helpers') -}; - -toobusy.maxLag(parseInt(meta.config.eventLoopLagThreshold, 10) || 100); -toobusy.interval(parseInt(meta.config.eventLoopInterval, 10) || 500); - -middleware.authenticate = function(req, res, next) { - if (req.user) { - return next(); - } else if (plugins.hasListeners('action:middleware.authenticate')) { - return plugins.fireHook('action:middleware.authenticate', { - req: req, - res: res, - next: next - }); - } - - controllers.helpers.notAllowed(req, res); -}; - -middleware.applyCSRF = csrf(); - -middleware.ensureLoggedIn = ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login'); - -middleware.pageView = function(req, res, next) { - analytics.pageView({ - ip: req.ip, - path: req.path, - uid: req.hasOwnProperty('user') && req.user.hasOwnProperty('uid') ? parseInt(req.user.uid, 10) : 0 - }); - - plugins.fireHook('action:middleware.pageView', {req: req}); - - if (req.user) { - user.updateLastOnlineTime(req.user.uid); - if (req.path.startsWith('/api/users') || req.path.startsWith('/users')) { - user.updateOnlineUsers(req.user.uid, next); - } else { - user.updateOnlineUsers(req.user.uid); - next(); - } - } else { - next(); - } -}; - -middleware.addHeaders = function (req, res, next) { - var defaults = { - 'X-Powered-By': 'NodeBB', - 'X-Frame-Options': 'SAMEORIGIN', - 'Access-Control-Allow-Origin': 'null' // yes, string null. - }; - var headers = { - 'X-Powered-By': meta.config['powered-by'], - 'X-Frame-Options': meta.config['allow-from-uri'] ? 'ALLOW-FROM ' + meta.config['allow-from-uri'] : undefined, - 'Access-Control-Allow-Origin': meta.config['access-control-allow-origin'], - 'Access-Control-Allow-Methods': meta.config['access-control-allow-methods'], - 'Access-Control-Allow-Headers': meta.config['access-control-allow-headers'] - }; - - _.defaults(headers, defaults); - headers = _.pick(headers, Boolean); // Remove falsy headers - - for(var key in headers) { - if (headers.hasOwnProperty(key)) { - res.setHeader(key, headers[key]); - } - } - - next(); -}; - -middleware.pluginHooks = function(req, res, next) { - async.each(plugins.loadedHooks['filter:router.page'] || [], function(hookObj, next) { - hookObj.method(req, res, next); - }, function() { - // If it got here, then none of the subscribed hooks did anything, or there were no hooks - next(); - }); -}; - -middleware.redirectToAccountIfLoggedIn = function(req, res, next) { - if (req.session.forceLogin) { - return next(); - } - - if (!req.user) { - return next(); - } - user.getUserField(req.user.uid, 'userslug', function (err, userslug) { - if (err) { - return next(err); - } - controllers.helpers.redirect(res, '/user/' + userslug); - }); -}; - -middleware.validateFiles = function(req, res, next) { - if (!Array.isArray(req.files.files) || !req.files.files.length) { - return next(new Error(['[[error:invalid-files]]'])); - } - - next(); -}; - -middleware.prepareAPI = function(req, res, next) { - res.locals.isAPI = true; - next(); -}; - -middleware.checkGlobalPrivacySettings = function(req, res, next) { - if (!req.user && !!parseInt(meta.config.privateUserInfo, 10)) { - return controllers.helpers.notAllowed(req, res); - } - - next(); -}; - -middleware.checkAccountPermissions = function(req, res, next) { - // This middleware ensures that only the requested user and admins can pass - async.waterfall([ - function (next) { - middleware.authenticate(req, res, next); - }, - function (next) { - user.getUidByUserslug(req.params.userslug, next); - }, - function (uid, next) { - if (parseInt(uid, 10) === req.uid) { - return next(null, true); - } - - user.isAdminOrGlobalMod(req.uid, next); - } - ], function (err, allowed) { - if (err || allowed) { - return next(err); - } - controllers.helpers.notAllowed(req, res); - }); -}; - -middleware.redirectUidToUserslug = function(req, res, next) { - var uid = parseInt(req.params.uid, 10); - if (!uid) { - return next(); - } - user.getUserField(uid, 'userslug', function(err, userslug) { - if (err || !userslug) { - return next(err); - } - - var path = req.path.replace(/^\/api/, '') - .replace('uid', 'user') - .replace(uid, function() { return userslug; }); - controllers.helpers.redirect(res, path); - }); -}; - -middleware.isAdmin = function(req, res, next) { - if (!req.uid) { - return controllers.helpers.notAllowed(req, res); - } - - user.isAdministrator(req.uid, function (err, isAdmin) { - if (err) { - return next(err); - } - - if (isAdmin) { - user.hasPassword(req.uid, function(err, hasPassword) { - if (err) { - return next(err); - } - - if (!hasPassword) { - return next(); - } - - var loginTime = req.session.meta ? req.session.meta.datetime : 0; - if (loginTime && parseInt(loginTime, 10) > Date.now() - 3600000) { - var timeLeft = parseInt(loginTime, 10) - (Date.now() - 3600000); - if (timeLeft < 300000) { - req.session.meta.datetime += 300000; - } - - return next(); - } - - req.session.returnTo = req.path.replace(/^\/api/, ''); - req.session.forceLogin = 1; - if (res.locals.isAPI) { - res.status(401).json({}); - } else { - res.redirect(nconf.get('relative_path') + '/login'); - } - }); - return; - } - - if (res.locals.isAPI) { - return controllers.helpers.notAllowed(req, res); - } - - middleware.buildHeader(req, res, function() { - controllers.helpers.notAllowed(req, res); - }); - }); -}; - -middleware.routeTouchIcon = function(req, res) { - if (meta.config['brand:touchIcon'] && validator.isURL(meta.config['brand:touchIcon'])) { - return res.redirect(meta.config['brand:touchIcon']); - } else { - return res.sendFile(path.join(__dirname, '../../public', meta.config['brand:touchIcon'] || '/logo.png'), { - maxAge: app.enabled('cache') ? 5184000000 : 0 - }); - } -}; - -middleware.addExpiresHeaders = function(req, res, next) { - if (app.enabled('cache')) { - res.setHeader("Cache-Control", "public, max-age=5184000"); - res.setHeader("Expires", new Date(Date.now() + 5184000000).toUTCString()); - } else { - res.setHeader("Cache-Control", "public, max-age=0"); - res.setHeader("Expires", new Date().toUTCString()); - } - - next(); -}; - -middleware.privateTagListing = function(req, res, next) { - if (!req.user && parseInt(meta.config.privateTagListing, 10) === 1) { - controllers.helpers.notAllowed(req, res); - } else { - next(); - } -}; - -middleware.exposeGroupName = function(req, res, next) { - expose('groupName', groups.getGroupNameByGroupSlug, 'slug', req, res, next); -}; - -middleware.exposeUid = function(req, res, next) { - expose('uid', user.getUidByUserslug, 'userslug', req, res, next); -}; - -function expose(exposedField, method, field, req, res, next) { - if (!req.params.hasOwnProperty(field)) { - return next(); - } - method(req.params[field], function(err, id) { - if (err) { - return next(err); - } - - res.locals[exposedField] = id; - next(); - }); -} - -middleware.requireUser = function(req, res, next) { - if (req.user) { - return next(); - } - - res.status(403).render('403', {title: '[[global:403.title]]'}); -}; - -middleware.privateUploads = function(req, res, next) { - if (req.user || parseInt(meta.config.privateUploads, 10) !== 1) { - return next(); - } - if (req.path.startsWith('/uploads/files')) { - return res.status(403).json('not-allowed'); - } - next(); -}; - -middleware.busyCheck = function(req, res, next) { - if (global.env === 'production' && (!meta.config.hasOwnProperty('eventLoopCheckEnabled') || parseInt(meta.config.eventLoopCheckEnabled, 10) === 1) && toobusy()) { - analytics.increment('errors:503'); - res.status(503).type('text/html').sendFile(path.join(__dirname, '../../public/503.html')); - } else { - next(); - } -}; - -middleware.applyBlacklist = function(req, res, next) { - meta.blacklist.test(req.ip, function(err) { - next(err); - }); -}; - -middleware.processLanguages = function(req, res, next) { - var code = req.params.code; - var key = req.path.match(/[\w]+\.json/); - - if (code && key) { - languages.get(code, key[0], function(err, language) { - if (err) { - return next(err); - } - - res.status(200).json(language); - }); - } else { - res.status(404).json('{}'); - } -}; - -middleware.processTimeagoLocales = function(req, res, next) { - var fallback = req.path.indexOf('-short') === -1 ? 'jquery.timeago.en.js' : 'jquery.timeago.en-short.js', - localPath = path.join(__dirname, '../../public/vendor/jquery/timeago/locales', req.path), - exists; - - try { - exists = fs.accessSync(localPath, fs.F_OK | fs.R_OK); - } catch(e) { - exists = false; - } - - if (exists) { - res.status(200).sendFile(localPath, { - maxAge: app.enabled('cache') ? 5184000000 : 0 - }); - } else { - res.status(200).sendFile(path.join(__dirname, '../../public/vendor/jquery/timeago/locales', fallback), { - maxAge: app.enabled('cache') ? 5184000000 : 0 - }); - } -}; - -middleware.registrationComplete = function(req, res, next) { - // If the user's session contains registration data, redirect the user to complete registration - if (!req.session.hasOwnProperty('registration')) { - return next(); - } else { - if (!req.path.endsWith('/register/complete')) { - controllers.helpers.redirect(res, '/register/complete'); - } else { - return next(); - } - } -}; - -module.exports = function(webserver) { - app = webserver; - middleware.admin = require('./admin')(webserver); - - require('./header')(app, middleware); - require('./render')(middleware); - require('./maintenance')(middleware); - - return middleware; -}; diff --git a/src/middleware/user.js b/src/middleware/user.js new file mode 100644 index 0000000000..b70f7639d7 --- /dev/null +++ b/src/middleware/user.js @@ -0,0 +1,154 @@ +'use strict'; + +var async = require('async'); +var nconf = require('nconf'); +var meta = require('../meta'); +var user = require('../user'); + +var controllers = { + helpers: require('../controllers/helpers') +}; + +module.exports = function(middleware) { + + middleware.checkGlobalPrivacySettings = function(req, res, next) { + if (!req.user && !!parseInt(meta.config.privateUserInfo, 10)) { + return controllers.helpers.notAllowed(req, res); + } + + next(); + }; + + middleware.checkAccountPermissions = function(req, res, next) { + // This middleware ensures that only the requested user and admins can pass + async.waterfall([ + function (next) { + middleware.authenticate(req, res, next); + }, + function (next) { + user.getUidByUserslug(req.params.userslug, next); + }, + function (uid, next) { + if (parseInt(uid, 10) === req.uid) { + return next(null, true); + } + + user.isAdminOrGlobalMod(req.uid, next); + } + ], function (err, allowed) { + if (err || allowed) { + return next(err); + } + controllers.helpers.notAllowed(req, res); + }); + }; + + middleware.redirectToAccountIfLoggedIn = function(req, res, next) { + if (req.session.forceLogin) { + return next(); + } + + if (!req.user) { + return next(); + } + user.getUserField(req.user.uid, 'userslug', function (err, userslug) { + if (err) { + return next(err); + } + controllers.helpers.redirect(res, '/user/' + userslug); + }); + }; + + middleware.redirectUidToUserslug = function(req, res, next) { + var uid = parseInt(req.params.uid, 10); + if (!uid) { + return next(); + } + user.getUserField(uid, 'userslug', function(err, userslug) { + if (err || !userslug) { + return next(err); + } + + var path = req.path.replace(/^\/api/, '') + .replace('uid', 'user') + .replace(uid, function() { return userslug; }); + controllers.helpers.redirect(res, path); + }); + }; + + middleware.isAdmin = function(req, res, next) { + if (!req.uid) { + return controllers.helpers.notAllowed(req, res); + } + + user.isAdministrator(req.uid, function (err, isAdmin) { + if (err) { + return next(err); + } + + if (isAdmin) { + user.hasPassword(req.uid, function(err, hasPassword) { + if (err) { + return next(err); + } + + if (!hasPassword) { + return next(); + } + + var loginTime = req.session.meta ? req.session.meta.datetime : 0; + if (loginTime && parseInt(loginTime, 10) > Date.now() - 3600000) { + var timeLeft = parseInt(loginTime, 10) - (Date.now() - 3600000); + if (timeLeft < 300000) { + req.session.meta.datetime += 300000; + } + + return next(); + } + + req.session.returnTo = req.path.replace(/^\/api/, ''); + req.session.forceLogin = 1; + if (res.locals.isAPI) { + res.status(401).json({}); + } else { + res.redirect(nconf.get('relative_path') + '/login'); + } + }); + return; + } + + if (res.locals.isAPI) { + return controllers.helpers.notAllowed(req, res); + } + + middleware.buildHeader(req, res, function() { + controllers.helpers.notAllowed(req, res); + }); + }); + }; + + middleware.requireUser = function(req, res, next) { + if (req.user) { + return next(); + } + + res.status(403).render('403', {title: '[[global:403.title]]'}); + }; + + middleware.registrationComplete = function(req, res, next) { + // If the user's session contains registration data, redirect the user to complete registration + if (!req.session.hasOwnProperty('registration')) { + return next(); + } else { + if (!req.path.endsWith('/register/complete')) { + controllers.helpers.redirect(res, '/register/complete'); + } else { + return next(); + } + } + }; + +}; + + + diff --git a/src/plugins.js b/src/plugins.js index c531dfa27d..fc56d799c6 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -10,12 +10,10 @@ var nconf = require('nconf'); var db = require('./database'); var emitter = require('./emitter'); -var translator = require('../public/src/modules/translator'); var utils = require('../public/src/utils'); var hotswap = require('./hotswap'); var file = require('./file'); -var controllers = require('./controllers'); var app; var middleware; @@ -149,11 +147,13 @@ var middleware; Plugins.reloadRoutes = function(callback) { callback = callback || function() {}; var router = express.Router(); + router.hotswapId = 'plugins'; router.render = function() { app.render.apply(app, arguments); }; + var controllers = require('./controllers'); Plugins.fireHook('static:app.load', {app: app, router: router, middleware: middleware, controllers: controllers}, function(err) { if (err) { return winston.error('[plugins] Encountered error while executing post-router plugins hooks: ' + err.message); diff --git a/src/webserver.js b/src/webserver.js index 5ed7f44cd6..65b1eae0ff 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -1,25 +1,35 @@ 'use strict'; -var path = require('path'), - fs = require('fs'), - nconf = require('nconf'), - express = require('express'), - app = express(), - server, - winston = require('winston'), - async = require('async'), - - emailer = require('./emailer'), - meta = require('./meta'), - languages = require('./languages'), - logger = require('./logger'), - plugins = require('./plugins'), - middleware = require('./middleware'), - routes = require('./routes'), - emitter = require('./emitter'), - - helpers = require('../public/src/modules/helpers'); +var fs = require('fs'); +var path = require('path'); +var nconf = require('nconf'); +var express = require('express'); +var app = express(); +var server; +var winston = require('winston'); +var async = require('async'); +var flash = require('connect-flash'); +var compression = require('compression'); +var bodyParser = require('body-parser'); +var cookieParser = require('cookie-parser'); +var session = require('express-session'); +var useragent = require('express-useragent'); + +var db = require('./database'); +var file = require('./file'); +var emailer = require('./emailer'); +var meta = require('./meta'); +var languages = require('./languages'); +var logger = require('./logger'); +var plugins = require('./plugins'); +var middleware = require('./middleware'); +var routes = require('./routes'); +var auth = require('./routes/authentication'); +var emitter = require('./emitter'); +var templates = require('templates.js'); + +var helpers = require('../public/src/modules/helpers'); if (nconf.get('ssl')) { server = require('https').createServer({ @@ -46,7 +56,7 @@ server.on('error', function(err) { module.exports.listen = function() { emailer.registerApp(app); - app.locals.middleware = middleware = middleware(app); + setupExpressApp(app); helpers.register(); @@ -71,6 +81,69 @@ module.exports.listen = function() { }); }; +function setupExpressApp(app) { + var relativePath = nconf.get('relative_path'); + + app.engine('tpl', templates.__express); + app.set('view engine', 'tpl'); + app.set('views', nconf.get('views_dir')); + app.set('json spaces', process.env.NODE_ENV === 'development' ? 4 : 0); + app.use(flash()); + + app.enable('view cache'); + + app.use(compression()); + + setupFavicon(app); + + app.use(relativePath + '/apple-touch-icon', middleware.routeTouchIcon); + + app.use(bodyParser.urlencoded({extended: true})); + app.use(bodyParser.json()); + app.use(cookieParser()); + app.use(useragent.express()); + + var cookie = { + maxAge: 1000 * 60 * 60 * 24 * (parseInt(meta.config.loginDays, 10) || 14) + }; + + if (meta.config.cookieDomain) { + cookie.domain = meta.config.cookieDomain; + } + + if (nconf.get('secure')) { + cookie.secure = true; + } + + if (relativePath !== '') { + cookie.path = relativePath; + } + + app.use(session({ + store: db.sessionStore, + secret: nconf.get('secret'), + key: 'express.sid', + cookie: cookie, + resave: true, + saveUninitialized: true + })); + + app.use(middleware.addHeaders); + app.use(middleware.processRender); + auth.initialize(app, middleware); + + var toobusy = require('toobusy-js'); + toobusy.maxLag(parseInt(meta.config.eventLoopLagThreshold, 10) || 100); + toobusy.interval(parseInt(meta.config.eventLoopInterval, 10) || 500); +} + +function setupFavicon(app) { + var faviconPath = path.join(__dirname, '../../', 'public', meta.config['brand:favicon'] ? meta.config['brand:favicon'] : 'favicon.ico'); + if (file.existsSync(faviconPath)) { + app.use(nconf.get('relative_path'), favicon(faviconPath)); + } +} + function initializeNodeBB(callback) { var skipJS, skipLess, fromFile = nconf.get('from-file') || '';