diff --git a/app.js b/app.js index d3c3190a..d0bf2b56 100644 --- a/app.js +++ b/app.js @@ -20,26 +20,21 @@ * */ -// TODO: Move all request handlers out of here, move authentication to auth.js - // Main Logging setup var logger = require('./logger.js'), system = require('./system'), - env = process.env.NODE_ENV || 'production', - config, - offlineNotificationTime; + config; //load and set our configuration, delete any cache first var loadConfig = function () { delete require.cache[require.resolve('./config.json')]; config = require('./config.json'); system.setConfiguration(config); - offlineNotificationTime = system.getOfflineNotificationTime() } loadConfig(); -var internalAddress = system.getInternalAddress(); +module.exports.config = config; //require('heapdump'); @@ -57,16 +52,11 @@ process.on('SIGHUP', function () { logger.info('Backend logging initialized...'); -// Initialize the main configuration -var taskEnv = process.env.TASK || 'main'; - // If Firebase Cloud Messaging is configured set it up if (system.isGcmConfigured()) { require('./fcm-xmpp'); } -module.exports.config = config; - // Setup all homepage var flash = require('connect-flash'), express = require('express'), @@ -75,144 +65,65 @@ var flash = require('connect-flash'), cookieParser = require('cookie-parser'), session = require('express-session'), favicon = require('serve-favicon'), - firebase = require('./notificationsender/firebase'); csurf = require('csurf'), serveStatic = require('serve-static'), - homepage = require('./routes/homepage'), - user = require('./routes/user'), - http = require('http'), path = require('path'), - fs = require('fs'), passport = require('passport'), RedisStore = require('connect-redis')(session), redis = require('./redis-helper'), date_util = require('./date_util.js'), - appleSender = require('./notificationsender/aps-helper'), - oauth2 = require('./routes/oauth2'), auth = require('./auth.js'), - Limiter = require('ratelimiter'), - requesttracker = require('./requesttracker'), routes = require('./routes'), MongoConnect = require('./system/mongoconnect'), - uuid = require('uuid'); + mongoose = require('mongoose'), + cachegoose = require('recachegoose'), + mongooseTypes = require('mongoose-types'), + SocketIO = require('./socket-io'); + -// MongoDB connection settings -var mongoose = require('mongoose'); -// MongoDB Caching for Item updates -var cachegoose = require('recachegoose'); cachegoose(mongoose, { engine: 'redis', port: config.redis.port, host: config.redis.host, password: config.redis.password, }); -var cacheTTL = config.cacheTTL || 600; // Try to setup a mongodb connection, otherwise stopping var mongoConnect = new MongoConnect(system); mongoConnect.connect(mongoose); - -var mongooseTypes = require('mongoose-types'); mongooseTypes.loadTypes(mongoose); var app = express(); -// A list of requests which are awaiting for responses from openHABs -var requestTracker = new requesttracker(); - -// A list of openHABs which lost their socket.io connection and are due for offline notification -// key is openHAB UUID, value is Date when openHAB was disconnected -var offlineOpenhabs = {}; - -// This timer runs every minute and checks if there are any openHABs in offline status for more then 300 sec -// Then it sends notifications to openHAB's owner if it is offline for more then 300 sec -// This timer only runs on the job task -if (taskEnv === 'main') { - setInterval(function () { - logger.debug('Checking for offline openHABs (' + Object.keys(offlineOpenhabs).length + ')'); - for (const offlineOpenhabUuid in offlineOpenhabs) { - const offlineOpenhab = offlineOpenhabs[offlineOpenhabUuid]; - if (Date.now() - offlineOpenhab.date < offlineNotificationTime) { - continue; - } - delete offlineOpenhabs[offlineOpenhabUuid]; - //check if our connection (connectionId) is still set, if not the user has reconnected - Openhab.findOne({ - connectionId: offlineOpenhab.connectionId - }).exec(function (error, openhab) { - if (!openhab || error) { - return; - } - logger.debug('openHAB with ' + offlineOpenhabUuid + ' is offline > ' + offlineNotificationTime + ' millis, time to notify the owner'); - notifyOpenHABStatusChange(openhab, 'offline'); - }); - } - }, 60000); -} - -//cancel restRequests that have become orphaned. For some reason neither close -//nor finish is being called on some response objects and we end up hanging on -//to these in our restRequests map. This goes through and finds those orphaned -//responses and cleans them up, otherwise memory goes through the roof. -setInterval(function () { - var requests = requestTracker.getAll(); - logger.debug('Checking orphaned rest requests (' + requestTracker.size() + ')'); - Object.keys(requests).forEach(function (requestId) { - var res = requests[requestId]; - if (res.finished) { - logger.debug('expiring orphaned response'); - requestTracker.remove(requestId); - if (res.openhab) { - io.sockets.in(res.openhab.uuid).emit('cancel', { - id: requestId - }); - } - } - }) -}, 60000); - -// Setup mongoose data models -var User = require('./models/user'); -var Openhab = require('./models/openhab'); -var Event = require('./models/event'); -var Item = require('./models/item'); -var UserDevice = require('./models/userdevice'); -var Notification = require('./models/notification'); - -logger.info('Scheduling a statistics job (every 5 min)'); var every5MinStatJob = require('./jobs/every5minstat'); - every5MinStatJob.start(); +// Configurable support for cross subdomain cookies +var cookie = {}; +if (config.system.subDomainCookies) { + cookie.path = '/'; + cookie.domain = '.' + system.getHost(); + logger.info('Cross sub domain cookie support is configured for domain: ' + cookie.domain); +} + // Configure the openHAB-cloud for development mode, if in development if (app.get('env') === 'development') { app.use(errorHandler()); } - +if (system.getLoggerMorganOption()){ + app.use(system.getLoggerMorganOption()); +} // App configuration for all environments app.set('port', process.env.PORT || 3000); app.set('views', __dirname + '/views'); app.set('view engine', 'ejs'); app.use(favicon(__dirname + '/public/img/favicon.ico')); -if (system.getLoggerMorganOption()) - app.use(system.getLoggerMorganOption()); - app.use(bodyParser.json({ verify: function (req, res, buf) { req.rawBody = buf } })) app.use(bodyParser.urlencoded({ verify: function (req, res, buf) { req.rawBody = buf }, extended: true - })); - app.use(cookieParser(config.express.key)); - -// Configurable support for cross subdomain cookies -var cookie = {}; -if (config.system.subDomainCookies) { - cookie.path = '/'; - cookie.domain = '.' + system.getHost(); - logger.info('Cross sub domain cookie support is configured for domain: ' + cookie.domain); -} app.use(session({ secret: config.express.key, store: new RedisStore({ @@ -225,7 +136,6 @@ app.use(session({ resave: false, saveUninitialized: false })); - app.use(flash()); app.use(passport.initialize()); app.use(passport.session()); @@ -260,37 +170,11 @@ app.use(function (req, res, next) { } next(); }); -app.use(function (req, res, next) { - if (req.user) { - Openhab.findOne({ - account: req.user.account - }).lean().exec(function (error, openhab) { - res.locals.baseurl = system.getBaseURL(); - res.locals.proxyUrl = system.getProxyURL(); - if (!error && openhab) { - res.locals.openhab = openhab; - res.locals.openhabstatus = openhab.status; - res.locals.openhablastonline = openhab.last_online; - if (openhab.openhabVersion !== undefined) { - res.locals.openhabMajorVersion = parseInt(openhab.openhabVersion.split('.')[0]); - } else { - res.locals.openhabMajorVersion = 0; - } - } else { - res.locals.openhab = undefined; - res.locals.openhabstatus = undefined; - res.locals.openhablastonline = undefined; - res.locals.openhabMajorVersion = undefined; - } - next(); - }); - } else { - next(); - } -}); - // Add global usable locals for templates app.use(function (req, res, next) { + res.locals.baseurl = system.getBaseURL(); + res.locals.proxyUrl = system.getProxyURL(); + if (req.session.timezone) { res.locals.timeZone = req.session.timezone; } else { @@ -308,605 +192,19 @@ app.use(function (req, res, next) { res.locals.registration_enabled = system.isUserRegistrationEnabled(); next(); }); - app.use(serveStatic(path.join(__dirname, 'public'))); var server = app.listen(system.getNodeProcessPort(), config.system.listenIp, function () { logger.info('express server listening on port ' + system.getNodeProcessPort()); }); -var io = require('socket.io')(server, { - maxHttpBufferSize: 1e8, //100mb, this was a previous default in engine.io before the upgrade to 3.6.0 which sets it to 1mb. May want to revisit. - logger: logger -}); - +// setup socket.io connections from openHABs +var socketIO = new SocketIO(server, system); // setup the routes for the app -var rt = new routes(requestTracker, logger); -rt.setSocketIO(io); +var rt = new routes(logger); +rt.setSocketIO(socketIO); rt.setupRoutes(app); -function sendNotificationToUser(user, message, icon, severity) { - var androidRegistrations = []; - var iosDeviceTokens = []; - var newNotification = new Notification({ - user: user.id, - message: message, - icon: icon, - severity: severity - }); - newNotification.save(function (error) { - if (error) { - logger.error('Error saving notification: ' + error); - } - }); - UserDevice.find({ - owner: user.id - }, function (error, userDevices) { - if (error) { - logger.warn('Error fetching devices for user: ' + error); - return; - } - if (!userDevices) { - // User don't have any registered devices, so we will skip it. - return; - } - - for (var i = 0; i < userDevices.length; i++) { - if (userDevices[i].deviceType === 'android') { - androidRegistrations.push(userDevices[i].androidRegistration); - } else if (userDevices[i].deviceType === 'ios') { - iosDeviceTokens.push(userDevices[i].iosDeviceToken); - } - } - // If we found any android devices, send notification - if (androidRegistrations.length > 0) { - firebase.sendNotification(androidRegistrations, newNotification); - } - // If we found any ios devices, send notification - if (iosDeviceTokens.length > 0) { - sendIosNotifications(iosDeviceTokens, newNotification); - } - }); -} - -function sendIosNotifications(iosDeviceTokens, notification) { - if (!config.apn) { - return; - } - var payload = { - severity: notification.severity, - icon: notification.icon, - persistedId: notification._id, - timestamp: notification.created.getTime() - }; - for (var i = 0; i < iosDeviceTokens.length; i++) { - appleSender.sendAppleNotification(iosDeviceTokens[i], notification.message, payload); - } -} - -// Socket.io Routes -io.use(function (socket, next) { - const uuid = socket.handshake.query['uuid'] || socket.handshake.headers['uuid']; - if (uuid) { - redis.ttl('blocked:' + uuid, (err, result) => { - if (err) { - logger.info('blocked: error talking with redis for ' + uuid + ' ' + err); - next(); - } else { - switch (result) { - case -2: // key does not exist - next(); - break; - case -1: // key exists but no TTL - next(new Error('your connection is blocked')); - break; - default: // seconds left on TTL - next(new Error('try again in ' + result + ' seconds')); - break; - } - } - - }) - } else { - next(new Error('missing uuid')); - } -}); - -io.use(function (socket, next) { - var handshakeData = socket.handshake; - handshakeData.uuid = handshakeData.query['uuid']; - handshakeData.openhabVersion = handshakeData.query['openhabversion']; - handshakeData.clientVersion = handshakeData.query['clientVersion']; - handshakeSecret = handshakeData.query['secret']; - if (!handshakeData.uuid) { - handshakeData.uuid = handshakeData.headers['uuid']; - handshakeSecret = handshakeData.headers['secret']; - handshakeData.openhabVersion = handshakeData.headers['openhabversion']; - handshakeData.clientVersion = handshakeData.headers['clientVersion']; - } - if (!handshakeData.openhabVersion) { - handshakeData.openhabVersion = 'unknown'; - } - if (!handshakeData.clientVersion) { - handshakeData.clientVersion = 'unknown'; - } - logger.info('Authorizing incoming openHAB connection for ' + handshakeData.uuid + ' version ' + handshakeData.openhabVersion); - Openhab.findOne({ - uuid: handshakeData.uuid, - secret: handshakeSecret - }, function (error, openhab) { - if (error) { - logger.error('openHAB lookup error: ' + error); - next(error); - } else { - if (openhab) { - socket.openhab = openhab; // will use this reference in 'connect' to save on mongo calls - next(); - } else { - logger.info('openHAB ' + handshakeData.uuid + ' not found'); - redis.set('blocked:' + handshakeData.uuid, handshakeData.openhabVersion, 'NX', 'EX', 60, (err, result) => { - if(err){ - logger.info('setting blocked: error talking with redis for ' + handshakeData.uuid + ' ' + err); - } - next(new Error('not authorized')); - }); - } - } - }); -}); - -io.use(function (socket, next) { - logger.info('obtaining lock for connection for uuid ' + socket.handshake.uuid); - socket.connectionId = uuid.v1(); //we will check this when we handle disconnects - //set a lock so only one connection from the same client can connect, avoids split brain when clients reconnect - socket.redisLockKey = 'connection:' + socket.handshake.uuid; - redis.set(socket.redisLockKey, socket.connectionId, 'NX', 'EX', system.getConnectionLockTimeSeconds(), (err, result) => { - if(err) { - logger.info('error attaining connection lock for uuid ' + socket.handshake.uuid + ' connectionId ' + socket.connectionId + ' ' + err); - return next(new Error('connection lock error')); - } - - if(!result){ - //this key already exists, which means another connection exists - logger.info('another connection has lock for uuid ' + socket.handshake.uuid + ' my connectionId ' + socket.connectionId); - return next(new Error('already connected')); - } - next(); - }); -}); - -io.sockets.on('connection', function (socket) { - logger.info('connection for uuid ' + socket.handshake.uuid + + ' connectionId ' + socket.connectionId); - socket.join(socket.handshake.uuid); - //listen for pings from the client - socket.conn.on('packet', function (packet) { - if (packet.type === 'ping') { - //reset the expire time - redis.expire(socket.redisLockKey, system.getConnectionLockTimeSeconds(), (error, number) => { - if(error){ - logger.error('error updating lock expire for uuid ' + socket.handshake.uuid + + ' connectionId ' + socket.connectionId + " " + error); - return; - } - if(number === 0){ - logger.error('lock no longer present for for uuid ' + socket.handshake.uuid + + ' connectionId ' + socket.connectionId + " " + error); - //we have lost our lock, something has gone wrong, lets cleanup - socket.disconnect(); - return; - } - }); - }; - }); - - const openhab = socket.openhab; - const lastOnline = openhab.last_online; - const lastStatus = openhab.status; - const lastServerAddress = openhab.serverAddress - - openhab.status = 'online'; - openhab.serverAddress = internalAddress; - openhab.connectionId = socket.connectionId; - openhab.last_online = new Date(); - openhab.openhabVersion = socket.handshake.openhabVersion; - openhab.clientVersion = socket.handshake.clientVersion; - openhab.save( - function (error) { - if(error){ - logger.error('save error: ' + error); - socket.disconnect(); - return; - } - logger.info('connect success uuid ' + openhab.uuid + ' connectionId ' + socket.connectionId + ' prevous address ' + lastServerAddress + " my address " + internalAddress); - - socket.openhabId = openhab.id; - - var connectevent = new Event({ - openhab: openhab.id, - source: 'openhab', - status: 'online', - color: 'good' - }); - connectevent.save(function (error) { - if (error) { - logger.error('Error saving connect event: ' + error); - } - }); - - //notify user that connection is online - if(lastStatus === 'offline' && Date.now() - lastOnline > offlineNotificationTime){ - notifyOpenHABStatusChange(openhab, 'online'); - } else if(lastStatus === 'online') { - logger.warn('connected openhab ' + socket.handshake.uuid + ' was previously marked as online') - } - } - ); - socket.on('disconnect', function () { - logger.info('Disconnected uuid ' + socket.handshake.uuid + ' connectionId ' + socket.connectionId); - - //remove our lock, but make sure its our connection id, and not a healthy reconnection from the same client - redis.get(socket.redisLockKey, (err, reply) => { - if (err) { - logger.info('error removing connection lock for uuid ' + openhab.uuid + ' connectionId ' + socket.connectionId + ' ' + err); - return; - } - - //check if either null, in which case the lock is gone and we should clean up, - //or check if its's there and belongs to this connectionId (and not a reconnect) - if (!reply || reply === socket.connectionId) { - redis.del(socket.redisLockKey); //just ignore if the key does not exist - Openhab.setOffline(socket.connectionId, function (error, openhab) { - if (error) { - logger.error('Error saving openHAB disconnect: ' + error); - return; - } - if(openhab) { - //we will try and notifiy users of being offline - offlineOpenhabs[openhab.uuid] = { - date: Date.now(), - connectionId: socket.connectionId - } - - var disconnectevent = new Event({ - openhab: openhab.id, - source: 'openhab', - status: 'offline', - color: 'bad' - }); - - disconnectevent.save(function (error) { - if (error) { - logger.error('Error saving disconnect event: ' + error); - } - }); - } else { - logger.warn(`${socket.handshake.uuid} Did not mark as offline, another instance is connected`); - } - }); - } else { - logger.info('will not delete lock, another connection has lock for uuid ' + socket.handshake.uuid + ' my connectionId ' + socket.connectionId + ' their info: ' + reply); - } - }); - }); - - socket.on('responseHeader', function (data) { - var self = this; - var requestId = data.id, - request; - if (requestTracker.has(requestId)) { - request = requestTracker.get(requestId); - if (self.handshake.uuid === request.openhab.uuid && !request.headersSent) { - request.writeHead(data.responseStatusCode, data.responseStatusText, data.headers); - } else { - logger.warn('responseHeader ' + self.handshake.uuid + ' tried to respond to request which it doesn\'t own ' + request.openhab.uuid + " or headers have already been sent"); - } - } else { - self.emit('cancel', { - id: requestId - }); - } - }); - socket.on('responseContentBinary', function (data) { - var self = this; - var requestId = data.id, - request; - if (requestTracker.has(requestId)) { - request = requestTracker.get(requestId); - if (self.handshake.uuid === request.openhab.uuid) { - request.write(data.body); - } else { - logger.warn('responseContentBinary ' + self.handshake.uuid + ' tried to respond to request which it doesn\'t own ' + request.openhab.uuid); - } - } else { - self.emit('cancel', { - id: requestId - }); - } - }); - socket.on('responseFinished', function (data) { - var self = this; - var requestId = data.id, - request; - if (requestTracker.has(requestId)) { - request = requestTracker.get(requestId); - if (self.handshake.uuid === request.openhab.uuid) { - request.end(); - } else { - logger.warn('responseFinished ' + self.handshake.uuid + ' tried to respond to request which it doesn\'t own' + request.openhab.uuid); - } - } - }); - socket.on('responseError', function (data) { - var self = this; - var requestId = data.id, - request; - if (requestTracker.has(requestId)) { - request = requestTracker.get(requestId); - if (self.handshake.uuid === request.openhab.uuid) { - request.send(500, data.responseStatusText); - } else { - logger.warn('responseError ' + self.handshake.uuid + ' tried to respond to request which it doesn\'t own' + request.openhab.uuid); - } - } - }); - socket.on('notification', function (data) { - var self = this; - logger.info('Notification request from ' + self.handshake.uuid + ' to user ' + data.userId); - User.findOne({ - username: data.userId - }, function (error, user) { - if (error) { - logger.error('User lookup error: ' + error); - return; - } - if (!user) { - return; - } - user.openhab(function (error, openhab) { - if (!error && openhab) { - if (openhab.uuid === self.handshake.uuid) { - logger.info('Notification from ' + self.handshake.uuid + ' to ' + user.username); - sendNotificationToUser(user, data.message, data.icon, data.severity); - } else { - logger.warn('oopenHAB ' + self.handshake.uuid + ' requested notification for user (' + user.username + ') which it does not belong to'); - } - } else { - if (error) { - logger.error('openHAB lookup error: ' + error); - } else { - logger.warn('Unable to find openHAB for user ' + user.username); - } - } - }); - }); - }); - - socket.on('broadcastnotification', function (data) { - Openhab.findById(this.openhabId, function (error, openhab) { - if (error) { - logger.error('openHAB lookup error: ' + error); - return; - } - if (!openhab) { - logger.debug('openHAB not found'); - return; - } - - User.find({ - account: openhab.account - }, function (error, users) { - if (error) { - logger.error('Error getting users list: ' + error); - return; - } - - if (!users) { - logger.debug('No users found for openHAB'); - return; - } - - for (var i = 0; i < users.length; i++) { - sendNotificationToUser(users[i], data.message, data.icon, data.severity); - } - }); - }); - }); - - socket.on('lognotification', function (data) { - Openhab.findById(this.openhabId, function (error, openhab) { - if (error) { - logger.error('openHAB lookup error: ' + error); - return; - } - if (!openhab) { - logger.debug('openHAB not found'); - return; - } - User.find({ - account: openhab.account - }, function (error, users) { - if (error) { - logger.error('Error getting users list: ' + error); - return; - } - - if (!users) { - logger.debug('No users found for openHAB'); - return; - } - - for (var i = 0; i < users.length; i++) { - newNotification = new Notification({ - user: users[i].id, - message: data.message, - icon: data.icon, - severity: data.severity - }); - newNotification.save(function (error) { - if (error) { - logger.error('Error saving notification: ' + error); - } - }); - } - }); - }); - }); - - socket.on('itemupdate', function (data) { - //disabling item updates for now - return; - var self = this; - //if openhabId is missing then user has not completed auth - if (self.openhabId === undefined) { - return; - } - var limiter = new Limiter({ - id: self.openhabId, - db: redis, - max: 10, - duration: 30000 - }); - limiter.get(function (err, limit) { - if (err) { - logger.error('Rate limit error ' + err); - return; - } - if (!limit.remaining) { - return; - } - var itemName = data.itemName; - var itemStatus = data.itemStatus; - // Find openhab - if (itemStatus && itemStatus.length > 100) { - logger.info('Item ' + itemName + ' status.length (' + (itemStatus ? itemStatus.length : 'null') + ') is too big or null, ignoring update'); - return; - } - Openhab.findById(self.openhabId).cache(cacheTTL).exec(function (error, openhab) { - if (error) { - logger.warn('Unable to find openHAB for itemUpdate: ' + error); - return; - } - if (!openhab) { - logger.info('Unable to find openHAB for itemUpdate: openHAB doesn\'t exist'); - return; - } - // Find the item (which should belong to this openhab) - var cacheKey = openhab.id + '-' + itemName; - Item.findOne({ - openhab: openhab.id, - name: itemName - }).cache(cacheTTL, cacheKey).exec(function (error, itemToUpdate) { - if (error) { - logger.warn('Unable to find item for itemUpdate: ' + error); - } - - // If no item found for this openhab with this name, create a new one - if (!itemToUpdate) { - logger.info('Item ' + itemName + ' for openHAB ' + openhab.uuid + ' not found, creating new one'); - itemToUpdate = new Item({ - openhab: openhab.id, - name: itemName, - last_change: new Date, - status: '' - }); - } - // If item status changed, update item and create new item status change event - if (itemToUpdate.status !== itemStatus) { - // Update previous status value - itemToUpdate.prev_status = itemToUpdate.status; - // Set new status value - itemToUpdate.status = itemStatus; - // Set last update timestamp to current time - itemToUpdate.last_update = new Date; - // Save the updated item - itemToUpdate.save(function (error) { - if (error) { - logger.error('Error saving item: ' + error); - } - cachegoose.clearCache(cacheKey); - }); - // Check if the new state is int or float to store it to Number and create new item update event - if (!isNaN(parseFloat(itemStatus))) { - // This is silly, but we need to check if previous status was int or float - if (!isNaN(parseFloat(itemToUpdate.prev_status))) { - Event.collection.insert({ - openhab: mongoose.Types.ObjectId(openhab.id), - source: itemName, - status: itemStatus, - oldStatus: itemToUpdate.prev_status, - numericStatus: parseFloat(itemStatus), - oldNumericStatus: parseFloat(itemToUpdate.prev_status), - color: 'info', - when: new Date - }, function (error) { - if (error) { - logger.error('Error saving event: ' + error); - } - }); - } else { - Event.collection.insert({ - openhab: mongoose.Types.ObjectId(openhab.id), - source: itemName, - status: itemStatus, - oldStatus: itemToUpdate.prev_status, - numericStatus: parseFloat(itemStatus), - color: 'info', - when: new Date - }, function (error) { - if (error) { - logger.error('Error saving event: ' + error); - } - }); - } - } else { - Event.collection.insert({ - openhab: mongoose.Types.ObjectId(openhab.id), - source: itemName, - status: itemStatus, - oldStatus: itemToUpdate.prev_status, - color: 'info', - when: new Date - }, function (error) { - if (error) { - logger.error('Error saving event: ' + error); - } - }); - } - // Thus if item status didn't change, there will be no event... - } - }); - }); - }); - }); -}); - -function notifyOpenHABStatusChange(openhab, status) { - - //we can mute notifications, for example when we are doing a deploy - if (system.getMuteNotifications()) { - return; - } - - User.find({ - account: openhab.account, - role: 'master' - }, function (error, users) { - if (!error && users) { - for (var i = 0; i < users.length; i++) { - if (status === 'online') { - sendNotificationToUser(users[i], 'openHAB is online', 'openhab', 'good'); - } else { - sendNotificationToUser(users[i], 'openHAB is offline', 'openhab', 'bad'); - } - } - } else { - if (error) { - logger.warn('Error finding users to notify: ' + error); - } else { - logger.warn('Unable to find any masters for openHAB ' + openhab.uuid); - } - } - }); -} - function shutdown() { // TODO: save current request id? logger.info('Stopping every5min statistics job'); @@ -925,5 +223,3 @@ process.on('SIGTERM', function () { logger.info('frontend is shutting down from SIGTERM'); shutdown(); }); - -module.exports.sio = io; diff --git a/date_util.js b/date_util.js index a1b5c05e..ac58bccd 100644 --- a/date_util.js +++ b/date_util.js @@ -3,9 +3,13 @@ var { DateTime } = require('luxon'); module.exports = function (date, timezone) { /** * Convert a Javascript Date into node-time wrapper with the appropriate timezone. - * @param date {Date} Javascript Date object + * @param date {Date} Javascript Date object or ISO String * @param timezone {String} Olson timezone for this date (e.g. 'America/New_York') * @return luxon object with the appropriate timezone */ - return DateTime.fromJSDate(date).setZone(timezone || 'UTC') + if(typeof date === 'string'){ + return DateTime.fromISO(date).setZone(timezone || 'UTC') + } else { + return DateTime.fromJSDate(date).setZone(timezone || 'UTC') + } } diff --git a/jobs/checkopenhabsoffline.js b/jobs/checkopenhabsoffline.js deleted file mode 100644 index b2a73201..00000000 --- a/jobs/checkopenhabsoffline.js +++ /dev/null @@ -1,66 +0,0 @@ -var cronJob = require('cron').CronJob, - logger = require('../logger'), - mailer = require('../mailer'), - // Mongoose models - User = require('../models/user'), - Openhab = require('../models/openhab'), - UserAccount = require('../models/useraccount'); - -// This job checks for openhabs which has been offline for more then 3 days and sends warning emails to their -// owners, pointing out that we didn't see their openhabs for quite long time, not more then one email per 3 days -module.exports = new cronJob('00 00 00 * * *', function () { - logger.info('checkopenhabsoffline job started'); - date3DaysAgo = new Date; - date3DaysAgo.setDate(date3DaysAgo.getDate()-3); - logger.info('date3DaysAgo = ' + date3DaysAgo); - Openhab.find({status:'offline', last_online: {'$lt':date3DaysAgo}}, function (error, openhabs) { - if (error) { - logger.error('Error finding offline openHABs: ' + error); - } - - if (!openhabs) { - logger.info('No offline openHABs found'); - } - logger.info('Found ' + openhabs.length + ' openhabs'); - for (var i in openhabs) { - var openhab = openhabs[i]; - - if (openhab.last_email_notification && openhab.last_email_notification > date3DaysAgo) { - continue; - } - - openhab.last_email_notification = new Date; - openhab.save(); - - UserAccount.findOne({_id:openhab.account}, function (error, userAccount) { - if (error) { - logger.error('Error finding user account for openhab: ' + error); - } - - if (!userAccount) { - logger.error('Unable to find user account for openhab which is nonsense'); - } - - User.find({account: userAccount.id, role:'master'}, function (error, users) { - if (error || !users) { - return; - } - - for (var i in users) { - var user = users[i]; - var locals = { - email: user.username - }; - mailer.sendEmail(user.username, 'We are worried about your openHAB', - 'openhaboffline', locals, function (error) { - if (error) { - logger.error('Error sending email: ' + error); - } - }); - } - }); - }); - } - }); - logger.info('checkopenhabsoffline job finished'); -}); diff --git a/jobs/every5minstat.js b/jobs/every5minstat.js index 00fa2e04..469c94fa 100644 --- a/jobs/every5minstat.js +++ b/jobs/every5minstat.js @@ -17,7 +17,7 @@ var cronJob = require('cron').CronJob, * * @private */ -function countCallback (err, count) { +function countCallback(err, count) { if (!err) { stats[this] = count; } @@ -31,10 +31,9 @@ function saveStats() { // validate the results if (Object.keys(stats).length !== 6) { // not all data could be retrieved - logger.info('The length of the stats object does not match the expected one, can not save statistical data.'); + logger.info('The length of the stats object does not match the expected one, can not save statistical data. %s', stats); return; } - // Set current statistics to redis redis.mset( [ @@ -61,15 +60,26 @@ function saveStats() { module.exports = new cronJob('00 */5 * * * *', function () { var promises = []; - logger.info('every5min statistics collection job started'); + //obtain a lock to update, we don't bother removing as it has a short expire, this is just to avoid unnecessary updates from multiple servers. + redis.set("jobs:every5minstat", "", 'NX', 'EX', 10, (error, result) => { + if (result) { + logger.info('every5min statistics collection job obtained lock'); + promises.push(Openhab.count({}, countCallback.bind('openhabCount')).exec()); + promises.push(new Promise(resolve => { + redis.eval("return #redis.pcall('keys', 'connection:*')", 0, (err, res) => { + const f = countCallback.bind('openhabOnlineCount'); + f(err, res); + resolve(res); + }); + })); + //promises.push(Openhab.count({status: 'online'}, countCallback.bind('openhabOnlineCount')).exec()); + promises.push(User.count({}, countCallback.bind('userCount')).exec()); + promises.push(Invitation.count({ used: true }, countCallback.bind('invitationUsedCount')).exec()); + promises.push(Invitation.count({ used: false }, countCallback.bind('invitationUnusedCount')).exec()); + promises.push(UserDevice.count({}, countCallback.bind('userDeviceCount')).exec()); - promises.push(Openhab.count({}, countCallback.bind('openhabCount')).exec()); - promises.push(Openhab.count({status: 'online'}, countCallback.bind('openhabOnlineCount')).exec()); - promises.push(User.count({}, countCallback.bind('userCount')).exec()); - promises.push(Invitation.count({used:true}, countCallback.bind('invitationUsedCount')).exec()); - promises.push(Invitation.count({used:false}, countCallback.bind('invitationUnusedCount')).exec()); - promises.push(UserDevice.count({}, countCallback.bind('userDeviceCount')).exec()); - - Promise.all(promises).then(saveStats); + Promise.all(promises).then(saveStats); + } + }); }); diff --git a/logger.js b/logger.js index 4f8c30b7..201a3e98 100644 --- a/logger.js +++ b/logger.js @@ -96,7 +96,7 @@ logger.auditRequest = function (req) { requestPath = requestPath.replace('/remote', ''); } - this.audit("%s | %s | %s | %s | %s | %s | %s", req.user.username, req.openhab.status, req.method, requestPath, headers[`x-real-ip`], headers['host'], headers['user-agent']) + this.audit("%s | %s | %s | %s | %s | %s | %s", req.user.username, req.connectionInfo.status, req.method, requestPath, headers[`x-real-ip`], headers['host'], headers['user-agent']) } module.exports = logger; diff --git a/mailchimp-myohversion.js b/mailchimp-myohversion.js deleted file mode 100644 index e764b60b..00000000 --- a/mailchimp-myohversion.js +++ /dev/null @@ -1,36 +0,0 @@ -var mongoose = require('mongoose'), - logger = require('./logger.js'), - User = require('./models/user'), - config = require('./config.json'), - Openhab = require('./models/openhab'), - system = require('./system'), - MongoConnect = require('./system/mongoconnect'), - mongoConnect; - -system.setConfiguration(config); -mongoConnect = new MongoConnect(system); -mongoConnect.connect(mongoose); - -mongoose.connect(mongoConnectionString, function (err) { - if (err) { - logger.error('mongo connection error: ' + err); - return; - } - - logger.info('connected to mongodb'); - Openhab.find({$or:[{clientVersion:'1.4.0.1'}, {clientVersion:'1.4.0.2'}]}, function (error, openhabs) { - User.find({role:'master'}, function(error, users) { - var usersArray = {}; - for (var j=0; j=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz", + "integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.1.tgz", + "integrity": "sha512-pqCXTc5e7wJJgUuJiC3hBgfoFRoPxYzwn0BEfKgejTM7M/9zP3IpUcqcjgfp8hF+LoV8rHZzcNTz7V+pEIY7LQ==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz", + "integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -2848,9 +2901,9 @@ } }, "node_modules/cluster-key-slot": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", - "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", "engines": { "node": ">=0.10.0" } @@ -4765,6 +4818,14 @@ "wide-align": "^1.1.0" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -10567,6 +10628,24 @@ "redis": "^3.0.2" } }, + "node_modules/recacheman-redis/node_modules/redis": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", + "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", + "dependencies": { + "denque": "^1.5.0", + "redis-commands": "^1.7.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-redis" + } + }, "node_modules/recacheman/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10585,21 +10664,16 @@ } }, "node_modules/redis": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.1.tgz", - "integrity": "sha512-QhkKhOuzhogR1NDJfBD34TQJz2ZJwDhhIC6ZmvpftlmfYShHHQXjjNspAJ+Z2HH5NwSBVYBVganbiZ8bgFMHjg==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.4.tgz", + "integrity": "sha512-wi2tgDdQ+Q8q+PR5FLRx4QvDiWaA+PoJbrzsyFqlClN5R4LplHqN3scs/aGjE//mbz++W19SgxiEnQ27jnCRaA==", "dependencies": { - "denque": "^1.5.0", - "redis-commands": "^1.7.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-redis" + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.5", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.4", + "@redis/search": "1.1.1", + "@redis/time-series": "1.0.4" } }, "node_modules/redis-commands": { @@ -11577,6 +11651,24 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/socket.io-redis/node_modules/redis": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", + "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", + "dependencies": { + "denque": "^1.5.0", + "redis-commands": "^1.7.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-redis" + } + }, "node_modules/socket.io-redis/node_modules/socket.io-adapter": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.0.3.tgz", @@ -13622,6 +13714,46 @@ "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", "optional": true }, + "@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "requires": {} + }, + "@redis/client": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.5.tgz", + "integrity": "sha512-fuMnpDYSjT5JXR9rrCW1YWA4L8N/9/uS4ImT3ZEC/hcaQRI1D/9FvwjriRj1UvepIgzZXthFVKMNRzP/LNL7BQ==", + "requires": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + } + }, + "@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "requires": {} + }, + "@redis/json": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz", + "integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==", + "requires": {} + }, + "@redis/search": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.1.tgz", + "integrity": "sha512-pqCXTc5e7wJJgUuJiC3hBgfoFRoPxYzwn0BEfKgejTM7M/9zP3IpUcqcjgfp8hF+LoV8rHZzcNTz7V+pEIY7LQ==", + "requires": {} + }, + "@redis/time-series": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz", + "integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==", + "requires": {} + }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -15169,9 +15301,9 @@ } }, "cluster-key-slot": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", - "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==" }, "code-point-at": { "version": "1.1.0", @@ -16638,6 +16770,11 @@ "wide-align": "^1.1.0" } }, + "generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==" + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -21046,6 +21183,19 @@ "each": "1.2.1", "parse-redis-url": "0.0.2", "redis": "^3.0.2" + }, + "dependencies": { + "redis": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", + "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", + "requires": { + "denque": "^1.5.0", + "redis-commands": "^1.7.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0" + } + } } }, "rechoir": { @@ -21058,14 +21208,16 @@ } }, "redis": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.1.tgz", - "integrity": "sha512-QhkKhOuzhogR1NDJfBD34TQJz2ZJwDhhIC6ZmvpftlmfYShHHQXjjNspAJ+Z2HH5NwSBVYBVganbiZ8bgFMHjg==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.4.tgz", + "integrity": "sha512-wi2tgDdQ+Q8q+PR5FLRx4QvDiWaA+PoJbrzsyFqlClN5R4LplHqN3scs/aGjE//mbz++W19SgxiEnQ27jnCRaA==", "requires": { - "denque": "^1.5.0", - "redis-commands": "^1.7.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0" + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.5", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.4", + "@redis/search": "1.1.1", + "@redis/time-series": "1.0.4" } }, "redis-commands": { @@ -21946,6 +22098,17 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "redis": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", + "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", + "requires": { + "denque": "^1.5.0", + "redis-commands": "^1.7.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0" + } + }, "socket.io-adapter": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.0.3.tgz", diff --git a/package.json b/package.json index 6fb420d3..95844ad9 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "qunitjs": "^2.4.1", "ratelimiter": "3.4.1", "recachegoose": "^9.0.0", - "redis": "3.1.1", + "redis": "3.1.2", "request": "^2.88.0", "serve-favicon": "^2.5.0", "serve-static": "^1.14.1", diff --git a/routes/devices.js b/routes/devices.js index bb21ca73..85dd8f4f 100644 --- a/routes/devices.js +++ b/routes/devices.js @@ -8,7 +8,6 @@ var ObjectId = mongoose.SchemaTypes.ObjectId; var UserDeviceLocationHistory = require('../models/userdevicelocationhistory'); var appleSender = require('../notificationsender/aps-helper'); var firebase = require('../notificationsender/firebase'); -var redis = require('../redis-helper'); var form = require('express-form'), field = form.field, system = require('../system'); diff --git a/routes/events.js b/routes/events.js index da4a84b4..a6a1cd78 100644 --- a/routes/events.js +++ b/routes/events.js @@ -3,6 +3,23 @@ var Openhab = require('../models/openhab'); var Event = require('../models/event'); var logger = require('../logger'); +/** + * When we move events to redis, use this + * + * const eventKey = 'events:' + req.openhab.id; + var perPage = 20, + page = req.query.page > 0 ? parseInt(req.query.page) : 0; + redis.zcount(eventKey,'-inf','+inf', (error, count) => { + logger.debug('zrange %s %d %d',eventKey, perPage * page, perPage); + redis.zrange(eventKey, perPage * page, perPage, (error, result) => { + logger.debug('events for key %s : %s', eventKey, events); + res.render('events', { events: events, pages: count / perPage, page: page, + title: "Events", user: req.user, source: req.query.source, + errormessages:req.flash('error'), infomessages:req.flash('info') }); + }); + }); + */ + exports.eventsget = function(req, res) { var perPage = 20, page = req.query.page > 0 ? parseInt(req.query.page) : 0; diff --git a/routes/homepage.js b/routes/homepage.js index 30680eec..1a9111dd 100644 --- a/routes/homepage.js +++ b/routes/homepage.js @@ -36,7 +36,3 @@ exports.docsifttt = function(req, res) { res.render('docs/ifttt', {title: "Docs - IFTTT", user: req.user, errormessages: errormessages, infomessages: infomessages}); }; - -exports.getv2 = function(req, res) { - res.send('Yes, I am!'); -}; diff --git a/routes/index.js b/routes/index.js index 2c67a038..5dd5dcbb 100644 --- a/routes/index.js +++ b/routes/index.js @@ -14,27 +14,26 @@ var system = require('../system'), oauth2 = require('./oauth2'), setSessionTimezone = require('./setTimezone'), androidRegistrationService = require('./androidRegistrationService'), - appleRegistrationService = require('./appleRegistrationService'); - ifttt_routes = require('./ifttt'); + appleRegistrationService = require('./appleRegistrationService'), + ifttt_routes = require('./ifttt'), + redis = require('../redis-helper'); /** * Constructs the Routes object. * - * @param {RequestTracker} requestTracker * @param {logger} logger * @constructor */ -var Routes = function (requestTracker, logger) { - this.requestTracker = requestTracker; +var Routes = function (logger) { this.logger = logger; }; /** - * @deprecated This function should not be used and will be returned as far as a better solution was found. - * @param io + * @param socketIO */ -Routes.prototype.setSocketIO = function (io) { - this.io = io; +Routes.prototype.setSocketIO = function (socketIO) { + this.io = socketIO.io; + this.requestTracker = socketIO.requestTracker }; Routes.prototype.setupRoutes = function (app) { @@ -49,7 +48,6 @@ Routes.prototype.setupRoutes = function (app) { this.setupOAuthRoutes(app); this.setupIFTTTRoutes(app); this.setupTimezoneRoutes(app); - this.setupApiRoutes(app); this.setupStaffRoutes(app); this.setupProxyRoutes(app); this.setupAppRoutes(app); @@ -57,24 +55,21 @@ Routes.prototype.setupRoutes = function (app) { Routes.prototype.setupGeneralRoutes = function (app) { // General homepage - app.get('/', homepage.index); - - // V2 route - response to this route means this openHAB-cloud is using v2 transport based on socket.io 1.0 - app.get('/v2', homepage.getv2); + app.get('/', this.setOpenhab, homepage.index); // Events - app.get('/events', this.ensureAuthenticated, events_routes.eventsget); + app.get('/events', this.ensureAuthenticated, this.setOpenhab, events_routes.eventsget); // Items - app.get('/items', this.ensureAuthenticated, items_routes.itemsget); + app.get('/items', this.ensureAuthenticated, this.setOpenhab, items_routes.itemsget); // Notifications - app.get('/notifications', this.ensureAuthenticated, notifications_routes.notificationsget); + app.get('/notifications', this.ensureAuthenticated, this.setOpenhab, notifications_routes.notificationsget); }; Routes.prototype.setupLoginLogoutRoutes = function (app) { app.get('/logout', function (req, res, next) { - req.logout(function(err) { + req.logout(function (err) { if (err) { return next(err); } res.redirect('/'); }); @@ -99,40 +94,40 @@ Routes.prototype.setupLoginLogoutRoutes = function (app) { }); }); - app.post('/login', account_routes.loginpostvalidate, - //use express-form sanitized data for passport - function(req, res, next) { - req.body.username = req.form.username; - req.body.password = req.form.password; - next(); - }, - passport.authenticate('local', { - successReturnToOrRedirect: '/', - failureRedirect: '/login', - failureFlash: true - })); + app.post('/login', account_routes.loginpostvalidate, + //use express-form sanitized data for passport + function (req, res, next) { + req.body.username = req.form.username; + req.body.password = req.form.password; + next(); + }, + passport.authenticate('local', { + successReturnToOrRedirect: '/', + failureRedirect: '/login', + failureFlash: true + })); }; Routes.prototype.setupAccountRoutes = function (app) { - app.get('/account', this.ensureAuthenticated, account_routes.accountget); - app.post('/account', this.ensureAuthenticated, this.ensureMaster, account_routes.accountpostvalidate, account_routes.accountpost); - app.post('/accountpassword', this.ensureAuthenticated, account_routes.accountpasswordpostvalidate, account_routes.accountpasswordpost); - app.get('/accountdelete', this.ensureAuthenticated, this.ensureMaster, account_routes.accountdeleteget); - app.post('/accountdelete', this.ensureAuthenticated, this.ensureMaster, account_routes.accountdeletepost); - app.get('/itemsdelete', this.ensureAuthenticated, this.ensureMaster, account_routes.itemsdeleteget); - app.post('/itemsdelete', this.ensureAuthenticated, this.ensureMaster, account_routes.itemsdeletepost); + app.get('/account', this.ensureAuthenticated, this.setOpenhab, account_routes.accountget); + app.post('/account', this.ensureAuthenticated, this.setOpenhab, this.ensureMaster, account_routes.accountpostvalidate, account_routes.accountpost); + app.post('/accountpassword', this.ensureAuthenticated, this.setOpenhab, account_routes.accountpasswordpostvalidate, account_routes.accountpasswordpost); + app.get('/accountdelete', this.ensureAuthenticated, this.setOpenhab, this.ensureMaster, account_routes.accountdeleteget); + app.post('/accountdelete', this.ensureAuthenticated, this.setOpenhab, this.ensureMaster, account_routes.accountdeletepost); + app.get('/itemsdelete', this.ensureAuthenticated, this.setOpenhab, this.ensureMaster, account_routes.itemsdeleteget); + app.post('/itemsdelete', this.ensureAuthenticated, this.setOpenhab, this.ensureMaster, account_routes.itemsdeletepost); }; Routes.prototype.setupDevicesRoutes = function (app) { - app.get('/devices', this.ensureAuthenticated, devices_routes.devicesget); - app.get('/devices/:id', this.ensureAuthenticated, devices_routes.devicesget); - app.get('/devices/:id/delete', this.ensureAuthenticated, devices_routes.devicesdelete); - app.post('/devices/:id/sendmessage', this.ensureAuthenticated, devices_routes.devicessendmessagevalidate, devices_routes.devicessendmessage); + app.get('/devices', this.ensureAuthenticated, this.setOpenhab, devices_routes.devicesget); + app.get('/devices/:id', this.ensureAuthenticated, this.setOpenhab, devices_routes.devicesget); + app.get('/devices/:id/delete', this.ensureAuthenticated, this.setOpenhab, devices_routes.devicesdelete); + app.post('/devices/:id/sendmessage', this.ensureAuthenticated, this.setOpenhab, devices_routes.devicessendmessagevalidate, devices_routes.devicessendmessage); }; Routes.prototype.setupApplicationsRoutes = function (app) { - app.get('/applications', this.ensureAuthenticated, applications_routes.applicationsget); - app.get('/applications/:id/delete', this.ensureAuthenticated, applications_routes.applicationsdelete); + app.get('/applications', this.ensureAuthenticated, this.setOpenhab, applications_routes.applicationsget); + app.get('/applications/:id/delete', this.ensureAuthenticated, this.setOpenhab, applications_routes.applicationsdelete); }; Routes.prototype.setupNewUserRegistrationRoutes = function (app) { @@ -150,8 +145,8 @@ Routes.prototype.setupNewUserRegistrationRoutes = function (app) { }; Routes.prototype.setupInvitationRoutes = function (app) { - app.get('/invitations', this.ensureAuthenticated, invitations_routes.invitationsget); - app.post('/invitations', this.ensureAuthenticated, invitations_routes.invitationspostvalidate, invitations_routes.invitationspost); + app.get('/invitations', this.ensureAuthenticated, this.setOpenhab, invitations_routes.invitationsget); + app.post('/invitations', this.ensureAuthenticated, this.setOpenhab, invitations_routes.invitationspostvalidate, invitations_routes.invitationspost); app.get('/lostpassword', account_routes.lostpasswordget); app.post('/lostpassword', account_routes.lostpasswordpostvalidate, account_routes.lostpasswordpost); app.get('/lostpasswordreset', account_routes.lostpasswordresetget); @@ -159,11 +154,11 @@ Routes.prototype.setupInvitationRoutes = function (app) { }; Routes.prototype.setupUserManagementRoutes = function (app) { - app.get('/users', this.ensureAuthenticated, this.ensureMaster, users_routes.usersget); - app.get('/users/add', this.ensureAuthenticated, this.ensureMaster, users_routes.usersaddget); - app.post('/users/add', this.ensureAuthenticated, this.ensureMaster, users_routes.usersaddpostvalidate, users_routes.usersaddpost); - app.get('/users/delete/:id', this.ensureAuthenticated, this.ensureMaster, users_routes.usersdeleteget); - app.get('/users/:id', this.ensureAuthenticated, this.ensureMaster, users_routes.usersget); + app.get('/users', this.ensureAuthenticated, this.setOpenhab, this.ensureMaster, users_routes.usersget); + app.get('/users/add', this.ensureAuthenticated, this.setOpenhab, this.ensureMaster, users_routes.usersaddget); + app.post('/users/add', this.ensureAuthenticated, this.setOpenhab, this.ensureMaster, users_routes.usersaddpostvalidate, users_routes.usersaddpost); + app.get('/users/delete/:id', this.ensureAuthenticated, this.setOpenhab, this.ensureMaster, users_routes.usersdeleteget); + app.get('/users/:id', this.ensureAuthenticated, this.setOpenhab, this.ensureMaster, users_routes.usersget); }; Routes.prototype.setupOAuthRoutes = function (app) { @@ -173,13 +168,13 @@ Routes.prototype.setupOAuthRoutes = function (app) { }; Routes.prototype.setupStaffRoutes = function (app) { - app.get('/staff', this.ensureAuthenticated, this.ensureStaff, staff_routes.staffget); - app.get('/staff/processenroll/:id', this.ensureAuthenticated, this.ensureStaff, staff_routes.processenroll); - app.get('/staff/stats', this.ensureAuthenticated, this.ensureStaff, staff_routes.statsget); - app.get('/staff/invitations', this.ensureAuthenticated, this.ensureStaff, staff_routes.invitationsget); - app.get('/staff/resendinvitation/:id', this.ensureAuthenticated, this.ensureStaff, staff_routes.resendinvitation); - app.get('/staff/deleteinvitation/:id', this.ensureAuthenticated, this.ensureStaff, staff_routes.deleteinvitation); - app.get('/staff/oauthclients', this.ensureAuthenticated, this.ensureStaff, staff_routes.oauthclientsget); + app.get('/staff', this.ensureAuthenticated, this.setOpenhab, this.ensureStaff, staff_routes.staffget); + app.get('/staff/processenroll/:id', this.ensureAuthenticated, this.setOpenhab, this.ensureStaff, staff_routes.processenroll); + app.get('/staff/stats', this.ensureAuthenticated, this.setOpenhab, this.ensureStaff, staff_routes.statsget); + app.get('/staff/invitations', this.ensureAuthenticated, this.setOpenhab, this.ensureStaff, staff_routes.invitationsget); + app.get('/staff/resendinvitation/:id', this.ensureAuthenticated, this.setOpenhab, this.ensureStaff, staff_routes.resendinvitation); + app.get('/staff/deleteinvitation/:id', this.ensureAuthenticated, this.setOpenhab, this.ensureStaff, staff_routes.deleteinvitation); + app.get('/staff/oauthclients', this.ensureAuthenticated, this.setOpenhab, this.ensureStaff, staff_routes.oauthclientsget); }; Routes.prototype.setupIFTTTRoutes = function (app) { @@ -205,42 +200,38 @@ Routes.prototype.setupTimezoneRoutes = function (app) { app.all('/setTimezone', setSessionTimezone); }; -Routes.prototype.setupApiRoutes = function (app) { - app.get('/api/events', this.ensureAuthenticated, events_routes.eventsvaluesget); -}; - Routes.prototype.setupProxyRoutes = function (app) { - app.all('/rest*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/images/*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/static/*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/rrdchart.png*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/chart*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/openhab.app*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/WebApp*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/CMD*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/cometVisu*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/proxy*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/greent*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/jquery.*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/classicui/*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/paperui/*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/basicui/*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/doc/*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/start/*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/icon*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/habmin/*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/remote*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); - app.all('/habpanel/*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/rest*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/images/*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/static/*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/rrdchart.png*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/chart*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/openhab.app*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/WebApp*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/CMD*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/cometVisu*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/proxy*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/greent*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/jquery.*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/classicui/*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/paperui/*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/basicui/*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/doc/*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/start/*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/icon*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/habmin/*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/remote*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); + app.all('/habpanel/*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, this.ensureServer, this.proxyRouteOpenhab.bind(this)); }; Routes.prototype.setupAppRoutes = function (app) { // myOH API for mobile apps - app.all('/api/v1/notifications*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, api_routes.notificationsget); - app.all('/api/v1/settings/notifications', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, api_routes.notificationssettingsget); + app.all('/api/v1/notifications*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, api_routes.notificationsget); + app.all('/api/v1/settings/notifications', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, api_routes.notificationssettingsget); // Android app registration - app.all('/addAndroidRegistration*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, androidRegistrationService); - app.all('/addAppleRegistration*', this.ensureRestAuthenticated, this.preassembleBody, this.setOpenhab, appleRegistrationService); + app.all('/addAndroidRegistration*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, androidRegistrationService); + app.all('/addAppleRegistration*', this.ensureRestAuthenticated, this.setOpenhab, this.preassembleBody, appleRegistrationService); }; // Ensure user is authenticated for web requests @@ -257,7 +248,7 @@ Routes.prototype.ensureRestAuthenticated = function (req, res, next) { if (req.isAuthenticated()) { return next(); } - return passport.authenticate(['basic','bearer'], {session: false})(req, res, next); + return passport.authenticate(['basic', 'bearer'], { session: false })(req, res, next); }; // Ensure user have 'master' role for certain homepage @@ -281,25 +272,34 @@ Routes.prototype.ensureStaff = function (req, res, next) { * is connected to. If we are not the right server we will send a redirect upstream * which should be handled internally by nginx, and not the requesting client **/ -Routes.prototype.ensureServer = function(req, res, next) { - if (req.openhab.serverAddress != system.getInternalAddress()){ - //redirect a request to correct cloud server - res.redirect(307, 'http://' + req.openhab.serverAddress + req.path); - } else { - res.cookie('CloudServer',system.getInternalAddress(), { maxAge: 900000, httpOnly: true }); +Routes.prototype.ensureServer = function (req, res, next) { + if (!req.connectionInfo.serverAddress) { + res.writeHead(500, 'openHAB is offline', { + 'content-type': 'text/plain' + }); + res.end('openHAB is offline'); + return; + } + + if (req.connectionInfo.serverAddress != system.getInternalAddress()) { + //redirect a request to correct cloud server + res.redirect(307, 'http://' + req.connectionInfo.serverAddress + req.path); + return; + } + + res.cookie('CloudServer', system.getInternalAddress(), { maxAge: 900000, httpOnly: true }); return next(); - } }; Routes.prototype.setOpenhab = function (req, res, next) { var self = this; + //ignore if no authentication + if (!req.isAuthenticated()) { + return next(); + } + req.user.openhab(function (error, openhab) { - if (!error && openhab) { - req.openhab = openhab; - next(); - return; - } if (error) { self.logger.error('openHAB lookup error: ' + error); return res.status(500).json({ @@ -307,7 +307,9 @@ Routes.prototype.setOpenhab = function (req, res, next) { message: error }] }); - } else { + } + + if (!openhab) { self.logger.warn('Can\'t find the openHAB of user which is unbelievable'); return res.status(500).json({ errors: [{ @@ -315,26 +317,51 @@ Routes.prototype.setOpenhab = function (req, res, next) { }] }); } + + req.openhab = openhab; + res.locals.openhab = openhab; + res.locals.openhablastonline = openhab.last_online; + + //Pulls connection info from redis and makes available to further calls and templates (local values) + redis.get('connection:' + req.openhab.id, (error, result) => { + if (error) { + self.logger.error('openHAB redis lookup error: ' + error); + } + if (!result) { + req.connectionInfo = {}; + res.locals.openhabstatus = "offline"; + res.locals.openhabMajorVersion = 0; + } else { + req.connectionInfo = JSON.parse(result) + res.locals.openhabstatus = "online" + if (req.connectionInfo.openhabVersion !== undefined) { + res.locals.openhabMajorVersion = parseInt(req.connectionInfo.openhabVersion.split('.')[0]); + } else { + res.locals.openhabMajorVersion = 0; + } + } + return next(); + }); }); }; -Routes.prototype.preassembleBody = function(req, res, next) { - //app.js will catch any JSON or URLEncoded related requests and - //store the rawBody on the request, all other requests need - //to have that data collected and stored here - var data = ''; - if (req.rawBody === undefined || req.rawBody === "") { - req.on('data', function(chunk) { - data += chunk; - }); - req.on('end', function() { - req.rawBody = data; - next(); - }); - } else { - req.rawBody = req.rawBody.toString(); - next(); - } +Routes.prototype.preassembleBody = function (req, res, next) { + //app.js will catch any JSON or URLEncoded related requests and + //store the rawBody on the request, all other requests need + //to have that data collected and stored here + var data = ''; + if (req.rawBody === undefined || req.rawBody === "") { + req.on('data', function (chunk) { + data += chunk; + }); + req.on('end', function () { + req.rawBody = data; + next(); + }); + } else { + req.rawBody = req.rawBody.toString(); + next(); + } }; Routes.prototype.proxyRouteOpenhab = function (req, res) { @@ -343,14 +370,6 @@ Routes.prototype.proxyRouteOpenhab = function (req, res) { this.logger.auditRequest(req); req.connection.setTimeout(600000); - if (req.openhab.status === 'offline') { - res.writeHead(500, 'openHAB is offline', { - 'content-type': 'text/plain' - }); - res.end('openHAB is offline'); - return; - } - //tell OH3 to use alternative Authentication header res.cookie('X-OPENHAB-AUTH-HEADER', 'true') diff --git a/socket-io.js b/socket-io.js new file mode 100644 index 00000000..3ea11d9d --- /dev/null +++ b/socket-io.js @@ -0,0 +1,488 @@ +var logger = require('./logger.js'), + redis = require('./redis-helper'), + uuid = require('uuid'), + requesttracker = require('./requesttracker'), + appleSender = require('./notificationsender/aps-helper'), + firebase = require('./notificationsender/firebase'), + config = require('./config.json'), + User = require('./models/user'), + Openhab = require('./models/openhab'), + Event = require('./models/event'), + UserDevice = require('./models/userdevice'), + Notification = require('./models/notification'); + + +/** + * Socket.IO Logic for incoming openHAB servers + * @param {*} server + * @param {*} system + */ +function SocketIO(server, system) { + const internalAddress = system.getInternalAddress(); + var requestTracker = new requesttracker(); + var io = require('socket.io')(server, { + maxHttpBufferSize: 1e8, //100mb, this was a previous default in engine.io before the upgrade to 3.6.0 which sets it to 1mb. May want to revisit. + logger: logger + }); + + this.io = io; + this.requestTracker = requestTracker; + + /** Socket.io Routes **/ + + //Check if we have blocked this connection + io.use(function (socket, next) { + const uuid = socket.handshake.query['uuid'] || socket.handshake.headers['uuid']; + if (uuid) { + redis.ttl('blocked:' + uuid, (err, result) => { + if (err) { + logger.info('blocked: error talking with redis for %s %s', uuid, err); + next(); + } else { + switch (result) { + case -2: // key does not exist + next(); + break; + case -1: // key exists but no TTL + next(new Error('your connection is blocked')); + break; + default: // seconds left on TTL + next(new Error(`try again in ${result} seconds`)); + break; + } + } + + }) + } else { + next(new Error('missing uuid')); + } + }); + + //Socket.io Handshake, this is during the initial http request, before the connection is upgraded to a websocket + io.use(function (socket, next) { + const handshakeData = socket.handshake; + const handshakeSecret = handshakeData.headers['secret']; + handshakeData.uuid = handshakeData.headers['uuid']; + handshakeData.openhabVersion = handshakeData.headers['openhabversion'] || 'unknown' + + logger.info('Authorizing incoming openHAB connection for %s version %s', handshakeData.uuid, handshakeData.openhabVersion); + Openhab.findOne({ + uuid: handshakeData.uuid, + secret: handshakeSecret + }, function (error, openhab) { + if (error) { + logger.error('openHAB lookup error: %s', error); + next(error); + } else { + if (openhab) { + socket.openhab = openhab; // will use this reference in 'connect' to save on mongo calls + next(); + } else { + logger.info('openHAB %s not found', handshakeData.uuid); + redis.set('blocked:' + handshakeData.uuid, handshakeData.openhabVersion, 'NX', 'EX', 60, (error, result) => { + if (error) { + logger.info('setting blocked: error talking with redis for %s %s ', handshakeData.uuid, error); + } + next(new Error('not authorized')); + }); + } + } + }); + }); + + //Authentication has succeeded, try and obtain a Redis Lock + io.use(function (socket, next) { + socket.connectionId = uuid.v1(); //we will check this when we handle disconnects + logger.info('obtaining lock for connection for uuid %s and connectionId %s', socket.handshake.uuid, socket.connectionId); + socket.redisLockKey = 'connection:' + socket.openhab.id; + const redisLockValue = { + serverAddress: internalAddress, + connectionId: socket.connectionId, + connectionTime: new Date().toISOString(), + openhabVersion: socket.handshake.openhabVersion + } + //Try obtaining a lock using the NX option, see see https://github.com/redis/node-redis/tree/v3.1.2#optimistic-locks + redis.set(socket.redisLockKey, JSON.stringify(redisLockValue), 'NX', 'EX', system.getConnectionLockTimeSeconds(), (error, result) => { + if (error) { + logger.info('error attaining connection lock for uuid %s connectionId %s %s', socket.handshake.uuid, socket.connectionId, error); + return next(new Error('connection lock error')); + } + + if (!result) { + //this key already exists, which means another connection exists + logger.info('another connection has lock for uuid %s my connectionId %s', socket.handshake.uuid, socket.connectionId); + return next(new Error('already connected')); + } + next(); + }); + }); + + //A valid websocket connection has been established + io.sockets.on('connection', function (socket) { + logger.info('connection success for uuid %s connectionId %s', socket.handshake.uuid, socket.connectionId); + socket.join(socket.handshake.uuid); + saveConnectionEvent(socket.openhab, 'online', 'good'); + + //listen for pings from the client + socket.conn.on('packet', function (packet) { + if (packet.type === 'ping') { + //reset the expire time on our lock + //When we upgrade redis we can replace multi with getex + //redis.getex(socket.redisLockKey, "EX", system.getConnectionLockTimeSeconds(), (error, result) => { + redis.multi() + .expire(socket.redisLockKey, system.getConnectionLockTimeSeconds()) + .get(socket.redisLockKey) + .exec((error, result) => { + if (!result || !result[1]) { + if (error) { + logger.error('ping: error updating lock expire for uuid %s connectionId %s %s', socket.handshake.uuid, socket.connectionId, error); + return; + } else { + logger.error('ping: lock no longer present for uuid %s connectionId %s result %s key %s', socket.handshake.uuid, socket.connectionId, result, socket.redisLockKey); + } + //we have lost our lock, something has gone wrong, lets cleanup + socket.disconnect(); + return; + } + const connection = JSON.parse(result[1]); + if (connection.connectionId !== socket.connectionId) { + logger.error('ping: connection %s has a lock for uuid %s, my connectionId %s', connection.connectionId, socket.handshake.uuid, socket.connectionId); + //we have lost our lock, something has gone wrong, lets cleanup + socket.disconnect(); + } + }); + }; + }); + + socket.on('disconnect', function () { + logger.info('Disconnected uuid %s connectionId %s', socket.handshake.uuid, socket.connectionId); + + //watch the current key to avoid race conditions with another connection replacing it + //see https://github.com/redis/node-redis/tree/v3.1.2#optimistic-locks + redis.watch(socket.redisLockKey, function (error) { + if (error) { + logger.info('error obtaining watch to delete for uuid %s connectionId %s error %s', socket.openhab.uuid, socket.connectionId, error); + return; + } + //make sure this is our connection id, and not a healthy reconnection from the same client + redis.get(socket.redisLockKey, (error, result) => { + var connection = result ? JSON.parse(result) : null; + if (!connection || connection.connectionId !== socket.connectionId) { + if (error) { + logger.info('error getting connection lock to remove for uuid %s connectionId %s error %s', socket.openhab.uuid, socket.connectionId, error); + } else { + logger.info('lock already removed, or not our ID for uuid %s connectionId %s, lock: %s', socket.openhab.uuid, socket.connectionId, connection); + } + //make sure to unwatch if we abort before the `multi.exec` which will unwatch automatically + redis.unwatch(); + return; + } + + redis.multi() + .del(socket.redisLockKey) + .exec(function (error, results) { + if (!results) { + if (error) { + logger.info('error removing connection lock for uuid %s connectionId %s error %s', socket.openhab.uuid, socket.connectionId, error); + } else { + //the key changed before we could delete it + logger.info('lock mutated before delete for uuid %s connectionId %s', socket.openhab.uuid, socket.connectionId); + } + } + }); + + //would be nice to remove this + Openhab.setLastOnline(socket.openhab.id, err => { + if (err) { + logger.error("error saving lastonline %s", err); + } else { + saveConnectionEvent(socket.openhab, 'offline', 'bad'); + } + }); + }); + }); + }); + socket.on('responseHeader', function (data) { + var self = this; + var requestId = data.id, + request; + if (requestTracker.has(requestId)) { + request = requestTracker.get(requestId); + if (self.handshake.uuid === request.openhab.uuid && !request.headersSent) { + request.writeHead(data.responseStatusCode, data.responseStatusText, data.headers); + } else { + logger.warn('responseHeader %s tried to respond to request which it doesn\'t own %s or headers have already been sent', self.handshake.uuid, request.openhab.uuid); + } + } else { + self.emit('cancel', { + id: requestId + }); + } + }); + socket.on('responseContentBinary', function (data) { + var self = this; + var requestId = data.id, + request; + if (requestTracker.has(requestId)) { + request = requestTracker.get(requestId); + if (self.handshake.uuid === request.openhab.uuid) { + request.write(data.body); + } else { + logger.warn('responseContentBinary %s tried to respond to request which it doesn\'t own %s', self.handshake.uuid, request.openhab.uuid); + } + } else { + self.emit('cancel', { + id: requestId + }); + } + }); + socket.on('responseFinished', function (data) { + var self = this; + var requestId = data.id, + request; + if (requestTracker.has(requestId)) { + request = requestTracker.get(requestId); + if (self.handshake.uuid === request.openhab.uuid) { + request.end(); + } else { + logger.warn('responseFinished %s tried to respond to request which it doesn\'t own %s', self.handshake.uuid, request.openhab.uuid); + } + } + }); + socket.on('responseError', function (data) { + var self = this; + var requestId = data.id, + request; + if (requestTracker.has(requestId)) { + request = requestTracker.get(requestId); + if (self.handshake.uuid === request.openhab.uuid) { + request.send(500, data.responseStatusText); + } else { + logger.warn('responseError %s tried to respond to request which it doesn\'t own %s', self.handshake.uuid, request.openhab.uuid); + } + } + }); + socket.on('notification', function (data) { + var self = this; + logger.info('Notification request from %s to user %s', self.handshake.uuid, data.userId); + User.findOne({ + username: data.userId + }, function (error, user) { + if (error) { + logger.error('User lookup error: %s', error); + return; + } + if (!user) { + return; + } + user.openhab(function (error, openhab) { + if (!error && openhab) { + if (openhab.uuid === self.handshake.uuid) { + logger.info('Notification from %s to %s', self.handshake.uuid, user.username); + sendNotificationToUser(user, data.message, data.icon, data.severity); + } else { + logger.warn('openHAB %s requested notification for user (%s) which it does not belong to', self.handshake.uuid, user.username); + } + } else { + if (error) { + logger.error('openHAB lookup error: %s', error); + } else { + logger.warn('Unable to find openHAB for user %s', user.username); + } + } + }); + }); + }); + socket.on('broadcastnotification', function (data) { + Openhab.findById(this.openhabId, function (error, openhab) { + if (error) { + logger.error('openHAB lookup error: %s', error); + return; + } + if (!openhab) { + logger.debug('openHAB not found'); + return; + } + + User.find({ + account: openhab.account + }, function (error, users) { + if (error) { + logger.error('Error getting users list: %s', error); + return; + } + + if (!users) { + logger.debug('No users found for openHAB'); + return; + } + + for (var i = 0; i < users.length; i++) { + sendNotificationToUser(users[i], data.message, data.icon, data.severity); + } + }); + }); + }); + socket.on('lognotification', function (data) { + Openhab.findById(this.openhabId, function (error, openhab) { + if (error) { + logger.error('openHAB lookup error: %s', error); + return; + } + if (!openhab) { + logger.debug('openHAB not found'); + return; + } + User.find({ + account: openhab.account + }, function (error, users) { + if (error) { + logger.error('Error getting users list: %s', error); + return; + } + + if (!users) { + logger.debug('No users found for openHAB'); + return; + } + + for (var i = 0; i < users.length; i++) { + newNotification = new Notification({ + user: users[i].id, + message: data.message, + icon: data.icon, + severity: data.severity + }); + newNotification.save(function (error) { + if (error) { + logger.error('Error saving notification: %s', error); + } + }); + } + }); + }); + }); + }); + + function saveConnectionEvent(openhab, status, color) { + var connectevent = new Event({ + openhab: openhab.id, + source: 'openhab', + status: status, + color: color + }); + connectevent.save(function (error) { + if (error) { + logger.error('Error saving connect event: %s', error); + } + }); + } + + /** + * When we move events to redis, use this instead of mongo + */ + function saveConnectionEventRedis(openhab, status, color) { + //move to config + const eventTTL = 60 * 60 * 24 * 14; // 14 days + const date = new Date(); + const eventKey = 'events:' + openhab.id; + const event = { + source: 'openhab', + status: status, + color: color, + when: date.toISOString() + } + //add event, reset expire time for 14 days, remove any events older then 14 days + redis.multi() + .zadd(eventKey, date.getTime(), JSON.stringify(event)) + .expire(eventKey, eventTTL) + .zremrangebyscore(eventKey, '-inf', date.getTime() - (eventTTL * 1000)) + .exec((error, reply) => { + if (error) { + logger.error('Could not modify events: %s', error) + } + }); + } + + function sendNotificationToUser(user, message, icon, severity) { + var androidRegistrations = []; + var iosDeviceTokens = []; + var newNotification = new Notification({ + user: user.id, + message: message, + icon: icon, + severity: severity + }); + newNotification.save(function (error) { + if (error) { + logger.error('Error saving notification: %s', error); + } + }); + UserDevice.find({ + owner: user.id + }, function (error, userDevices) { + if (error) { + logger.warn('Error fetching devices for user: %s', error); + return; + } + if (!userDevices) { + // User don't have any registered devices, so we will skip it. + return; + } + + for (var i = 0; i < userDevices.length; i++) { + if (userDevices[i].deviceType === 'android') { + androidRegistrations.push(userDevices[i].androidRegistration); + } else if (userDevices[i].deviceType === 'ios') { + iosDeviceTokens.push(userDevices[i].iosDeviceToken); + } + } + // If we found any android devices, send notification + if (androidRegistrations.length > 0) { + firebase.sendNotification(androidRegistrations, newNotification); + } + // If we found any ios devices, send notification + if (iosDeviceTokens.length > 0) { + sendIosNotifications(iosDeviceTokens, newNotification); + } + }); + } + + function sendIosNotifications(iosDeviceTokens, notification) { + if (!config.apn) { + return; + } + var payload = { + severity: notification.severity, + icon: notification.icon, + persistedId: notification._id, + timestamp: notification.created.getTime() + }; + for (var i = 0; i < iosDeviceTokens.length; i++) { + appleSender.sendAppleNotification(iosDeviceTokens[i], notification.message, payload); + } + } + + //cancel restRequests that have become orphaned. For some reason neither close + //nor finish is being called on some response objects and we end up hanging on + //to these in our restRequests map. This goes through and finds those orphaned + //responses and cleans them up, otherwise memory goes through the roof. + setInterval(function () { + var requests = requestTracker.getAll(); + logger.debug('Checking orphaned rest requests (%d)', requestTracker.size()); + Object.keys(requests).forEach(function (requestId) { + var res = requests[requestId]; + if (res.finished) { + logger.debug('expiring orphaned response'); + requestTracker.remove(requestId); + if (res.openhab) { + io.sockets.in(res.openhab.uuid).emit('cancel', { + id: requestId + }); + } + } + }) + }, 60000); +} + +module.exports = SocketIO; \ No newline at end of file diff --git a/system/mongoconnect.js b/system/mongoconnect.js index ca6de4b5..2ff49ad2 100644 --- a/system/mongoconnect.js +++ b/system/mongoconnect.js @@ -23,7 +23,7 @@ MongoConnect.prototype.connect = function (mongoose, callback) { } mongoose.set('useNewUrlParser', true); mongoose.set('useFindAndModify', false); - mongoose.set('useCreateIndex', true); + mongoose.set('useCreateIndex', true); logger.info('Trying to connect to mongodb at: ' + this.system.getDbHostsString()); mongoose.connect(this.getMongoUri(), callback); };