diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml index 3f75544..e3bd91d 100644 --- a/.github/workflows/test-and-release.yml +++ b/.github/workflows/test-and-release.yml @@ -136,7 +136,7 @@ jobs: tag_name: ${{ github.ref }} release_name: Release v${{ steps.extract_release.outputs.VERSION }} draft: false - # Prerelease versions create pre-releases on Github + # Prerelease versions create pre-releases on GitHub prerelease: ${{ contains(steps.extract_release.outputs.VERSION, '-') }} body: ${{ steps.extract_release.outputs.BODY }} diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 827874f..0000000 --- a/.npmignore +++ /dev/null @@ -1,14 +0,0 @@ -/**/* -/gulpfile.js -gulpfile.js -!/lib/**/* -!/lib/* -!/admin/**/* -!/admin/* -!/example/* -!/example/**/* -!/io-package.json -!/package.json -!/LICENSE -!/main.js -!/README.md diff --git a/LICENSE b/LICENSE index 7b0810a..02c33d7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014-2021 bluefox +Copyright (c) 2014-2022 bluefox Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6ce9e4d..1e41a06 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ It is useful to read about the [structure of the objects](https://github.com/ioB ## Brief description of concept ### Object -Object is description of data point or group. Group could content other datapoints in this case it called channel. If group consists of other channels in this case it called device. +Object is description of data point or group. Group could content other data points in this case it called channel. If group consists of other channels in this case it called device. Object is meta information that describes data point and could content: max/min value, unit, name, default value, type of value, information for adapter for communication (e.g. ip address) and so on. @@ -38,34 +38,33 @@ State is actual value of the data point and presented by javascript object: } ``` -States change itself very frequently in compare to objects. (Normally objects should be changed once by creation and that's all) +States change itself very frequently in compare to the objects. (Normally objects should be changed once by creation and that's all) ### Acknowledgment -Every state has attribute "ack". It shows the direction of command. +Every state has the attribute "ack". It shows the direction of command. - If ack=false, it means some other adapter wants to control (write) this variable, so that command will be executed (e.g. light will be switched on). - If ack=true, it means that device informs about new value. (e.g. light was switched on manually or motion was detected) -**Example**: we have some home automation adapter (HAA) that has one lamp connected under address *haa.0.lamp1*. -- Lamp can be switched on manually with physical switch or via wifi with he help of HAA. -- If vis wants to switch the lamp on via wifi it should set the new value with ```{value: true, ack: false}```. +**Example**: we have some home automation adapter (HAA) that has one lamp connected under address `haa.0.lamp1`. +- Lamp can be switched on manually with physical switch or via Wi-Fi with the help of HAA. +- If vis wants to switch the lamp on via Wi-Fi it should set the new value with ```{value: true, ack: false}```. - When the lamp is switched on it is normally inform HAA about new state and the value should be immediately overwritten with ```{value: true, ack: true}```. - If the lamp is switched off manually via physical switch it informs HAA about new state with ```{value: false, ack: true}```. ### Quality -Every data point has attribute **q** - *quality*. - +Every data point has an attribute `q` - *quality*. ## Usage It is suggested to use example/conn.js for communication. -After inclusion of conn.js file the global object **servConn** could be used to establish the communication with socketio adapter. +After inclusion of conn.js file the global object `servConn` could be used to establish the communication with socketio adapter. -**servConn** object has hollowing methods: +`servConn` object has hollowing methods: ### init - function (connOptions, connCallbacks, objectsRequired) -**connOptions** - is optional parameter: +`connOptions` - is optional parameter: ``` connOptions = { @@ -83,7 +82,7 @@ var socketSession = ''; // is connOptions.socketSession servConn.namespace = 'myapp'; // is connOptions.name ``` -**connCallbacks** - object with callbacks: +`connCallbacks` - object with callbacks: ``` connCallbacks = { @@ -101,26 +100,25 @@ set new value of some data point. E.g. ```servConn.setState('adapter.0.myvalue', true)``` writes ```{val: true, ack: false}``` into *adapter.0.myvalue*. -- **pointId** - is ID of the state, like *adapter.0.myvalue*, -- **value** - new value of the state, could be simple value (string, number, boolean) or object like ```{val: newValue, ack: false, q: 0}```. +- `pointId` - is ID of the state, like `adapter.0.myvalue`, +- `value` - new value of the state, could be simple value (string, number, boolean) or object like ```{val: newValue, ack: false, q: 0}```. In case if used simple value, "ack" will be set to "false". -- **callback** - ```function (error) {}``` - called when the write of new value into DB is performed (not when the device was controlled). - +- `callback` - ```function (error) {}``` - called when the write of new value into DB is performed (not when the device was controlled). ### getStates - function (IDs, callback) get the states of more than one state. This command normally is called after the connection is established to get the actual states of used data points. -- **IDs** - pattern or array with IDs. Could be omitted to get all states. Patterns could have wildcards, like: '*.STATE', 'haa.0.*' -- **callback** - ```function (error, states) {}``` - *states* is object like ```{'id1': 'state1', 'id2': 'state2', ...}```. *stateX* are objects with the structure described [above](#state). +- `IDs` - pattern or array with IDs. Could be omitted to get all states. Patterns could have wildcards, like: '*.STATE', 'haa.0.*' +- `callback` - ```function (error, states) {}``` - *states* is object like ```{'id1': 'state1', 'id2': 'state2', ...}```. *stateX* are objects with the structure described [above](#state). ### httpGet - function (url, callback) calls this URL from PC, where socketio adapter runs. -- **url** - is address to call. -- **callback** - ```function (data) {}``` - result of the request (html body). +- `url` - is address to call. +- `callback` - ```function (data) {}``` - result of the request (html body). ### logError - function (errorText) @@ -132,7 +130,7 @@ writes error message into controller's log. reads controller configuration like language, temperature units, point or comma delimiter in floats, date format. -- **callback** - ```function (err, config) {}``` - config looks like: +- `callback` - ```function (err, config) {}``` - config looks like: ``` { @@ -158,8 +156,8 @@ reads controller configuration like language, temperature units, point or comma read specific object from DB. With this function the meta information of some object could be read. -- **id** - id of the state, like "haa.0.light1", -- **callback** - ```function (error, obj)``` - obj looks like: +- `id` - id of the state, like "haa.0.light1", +- `callback` - ```function (error, obj)``` - obj looks like: ``` { @@ -198,7 +196,7 @@ read specific object from DB. With this function the meta information of some ob read all objects from DB. -- **callback** - ```function (error, objs)``` - objs looks like: ```{'id1': 'object1', 'id2': 'object2', ...}``` +- `callback` - ```function (error, objs)``` - objs looks like: ```{'id1': 'object1', 'id2': 'object2', ...}``` ### readDir - function (dirName, callback) @@ -246,7 +244,7 @@ Files are stored in DB (or similar) and normally should not be accessed directly ### mkdir - function (dirName, callback) -- **callback** - ```function (error) {}``` +- `callback` - ```function (error) {}``` ### unlink - function (name, callback) @@ -254,53 +252,53 @@ Files are stored in DB (or similar) and normally should not be accessed directly deletes file or directory. Directory must be empty to be deleted. - dirName - name of the directory or file like */mobile.0/data*. -- **callback** - ```function (error) {}``` +- `callback` - ```function (error) {}``` ### readFile - function (filename, callback) -- **callback** - ```function (error, fileData, mimeType)``` +- `callback` - ```function (error, fileData, mimeType)``` ### readFile64 - function (filename, callback) -- **callback** - ```function (error, data)``` - data is ```{mime: mimeType, data: base64data}``` +- `callback` - ```function (error, data)``` - data is ```{mime: mimeType, data: base64data}``` ### writeFile - function (filename, data, mode, callback) -- **callback** - ```function (error) {}``` +- `callback` - ```function (error) {}``` ### writeFile64 - function (filename, data, mode, callback) -- **callback** - ```function (error) {}``` +- `callback` - ```function (error) {}``` ### renameFile - function (oldName, newName, callback) -- **callback** - ```function (error) {}``` +- `callback` - ```function (error) {}``` ### getHistory - function (instance, options, callback) -- **callback** - ```function (error, data, step, sessionId) {}``` +- `callback` - ```function (error, data, step, sessionId) {}``` ### requireLog - function (isRequire, callback) activates/deactivates log receiving for this socket. -- **callback** - ```function (error) {}``` +- `callback` - ```function (error) {}``` ### authEnabled - function () reads if the authentication is enabled and which user is logged in -- **callback** - ```function (authEnabled, currentUser) {}``` +- `callback` - ```function (authEnabled, currentUser) {}``` -If authentication is enabled, so current logged in user will be returned, if auth is disabled, so the default user "running as" will be returned. +If authentication is enabled, so current logged-in user will be returned, if auth is disabled, so the default user "running as" will be returned. ## Tuning Web-Sockets On some web-sockets clients there is performance problem with communication. Sometimes this problem is due to fallback of socket.io communication on long polling mechanism. @@ -312,10 +310,12 @@ You can set option *Force Web-Sockets* to force using only web-sockets transport --> ## Changelog +### __WORK IN PROGRESS__ +* (bluefox) Remove socket-io and use only web sockets ### 3.1.5 (2021-10-22) * (foxriver76) make error logging on failed authentication more specific -* (foxriver76) "request" was repalced by "axios" +* (foxriver76) "request" was replaced by "axios" ### 3.1.4 (2021-01-13) * (Apollon77) Define instanceObject "connected" to prevent warning with js-controller 3.2 @@ -336,7 +336,7 @@ You can set option *Force Web-Sockets* to force using only web-sockets transport * (Apollon77) make sure web adapter gets restarted on socketio adapter upgrade ### 3.0.10 (2020-07-16) -* (Apollon77) Error catched when trying to write an empty base64 value into a file (Sentry ) +* (Apollon77) Error caught when trying to write an empty base64 value into a file (Sentry ) ### 3.0.9 (2020-06-11) * (Apollon77) optimize error handling on webserver initialization again @@ -422,7 +422,7 @@ You can set option *Force Web-Sockets* to force using only web-sockets transport * (bluefox) Fix authentication for app ### 1.7.0 (2016-08-30) -* (bluefox) сompatible only with new admin +* (bluefox) compatible only with new admin ### 1.6.1 (2016-08-29) * (bluefox) fix error by checking user name @@ -540,4 +540,4 @@ You can set option *Force Web-Sockets* to force using only web-sockets transport The MIT License (MIT) -Copyright (c) 2014-2021 bluefox +Copyright (c) 2014-2022 bluefox diff --git a/gulpfile.js b/gulpfile.js index 9e79e1f..2d4ea23 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -154,7 +154,7 @@ function words2languages(src) { fs.mkdirSync(src + 'i18n/' + l); } - fs.writeFileSync(src + 'i18n/' + l + '/translations.json', lang2data(obj)); + fs.writeFileSync(`${src}i18n/${l}/translations.json`, lang2data(obj)); } } else { console.error('Cannot read or parse ' + fileName); @@ -392,7 +392,7 @@ gulp.task('updateReadme', done => { news += '* ' + iopackage.common.news[pkg.version].en; } - fs.writeFileSync('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd); + fs.writeFileSync('README.md', `${readmeStart}### ${version} (${date})\n${news ? news + '\n\n' : '\n'}${readmeEnd}`); } } done(); diff --git a/lib/socket.js b/lib/socket.js index 23a577f..5805595 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -12,14 +12,18 @@ const EventEmitter = require('events'); const util = require('util'); let axios = null; +const ERROR_PERMISSION = 'permissionError'; +const COMMAND_RE_AUTHENTICATE = 'reauthenticate'; + // From settings used only secure, auth and crossDomain -function IOSocket(server, settings, adapter) { +function IOSocket(server, settings, adapter, ignore, store) { if (!(this instanceof IOSocket)) { - return new IOSocket(server, settings, adapter); + return new IOSocket(server, settings, adapter, ignore, store); } - // socketio = require('./ws'); - socketio = require('socket.io'); + socketio = require('./ws'); + + store = store || settings.store; this.settings = settings || {}; this.adapter = adapter; @@ -27,9 +31,9 @@ function IOSocket(server, settings, adapter) { this.subscribes = {}; this.thersholdInterval = null; - let that = this; + const that = this; // do not send too many state updates - let eventsThreshold = { + const eventsThreshold = { count: 0, timeActivated: 0, active: false, @@ -39,21 +43,25 @@ function IOSocket(server, settings, adapter) { checkInterval: 1000 // duration of one check interval }; - // Extract user name from socket + // Extract username 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=(.+)/); + const query = socket.query || (socket.request && socket.request._query) || {}; + + const _cookie = (socket.handshake && socket.handshake.headers && socket.handshake.headers.cookie) || (socket.headers && socket.headers.cookie); + + if (_cookie && !query.user) { + const cookie = decodeURIComponent(_cookie); + const 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); + const c = m[1].split(';')[0]; + const sessionID = cookieParser.signedCookie(c, that.settings.secret); if (sessionID) { // Get user for session wait = true; - that.settings.store.get(sessionID, function (err, obj) { + store.get(sessionID, (err, obj) => { if (obj && obj.passport && obj.passport.user) { socket._sessionID = sessionID; if (typeof callback === 'function') { @@ -65,7 +73,7 @@ function IOSocket(server, settings, adapter) { if (typeof callback === 'function') { callback('unknown user'); } else { - that.adapter.log.warn('[getUserFromSocket] Invalid callback') + that.adapter.log.warn('[getUserFromSocket] Invalid callback'); } } }); @@ -73,8 +81,9 @@ function IOSocket(server, settings, adapter) { } } if (!wait) { - let user = socket.request._query.user; - let pass = socket.request._query.pass; + const query = socket.query || (socket.request && socket.request._query) || {}; + const user = query.user; + const pass = query.pass; if (user && pass) { wait = true; that.adapter.checkPassword(user, pass, function (res) { @@ -83,14 +92,14 @@ function IOSocket(server, settings, adapter) { if (typeof callback === 'function') { callback(null, user); } else { - that.adapter.log.warn('[getUserFromSocket] Invalid callback') + 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') + that.adapter.log.warn('[getUserFromSocket] Invalid callback'); } } }); @@ -113,16 +122,16 @@ function IOSocket(server, settings, adapter) { eventsThreshold.timeActivated = 0; that.adapter.log.info('Subscribe on all states again'); - setTimeout(function () { + setTimeout(() => { if (readAll) { - that.adapter.getForeignStates('*', function (err, res) { + that.adapter.getForeignStates('*', ()/*(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])) { + /*for (let id in res) { + if (Object.prototype.hasOwnProperty.call(res, id) && JSON.stringify(states[id]) !== JSON.stringify(res[id])) { that.server && that.server.sockets && that.server.sockets.emit('stateChange', id, res[id]); states[id] = res[id]; } - } + }*/ }); } @@ -138,9 +147,9 @@ function IOSocket(server, settings, adapter) { 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(); + setTimeout(() => { + 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 = Date.now(); that.server && that.server.sockets && that.server.sockets.emit('eventsThreshold', true); that.adapter.unsubscribeForeignStates('*'); @@ -151,11 +160,13 @@ function IOSocket(server, settings, adapter) { function getClientAddress(socket) { let address; - if (socket.handshake) { + address = socket.connection && socket.connection.remoteAddress; + + if (!address && socket.handshake) { address = socket.handshake.address; } - if (!address && socket.request && socket.request.connection) { - address = socket.request.connection.remoteAddress; + if (!address && socket.conn.request && socket.conn.request.connection) { + address = socket.conn.request.connection.remoteAddress; } return address; } @@ -165,14 +176,13 @@ function IOSocket(server, settings, adapter) { if (that.settings.auth) { getUserFromSocket(socket, (err, user) => { if (err || !user) { - socket.emit('reauthenticate'); - that.adapter.log.error(`socket.io [init] ${err || 'No user found in cookies'}`); - socket.disconnect(); + socket.emit(COMMAND_RE_AUTHENTICATE); + that.adapter.log.error(`socket.io ${err || 'No user found in cookies'}`); } 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); + that.adapter.calculatePermissions('system.user.' + user, commandsPermissions, acl => { + const address = getClientAddress(socket); // socket._acl = acl; socket._acl = mergeACLs(address, acl, that.settings.whiteListSettings); socketEvents(socket, address); @@ -180,15 +190,15 @@ function IOSocket(server, settings, adapter) { } }); } else { - that.adapter.calculatePermissions(that.settings.defaultUser, commandsPermissions, function (acl) { - let address = getClientAddress(socket); + that.adapter.calculatePermissions(that.settings.defaultUser, commandsPermissions, acl => { + const address = getClientAddress(socket); // socket._acl = acl; socket._acl = mergeACLs(address, acl, that.settings.whiteListSettings); socketEvents(socket, address); }); } } else { - let address = getClientAddress(socket); + const address = getClientAddress(socket); socketEvents(socket, address); } }; @@ -203,29 +213,26 @@ function IOSocket(server, settings, adapter) { } // check IPv6 or IPv4 direct match - if (whiteList.hasOwnProperty(address)) { + if (Object.prototype.hasOwnProperty.call(whiteList, address)) { return address; } // check if address is IPv4 - let addressParts = address.split('.'); + const 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; - }); + const wildCardIps = Object.keys(whiteList).filter(key => key.includes('*')); - - if (wildCardIps.length === 0) { + if (!wildCardIps.length) { // no wild carded ips => no ip configured return null; } wildCardIps.forEach(function (ip) { - let ipParts = ip.split('.'); + const ipParts = ip.split('.'); if (ipParts.length === 4) { for (let i = 0; i < 4; i++) { if (ipParts[i] === '*' && i === 3) { @@ -233,7 +240,9 @@ function IOSocket(server, settings, adapter) { return ip; } - if (ipParts[i] !== addressParts[i]) break; + if (ipParts[i] !== addressParts[i]) { + break; + } } } }); @@ -247,15 +256,15 @@ function IOSocket(server, settings, adapter) { function mergeACLs(address, acl, whiteList) { if (whiteList && address) { - let whiteListAcl = getPermissionsForIp(address, whiteList); + const 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)) { + ['object', 'state', 'file'].forEach(key => { + if (Object.prototype.hasOwnProperty.call(acl, key) && Object.prototype.hasOwnProperty.call(whiteListAcl, key)) { + Object.keys(acl[key]).forEach(permission => { + if (Object.prototype.hasOwnProperty.call(whiteListAcl[key], permission)) { acl[key][permission] = acl[key][permission] && whiteListAcl[key][permission]; } - }) + }); } }); @@ -274,8 +283,12 @@ function IOSocket(server, settings, adapter) { } pattern = pattern.toString(); if (pattern !== '*') { - if (pattern[0] === '*' && pattern[pattern.length - 1] !== '*') pattern += '$'; - if (pattern[0] !== '*' && pattern[pattern.length - 1] === '*') pattern = '^' + 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, '\\.'); @@ -292,20 +305,23 @@ function IOSocket(server, settings, adapter) { if (socket) { socket._subscribe = socket._subscribe || {}; } - if (!this.subscribes[type]) this.subscribes[type] = {}; + 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; + if (s[i].pattern === pattern) { + return; + } } } - let p = pattern2RegEx(pattern); + const p = pattern2RegEx(pattern); if (p === null) { - this.adapter.log.warn('Empty pattern on subscribe!'); - return; + return this.adapter.log.warn('Empty pattern!'); } if (socket) { s.push({pattern: pattern, regex: new RegExp(p)}); @@ -320,7 +336,9 @@ function IOSocket(server, settings, adapter) { this.adapter.subscribeForeignObjects(pattern); } } else if (type === 'log') { - this.adapter.requireLog && this.adapter.requireLog(true); + if (this.adapter.requireLog) { + this.adapter.requireLog(true); + } } } else { this.subscribes[type][pattern]++; @@ -329,8 +347,8 @@ function IOSocket(server, settings, adapter) { function showSubscribes(socket, type) { if (socket && socket._subscribe) { - let s = socket._subscribe[type] || []; - let ids = []; + const s = socket._subscribe[type] || []; + const ids = []; for (let i = 0; i < s.length; i++) { ids.push(s[i].pattern); } @@ -342,10 +360,14 @@ function IOSocket(server, settings, adapter) { this.unsubscribe = function (socket, type, pattern) { //console.log((socket._name || socket.id) + ' unsubscribe ' + pattern); - if (!this.subscribes[type]) this.subscribes[type] = {}; + if (!this.subscribes[type]) { + this.subscribes[type] = {}; + } if (socket) { - if (!socket._subscribe || !socket._subscribe[type]) return; + 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) { @@ -358,10 +380,14 @@ function IOSocket(server, settings, adapter) { this.adapter.unsubscribeForeignStates(pattern); } else if (type === 'objectChange') { //console.log((socket._name || socket.id) + ' unsubscribeForeignObjects ' + pattern); - this.adapter.unsubscribeForeignObjects && this.adapter.unsubscribeForeignObjects(pattern); + if (this.adapter.unsubscribeForeignObjects) { + this.adapter.unsubscribeForeignObjects(pattern); + } } else if (type === 'log') { //console.log((socket._name || socket.id) + ' requireLog false'); - this.adapter.requireLog && this.adapter.requireLog(false); + if (this.adapter.requireLog) { + this.adapter.requireLog(false); + } } delete this.subscribes[type][pattern]; } @@ -382,26 +408,36 @@ function IOSocket(server, settings, adapter) { this.adapter.unsubscribeForeignStates(pattern); } else if (type === 'objectChange') { //console.log((socket._name || socket.id) + ' unsubscribeForeignObjects ' + pattern); - this.adapter.unsubscribeForeignObjects && this.adapter.unsubscribeForeignObjects(pattern); + if (this.adapter.unsubscribeForeignObjects) { + this.adapter.unsubscribeForeignObjects(pattern); + } } else if (type === 'log') { //console.log((socket._name || socket.id) + ' requireLog false'); - this.adapter.requireLog && this.adapter.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 (!Object.prototype.hasOwnProperty.call(this.subscribes[type], 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); - this.adapter.unsubscribeForeignObjects && this.adapter.unsubscribeForeignObjects(pattern); + if (this.adapter.unsubscribeForeignObjects) { + this.adapter.unsubscribeForeignObjects(pattern); + } } else if (type === 'log') { //console.log((socket._name || socket.id) + ' requireLog false'); - this.adapter.requireLog && this.adapter.requireLog(false); + if (this.adapter.requireLog) { + this.adapter.requireLog(false); + } } delete this.subscribes[type][pattern]; } @@ -410,8 +446,8 @@ function IOSocket(server, settings, adapter) { this.unsubscribeAll = function () { if (this.server && this.server.sockets) { - for (let s in this.server.sockets) { - if (this.server.sockets.hasOwnProperty(s)) { + for (const s in this.server.sockets) { + if (Object.prototype.hasOwnProperty.call(this.server.sockets, s)) { unsubscribeSocket(s, 'stateChange'); unsubscribeSocket(s, 'objectChange'); unsubscribeSocket(s, 'log'); @@ -421,19 +457,25 @@ function IOSocket(server, settings, adapter) { }; function unsubscribeSocket(socket, type) { - if (!socket._subscribe || !socket._subscribe[type]) return; + if (!socket._subscribe || !socket._subscribe[type]) { + return; + } for (let i = 0; i < socket._subscribe[type].length; i++) { - let pattern = socket._subscribe[type][i].pattern; + const 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') { - that.adapter.unsubscribeForeignObjects && that.adapter.unsubscribeForeignObjects(pattern); + if (that.adapter.unsubscribeForeignObjects) { + that.adapter.unsubscribeForeignObjects(pattern); + } } else if (type === 'log') { - that.adapter.requireLog && that.adapter.requireLog(false); + if (that.adapter.requireLog) { + that.adapter.requireLog(false); + } } delete that.subscribes[type][pattern]; } @@ -443,18 +485,24 @@ function IOSocket(server, settings, adapter) { function subscribeSocket(socket, type) { //console.log((socket._name || socket.id) + ' subscribeSocket'); - if (!socket._subscribe || !socket._subscribe[type]) return; + if (!socket._subscribe || !socket._subscribe[type]) { + return; + } for (let i = 0; i < socket._subscribe[type].length; i++) { - let pattern = socket._subscribe[type][i].pattern; + const 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') { - that.adapter.subscribeForeignObjects && that.adapter.subscribeForeignObjects(pattern); + if (that.adapter.subscribeForeignObjects) { + that.adapter.subscribeForeignObjects(pattern); + } } else if (type === 'log') { - that.adapter.requireLog && that.adapter.requireLog(true); + if (that.adapter.requireLog) { + that.adapter.requireLog(true); + } } } else { that.subscribes[type][pattern]++; @@ -463,46 +511,70 @@ function IOSocket(server, settings, adapter) { } function publish(socket, type, id, obj) { - if (!socket._subscribe || !socket._subscribe[type]) return; - let s = socket._subscribe[type]; + if (!socket._subscribe || !socket._subscribe[type]) { + return; + } + const 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; + return socket.emit(type, id, obj); } } } + function waitForSessionEnd(socket) { + if (socket._sessionTimer) { + clearTimeout(socket._sessionTimer); + socket._sessionTimer = null; + } + const sessionId = socket._sessionID; + store && store.get(sessionId, (err, obj) => { + if (obj) { + const expires = new Date(obj.cookie.expires); + const interval = expires.getTime() - Date.now(); + if (interval > 0) { + socket._sessionTimer = socket._sessionTimer || setTimeout(() => waitForSessionEnd(socket), interval > 3600000 ? 3600000 : interval); + socket.emit('expire', expires.getTime()); + } else { + adapter.log.warn('REAUTHENTICATE!'); + socket.emit(COMMAND_RE_AUTHENTICATE); + } + } else { + adapter.log.warn('REAUTHENTICATE!'); + socket && socket.emit && socket.emit(COMMAND_RE_AUTHENTICATE); + } + }); + } + // update session ID, but not ofter 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); - } + const sessionId = socket._sessionID; + const now = Date.now(); + if (sessionId && (!socket._lastUpdate || now - socket._lastUpdate > 10000)) { + socket._lastUpdate = now; + store && store.get(sessionId, (err, obj) => { + // obj = {"cookie":{"originalMaxAge":2592000000,"expires":"2020-09-24T18:09:50.377Z","httpOnly":true,"path":"/"},"passport":{"user":"admin"}} + if (obj) { + // start timer + !socket._sessionTimer && waitForSessionEnd(socket); + /*obj.ttl = obj.ttl || (new Date(obj.cookie.expires).getTime() - now); + const expires = new Date(); + expires.setMilliseconds(expires.getMilliseconds() + obj.ttl + 10000); + obj.cookie.expires = expires.toISOString(); + console.log('Session ' + sessionId + ' expires on ' + obj.cookie.expires); + + store.set(sessionId, obj);*/ + } else { + adapter.log.warn('REAUTHENTICATE!'); + socket.emit(COMMAND_RE_AUTHENTICATE); + } + }); } return true; } // static information - let commandsPermissions = { + const commandsPermissions = { getObject: {type: 'object', operation: 'read'}, getObjects: {type: 'object', operation: 'list'}, getObjectView: {type: 'object', operation: 'list'}, @@ -600,12 +672,9 @@ function IOSocket(server, settings, adapter) { callback('Cannot delete user, while is system user'); } } else { - that.adapter.delForeignObject('system.user.' + user, options, err => { + that.adapter.delForeignObject('system.user.' + user, options, err => // Remove this user from all groups in web client - if (typeof callback === 'function') { - callback(err); - } - }); + typeof callback === 'function' && callback(err)); } } }); @@ -631,17 +700,12 @@ function IOSocket(server, settings, adapter) { 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; + return typeof callback === 'function' && callback('Invalid characters in the group name. Only following special characters are allowed: -@+$§=?!&# and letters'); } that.adapter.getForeignObject('system.group.' + group, options, (err, obj) => { if (obj) { - if (typeof callback === 'function') { - callback('Group yet exists'); - } + typeof callback === 'function' && callback('Group yet exists'); } else { obj = { _id: 'system.group.' + group, @@ -653,11 +717,8 @@ function IOSocket(server, settings, adapter) { acl: acl } }; - that.adapter.setForeignObject('system.group.' + group, obj, options, err => { - if (typeof callback === 'function') { - callback(err, obj); - } - }); + that.adapter.setForeignObject('system.group.' + group, obj, options, err => + typeof callback === 'function' && callback(err, obj)); } }); } @@ -665,20 +726,14 @@ function IOSocket(server, settings, adapter) { 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'); - } + 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'); - } + 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); - } + typeof callback === 'function' && callback(err); }); } } @@ -696,7 +751,7 @@ function IOSocket(server, settings, adapter) { 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 + '"'); + that.adapter.log.warn(`No permission for "${socket._acl.user}" to call ${command}. Need "${commandsPermissions[command].type}"."${commandsPermissions[command].operation}"`); } } else { return true; @@ -706,20 +761,17 @@ function IOSocket(server, settings, adapter) { } if (typeof callback === 'function') { - callback('permissionError'); + callback(ERROR_PERMISSION); } else { if (commandsPermissions[command]) { - socket.emit('permissionError', { + socket.emit(ERROR_PERMISSION, { command: command, type: commandsPermissions[command].type, operation: commandsPermissions[command].operation, arg: arg }); } else { - socket.emit('permissionError', { - command: command, - arg: arg - }); + socket.emit(ERROR_PERMISSION, {command, arg}); } } return false; @@ -734,25 +786,24 @@ function IOSocket(server, settings, adapter) { return true; } - if (options.user !== 'system.user.admin' && - options.groups.indexOf('system.group.administrator') === -1) { + if (options.user !== 'system.user.admin' && !options.groups.includes('system.group.administrator')) { if (obj.acl.owner !== options.user) { // Check if the user is in the group - if (options.groups.indexOf(obj.acl.ownerGroup) !== -1) { + if (options.groups.includes(obj.acl.ownerGroup)) { // Check group rights if (!(obj.acl.object & (flag << 4))) { - return false + return false; } } else { // everybody if (!(obj.acl.object & flag)) { - return false + return false; } } } else { // Check group rights if (!(obj.acl.object & (flag << 8))) { - return false + return false; } } } @@ -771,8 +822,8 @@ function IOSocket(server, settings, adapter) { err && that.adapter.log.error('[getForeignObject]: ' + err); if (obj) { obj.common.enabled = false; - setTimeout(function () { - that.adapter.setForeignObject(obj._id, obj, function (err) { + setTimeout(() => { + that.adapter.setForeignObject(obj._id, obj, err => { err && that.adapter.log.error('[setForeignObject]: ' + err); callback && callback(); }); @@ -791,8 +842,8 @@ function IOSocket(server, settings, adapter) { err && that.adapter.log.error('redirectAdapter [getForeignObject]: ' + err); if (obj) { obj.native.cloudUrl = url; - setTimeout(function () { - that.adapter.setForeignObject(obj._id, obj, function (err) { + setTimeout(() => { + that.adapter.setForeignObject(obj._id, obj, err => { err && that.adapter.log.error('redirectAdapter [setForeignObject]: ' + err); callback && callback(); }); @@ -810,27 +861,25 @@ function IOSocket(server, settings, adapter) { function socketEvents(socket, address) { if (socket.conn) { - that.adapter.log.info(`==> Connected ${socket._acl.user} from ${address}`); + 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); - } + 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}]`); + that.adapter.log.debug(`${new Date().toISOString()} Request authenticate [${socket._acl.user}]`); if (typeof user === 'function') { callback = user; - user = undefined; + // user = undefined; } if (socket._acl.user !== null) { if (typeof callback === 'function') { callback(true, socket._secure); } } else { - that.adapter.log.debug(`${(new Date()).toISOString()} Request authenticate [${socket._acl.user}]`); + that.adapter.log.debug(`${new Date().toISOString()} Request authenticate [${socket._acl.user}]`); socket._authPending = callback; } }); @@ -840,16 +889,13 @@ function IOSocket(server, settings, adapter) { updateSession(socket); if (this._name === undefined) { this._name = name; - if (!that.infoTimeout) { - that.infoTimeout = setTimeout(updateConnectedInfo, 1000); - } + 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(); - } + + typeof cb === 'function' && cb(); }); /* @@ -867,7 +913,7 @@ function IOSocket(server, settings, adapter) { if (typeof callback === 'function') { callback(err, objs); } else { - that.adapter.log.warn('[getObjects] Invalid callback') + that.adapter.log.warn('[getObjects] Invalid callback'); } }); } @@ -928,16 +974,6 @@ function IOSocket(server, settings, adapter) { } }); - socket.on('getForeignStates', 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); }); @@ -949,11 +985,11 @@ function IOSocket(server, settings, adapter) { that.adapter.getObjectList({include_docs: true}, function (err, res) { that.adapter.log.info('received all objects'); res = res.rows; - let objects = {}; + const objects = {}; if (socket._acl && socket._acl.user !== 'system.user.admin' && - socket._acl.groups.indexOf('system.group.administrator') === -1) { + !socket._acl.groups.includes('system.group.administrator')) { 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; @@ -962,7 +998,7 @@ function IOSocket(server, settings, adapter) { if (typeof callback === 'function') { callback(null, objects); } else { - that.adapter.log.warn('[getAllObjects] Invalid callback') + that.adapter.log.warn('[getAllObjects] Invalid callback'); } } else { for (let j = 0; j < res.length; j++) { @@ -971,7 +1007,7 @@ function IOSocket(server, settings, adapter) { if (typeof callback === 'function') { callback(null, objects); } else { - that.adapter.log.warn('[getAllObjects] Invalid callback') + that.adapter.log.warn('[getAllObjects] Invalid callback'); } } }); @@ -997,20 +1033,22 @@ function IOSocket(server, settings, adapter) { if (typeof callback === 'function') { callback(ip, data.rows[i].value); } else { - that.adapter.log.warn('[getHostByIp] Invalid callback') + 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; + const net = data.rows[i].value.native.hardware.networkInterfaces; + for (const eth in net) { + if (!Object.prototype.hasOwnProperty.call(net, 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') + that.adapter.log.warn('[getHostByIp] Invalid callback'); } return; } @@ -1023,7 +1061,7 @@ function IOSocket(server, settings, adapter) { if (typeof callback === 'function') { callback(ip, null); } else { - that.adapter.log.warn('[getHostByIp] Invalid callback') + that.adapter.log.warn('[getHostByIp] Invalid callback'); } }); } @@ -1040,7 +1078,7 @@ function IOSocket(server, settings, adapter) { if (typeof callback === 'function') { callback(err, objs); } else { - that.adapter.log.warn('[getObjects] Invalid callback') + that.adapter.log.warn('[getObjects] Invalid callback'); } }); } @@ -1052,7 +1090,7 @@ function IOSocket(server, settings, adapter) { if (typeof callback === 'function') { callback(err, objs); } else { - that.adapter.log.warn('[getObjects] Invalid callback') + that.adapter.log.warn('[getObjects] Invalid callback'); } }); } @@ -1061,11 +1099,13 @@ function IOSocket(server, settings, adapter) { socket.on('requireLog', function (isEnabled, callback) { if (updateSession(socket) && checkPermissions(socket, 'setObject', callback)) { if (isEnabled) { - that.subscribe(this, 'log', 'dummy') + that.subscribe(this, 'log', 'dummy'); } else { - that.unsubscribe(this, 'log', 'dummy') + that.unsubscribe(this, 'log', 'dummy'); + } + if (that.adapter.log.level === 'debug') { + showSubscribes(socket, 'log'); } - if (that.adapter.log.level === 'debug') showSubscribes(socket, 'log'); if (typeof callback === 'function') { setImmediate(callback, null); @@ -1075,24 +1115,24 @@ function IOSocket(server, settings, adapter) { socket.on('readLogs', function (callback) { if (updateSession(socket) && checkPermissions(socket, 'readLogs', callback)) { - let result = {list: []}; + const result = {list: []}; // deliver file list try { - let config = adapter.systemConfig; + const 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') { + for (const transport in config.log.transport) { + if (Object.prototype.hasOwnProperty.call(config.log.transport, transport) && config.log.transport[transport].type === 'file') { let filename = config.log.transport[transport].filename || 'log/'; - let parts = filename.replace(/\\/g, '/').split('/'); + const 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); + const files = fs.readdirSync(filename); for (let f = 0; f < files.length; f++) { try { if (!fs.lstatSync(filename + '/' + files[f]).isDirectory()) { @@ -1130,7 +1170,7 @@ function IOSocket(server, settings, adapter) { } } else { if (typeof callback === 'function') { - callback('permissionError'); + callback(ERROR_PERMISSION); } } }); @@ -1144,7 +1184,9 @@ function IOSocket(server, settings, adapter) { socket.on('setState', function (id, state, callback) { if (updateSession(socket) && checkPermissions(socket, 'setState', callback, id)) { - if (typeof state !== 'object') state = {val: state}; + 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); @@ -1195,7 +1237,7 @@ function IOSocket(server, settings, adapter) { // 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.log.debug(`cmdExec on ${host}(${id}): ${cmd}`); that.adapter.sendToHost(host, 'cmdExec', {data: cmd, id: id}); } }); @@ -1238,7 +1280,9 @@ function IOSocket(server, settings, adapter) { } else { that.subscribe(this, 'stateChange', pattern); } - if (that.adapter.log.level === 'debug') showSubscribes(socket, 'stateChange'); + if (that.adapter.log.level === 'debug') { + showSubscribes(socket, 'stateChange'); + } if (typeof callback === 'function') { setImmediate(callback, null); } @@ -1254,14 +1298,15 @@ function IOSocket(server, settings, adapter) { } else { that.unsubscribe(this, 'stateChange', pattern); } - if (that.adapter.log.level === 'debug') showSubscribes(socket, 'stateChange'); + 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)) { @@ -1271,7 +1316,7 @@ function IOSocket(server, settings, adapter) { if (typeof callback === 'function') { callback(err, data, step, sessionId); } else { - that.adapter.log.warn('[getHistory] Invalid callback') + that.adapter.log.warn('[getHistory] Invalid callback'); } }); } @@ -1305,7 +1350,7 @@ function IOSocket(server, settings, adapter) { 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)) { + if (updateSession(socket) && checkPermissions(socket, protectedCommands.includes(command) ? 'cmdExec' : 'sendToHost', callback, command)) { that.adapter.sendToHost(host, command, message, callback); } }); @@ -1315,7 +1360,7 @@ function IOSocket(server, settings, adapter) { if (typeof callback === 'function') { callback(that.settings.auth, (socket._acl.user || '').replace(/^system\.user\./, '')); } else { - that.adapter.log.warn('[authEnabled] Invalid callback') + that.adapter.log.warn('[authEnabled] Invalid callback'); } } }); @@ -1329,14 +1374,14 @@ function IOSocket(server, settings, adapter) { 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) { + that.adapter.readFile(_adapter, fileName, {user: socket._acl.user}, (err, buffer, type) => { let data64; if (buffer) { if (type === 'application/json') { - data64 = new Buffer(encodeURIComponent(buffer)).toString('base64'); + data64 = Buffer.from(encodeURIComponent(buffer)).toString('base64'); } else { if (typeof buffer === 'string') { - data64 = new Buffer(buffer).toString('base64'); + data64 = Buffer.from(buffer).toString('base64'); } else { data64 = buffer.toString('base64'); } @@ -1347,7 +1392,7 @@ function IOSocket(server, settings, adapter) { if (typeof callback === 'function') { callback(err, data64 || '', type); } else { - that.adapter.log.warn('[readFile64] Invalid callback') + that.adapter.log.warn('[readFile64] Invalid callback'); } }); } @@ -1358,23 +1403,17 @@ function IOSocket(server, settings, adapter) { callback = options; options = {user: socket._acl.user}; } - if (!options) options = {}; + options = options || {}; options.user = socket._acl.user; if (updateSession(socket) && checkPermissions(socket, 'writeFile64', callback, fileName)) { if (!data64) { - if (typeof callback === 'function') { - callback('No data provided'); - } - return; + return typeof callback === 'function' && callback('No data provided'); } //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); - } - }); + const buffer = Buffer.from(data64, 'base64'); + that.adapter.writeFile(_adapter, fileName, buffer, options, err => + typeof callback === 'function' && callback(err)); } }); @@ -1383,7 +1422,7 @@ function IOSocket(server, settings, adapter) { callback = options; options = {user: socket._acl.user}; } - if (!options) options = {}; + options = options || {}; options.user = socket._acl.user; if (updateSession(socket) && checkPermissions(socket, 'writeFile', callback, fileName)) { that.adapter.writeFile(_adapter, fileName, data, options, callback); @@ -1416,7 +1455,9 @@ function IOSocket(server, settings, adapter) { options = options || {}; options.user = socket._acl.user; - if (options.filter === undefined) options.filter = true; + if (options.filter === undefined) { + options.filter = true; + } if (updateSession(socket) && checkPermissions(socket, 'readDir', callback, dirName)) { that.adapter.readDir(_adapter, dirName, options, callback); @@ -1431,7 +1472,9 @@ function IOSocket(server, settings, adapter) { options = options || {}; options.user = socket._acl.user; - if (options.filter === undefined) options.filter = true; + if (options.filter === undefined) { + options.filter = true; + } if (updateSession(socket) && checkPermissions(socket, 'chmodFile', callback, dirName)) { that.adapter.chmodFile(_adapter, dirName, options, callback); @@ -1444,8 +1487,11 @@ function IOSocket(server, settings, adapter) { unsubscribeSocket(this, 'stateChange'); unsubscribeSocket(this, 'objectChange'); unsubscribeSocket(this, 'log'); - if (!that.infoTimeout) { - that.infoTimeout = setTimeout(updateConnectedInfo, 1000); + that.infoTimeout = that.infoTimeout || setTimeout(updateConnectedInfo, 1000); + + if (socket._sessionTimer) { + clearTimeout(socket._sessionTimer); + socket._sessionTimer = null; } // if client mode @@ -1474,7 +1520,7 @@ function IOSocket(server, settings, adapter) { if (typeof callback === 'function') { callback(null, socket._acl); } else { - that.adapter.log.warn('[getUserPermissions] Invalid callback') + that.adapter.log.warn('[getUserPermissions] Invalid callback'); } } }); @@ -1496,7 +1542,7 @@ function IOSocket(server, settings, adapter) { }); socket.on('cloudConnect', function () { - // do not autosubscribe. The client must resubscribe all states anew + // do not auto-subscribe. The client must resubscribe all states anew // subscribeSocket(socket, 'stateChange'); // subscribeSocket(socket, 'objectChange'); // subscribeSocket(socket, 'log'); @@ -1519,7 +1565,7 @@ function IOSocket(server, settings, adapter) { socket._apiKeyOk = false; // 2018_01_20 workaround for pro: Remove it after next pro maintenance - if (that.settings.apikey.indexOf('@pro_') !== -1) { + if (that.settings.apikey.includes('@pro_')) { socket._apiKeyOk = true; that.emit && that.emit('connect'); } @@ -1554,7 +1600,7 @@ function IOSocket(server, settings, adapter) { that.emit && that.emit('connect'); } else { - if (err.indexOf('Please buy remote access to use pro.') !== -1) { + if (err.includes('Please buy remote access to use pro.')) { stopAdapter('Please buy remote access to use pro.'); } that.adapter.log.error(err); @@ -1568,8 +1614,7 @@ function IOSocket(server, settings, adapter) { socket._acl.user = obj.passport.user; } else { socket._acl.user = ''; - socket.emit('reauthenticate'); - socket.disconnect(); + socket.emit(COMMAND_RE_AUTHENTICATE); } if (socket._authPending) { socket._authPending(!!socket._acl.user, true); @@ -1600,15 +1645,13 @@ function IOSocket(server, settings, adapter) { });*/ } else { // if server mode - if (socket.conn.request.sessionID) { + if (socket._sessionID && adapter.config.auth) { socket._secure = true; - socket._sessionID = socket.conn.request.sessionID; // Get user for session - that.settings.store.get(socket.conn.request.sessionID, function (err, obj) { + store && store.get(socket._sessionID, (err, obj) => { if (!obj || !obj.passport) { socket._acl.user = ''; - socket.emit('reauthenticate'); - socket.disconnect(); + socket.emit(COMMAND_RE_AUTHENTICATE); } if (socket._authPending) { socket._authPending(!!socket._acl.user, true); @@ -1629,12 +1672,12 @@ function IOSocket(server, settings, adapter) { that.infoTimeout = null; } if (that.server && that.server.sockets) { - let clientsArray = []; + const clientsArray = []; if (that.server) { - let clients = that.server.sockets.connected; + const clients = that.server.sockets.connected; - for (let i in clients) { - if (clients.hasOwnProperty(i)) { + for (const i in clients) { + if (Object.prototype.hasOwnProperty.call(clients, i)) { clientsArray.push(clients[i]._name || 'noname'); } } @@ -1652,10 +1695,10 @@ function IOSocket(server, settings, adapter) { return; } - let clients = this.server.sockets.connected; + const clients = this.server.sockets.connected; - for (let i in clients) { - if (clients.hasOwnProperty(i)) { + for (const i in clients) { + if (Object.prototype.hasOwnProperty.call(clients, i)) { publish(clients[i], type, id, obj); } } @@ -1674,7 +1717,7 @@ function IOSocket(server, settings, adapter) { that.server && that.server.close && that.server.close(); that.server = null; } catch (e) { - + // ignore } this.thersholdInterval && clearInterval(this.thersholdInterval); this.thersholdInterval = null; @@ -1695,11 +1738,11 @@ function IOSocket(server, settings, adapter) { eventsThreshold.accidents = 0; } eventsThreshold.count = 0; - } else if (new Date().getTime() - eventsThreshold.timeActivated > 60000) { + } else if (Date.now() - eventsThreshold.timeActivated > 60000) { disableEventThreshold(); } }, eventsThreshold.checkInterval); - } + } // it can be used as client too for cloud if (!that.settings.apikey) { @@ -1720,8 +1763,8 @@ function IOSocket(server, settings, adapter) { // We want to have the same client files as provided by socket.io // So lookup socket.io first ... const socketIoDir = require.resolve('socket.io'); - // ... and then from their (with normally unneeded failback to "us") - // we lookup the client library + // ... and then from their (with normally unneeded fallback to "us") + // we look up the client library const clientPath = require.resolve('socket.io-client', { paths: [path.dirname(socketIoDir), __dirname] }); @@ -1755,20 +1798,23 @@ function IOSocket(server, settings, adapter) { // 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.defaultUser.match(/^system\.user\./)) { + that.settings.defaultUser = 'system.user.' + that.settings.defaultUser; + } if (that.settings.auth && that.server) { - that.server.use((socket, next) => { - if (!socket.request._query.user || !socket.request._query.pass) { - getUserFromSocket(socket, (err, user) => { + that.server.use(function (socket, next) { + const query = socket.query || (socket.request && socket.request._query) || {}; + + if (!query.user || !query.pass) { + getUserFromSocket(socket, function (err, user) { if (err || !user) { - socket.emit('reauthenticate'); - that.adapter.log.error(`socket.io [use] ${err || 'User not found'}`); - socket.disconnect(); + socket.emit(COMMAND_RE_AUTHENTICATE); + that.adapter.log.error('socket.io ' + (err || 'User not found')); } else { socket._secure = true; - that.adapter.calculatePermissions(`system.user.${user}`, commandsPermissions, function (acl) { - let address = getClientAddress(socket); + that.adapter.calculatePermissions('system.user.' + user, commandsPermissions, acl => { + const address = getClientAddress(socket); // socket._acl = acl; socket._acl = mergeACLs(address, acl, that.settings.whiteListSettings); next(); @@ -1776,13 +1822,14 @@ function IOSocket(server, settings, adapter) { } }); } else { - that.adapter.checkPassword(socket.request._query.user, socket.request._query.pass, function (res) { + const query = socket.query || (socket.request && socket.request._query) || {}; + that.adapter.checkPassword(query.user, query.pass, res => { if (res) { - that.adapter.log.debug(`Logged in: ${socket.request._query.user}, ${socket.request._query.pass}`); + that.adapter.log.debug(`Logged in: ${query.user}, ${query.pass}`); next(); } else { - that.adapter.log.warn(`Invalid password or user name: ${socket.request._query.user}, ${socket.request._query.pass}`); - socket.emit('reauthenticate'); + that.adapter.log.warn(`Invalid password or user name: ${query.user}, ${query.pass}`); + socket.emit(COMMAND_RE_AUTHENTICATE); next(new Error('Invalid password or user name')); } }); @@ -1804,7 +1851,9 @@ function IOSocket(server, settings, adapter) { that.adapter.log.info(`${settings.secure ? 'Secure ' : ''}socket.io server listening on port ${settings.port}`); } - that.infoTimeout = that.infoTimeout || setTimeout(updateConnectedInfo, 1000); + if (!that.infoTimeout) { + that.infoTimeout = setTimeout(updateConnectedInfo, 1000); + } // if client mode => add event handlers if (that.settings.apikey) { diff --git a/lib/ws.js b/lib/ws.js index ab4189b..4338797 100644 --- a/lib/ws.js +++ b/lib/ws.js @@ -4,8 +4,8 @@ /* jshint -W061 */ 'use strict'; -const WebSocket = require('ws'); -const querystring = require('querystring'); +const WebSocket = require('ws'); +const querystring = require('querystring'); const MESSAGE_TYPES = { MESSAGE: 0, @@ -16,7 +16,7 @@ const MESSAGE_TYPES = { const DEBUG = false; -function Socket (ws, sessionID, name) { +function Socket(ws, sessionID, name) { this.ws = ws; const handlers = {}; let lastPong = Date.now(); @@ -104,39 +104,40 @@ function Socket (ws, sessionID, name) { this.withCallback = (name, id, args) => { if (!args || !args.length) { setImmediate(() => - handlers[name].forEach(cb => cb.call(this, (arg1, arg2, arg3, arg4, arg5) => + handlers[name] && handlers[name].forEach(cb => cb.call(this, (arg1, arg2, arg3, arg4, arg5) => this.responseWithCallback(name, id, arg1, arg2, arg3, arg4, arg5)))); } else if (args.length === 1) { setImmediate(() => - handlers[name].forEach(cb => cb.call(this, args[0], (arg1, arg2, arg3, arg4, arg5) => + handlers[name] && handlers[name].forEach(cb => cb.call(this, args[0], (arg1, arg2, arg3, arg4, arg5) => this.responseWithCallback(name, id, arg1, arg2, arg3, arg4, arg5)))); } else if (args.length === 2) { setImmediate(() => - handlers[name].forEach(cb => cb.call(this, args[0], args[1], (arg1, arg2, arg3, arg4, arg5) => + handlers[name] && handlers[name].forEach(cb => cb.call(this, args[0], args[1], (arg1, arg2, arg3, arg4, arg5) => this.responseWithCallback(name, id, arg1, arg2, arg3, arg4, arg5)))); } else if (args.length === 3) { setImmediate(() => - handlers[name].forEach(cb => cb.call(this, args[0], args[1], args[2], (arg1, arg2, arg3, arg4, arg5) => + handlers[name] && handlers[name].forEach(cb => cb.call(this, args[0], args[1], args[2], (arg1, arg2, arg3, arg4, arg5) => this.responseWithCallback(name, id, arg1, arg2, arg3, arg4, arg5)))); } else if (args.length === 4) { setImmediate(() => - handlers[name].forEach(cb => cb.call(this, args[0], args[1], args[2], args[3], (arg1, arg2, arg3, arg4, arg5) => + handlers[name] && handlers[name].forEach(cb => cb.call(this, args[0], args[1], args[2], args[3], (arg1, arg2, arg3, arg4, arg5) => this.responseWithCallback(name, id, arg1, arg2, arg3, arg4, arg5)))); } else { setImmediate(() => - handlers[name].forEach(cb => cb.call(this, args[0], args[1], args[2], args[3], args[4], (arg1, arg2, arg3, arg4, arg5) => + handlers[name] && handlers[name].forEach(cb => cb.call(this, args[0], args[1], args[2], args[3], args[4], (arg1, arg2, arg3, arg4, arg5) => this.responseWithCallback(name, id, arg1, arg2, arg3, arg4, arg5)))); } }; - ws.on('message', message => { + ws.on('message', (data, isBinary) => { lastPong = Date.now(); + let message = isBinary ? data : data.toString(); if (!message || typeof message !== 'string') { return console.error('Received invalid message: ' + JSON.stringify(message)); } try { - message = JSON.parse(message) + message = JSON.parse(message); } catch (e) { return console.error('Received invalid message: ' + JSON.stringify(message)); } @@ -148,13 +149,13 @@ function Socket (ws, sessionID, name) { } else if (type === MESSAGE_TYPES.MESSAGE) { DEBUG && console.log(name); - handlers[name] && setImmediate(() => handlers[name].forEach(cb => cb.call(this, args[0], args[1], args[2], args[3], args[4]))); + handlers[name] && setImmediate(() => handlers[name] && handlers[name].forEach(cb => cb.call(this, args[0], args[1], args[2], args[3], args[4]))); } else if (type === MESSAGE_TYPES.PING) { ws.send(JSON.stringify([MESSAGE_TYPES.PONG])); } else if (type === MESSAGE_TYPES.PONG) { // lastPong saved } else { - console.log('Received unknown message type: ' + type) + console.log('Received unknown message type: ' + type); } }); @@ -169,20 +170,20 @@ function Socket (ws, sessionID, name) { try { ws.close(); } catch (e) { - + // ignore } - } + }; } function SocketIO (server) { const handlers = {}; const run = []; - let socketsList = []; + const socketsList = []; const wss = new WebSocket.Server({ server, - verifyClient: function(info, done) { + verifyClient: function (info, done) { if (run.length) { run.forEach(cb => cb(info.req, err => { if (err) { @@ -195,17 +196,34 @@ function SocketIO (server) { done && done(true); done = null; } + }, + perMessageDeflate: { + zlibDeflateOptions: { + chunkSize: 1024, + memLevel: 9, + level: 9 + }, + zlibInflateOptions: { + chunkSize: 16 * 1024 + }, + clientNoContextTakeover: true, + serverNoContextTakeover: true } }); wss.on('connection', (ws, request) => { DEBUG && console.log('connected'); - if (request._wsNotAuth) { + + if (!request) { + console.error('Unexpected behaviour: request is NULL!'); + } + + if (request && request._wsNotAuth) { const ip = request.headers['x-forwarded-for'] || request.connection.remoteAddress || request.socket.remoteAddress || (request.connection.socket ? request.connection.socket.remoteAddress : null); - handlers.error && handlers.error.forEach(cb => cb('error', 'authentication failed for ' + ip)); + handlers.error && handlers.error.forEach(cb => cb('error', `authentication failed for ${ip}`)); ws.send(JSON.stringify([MESSAGE_TYPES.MESSAGE, 401, 'reauthenticate'])); setTimeout(() => ws && ws.destroy && ws.destroy(), 500); @@ -213,14 +231,17 @@ function SocketIO (server) { let query; try { - const queryString = request.url.split('?')[1]; - query = querystring.parse(queryString || ''); + if (request) { + const queryString = request.url.split('?')[1]; + query = querystring.parse(queryString || ''); + } } catch (e) { query = null; } if (query && query.sid) { const socket = new Socket(ws, request.sessionID || query.sid, query.name); + socket.query = query; socketsList.push(socket); this.sockets.engine.clientsCount = socketsList.length; @@ -237,8 +258,6 @@ function SocketIO (server) { } }); - - // install handlers if (handlers.connection && handlers.connection.length) { // we have a race condition here. @@ -247,7 +266,8 @@ function SocketIO (server) { let timeout = setTimeout(() => { timeout = null; socket.emit('___ready___'); - }, 500); + console.warn('Sent ready, but not all handlers installed!'); + }, 1500); // TODO, This parameter must be configurable handlers.connection.forEach(cb => cb(socket, () => { if (timeout) { @@ -261,11 +281,17 @@ function SocketIO (server) { socket.emit('___ready___'); } } else { - const ip = request.headers['x-forwarded-for'] || - request.connection.remoteAddress || - request.socket.remoteAddress || - (request.connection.socket ? request.connection.socket.remoteAddress : null); - handlers.error && handlers.error.forEach(cb => cb('error', 'No sid found from ' + ip)); + if (request) { + const ip = request.headers['x-forwarded-for'] || + request.connection.remoteAddress || + request.socket.remoteAddress || + (request.connection.socket ? request.connection.socket.remoteAddress : null); + + handlers.error && handlers.error.forEach(cb => cb('error', 'No sid found from ' + ip)); + } else { + handlers.error && handlers.error.forEach(cb => cb('error', 'No sid found')); + } + ws.send(JSON.stringify([MESSAGE_TYPES.MESSAGE, 501, 'error', ['invalid sid']])); setTimeout(() => ws && ws.destroy && ws.destroy(), 500); @@ -294,12 +320,11 @@ function SocketIO (server) { this.sockets = { connected: socketsList, - emit: (arg1, arg2, arg3, arg4, arg5) => { + emit: (arg1, arg2, arg3, arg4, arg5) => socketsList.forEach(socket => - socket.emit(arg1, arg2, arg3, arg4, arg5)); - }, + socket.emit(arg1, arg2, arg3, arg4, arg5)), engine: { - clientsCount: 0, + clientsCount: 0 } }; @@ -315,4 +340,4 @@ function SocketIO (server) { module.exports = { listen: server => new SocketIO(server) -}; \ No newline at end of file +}; diff --git a/main.js b/main.js index 818094b..cf480b6 100644 --- a/main.js +++ b/main.js @@ -8,9 +8,9 @@ const utils = require('@iobroker/adapter-core'); // Get common adapter uti const IOSocket = require('./lib/socket.js'); const LE = require(utils.controllerDir + '/lib/letsencrypt.js'); -let webServer = null; -let store = null; -let secret = 'Zgfr56gFe87jJOM'; // Will be generated by first start +let webServer = null; +let store = null; +let secret = 'Zgfr56gFe87jJOM'; // Will be generated by first start let adapter; function startAdapter(options) { @@ -34,7 +34,7 @@ function startAdapter(options) { adapter.on('unload', callback => { try { - adapter.log.info('terminating http' + (webServer.settings.secure ? 's' : '') + ' server on port ' + webServer.settings.port); + adapter.log.info(`terminating http${webServer.settings.secure ? 's' : ''} server on port ${webServer.settings.port}`); webServer.io.close(); webServer.server.close(); @@ -122,7 +122,7 @@ function initWebServer(settings) { adapter.getPort(settings.port, async port => { if (parseInt(port, 10) !== settings.port && !adapter.config.findNextPort) { - adapter.log.error('port ' + settings.port + ' already in use'); + adapter.log.error(`port ${settings.port} already in use`); return adapter.terminate ? adapter.terminate(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION) : process.exit(utils.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION); } @@ -155,8 +155,8 @@ function initWebServer(settings) { server.server.on('error', e => { if (e.toString().includes('EACCES') && port <= 1024) { adapter.log.error(`node.js process has no rights to start server on the port ${port}.\n` + - `Do you know that on linux you need special permissions for ports under 1024?\n` + - `You can call in shell following scrip to allow it for node.js: "iobroker fix"` + 'Do you know that on linux you need special permissions for ports under 1024?\n' + + 'You can call in shell following scrip to allow it for node.js: "iobroker fix"' ); } else { adapter.log.error(`Cannot start server on ${settings.bind || '0.0.0.0'}:${port}: ${e}`); diff --git a/package.json b/package.json index 63f3ee7..d071092 100644 --- a/package.json +++ b/package.json @@ -16,28 +16,37 @@ "url": "https://github.com/ioBroker/ioBroker.socketio" }, "dependencies": { - "socket.io": "^2.4.1", - "axios": "^0.24.0", + "axios": "^0.25.0", "cookie-parser": "^1.4.6", "express-session": "^1.17.2", - "@iobroker/adapter-core": "^2.5.1" + "@iobroker/adapter-core": "^2.5.1", + "ws": "^8.4.2" }, "devDependencies": { - "@alcalzone/release-script": "^3.4.2", - "@alcalzone/release-script-plugin-iobroker": "^3.4.1", - "@alcalzone/release-script-plugin-license": "^3.4.1", + "@alcalzone/release-script": "^3.5.0", + "@alcalzone/release-script-plugin-iobroker": "^3.5.1", + "@alcalzone/release-script-plugin-license": "^3.5.0", "gulp": "^4.0.2", - "mocha": "^9.1.3", - "chai": "^4.3.4" + "mocha": "^9.2.0", + "chai": "^4.3.6" }, "bugs": { "url": "https://github.com/ioBroker/ioBroker.socketio/issues" }, "main": "main.js", + "files": [ + "admin/", + "lib/", + "io-package.json", + "LICENSE", + "main.js" + ], "scripts": { "test": "node node_modules/mocha/bin/mocha --exit", - "release": "release-script patch --yes", - "release-minor": "release-script minor --yes" + "release": "release-script", + "release-patch": "release-script patch --yes", + "release-minor": "release-script minor --yes", + "release-major": "release-script major --yes" }, "license": "MIT", "readmeFilename": "README.md"