/* jshint -W097 */ /* jshint strict: false */ /* jslint node: true */ /* jshint -W061 */ 'use strict'; const socketio = require('socket.io'); const path = require('path'); const fs = require('fs'); const cookieParser = require('cookie-parser'); const EventEmitter = require('events'); const util = require('util'); let request = null; // From settings used only secure, auth and crossDomain function IOSocket(server, settings, adapter) { if (!(this instanceof IOSocket)) return new IOSocket(server, settings, adapter); this.settings = settings || {}; this.adapter = adapter; this.webServer = server; this.subscribes = {}; let that = this; // do not send too many state updates let eventsThreshold = { count: 0, timeActivated: 0, active: false, accidents: 0, repeatSeconds: 3, // how many seconds continuously must be number of events > value value: 200, // how many events allowed in one check interval checkInterval: 1000 // duration of one check interval }; // Extract user name from socket function getUserFromSocket(socket, callback) { let wait = false; try { if (socket.handshake.headers.cookie && (!socket.request || !socket.request._query || !socket.request._query.user)) { let cookie = decodeURIComponent(socket.handshake.headers.cookie); let m = cookie.match(/connect\.sid=(.+)/); if (m) { // If session cookie exists let c = m[1].split(';')[0]; let sessionID = cookieParser.signedCookie(c, that.settings.secret); if (sessionID) { // Get user for session wait = true; that.settings.store.get(sessionID, function (err, obj) { if (obj && obj.passport && obj.passport.user) { socket._sessionID = sessionID; if (typeof callback === 'function') { callback(null, obj.passport.user); } else { that.adapter.log.warn('[getUserFromSocket] Invalid callback') } } else { if (typeof callback === 'function') { callback('unknown user'); } else { that.adapter.log.warn('[getUserFromSocket] Invalid callback') } } }); } } } if (!wait) { let user = socket.request._query.user; let pass = socket.request._query.pass; if (user && pass) { wait = true; that.adapter.checkPassword(user, pass, function (res) { if (res) { that.adapter.log.debug('Logged in: ' + user); if (typeof callback === 'function') { callback(null, user); } else { that.adapter.log.warn('[getUserFromSocket] Invalid callback') } } else { that.adapter.log.warn('Invalid password or user name: ' + user + ', ' + pass[0] + '***(' + pass.length + ')'); if (typeof callback === 'function') { callback('unknown user'); } else { that.adapter.log.warn('[getUserFromSocket] Invalid callback') } } }); } } } catch (e) { that.adapter.log.error(e); wait = false; } if (!wait && typeof callback === 'function') { callback('Cannot detect user'); } } function disableEventThreshold(readAll) { if (eventsThreshold.active) { eventsThreshold.accidents = 0; eventsThreshold.count = 0; eventsThreshold.active = false; eventsThreshold.timeActivated = 0; that.adapter.log.info('Subscribe on all states again'); setTimeout(function () { if (readAll) { that.adapter.getForeignStates('*', function (err, res) { that.adapter.log.info('received all states'); for (let id in res) { if (res.hasOwnProperty(id) && JSON.stringify(states[id]) !== JSON.stringify(res[id])) { that.server.sockets.emit('stateChange', id, res[id]); states[id] = res[id]; } } }); } that.server.sockets.emit('eventsThreshold', false); that.adapter.unsubscribeForeignStates('system.adapter.*'); that.adapter.subscribeForeignStates('*'); }, 50); } } function enableEventThreshold() { if (!eventsThreshold.active) { eventsThreshold.active = true; setTimeout(function () { that.adapter.log.info('Unsubscribe from all states, except system\'s, because over ' + eventsThreshold.repeatSeconds + ' seconds the number of events is over ' + eventsThreshold.value + ' (in last second ' + eventsThreshold.count + ')'); eventsThreshold.timeActivated = new Date().getTime(); that.server.sockets.emit('eventsThreshold', true); that.adapter.unsubscribeForeignStates('*'); that.adapter.subscribeForeignStates('system.adapter.*'); }, 100); } } function getClientAddress(socket) { let address; if (socket.handshake) { address = socket.handshake.address; } if (!address && socket.request && socket.request.connection) { address = socket.request.connection.remoteAddress; } return address; } this.initSocket = function (socket) { if (!socket._acl) { if (that.settings.auth) { getUserFromSocket(socket, function (err, user) { if (err || !user) { socket.emit('reauthenticate'); that.adapter.log.error('socket.io ' + (err || 'No user found in cookies')); socket.disconnect(); } else { socket._secure = true; that.adapter.log.debug('socket.io client ' + user + ' connected'); that.adapter.calculatePermissions('system.user.' + user, commandsPermissions, function (acl) { let address = getClientAddress(socket); // socket._acl = acl; socket._acl = mergeACLs(address, acl, that.settings.whiteListSettings); socketEvents(socket, address); }); } }); } else { that.adapter.calculatePermissions(that.settings.defaultUser, commandsPermissions, function (acl) { let address = getClientAddress(socket); // socket._acl = acl; socket._acl = mergeACLs(address, acl, that.settings.whiteListSettings); socketEvents(socket, address); }); } } else { let address = getClientAddress(socket); socketEvents(socket, address); } }; this.getWhiteListIpForAddress = function (address, whiteList){ return getWhiteListIpForAddress(address, whiteList); }; function getWhiteListIpForAddress(address, whiteList) { if (!whiteList) return null; // check IPv6 or IPv4 direct match if (whiteList.hasOwnProperty(address)) { return address; } // check if address is IPv4 let addressParts = address.split('.'); if (addressParts.length !== 4) { return null; } // do we have settings for wild carded ips? let wildCardIps = Object.keys(whiteList).filter(function (key) { return key.indexOf('*') !== -1; }); if (wildCardIps.length === 0) { // no wild carded ips => no ip configured return null; } wildCardIps.forEach(function (ip) { let ipParts = ip.split('.'); if (ipParts.length === 4) { for (let i = 0; i < 4; i++) { if (ipParts[i] === '*' && i === 3) { // match return ip; } if (ipParts[i] !== addressParts[i]) break; } } }); return null; } function getPermissionsForIp(address, whiteList) { return whiteList[getWhiteListIpForAddress(address, whiteList) || 'default']; } function mergeACLs(address, acl, whiteList) { if (whiteList && address) { let whiteListAcl = getPermissionsForIp(address, whiteList); if (whiteListAcl) { ['object', 'state', 'file'].forEach(function (key) { if (acl.hasOwnProperty(key) && whiteListAcl.hasOwnProperty(key)) { Object.keys(acl[key]).forEach(function (permission) { if (whiteListAcl[key].hasOwnProperty(permission)) { acl[key][permission] = acl[key][permission] && whiteListAcl[key][permission]; } }) } }); if (whiteListAcl.user !== 'auth') { acl.user = 'system.user.' + whiteListAcl.user; } } } return acl; } function pattern2RegEx(pattern) { if (!pattern) { return null; } if (pattern !== '*') { if (pattern[0] === '*' && pattern[pattern.length - 1] !== '*') pattern += '$'; if (pattern[0] !== '*' && pattern[pattern.length - 1] === '*') pattern = '^' + pattern; } pattern = pattern.replace(/\./g, '\\.'); pattern = pattern.replace(/\*/g, '.*'); pattern = pattern.replace(/\[/g, '\\['); pattern = pattern.replace(/]/g, '\\]'); pattern = pattern.replace(/\(/g, '\\('); pattern = pattern.replace(/\)/g, '\\)'); return pattern; } this.subscribe = function (socket, type, pattern) { //console.log((socket._name || socket.id) + ' subscribe ' + pattern); if (socket) { socket._subscribe = socket._subscribe || {}; } if (!this.subscribes[type]) this.subscribes[type] = {}; let s; if (socket) { s = socket._subscribe[type] = socket._subscribe[type] || []; for (let i = 0; i < s.length; i++) { if (s[i].pattern === pattern) return; } } let p = pattern2RegEx(pattern); if (p === null) { this.adapter.log.warn('Empty pattern!'); return; } if (socket) { s.push({pattern: pattern, regex: new RegExp(p)}); } if (this.subscribes[type][pattern] === undefined) { this.subscribes[type][pattern] = 1; if (type === 'stateChange') { this.adapter.subscribeForeignStates(pattern); } else if (type === 'objectChange') { if (this.adapter.subscribeForeignObjects) { this.adapter.subscribeForeignObjects(pattern); } } else if (type === 'log') { if (this.adapter.requireLog) this.adapter.requireLog(true); } } else { this.subscribes[type][pattern]++; } }; function showSubscribes(socket, type) { if (socket && socket._subscribe) { let s = socket._subscribe[type] || []; let ids = []; for (let i = 0; i < s.length; i++) { ids.push(s[i].pattern); } that.adapter.log.debug('Subscribes: ' + ids.join(', ')); } else { that.adapter.log.debug('Subscribes: no subscribes'); } } this.unsubscribe = function (socket, type, pattern) { //console.log((socket._name || socket.id) + ' unsubscribe ' + pattern); if (!this.subscribes[type]) this.subscribes[type] = {}; if (socket) { if (!socket._subscribe || !socket._subscribe[type]) return; for (let i = socket._subscribe[type].length - 1; i >= 0; i--) { if (socket._subscribe[type][i].pattern === pattern) { // Remove pattern from global list if (this.subscribes[type][pattern] !== undefined) { this.subscribes[type][pattern]--; if (this.subscribes[type][pattern] <= 0) { if (type === 'stateChange') { //console.log((socket._name || socket.id) + ' unsubscribeForeignStates ' + pattern); this.adapter.unsubscribeForeignStates(pattern); } else if (type === 'objectChange') { //console.log((socket._name || socket.id) + ' unsubscribeForeignObjects ' + pattern); if (this.adapter.unsubscribeForeignObjects) this.adapter.unsubscribeForeignObjects(pattern); } else if (type === 'log') { //console.log((socket._name || socket.id) + ' requireLog false'); if (this.adapter.requireLog) this.adapter.requireLog(false); } delete this.subscribes[type][pattern]; } } delete socket._subscribe[type][i]; socket._subscribe[type].splice(i, 1); return; } } } else if (pattern) { // Remove pattern from global list if (this.subscribes[type][pattern] !== undefined) { this.subscribes[type][pattern]--; if (this.subscribes[type][pattern] <= 0) { if (type === 'stateChange') { //console.log((socket._name || socket.id) + ' unsubscribeForeignStates ' + pattern); this.adapter.unsubscribeForeignStates(pattern); } else if (type === 'objectChange') { //console.log((socket._name || socket.id) + ' unsubscribeForeignObjects ' + pattern); if (this.adapter.unsubscribeForeignObjects) this.adapter.unsubscribeForeignObjects(pattern); } else if (type === 'log') { //console.log((socket._name || socket.id) + ' requireLog false'); if (this.adapter.requireLog) this.adapter.requireLog(false); } delete this.subscribes[type][pattern]; } } } else { for (pattern in this.subscribes[type]) { if (!this.subscribes[type].hasOwnProperty(pattern)) continue; if (type === 'stateChange') { //console.log((socket._name || socket.id) + ' unsubscribeForeignStates ' + pattern); this.adapter.unsubscribeForeignStates(pattern); } else if (type === 'objectChange') { //console.log((socket._name || socket.id) + ' unsubscribeForeignObjects ' + pattern); if (this.adapter.unsubscribeForeignObjects) this.adapter.unsubscribeForeignObjects(pattern); } else if (type === 'log') { //console.log((socket._name || socket.id) + ' requireLog false'); if (this.adapter.requireLog) this.adapter.requireLog(false); } delete this.subscribes[type][pattern]; } } }; this.unsubscribeAll = function () { if (this.server && this.server.sockets) { for (let s in this.server.sockets) { if (this.server.sockets.hasOwnProperty(s)) { unsubscribeSocket(s, 'stateChange'); unsubscribeSocket(s, 'objectChange'); unsubscribeSocket(s, 'log'); } } } }; function unsubscribeSocket(socket, type) { if (!socket._subscribe || !socket._subscribe[type]) return; for (let i = 0; i < socket._subscribe[type].length; i++) { let pattern = socket._subscribe[type][i].pattern; if (that.subscribes[type][pattern] !== undefined) { that.subscribes[type][pattern]--; if (that.subscribes[type][pattern] <= 0) { if (type === 'stateChange') { that.adapter.unsubscribeForeignStates(pattern); } else if (type === 'objectChange') { if (that.adapter.unsubscribeForeignObjects) that.adapter.unsubscribeForeignObjects(pattern); } else if (type === 'log') { if (that.adapter.requireLog) that.adapter.requireLog(false); } delete that.subscribes[type][pattern]; } } } } function subscribeSocket(socket, type) { //console.log((socket._name || socket.id) + ' subscribeSocket'); if (!socket._subscribe || !socket._subscribe[type]) return; for (let i = 0; i < socket._subscribe[type].length; i++) { let pattern = socket._subscribe[type][i].pattern; if (that.subscribes[type][pattern] === undefined) { that.subscribes[type][pattern] = 1; if (type === 'stateChange') { that.adapter.subscribeForeignStates(pattern); } else if (type === 'objectChange') { if (that.adapter.subscribeForeignObjects) that.adapter.subscribeForeignObjects(pattern); } else if (type === 'log') { if (that.adapter.requireLog) that.adapter.requireLog(true); } } else { that.subscribes[type][pattern]++; } } } function publish(socket, type, id, obj) { if (!socket._subscribe || !socket._subscribe[type]) return; let s = socket._subscribe[type]; for (let i = 0; i < s.length; i++) { if (s[i].regex.test(id)) { updateSession(socket); socket.emit(type, id, obj); return; } } } // update session ID, but not offter than 60 seconds function updateSession(socket) { if (socket._sessionID) { let time = (new Date()).getTime(); if (socket._lastActivity && time - socket._lastActivity > settings.ttl * 1000) { socket.emit('reauthenticate'); socket.disconnect(); return false; } socket._lastActivity = time; if (!socket._sessionTimer) { socket._sessionTimer = setTimeout(function () { socket._sessionTimer = null; that.settings.store.get(socket._sessionID, function (err, obj) { if (obj) { that.adapter.setSession(socket._sessionID, settings.ttl, obj); } else { socket.emit('reauthenticate'); socket.disconnect(); } }); }, 60000); } } return true; } // static information let commandsPermissions = { getObject: {type: 'object', operation: 'read'}, getObjects: {type: 'object', operation: 'list'}, getObjectView: {type: 'object', operation: 'list'}, setObject: {type: 'object', operation: 'write'}, requireLog: {type: 'object', operation: 'write'}, // just mapping to some command delObject: {type: 'object', operation: 'delete'}, extendObject: {type: 'object', operation: 'write'}, getHostByIp: {type: 'object', operation: 'list'}, subscribeObjects: {type: 'object', operation: 'read'}, unsubscribeObjects: {type: 'object', operation: 'read'}, getStates: {type: 'state', operation: 'list'}, getState: {type: 'state', operation: 'read'}, setState: {type: 'state', operation: 'write'}, delState: {type: 'state', operation: 'delete'}, createState: {type: 'state', operation: 'create'}, subscribe: {type: 'state', operation: 'read'}, unsubscribe: {type: 'state', operation: 'read'}, getStateHistory: {type: 'state', operation: 'read'}, getVersion: {type: '', operation: ''}, addUser: {type: 'users', operation: 'create'}, delUser: {type: 'users', operation: 'delete'}, addGroup: {type: 'users', operation: 'create'}, delGroup: {type: 'users', operation: 'delete'}, changePassword: {type: 'users', operation: 'write'}, httpGet: {type: 'other', operation: 'http'}, cmdExec: {type: 'other', operation: 'execute'}, sendTo: {type: 'other', operation: 'sendto'}, sendToHost: {type: 'other', operation: 'sendto'}, readLogs: {type: 'other', operation: 'execute'}, readDir: {type: 'file', operation: 'list'}, createFile: {type: 'file', operation: 'create'}, writeFile: {type: 'file', operation: 'write'}, readFile: {type: 'file', operation: 'read'}, deleteFile: {type: 'file', operation: 'delete'}, readFile64: {type: 'file', operation: 'read'}, writeFile64: {type: 'file', operation: 'write'}, unlink: {type: 'file', operation: 'delete'}, rename: {type: 'file', operation: 'write'}, mkdir: {type: 'file', operation: 'write'}, chmodFile: {type: 'file', operation: 'write'}, authEnabled: {type: '', operation: ''}, disconnect: {type: '', operation: ''}, listPermissions: {type: '', operation: ''}, getUserPermissions: {type: 'object', operation: 'read'} }; function addUser(user, pw, options, callback) { if (typeof options === 'function') { callback = options; options = null; } if (!user.match(/^[-.A-Za-züäößÖÄÜа-яА-Я@+$§0-9=?!&# ]+$/)) { if (typeof callback === 'function') { callback('Invalid characters in the name. Only following special characters are allowed: -@+$§=?!&# and letters'); } return; } that.adapter.getForeignObject('system.user.' + user, options, (err, obj) => { if (obj) { if (typeof callback === 'function') { callback('User yet exists'); } } else { that.adapter.setForeignObject('system.user.' + user, { type: 'user', common: { name: user, enabled: true, groups: [] } }, options, () => { that.adapter.setPassword(user, pw, options, callback); }); } }); } function delUser(user, options, callback) { that.adapter.getForeignObject('system.user.' + user, options, (err, obj) => { if (err || !obj) { if (typeof callback === 'function') { callback('User does not exist'); } } else { if (obj.common.dontDelete) { if (typeof callback === 'function') { callback('Cannot delete user, while is system user'); } } else { that.adapter.delForeignObject('system.user.' + user, options, err => { // Remove this user from all groups in web client if (typeof callback === 'function') { callback(err); } }); } } }); } function addGroup(group, desc, acl, options, callback) { let name = group; if (typeof acl === 'function') { callback = acl; acl = null; } if (typeof desc === 'function') { callback = desc; desc = null; } if (typeof options === 'function') { callback = options; options = null; } if (name && name.substring(0, 1) !== name.substring(0, 1).toUpperCase()) { name = name.substring(0, 1).toUpperCase() + name.substring(1); } group = group.substring(0, 1).toLowerCase() + group.substring(1); if (!group.match(/^[-.A-Za-züäößÖÄÜа-яА-Я@+$§0-9=?!&#_ ]+$/)) { if (typeof callback === 'function') { callback('Invalid characters in the group name. Only following special characters are allowed: -@+$§=?!&# and letters'); } return; } that.adapter.getForeignObject('system.group.' + group, options, (err, obj) => { if (obj) { if (typeof callback === 'function') { callback('Group yet exists'); } } else { obj = { _id: 'system.group.' + group, type: 'group', common: { name: name, desc: desc, members: [], acl: acl } }; that.adapter.setForeignObject('system.group.' + group, obj, options, err => { if (typeof callback === 'function') { callback(err, obj); } }); } }); } function delGroup(group, options, callback) { that.adapter.getForeignObject('system.group.' + group, options, (err, obj) => { if (err || !obj) { if (typeof callback === 'function') { callback('Group does not exist'); } } else { if (obj.common.dontDelete) { if (typeof callback === 'function') { callback('Cannot delete group, while is system group'); } } else { that.adapter.delForeignObject('system.group.' + group, options, err => { // Remove this group from all users in web client if (typeof callback === 'function') { callback(err); } }); } } }); } function checkPermissions(socket, command, callback, arg) { if (socket._acl.user !== 'system.user.admin') { // type: file, object, state, other // operation: create, read, write, list, delete, sendto, execute, sendToHost, readLogs if (commandsPermissions[command]) { // If permission required if (commandsPermissions[command].type) { if (socket._acl[commandsPermissions[command].type] && socket._acl[commandsPermissions[command].type][commandsPermissions[command].operation]) { return true; } else { that.adapter.log.warn('No permission for "' + socket._acl.user + '" to call ' + command + '. Need "' + commandsPermissions[command].type + '"."' + commandsPermissions[command].operation + '"'); } } else { return true; } } else { that.adapter.log.warn('No rule for command: ' + command); } if (typeof callback === 'function') { callback('permissionError'); } else { if (commandsPermissions[command]) { socket.emit('permissionError', { command: command, type: commandsPermissions[command].type, operation: commandsPermissions[command].operation, arg: arg }); } else { socket.emit('permissionError', { command: command, arg: arg }); } } return false; } else { return true; } } function checkObject(obj, options, flag) { // read rights of object if (!obj || !obj.common || !obj.acl || flag === 'list') { return true; } if (options.user !== 'system.user.admin' && options.groups.indexOf('system.group.administrator') === -1) { if (obj.acl.owner !== options.user) { // Check if the user is in the group if (options.groups.indexOf(obj.acl.ownerGroup) !== -1) { // Check group rights if (!(obj.acl.object & (flag << 4))) { return false } } else { // everybody if (!(obj.acl.object & flag)) { return false } } } else { // Check group rights if (!(obj.acl.object & (flag << 8))) { return false } } } return true; } this.send = function (socket, cmd, id, data) { if (socket._apiKeyOk) { socket.emit(cmd, id, data); } }; function stopAdapter(reason, callback) { reason && that.adapter.log.warn('Adapter stopped. Reason: ' + reason); that.adapter.getForeignObject('system.adapter.' + that.adapter.namespace, function (err, obj) { if (err) that.adapter.log.error('[getForeignObject]: ' + err); if (obj) { obj.common.enabled = false; setTimeout(function () { that.adapter.setForeignObject(obj._id, obj, function (err) { if (err) that.adapter.log.error('[setForeignObject]: ' + err); callback && callback(); }); }, 5000); } else { callback && callback(); } }); } function redirectAdapter(url, callback) { if (!url) { that.adapter.log.warn('Received redirect command, but no URL'); } else { that.adapter.getForeignObject('system.adapter.' + that.adapter.namespace, function (err, obj) { if (err) that.adapter.log.error('redirectAdapter [getForeignObject]: ' + err); if (obj) { obj.native.cloudUrl = url; setTimeout(function () { that.adapter.setForeignObject(obj._id, obj, function (err) { if (err) that.adapter.log.error('redirectAdapter [setForeignObject]: ' + err); callback && callback(); }); }, 3000); } else { callback && callback(); } }); } } function waitForConnect(delaySeconds) { that.emit && that.emit('connectWait', delaySeconds); } function socketEvents(socket, address) { if (socket.conn) { that.adapter.log.info('==>Connected ' + socket._acl.user + ' from ' + address); } else { that.adapter.log.info('Trying to connect as ' + socket._acl.user + ' from ' + address); } if (!that.infoTimeout) { that.infoTimeout = setTimeout(updateConnectedInfo, 1000); } socket.on('authenticate', function (user, pass, callback) { that.adapter.log.debug((new Date()).toISOString() + ' Request authenticate [' + socket._acl.user + ']'); if (typeof user === 'function') { callback = user; user = undefined; } if (socket._acl.user !== null) { if (typeof callback === 'function') { callback(socket._acl.user !== null, socket._secure); } } else { that.adapter.log.debug((new Date()).toISOString() + ' Request authenticate [' + socket._acl.user + ']'); socket._authPending = callback; } }); socket.on('name', function (name, cb) { that.adapter.log.debug('Connection from ' + name); updateSession(socket); if (this._name === undefined) { this._name = name; if (!that.infoTimeout) that.infoTimeout = setTimeout(updateConnectedInfo, 1000); } else if (this._name !== name) { that.adapter.log.warn('socket ' + this.id + ' changed socket name from ' + this._name + ' to ' + name); this._name = name; } if (typeof cb === 'function') { cb(); } }); /* * objects */ socket.on('getObject', function (id, callback) { if (updateSession(socket) && checkPermissions(socket, 'getObject', callback, id)) { that.adapter.getForeignObject(id, {user: socket._acl.user}, callback); } }); socket.on('getObjects', function (callback) { if (updateSession(socket) && checkPermissions(socket, 'getObjects', callback)) { that.adapter.getForeignObjects('*', 'state', 'rooms', {user: socket._acl.user}, function (err, objs) { if (typeof callback === 'function') { callback(err, objs); } else { that.adapter.log.warn('[getObjects] Invalid callback') } }); } }); socket.on('subscribeObjects', function (pattern, callback) { if (updateSession(socket) && checkPermissions(socket, 'subscribeObjects', callback, pattern)) { if (pattern && typeof pattern === 'object' && pattern instanceof Array) { for (let p = 0; p < pattern.length; p++) { that.subscribe(this, 'objectChange', pattern[p]); } } else { that.subscribe(this, 'objectChange', pattern); } if (typeof callback === 'function') { setImmediate(callback, null); } } }); socket.on('unsubscribeObjects', function (pattern, callback) { if (updateSession(socket) && checkPermissions(socket, 'unsubscribeObjects', callback, pattern)) { if (pattern && typeof pattern === 'object' && pattern instanceof Array) { for (let p = 0; p < pattern.length; p++) { that.unsubscribe(this, 'objectChange', pattern[p]); } } else { that.unsubscribe(this, 'objectChange', pattern); } if (typeof callback === 'function') { setImmediate(callback, null); } } }); socket.on('getObjectView', function (design, search, params, callback) { if (updateSession(socket) && checkPermissions(socket, 'getObjectView', callback, search)) { that.adapter.objects.getObjectView(design, search, params, {user: socket._acl.user}, callback); } }); socket.on('setObject', function (id, obj, callback) { if (updateSession(socket) && checkPermissions(socket, 'setObject', callback, id)) { that.adapter.setForeignObject(id, obj, {user: socket._acl.user}, callback); } }); /* * states */ socket.on('getStates', function (pattern, callback) { if (updateSession(socket) && checkPermissions(socket, 'getStates', callback, pattern)) { if (typeof pattern === 'function') { callback = pattern; pattern = null; } that.adapter.getForeignStates(pattern || '*', {user: socket._acl.user}, callback); } }); socket.on('error', function (err) { that.adapter.log.error('Socket error: ' + err); }); // allow admin access if (that.settings.allowAdmin) { socket.on('getAllObjects', function (callback) { if (updateSession(socket) && checkPermissions(socket, 'getObjects', callback)) { that.adapter.objects.getObjectList({include_docs: true}, function (err, res) { that.adapter.log.info('received all objects'); res = res.rows; let objects = {}; if (socket._acl && socket._acl.user !== 'system.user.admin' && socket._acl.groups.indexOf('system.group.administrator') === -1) { for (let i = 0; i < res.length; i++) { if (checkObject(res[i].doc, socket._acl, 4 /* 'read' */)) { objects[res[i].doc._id] = res[i].doc; } } if (typeof callback === 'function') { callback(null, objects); } else { that.adapter.log.warn('[getAllObjects] Invalid callback') } } else { for (let j = 0; j < res.length; j++) { objects[res[j].doc._id] = res[j].doc; } if (typeof callback === 'function') { callback(null, objects); } else { that.adapter.log.warn('[getAllObjects] Invalid callback') } } }); } }); socket.on('delObject', function (id, callback) { if (updateSession(socket) && checkPermissions(socket, 'delObject', callback, id)) { that.adapter.delForeignObject(id, {user: socket._acl.user}, callback); } }); socket.on('extendObject', function (id, obj, callback) { if (updateSession(socket) && checkPermissions(socket, 'extendObject', callback, id)) { that.adapter.extendForeignObject(id, obj, {user: socket._acl.user}, callback); } }); socket.on('getHostByIp', function (ip, callback) { if (updateSession(socket) && checkPermissions(socket, 'getHostByIp', ip)) { that.adapter.objects.getObjectView('system', 'host', {}, {user: socket._acl.user}, function (err, data) { if (data.rows.length) { for (let i = 0; i < data.rows.length; i++) { if (data.rows[i].value.common.hostname === ip) { if (typeof callback === 'function') { callback(ip, data.rows[i].value); } else { that.adapter.log.warn('[getHostByIp] Invalid callback') } return; } if (data.rows[i].value.native.hardware && data.rows[i].value.native.hardware.networkInterfaces) { let net = data.rows[i].value.native.hardware.networkInterfaces; for (let eth in net) { if (!net.hasOwnProperty(eth)) continue; for (let j = 0; j < net[eth].length; j++) { if (net[eth][j].address === ip) { if (typeof callback === 'function') { callback(ip, data.rows[i].value); } else { that.adapter.log.warn('[getHostByIp] Invalid callback') } return; } } } } } } if (typeof callback === 'function') { callback(ip, null); } else { that.adapter.log.warn('[getHostByIp] Invalid callback') } }); } }); socket.on('getForeignObjects', function (pattern, type, callback) { if (updateSession(socket) && checkPermissions(socket, 'getObjects', callback)) { if (typeof type === 'function') { callback = type; type = undefined; } that.adapter.getForeignObjects(pattern, type, {user: socket._acl.user}, function (err, objs) { if (typeof callback === 'function') { callback(err, objs); } else { that.adapter.log.warn('[getObjects] Invalid callback') } }); } }); socket.on('getForeignStates', function (pattern, callback) { if (updateSession(socket) && checkPermissions(socket, 'getStates', callback)) { that.adapter.getForeignStates(pattern, {user: socket._acl.user}, function (err, objs) { if (typeof callback === 'function') { callback(err, objs); } else { that.adapter.log.warn('[getObjects] Invalid callback') } }); } }); socket.on('requireLog', function (isEnabled, callback) { if (updateSession(socket) && checkPermissions(socket, 'setObject', callback)) { if (isEnabled) { that.subscribe(this, 'log', 'dummy') } else { that.unsubscribe(this, 'log', 'dummy') } if (that.adapter.log.level === 'debug') showSubscribes(socket, 'log'); if (typeof callback === 'function') { setImmediate(callback, null); } } }); socket.on('readLogs', function (callback) { if (updateSession(socket) && checkPermissions(socket, 'readLogs', callback)) { let result = {list: []}; // deliver file list try { let config = adapter.systemConfig; // detect file log if (config && config.log && config.log.transport) { for (let transport in config.log.transport) { if (config.log.transport.hasOwnProperty(transport) && config.log.transport[transport].type === 'file') { let filename = config.log.transport[transport].filename || 'log/'; let parts = filename.replace(/\\/g, '/').split('/'); parts.pop(); filename = parts.join('/'); if (filename[0] === '.') { filename = path.normalize(__dirname + '/../../../') + filename; } if (fs.existsSync(filename)) { let files = fs.readdirSync(filename); for (let f = 0; f < files.length; f++) { try { if (!fs.lstatSync(filename + '/' + files[f]).isDirectory()) { result.list.push('log/' + transport + '/' + files[f]); } } catch (e) { // push unchecked // result.list.push('log/' + transport + '/' + files[f]); adapter.log.error('Cannot check file: ' + filename + '/' + files[f]); } } } } } } else { result.error = 'no file loggers'; result.list = undefined; } } catch (e) { adapter.log.error(e); result.error = e; result.list = undefined; } if (typeof callback === 'function') { callback(result.error, result.list); } } }); } else { // only flot allowed socket.on('delObject', function (id, callback) { if (id.match(/^flot\./)) { if (updateSession(socket) && checkPermissions(socket, 'delObject', callback, id)) { that.adapter.delForeignObject(id, {user: socket._acl.user}, callback); } } else { if (typeof callback === 'function') { callback('permissionError'); } } }); } socket.on('getState', function (id, callback) { if (updateSession(socket) && checkPermissions(socket, 'getState', callback, id)) { that.adapter.getForeignState(id, {user: socket._acl.user}, callback); } }); socket.on('setState', function (id, state, callback) { if (updateSession(socket) && checkPermissions(socket, 'setState', callback, id)) { if (typeof state !== 'object') state = {val: state}; that.adapter.setForeignState(id, state, {user: socket._acl.user}, function (err, res) { if (typeof callback === 'function') { callback(err, res); } }); } }); // allow admin access if (that.settings.allowAdmin) { socket.on('delState', function (id, callback) { if (updateSession(socket) && checkPermissions(socket, 'delState', callback, id)) { that.adapter.delForeignState(id, {user: socket._acl.user}, callback); } }); socket.on('addUser', function (user, pass, callback) { if (updateSession(socket) && checkPermissions(socket, 'addUser', callback, user)) { addUser(user, pass, {user: socket._acl.user}, callback); } }); socket.on('delUser', function (user, callback) { if (updateSession(socket) && checkPermissions(socket, 'delUser', callback, user)) { delUser(user, {user: socket._acl.user}, callback); } }); socket.on('addGroup', function (group, desc, acl, callback) { if (updateSession(socket) && checkPermissions(socket, 'addGroup', callback, group)) { addGroup(group, desc, acl, {user: socket._acl.user}, callback); } }); socket.on('delGroup', function (group, callback) { if (updateSession(socket) && checkPermissions(socket, 'delGroup', callback, group)) { delGroup(group, {user: socket._acl.user}, callback); } }); socket.on('changePassword', function (user, pass, callback) { if (updateSession(socket)) { if (user === socket._acl.user || checkPermissions(socket, 'changePassword', callback, user)) { that.adapter.setPassword(user, pass, {user: socket._acl.user}, callback); } } }); // commands will be executed on host/controller // following response commands are expected: cmdStdout, cmdStderr, cmdExit socket.on('cmdExec', function (host, id, cmd, callback) { if (updateSession(socket) && checkPermissions(socket, 'cmdExec', callback, cmd)) { that.adapter.log.debug('cmdExec on ' + host + '(' + id + '): ' + cmd); that.adapter.sendToHost(host, 'cmdExec', {data: cmd, id: id}); } }); socket.on('eventsThreshold', function (isActive) { if (!isActive) { disableEventThreshold(true); } else { enableEventThreshold(); } }); } socket.on('getVersion', function (callback) { if (updateSession(socket) && checkPermissions(socket, 'getVersion', callback)) { if (typeof callback === 'function') { callback(that.adapter.version); } else { that.adapter.log.warn('[getVersion] Invalid callback') } } }); socket.on('subscribe', function (pattern, callback) { if (updateSession(socket) && checkPermissions(socket, 'subscribe', callback, pattern)) { if (pattern && typeof pattern === 'object' && pattern instanceof Array) { for (let p = 0; p < pattern.length; p++) { that.subscribe(this, 'stateChange', pattern[p]); } } else { that.subscribe(this, 'stateChange', pattern); } if (that.adapter.log.level === 'debug') showSubscribes(socket, 'stateChange'); if (typeof callback === 'function') { setImmediate(callback, null); } } }); socket.on('unsubscribe', function (pattern, callback) { if (updateSession(socket) && checkPermissions(socket, 'unsubscribe', callback, pattern)) { if (pattern && typeof pattern === 'object' && pattern instanceof Array) { for (let p = 0; p < pattern.length; p++) { that.unsubscribe(this, 'stateChange', pattern[p]); } } else { that.unsubscribe(this, 'stateChange', pattern); } if (that.adapter.log.level === 'debug') showSubscribes(socket, 'stateChange'); if (typeof callback === 'function') { setImmediate(callback, null); } } }); // new History socket.on('getHistory', function (id, options, callback) { if (updateSession(socket) && checkPermissions(socket, 'getStateHistory', callback, id)) { options = options || {}; options.user = socket._acl.user; that.adapter.getHistory(id, options, function (err, data, step, sessionId) { if (typeof callback === 'function') { callback(err, data, step, sessionId); } else { that.adapter.log.warn('[getHistory] Invalid callback') } }); } }); // HTTP socket.on('httpGet', function (url, callback) { if (updateSession(socket) && checkPermissions(socket, 'httpGet', callback, url)) { if (!request) request = require('request'); that.adapter.log.debug('httpGet: ' + url); request(url, callback); } }); // commands socket.on('sendTo', function (adapterInstance, command, message, callback) { if (updateSession(socket) && checkPermissions(socket, 'sendTo', callback, command)) { that.adapter.sendTo(adapterInstance, command, message, callback); } }); // following commands are protected and require the extra permissions const protectedCommands = ['cmdExec', 'getLocationOnDisk', 'getDiagData', 'getDevList', 'delLogs', 'writeDirAsZip', 'writeObjectsAsZip', 'readObjectsAsZip', 'checkLogging', 'updateMultihost']; socket.on('sendToHost', function (host, command, message, callback) { // host can answer following commands: cmdExec, getRepository, getInstalled, getInstalledAdapter, getVersion, getDiagData, getLocationOnDisk, getDevList, getLogs, getHostInfo, // delLogs, readDirAsZip, writeDirAsZip, readObjectsAsZip, writeObjectsAsZip, checkLogging, updateMultihost if (updateSession(socket) && checkPermissions(socket, protectedCommands.indexOf(command) !== -1 ? 'cmdExec' : 'sendToHost', callback, command)) { that.adapter.sendToHost(host, command, message, callback); } }); socket.on('authEnabled', callback => { if (updateSession(socket) && checkPermissions(socket, 'authEnabled', callback)) { if (typeof callback === 'function') { callback(that.settings.auth, socket._acl.user.replace(/^system\.user\./, '')); } else { that.adapter.log.warn('[authEnabled] Invalid callback') } } }); // file operations socket.on('readFile', function (_adapter, fileName, callback) { if (updateSession(socket) && checkPermissions(socket, 'readFile', callback, fileName)) { that.adapter.readFile(_adapter, fileName, {user: socket._acl.user}, callback); } }); socket.on('readFile64', function (_adapter, fileName, callback) { if (updateSession(socket) && checkPermissions(socket, 'readFile64', callback, fileName)) { that.adapter.readFile(_adapter, fileName, {user: socket._acl.user}, function (err, buffer, type) { let data64; if (buffer) { if (type === 'application/json') { data64 = new Buffer(encodeURIComponent(buffer)).toString('base64'); } else { if (typeof buffer === 'string') { data64 = new Buffer(buffer).toString('base64'); } else { data64 = buffer.toString('base64'); } } } //Convert buffer to base 64 if (typeof callback === 'function') { callback(err, data64 || '', type); } else { that.adapter.log.warn('[readFile64] Invalid callback') } }); } }); socket.on('writeFile64', function (_adapter, fileName, data64, options, callback) { if (typeof options === 'function') { callback = options; options = {user: socket._acl.user}; } if (!options) options = {}; options.user = socket._acl.user; if (updateSession(socket) && checkPermissions(socket, 'writeFile64', callback, fileName)) { //Convert base 64 to buffer let buffer = new Buffer(data64, 'base64'); that.adapter.writeFile(_adapter, fileName, buffer, options, function (err) { if (typeof callback === 'function') { callback(err); } }); } }); socket.on('writeFile', function (_adapter, fileName, data, options, callback) { if (typeof options === 'function') { callback = options; options = {user: socket._acl.user}; } if (!options) options = {}; options.user = socket._acl.user; if (updateSession(socket) && checkPermissions(socket, 'writeFile', callback, fileName)) { that.adapter.writeFile(_adapter, fileName, data, options, callback); } }); socket.on('unlink', function (_adapter, name, callback) { if (updateSession(socket) && checkPermissions(socket, 'unlink', callback, name)) { that.adapter.unlink(_adapter, name, {user: socket._acl.user}, callback); } }); socket.on('rename', function (_adapter, oldName, newName, callback) { if (updateSession(socket) && checkPermissions(socket, 'rename', callback, oldName)) { that.adapter.rename(_adapter, oldName, newName, {user: socket._acl.user}, callback); } }); socket.on('mkdir', function (_adapter, dirName, callback) { if (updateSession(socket) && checkPermissions(socket, 'mkdir', callback, dirName)) { that.adapter.mkdir(_adapter, dirName, {user: socket._acl.user}, callback); } }); socket.on('readDir', function (_adapter, dirName, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } options = options || {}; options.user = socket._acl.user; if (options.filter === undefined) options.filter = true; if (updateSession(socket) && checkPermissions(socket, 'readDir', callback, dirName)) { that.adapter.readDir(_adapter, dirName, options, callback); } }); socket.on('chmodFile', function (_adapter, dirName, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } options = options || {}; options.user = socket._acl.user; if (options.filter === undefined) options.filter = true; if (updateSession(socket) && checkPermissions(socket, 'chmodFile', callback, dirName)) { that.adapter.chmodFile(_adapter, dirName, options, callback); } }); // connect/disconnect socket.on('disconnect', function (error) { that.adapter.log.info('<==Disconnect ' + socket._acl.user + ' from ' + getClientAddress(socket) + ' ' + (socket._name || '')); unsubscribeSocket(this, 'stateChange'); unsubscribeSocket(this, 'objectChange'); unsubscribeSocket(this, 'log'); if (!that.infoTimeout) that.infoTimeout = setTimeout(updateConnectedInfo, 1000); // if client mode if (!socket.conn) { socket._apiKeyOk = false; that.emit && that.emit('disconnect', error); } }); socket.on('logout', function (callback) { that.adapter.destroySession(socket._sessionID, callback); }); socket.on('listPermissions', function (callback) { if (updateSession(socket)) { if (typeof callback === 'function') { callback(commandsPermissions); } else { that.adapter.log.warn('[listPermissions] Invalid callback'); } } }); socket.on('getUserPermissions', function (callback) { if (updateSession(socket) && checkPermissions(socket, 'getUserPermissions', callback)) { if (typeof callback === 'function') { callback(null, socket._acl); } else { that.adapter.log.warn('[getUserPermissions] Invalid callback') } } }); if (typeof that.settings.extensions === 'function') { that.settings.extensions(socket); } // if client mode if (!socket.conn) { socket._apiKeyOk = false; socket.on('cloudDisconnect', function (err) { err && that.adapter.log.warn('User disconnected from cloud: ' + err); unsubscribeSocket(socket, 'stateChange'); unsubscribeSocket(socket, 'objectChange'); unsubscribeSocket(socket, 'log'); that.emit('cloudDisconnect'); }); socket.on('cloudConnect', function () { // do not autosubscribe. The client must resubscribe all states anew // subscribeSocket(socket, 'stateChange'); // subscribeSocket(socket, 'objectChange'); // subscribeSocket(socket, 'log'); that.emit('cloudConnect'); }); socket.on('cloudCommand', function (cmd, data) { if (cmd === 'stop') { stopAdapter(data); } else if (cmd === 'redirect') { redirectAdapter(data); } else if (cmd === 'wait') { waitForConnect(data || 30); } }); // only active in client mode socket.on('connect', function () { that.adapter.log.debug('Connected. Check api key...'); socket._apiKeyOk = false; // 2018_01_20 workaround for pro: Remove it after next pro maintenance if (that.settings.apikey.indexOf('@pro_') !== -1) { socket._apiKeyOk = true; that.emit && that.emit('connect'); } // send api key if exists this.emit('apikey', that.settings.apikey, that.settings.version, that.settings.uuid, function (err, instructions) { // instructions = { // validTill: '2018-03-14T01:01:01.567Z', // command: 'wait' | 'stop' | 'redirect' // data: some data for command (URL for redirect or seconds for wait' if (instructions) { if (typeof instructions !== 'object') { that.adapter.setState('info.remoteTill', new Date(instructions).toISOString(), true); } else { if (instructions.validTill) { that.adapter.setState('info.remoteTill', new Date(instructions.validTill).toISOString(), true); } if (instructions.command === 'stop') { stopAdapter(instructions.data); } else if (instructions.command === 'redirect') { redirectAdapter(instructions.data); } else if (instructions.command === 'wait') { waitForConnect(instructions.data || 30); } } } if (!err) { that.adapter.log.debug('API KEY OK'); socket._apiKeyOk = true; that.emit && that.emit('connect'); } else { if (err.indexOf('Please buy remote access to use pro.') !== -1) { stopAdapter('Please buy remote access to use pro.'); } that.adapter.log.error(err); this.close(); // disconnect } }); if (socket._sessionID) { that.adapter.getSession(socket._sessionID, function (obj) { if (obj && obj.passport) { socket._acl.user = obj.passport.user; } else { socket._acl.user = ''; socket.emit('reauthenticate'); socket.disconnect(); } if (socket._authPending) { socket._authPending(!!socket._acl.user, true); delete socket._authPending; } }); } subscribeSocket(this, 'stateChange'); subscribeSocket(this, 'objectChange'); subscribeSocket(this, 'log'); }); /*socket.on('reconnect', function (attempt) { that.adapter.log.debug('Connected after attempt ' + attempt); }); socket.on('reconnect_attempt', function (attempt) { that.adapter.log.debug('reconnect_attempt'); }); socket.on('connect_error', function (error) { that.adapter.log.debug('connect_error: ' + error); }); socket.on('connect_timeout', function (error) { that.adapter.log.debug('connect_timeout'); }); socket.on('reconnect_failed', function (error) { that.adapter.log.debug('reconnect_failed'); });*/ } else { // if server mode if (socket.conn.request.sessionID) { socket._secure = true; socket._sessionID = socket.conn.request.sessionID; // Get user for session that.settings.store.get(socket.conn.request.sessionID, function (err, obj) { if (!obj || !obj.passport) { socket._acl.user = ''; socket.emit('reauthenticate'); socket.disconnect(); } if (socket._authPending) { socket._authPending(!!socket._acl.user, true); delete socket._authPending; } }); } subscribeSocket(socket, 'stateChange'); subscribeSocket(socket, 'objectChange'); subscribeSocket(socket, 'log'); } } function updateConnectedInfo() { if (that.infoTimeout) { clearTimeout(that.infoTimeout); that.infoTimeout = null; } if (that.server.sockets) { let text = ''; let cnt = 0; if (that.server) { let clients = that.server.sockets.connected; for (let i in clients) { if (clients.hasOwnProperty(i)) { text += (text ? ', ' : '') + (clients[i]._name || 'noname'); cnt++; } } } text = '[' + cnt + ']' + text; that.adapter.setState('connected', text, true); } } this.publishAll = function (type, id, obj) { if (id === undefined) { console.log('Problem'); } let clients = this.server.sockets.connected; for (let i in clients) { if (clients.hasOwnProperty(i)) { publish(clients[i], type, id, obj); } } }; this.sendLog = function (obj) { // TODO Build in some threshold if (this.server && this.server.sockets) { this.server.sockets.emit('log', obj); } }; (function __constructor() { if (that.settings.allowAdmin) { // detect event bursts setInterval(function () { if (!eventsThreshold.active) { if (eventsThreshold.count > eventsThreshold.value) { eventsThreshold.accidents++; if (eventsThreshold.accidents >= eventsThreshold.repeatSeconds) { enableEventThreshold(); } } else { eventsThreshold.accidents = 0; } eventsThreshold.count = 0; } else if (new Date().getTime() - eventsThreshold.timeActivated > 60000) { disableEventThreshold(); } }, eventsThreshold.checkInterval); } // it can be used as client too for cloud if (!that.settings.apikey) { if (!that.webServer.__inited) { that.server = socketio.listen(that.webServer); that.webServer.__inited = true; } // force using only websockets if (that.settings.forceWebSockets) that.server.set('transports', ['websocket']); } else { that.server = server; } // socket = socketio.listen(settings.port, (settings.bind && settings.bind !== "0.0.0.0") ? settings.bind : undefined); that.settings.defaultUser = that.settings.defaultUser || 'system.user.admin'; if (!that.settings.defaultUser.match(/^system\.user\./)) that.settings.defaultUser = 'system.user.' + that.settings.defaultUser; if (that.settings.auth && that.server) { that.server.use(function (socket, next) { if (!socket.request._query.user || !socket.request._query.pass) { getUserFromSocket(socket, function (err, user) { if (err || !user) { socket.emit('reauthenticate'); that.adapter.log.error('socket.io ' + (err || 'User not found')); socket.disconnect(); } else { socket._secure = true; that.adapter.calculatePermissions('system.user.' + user, commandsPermissions, function (acl) { let address = getClientAddress(socket); // socket._acl = acl; socket._acl = mergeACLs(address, acl, that.settings.whiteListSettings); next(); }); } }); } else { that.adapter.checkPassword(socket.request._query.user, socket.request._query.pass, function (res) { if (res) { that.adapter.log.debug('Logged in: ' + socket.request._query.user + ', ' + socket.request._query.pass); next(); } else { that.adapter.log.warn('Invalid password or user name: ' + socket.request._query.user + ', ' + socket.request._query.pass); socket.emit('reauthenticate'); next(new Error('Invalid password or user name')); } }); } }); } // Enable cross domain access if (that.settings.crossDomain && that.server.set) { that.server.set('origins', '*:*'); } that.settings.ttl = that.settings.ttl || 3600; that.server.on('connection', that.initSocket); if (settings.port) { that.adapter.log.info((settings.secure ? 'Secure ' : '') + 'socket.io server listening on port ' + settings.port); } if (!that.infoTimeout) that.infoTimeout = setTimeout(updateConnectedInfo, 1000); // if client mode => add event handlers if (that.settings.apikey) { that.initSocket(that.server); } })(); } util.inherits(IOSocket, EventEmitter); module.exports = IOSocket;