From a15aaaf38967ee89edfeb18af23c2e552a2d9723 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 3 Feb 2017 19:02:38 +0300 Subject: [PATCH] closes #5394 dont allow socket.emits during maintenance mode --- src/middleware/maintenance.js | 28 +-- src/middleware/ratelimit.js | 4 +- src/routes/api.js | 3 +- src/routes/feeds.js | 16 +- src/routes/helpers.js | 2 +- src/routes/index.js | 4 +- src/socket.io/index.js | 360 ++++++++++++++++++---------------- 7 files changed, 203 insertions(+), 214 deletions(-) diff --git a/src/middleware/maintenance.js b/src/middleware/maintenance.js index 31ba25e250..3193e820fa 100644 --- a/src/middleware/maintenance.js +++ b/src/middleware/maintenance.js @@ -12,33 +12,7 @@ module.exports = function (middleware) { } var url = req.url.replace(nconf.get('relative_path'), ''); - var allowedRoutes = [ - '^/ping', - '^/sping', - '^/login', - '^/stylesheet.css', - '^/favicon', - '^/nodebb.min.js', - '^/vendor/fontawesome/fonts/fontawesome-webfont.woff', - '^/src/(modules|client)/[\\w/]+.js', - '^/templates/[\\w/]+.tpl', - '^/api/login', - '^/api/widgets/render', - '^/public/language', - '^/uploads/system/site-logo.png' - ]; - - var isAllowed = function (url) { - for(var x = 0,numAllowed = allowedRoutes.length,route; x < numAllowed; x++) { - route = new RegExp(allowedRoutes[x]); - if (route.test(url)) { - return true; - } - } - return false; - }; - - if (isAllowed(url)) { + if (url.startsWith('/login') || url.startsWith('/api/login')) { return next(); } diff --git a/src/middleware/ratelimit.js b/src/middleware/ratelimit.js index 818fe99c12..f02bf74c3e 100644 --- a/src/middleware/ratelimit.js +++ b/src/middleware/ratelimit.js @@ -3,7 +3,7 @@ 'use strict'; var winston = require('winston'); -var ratelimit = {}; +var ratelimit = module.exports; var allowedCalls = 100; var timeframe = 10000; @@ -31,5 +31,3 @@ ratelimit.isFlooding = function (socket) { socket.lastCallTime = now; return false; }; - -module.exports = ratelimit; diff --git a/src/routes/api.js b/src/routes/api.js index be76336c77..98c884c237 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -29,12 +29,13 @@ module.exports = function (app, middleware, controllers) { var multipart = require('connect-multiparty'); var multipartMiddleware = multipart(); - var middlewares = [multipartMiddleware, middleware.validateFiles, middleware.applyCSRF]; + var middlewares = [middleware.maintenanceMode, multipartMiddleware, middleware.validateFiles, middleware.applyCSRF]; router.post('/post/upload', middlewares, uploadsController.uploadPost); router.post('/topic/thumb/upload', middlewares, uploadsController.uploadThumb); router.post('/user/:userslug/uploadpicture', middlewares.concat([middleware.authenticate, middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions]), controllers.accounts.edit.uploadPicture); router.post('/user/:userslug/uploadcover', middlewares.concat([middleware.authenticate, middleware.checkGlobalPrivacySettings, middleware.checkAccountPermissions]), controllers.accounts.edit.uploadCoverPicture); router.post('/groups/uploadpicture', middlewares.concat([middleware.authenticate]), controllers.groups.uploadCover); + }; diff --git a/src/routes/feeds.js b/src/routes/feeds.js index 50ebbc9342..4638582cb4 100644 --- a/src/routes/feeds.js +++ b/src/routes/feeds.js @@ -365,12 +365,12 @@ function sendFeed(feed, res) { } module.exports = function (app, middleware, controllers) { - app.get('/topic/:topic_id.rss', generateForTopic); - app.get('/category/:category_id.rss', generateForCategory); - app.get('/recent.rss', generateForRecent); - app.get('/popular.rss', generateForPopular); - app.get('/popular/:term.rss', generateForPopular); - app.get('/recentposts.rss', generateForRecentPosts); - app.get('/category/:category_id/recentposts.rss', generateForCategoryRecentPosts); - app.get('/user/:userslug/topics.rss', generateForUserTopics); + app.get('/topic/:topic_id.rss', middleware.maintenanceMode, generateForTopic); + app.get('/category/:category_id.rss', middleware.maintenanceMode, generateForCategory); + app.get('/recent.rss', middleware.maintenanceMode, generateForRecent); + app.get('/popular.rss', middleware.maintenanceMode, generateForPopular); + app.get('/popular/:term.rss', middleware.maintenanceMode, generateForPopular); + app.get('/recentposts.rss', middleware.maintenanceMode, generateForRecentPosts); + app.get('/category/:category_id/recentposts.rss', middleware.maintenanceMode, generateForCategoryRecentPosts); + app.get('/user/:userslug/topics.rss', middleware.maintenanceMode, generateForUserTopics); }; diff --git a/src/routes/helpers.js b/src/routes/helpers.js index 052d99292c..2379c7ebf1 100644 --- a/src/routes/helpers.js +++ b/src/routes/helpers.js @@ -3,7 +3,7 @@ var helpers = {}; helpers.setupPageRoute = function (router, name, middleware, middlewares, controller) { - middlewares = middlewares.concat([middleware.registrationComplete, middleware.pageView, middleware.pluginHooks]); + middlewares = middlewares.concat([middleware.maintenanceMode, middleware.registrationComplete, middleware.pageView, middleware.pluginHooks]); router.get(name, middleware.busyCheck, middleware.buildHeader, middlewares, controller); router.get('/api' + name, middlewares, controller); diff --git a/src/routes/index.js b/src/routes/index.js index 13633f2bc8..6e0371a59c 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -117,8 +117,6 @@ module.exports = function (app, middleware, hotswapIds) { app.all(relativePath + '(/api/admin|/api/admin/*?)', middleware.isAdmin); app.all(relativePath + '(/admin|/admin/*?)', ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login?local=1'), middleware.applyCSRF, middleware.isAdmin); - app.use(middleware.maintenanceMode); - adminRoutes(router, middleware, controllers); metaRoutes(router, middleware, controllers); apiRoutes(router, middleware, controllers); @@ -151,7 +149,7 @@ module.exports = function (app, middleware, hotswapIds) { // DEPRECATED app.use(relativePath + '/api/language', function (req, res) { - winston.warn('[deprecated] Accessing language files from `/api/language` is deprecated. ' + + winston.warn('[deprecated] Accessing language files from `/api/language` is deprecated. ' + 'Use `/assets/language' + req.path + '.json` for prefetch paths.'); res.redirect(relativePath + '/assets/language' + req.path + '.json?' + meta.config['cache-buster']); }); diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 38e73c1cc1..82f597011d 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -7,223 +7,241 @@ var url = require('url'); var cookieParser = require('cookie-parser')(nconf.get('secret')); var db = require('../database'); +var user = require('../user'); var logger = require('../logger'); var ratelimit = require('../middleware/ratelimit'); -(function (Sockets) { - var Namespaces = {}; - var io; - Sockets.init = function (server) { - requireModules(); +var Namespaces = {}; +var io; - var SocketIO = require('socket.io'); - var socketioWildcard = require('socketio-wildcard')(); - io = new SocketIO({ - path: nconf.get('relative_path') + '/socket.io' - }); +var Sockets = module.exports; - addRedisAdapter(io); +Sockets.init = function (server) { + requireModules(); - io.use(socketioWildcard); - io.use(authorize); + var SocketIO = require('socket.io'); + var socketioWildcard = require('socketio-wildcard')(); + io = new SocketIO({ + path: nconf.get('relative_path') + '/socket.io' + }); - io.on('connection', onConnection); + addRedisAdapter(io); - io.listen(server, { - transports: nconf.get('socket.io:transports') - }); + io.use(socketioWildcard); + io.use(authorize); - Sockets.server = io; - }; + io.on('connection', onConnection); - function onConnection(socket) { - socket.ip = socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress; + io.listen(server, { + transports: nconf.get('socket.io:transports') + }); - logger.io_one(socket, socket.uid); + Sockets.server = io; +}; - onConnect(socket); +function onConnection(socket) { + socket.ip = socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress; - socket.on('*', function (payload) { - onMessage(socket, payload); - }); - } + logger.io_one(socket, socket.uid); - function onConnect(socket) { - if (socket.uid) { - socket.join('uid_' + socket.uid); - socket.join('online_users'); - } else { - socket.join('online_guests'); - } + onConnect(socket); - socket.join('sess_' + socket.request.signedCookies[nconf.get('sessionKey')]); - io.sockets.sockets[socket.id].emit('checkSession', socket.uid); - } + socket.on('*', function (payload) { + onMessage(socket, payload); + }); +} - function onMessage(socket, payload) { - if (!payload.data.length) { - return winston.warn('[socket.io] Empty payload'); - } +function onConnect(socket) { + if (socket.uid) { + socket.join('uid_' + socket.uid); + socket.join('online_users'); + } else { + socket.join('online_guests'); + } - var eventName = payload.data[0]; - var params = payload.data[1]; - var callback = typeof payload.data[payload.data.length - 1] === 'function' ? payload.data[payload.data.length - 1] : function () { - }; + socket.join('sess_' + socket.request.signedCookies[nconf.get('sessionKey')]); + io.sockets.sockets[socket.id].emit('checkSession', socket.uid); +} - if (!eventName) { - return winston.warn('[socket.io] Empty method name'); - } +function onMessage(socket, payload) { + if (!payload.data.length) { + return winston.warn('[socket.io] Empty payload'); + } - var parts = eventName.toString().split('.'); - var namespace = parts[0]; - var methodToCall = parts.reduce(function (prev, cur) { - if (prev !== null && prev[cur]) { - return prev[cur]; - } else { - return null; - } - }, Namespaces); + var eventName = payload.data[0]; + var params = payload.data[1]; + var callback = typeof payload.data[payload.data.length - 1] === 'function' ? payload.data[payload.data.length - 1] : function () { + }; - if (!methodToCall) { - if (process.env.NODE_ENV === 'development') { - winston.warn('[socket.io] Unrecognized message: ' + eventName); - } - return callback({message: '[[error:invalid-event]]'}); - } + if (!eventName) { + return winston.warn('[socket.io] Empty method name'); + } - socket.previousEvents = socket.previousEvents || []; - socket.previousEvents.push(eventName); - if (socket.previousEvents.length > 20) { - socket.previousEvents.shift(); + var parts = eventName.toString().split('.'); + var namespace = parts[0]; + var methodToCall = parts.reduce(function (prev, cur) { + if (prev !== null && prev[cur]) { + return prev[cur]; + } else { + return null; } + }, Namespaces); - if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) { - winston.warn('[socket.io] Too many emits! Disconnecting uid : ' + socket.uid + '. Events : ' + socket.previousEvents); - return socket.disconnect(); + if (!methodToCall) { + if (process.env.NODE_ENV === 'development') { + winston.warn('[socket.io] Unrecognized message: ' + eventName); } - - async.waterfall([ - function (next) { - validateSession(socket, next); - }, - function (next) { - if (Namespaces[namespace].before) { - Namespaces[namespace].before(socket, eventName, params, next); - } else { - next(); - } - }, - function (next) { - methodToCall(socket, params, next); - } - ], function (err, result) { - callback(err ? {message: err.message} : null, result); - }); + return callback({message: '[[error:invalid-event]]'}); } - function requireModules() { - var modules = ['admin', 'categories', 'groups', 'meta', 'modules', - 'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist' - ]; + socket.previousEvents = socket.previousEvents || []; + socket.previousEvents.push(eventName); + if (socket.previousEvents.length > 20) { + socket.previousEvents.shift(); + } - modules.forEach(function (module) { - Namespaces[module] = require('./' + module); - }); + if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) { + winston.warn('[socket.io] Too many emits! Disconnecting uid : ' + socket.uid + '. Events : ' + socket.previousEvents); + return socket.disconnect(); } - function validateSession(socket, callback) { - var req = socket.request; - if (!req.signedCookies || !req.signedCookies[nconf.get('sessionKey')]) { - return callback(new Error('[[error:invalid-session]]')); - } - db.sessionStore.get(req.signedCookies[nconf.get('sessionKey')], function (err, sessionData) { - if (err || !sessionData) { - return callback(err || new Error('[[error:invalid-session]]')); + async.waterfall([ + function (next) { + checkMaintenance(socket, next); + }, + function (next) { + validateSession(socket, next); + }, + function (next) { + if (Namespaces[namespace].before) { + Namespaces[namespace].before(socket, eventName, params, next); + } else { + next(); } + }, + function (next) { + methodToCall(socket, params, next); + } + ], function (err, result) { + callback(err ? {message: err.message} : null, result); + }); +} + +function requireModules() { + var modules = ['admin', 'categories', 'groups', 'meta', 'modules', + 'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist' + ]; + + modules.forEach(function (module) { + Namespaces[module] = require('./' + module); + }); +} + +function checkMaintenance(socket, callback) { + var meta = require('../meta'); + if (parseInt(meta.config.maintenanceMode, 10) !== 1) { + return setImmediate(callback); + } + user.isAdministrator(socket.uid, function (err, isAdmin) { + if (err || isAdmin) { + return callback(err); + } + }); +} - callback(); - }); +function validateSession(socket, callback) { + var req = socket.request; + if (!req.signedCookies || !req.signedCookies[nconf.get('sessionKey')]) { + return callback(new Error('[[error:invalid-session]]')); } + db.sessionStore.get(req.signedCookies[nconf.get('sessionKey')], function (err, sessionData) { + if (err || !sessionData) { + return callback(err || new Error('[[error:invalid-session]]')); + } - function authorize(socket, callback) { - var request = socket.request; + callback(); + }); +} - if (!request) { - return callback(new Error('[[error:not-authorized]]')); - } +function authorize(socket, callback) { + var request = socket.request; - async.waterfall([ - function (next) { - cookieParser(request, {}, next); - }, - function (next) { - db.sessionStore.get(request.signedCookies[nconf.get('sessionKey')], function (err, sessionData) { - if (err) { - return next(err); - } - if (sessionData && sessionData.passport && sessionData.passport.user) { - request.session = sessionData; - socket.uid = parseInt(sessionData.passport.user, 10); - } else { - socket.uid = 0; - } - next(); - }); - } - ], callback); + if (!request) { + return callback(new Error('[[error:not-authorized]]')); } - function addRedisAdapter(io) { - if (nconf.get('redis')) { - var redisAdapter = require('socket.io-redis'); - var redis = require('../database/redis'); - var pub = redis.connect(); - var sub = redis.connect({return_buffers: true}); - io.adapter(redisAdapter({pubClient: pub, subClient: sub})); - } else if (nconf.get('isCluster') === 'true') { - winston.warn('[socket.io] Clustering detected, you are advised to configure Redis as a websocket store.'); + async.waterfall([ + function (next) { + cookieParser(request, {}, next); + }, + function (next) { + db.sessionStore.get(request.signedCookies[nconf.get('sessionKey')], function (err, sessionData) { + if (err) { + return next(err); + } + if (sessionData && sessionData.passport && sessionData.passport.user) { + request.session = sessionData; + socket.uid = parseInt(sessionData.passport.user, 10); + } else { + socket.uid = 0; + } + next(); + }); } + ], callback); +} + +function addRedisAdapter(io) { + if (nconf.get('redis')) { + var redisAdapter = require('socket.io-redis'); + var redis = require('../database/redis'); + var pub = redis.connect(); + var sub = redis.connect({return_buffers: true}); + io.adapter(redisAdapter({pubClient: pub, subClient: sub})); + } else if (nconf.get('isCluster') === 'true') { + winston.warn('[socket.io] Clustering detected, you are advised to configure Redis as a websocket store.'); } +} - Sockets.in = function (room) { - return io.in(room); - }; +Sockets.in = function (room) { + return io.in(room); +}; - Sockets.getUserSocketCount = function (uid) { - if (!io) { - return 0; - } +Sockets.getUserSocketCount = function (uid) { + if (!io) { + return 0; + } - var room = io.sockets.adapter.rooms['uid_' + uid]; - return room ? room.length : 0; - }; + var room = io.sockets.adapter.rooms['uid_' + uid]; + return room ? room.length : 0; +}; - Sockets.reqFromSocket = function (socket, payload, event) { - var headers = socket.request ? socket.request.headers : {}; - var encrypted = socket.request ? !!socket.request.connection.encrypted : false; - var host = headers.host; - var referer = headers.referer || ''; - var data = ((payload || {}).data || []); +Sockets.reqFromSocket = function (socket, payload, event) { + var headers = socket.request ? socket.request.headers : {}; + var encrypted = socket.request ? !!socket.request.connection.encrypted : false; + var host = headers.host; + var referer = headers.referer || ''; + var data = ((payload || {}).data || []); - if (!host) { - host = url.parse(referer).host || ''; - } + if (!host) { + host = url.parse(referer).host || ''; + } - return { - uid: socket.uid, - params: data[1], - method: event || data[0], - body: payload, - ip: headers['x-forwarded-for'] || socket.ip, - host: host, - protocol: encrypted ? 'https' : 'http', - secure: encrypted, - url: referer, - path: referer.substr(referer.indexOf(host) + host.length), - headers: headers - }; + return { + uid: socket.uid, + params: data[1], + method: event || data[0], + body: payload, + ip: headers['x-forwarded-for'] || socket.ip, + host: host, + protocol: encrypted ? 'https' : 'http', + secure: encrypted, + url: referer, + path: referer.substr(referer.indexOf(host) + host.length), + headers: headers }; +}; + -}(exports));