UNPKG

iobroker.js-controller

Version:

Updated by reinstall.js on 2018-06-11T15:19:56.688Z

2,954 lines (2,732 loc) 106 kB
'use strict'; const fs = require('fs-extra'); const path = require('path'); const semver = require('semver'); const os = require('os'); const forge = require('node-forge'); const deepClone = require('deep-clone'); const cpPromise = require('promisify-child-process'); const jwt = require('jsonwebtoken'); const { createInterface } = require('readline'); // @ts-ignore require('events').EventEmitter.prototype._maxListeners = 100; let request; let extend; let password; let npmVersion; let crypto; let diskusage; const randomID = Math.round(Math.random() * 10000000000000); // Used for creation of User-Agent const VENDOR_FILE = '/etc/iob-vendor.json'; let lastCalculationOfIps; let ownIpArr = []; // Here we define all characters that are forbidden in IDs. Since we want to allow multiple // unicode character classes, we do that by OR-ing the character classes and negating the result. // Also, we can easily whitelist characters this way. // // We allow: // · Ll = lowercase letters // · Lu = uppercase letters // · Nd = numbers // · ".", "_", "-" (common in IDs) // · "/" (required for designs) // · " :!#$%&()+=@^{}|~" (for legacy reasons) // /** All characters that may not appear in an object ID. */ const FORBIDDEN_CHARS = /[^._\-/ :!#$%&()+=@^{}|~\p{Ll}\p{Lu}\p{Nd}]+/gu; /** * recursively copy values from old object to new one * * @alias copyAttributes * @memberof tools * @param {object} oldObj source object * @param {object} newObj destination object * @param {object} [originalObj] optional object for read __no_change__ values * @param {boolean} [isNonEdit] optional indicator if copy is in nonEdit part * */ function copyAttributes(oldObj, newObj, originalObj, isNonEdit) { for (const attr of Object.keys(oldObj)) { if (typeof oldObj[attr] !== 'object' || oldObj[attr] instanceof Array) { if (oldObj[attr] === '__no_change__' && originalObj && !isNonEdit) { if (originalObj[attr] !== undefined) { newObj[attr] = deepClone(originalObj[attr]); } else { console.log(`Attribute ${attr} ignored by copying`); } } else if (oldObj[attr] === '__delete__' && !isNonEdit) { if (newObj[attr] !== undefined) { delete newObj[attr]; } } else { newObj[attr] = oldObj[attr]; } } else { newObj[attr] = newObj[attr] || {}; copyAttributes(oldObj[attr], newObj[attr], originalObj && originalObj[attr], isNonEdit || attr === 'nonEdit'); } } } /** * Checks the flag nonEdit and restores non-changeable values if required * * @alias checkNonEditable * @memberof tools * @param {object} oldObject source object * @param {object} newObject destination object * */ function checkNonEditable(oldObject, newObject) { if (!oldObject) { return true; } if (!oldObject.nonEdit && !newObject.nonEdit) { return true; } // if nonEdit is protected with password if (oldObject.nonEdit && oldObject.nonEdit.passHash) { // If new Object wants to update the nonEdit information if (newObject.nonEdit && newObject.nonEdit.password) { crypto = crypto || require('crypto'); const hash = crypto.createHash('sha256').update(newObject.nonEdit.password.toString()).digest('base64'); if (oldObject.nonEdit.passHash !== hash) { delete newObject.nonEdit; return false; } else { oldObject.nonEdit = deepClone(newObject.nonEdit); delete oldObject.nonEdit.password; delete newObject.nonEdit.password; oldObject.nonEdit.passHash = hash; newObject.nonEdit.passHash = hash; } copyAttributes(newObject.nonEdit, newObject, newObject); if (newObject.passHash) { delete newObject.passHash; } if (newObject.nonEdit && newObject.nonEdit.password) { delete newObject.nonEdit.password; } return true; } else { newObject.nonEdit = oldObject.nonEdit; } } else if (newObject.nonEdit) { oldObject.nonEdit = deepClone(newObject.nonEdit); if (newObject.nonEdit.password) { crypto = crypto || require('crypto'); const hash = crypto.createHash('sha256').update(newObject.nonEdit.password.toString()).digest('base64'); delete oldObject.nonEdit.password; delete newObject.nonEdit.password; oldObject.nonEdit.passHash = hash; newObject.nonEdit.passHash = hash; } } // restore settings copyAttributes(oldObject.nonEdit, newObject, oldObject); if (newObject.passHash) { delete newObject.passHash; } if (newObject.nonEdit && newObject.nonEdit.password) { delete newObject.nonEdit.password; } return true; } /** * @param {string} repoVersion * @param {string} installedVersion */ function upToDate(repoVersion, installedVersion) { // Check if the installed version is at least the repo version return semver.gte(installedVersion, repoVersion); } // TODO: this is only here for backward compatibility, if MULTIHOST password was still setup with old decryption function decryptPhrase(password, data, callback) { crypto = crypto || require('crypto'); const decipher = crypto.createDecipher('aes192', password); try { let decrypted = ''; decipher.on('readable', () => { const data = decipher.read(); if (data) { decrypted += data.toString('utf8'); } }); decipher.on('error', error => { console.error('Cannot decode secret: ' + error); callback(null); }); decipher.on('end', () => callback(decrypted)); decipher.write(data, 'hex'); decipher.end(); } catch (e) { console.error('Cannot decode secret: ' + e.message); callback(null); } } function getAppName() { const parts = __dirname.replace(/\\/g, '/').split('/'); return parts[parts.length - 2].split('.')[0]; } function rmdirRecursiveSync(path) { if (fs.existsSync(path)) { fs.readdirSync(path).forEach(file => { const curPath = path + '/' + file; if (fs.statSync(curPath).isDirectory()) { // recurse rmdirRecursiveSync(curPath); } else { // delete file fs.unlinkSync(curPath); } }); // delete (hopefully) empty folder try { fs.rmdirSync(path); } catch (e) { console.log('Cannot delete directory ' + path + ': ' + e.message); } } } function findIPs() { if (!lastCalculationOfIps || Date.now() - lastCalculationOfIps > 10000) { lastCalculationOfIps = Date.now(); ownIpArr = []; try { const ifaces = require('os').networkInterfaces(); Object.keys(ifaces).forEach(dev => ifaces[dev].forEach(details => // noinspection JSUnresolvedVariable !details.internal && ownIpArr.push(details.address))); } catch (e) { console.error(`Can not find local IPs: ${e.message}`); } } return ownIpArr; } function findPath(path, url) { if (!url) { return ''; } if (url.substring(0, 'http://'.length) === 'http://' || url.substring(0, 'https://'.length) === 'https://') { return url; } else { if (path.substring(0, 'http://'.length) === 'http://' || path.substring(0, 'https://'.length) === 'https://') { return (path + url).replace(/\/\//g, '/').replace('http:/', 'http://').replace('https:/', 'https://'); } else { if (url[0] === '/') { return __dirname + '/..' + url; } else { return __dirname + '/../' + path + url; } } } } function getMac(callback) { const macRegex = /(?:[a-z0-9]{2}[:-]){5}[a-z0-9]{2}/ig; const zeroRegex = /(?:[0]{2}[:-]){5}[0]{2}/; const command = (process.platform.indexOf('win') === 0) ? 'getmac' : 'ifconfig || ip link'; require('child_process').exec(command, {windowsHide: true}, (err, stdout, _stderr) => { if (err) { callback(err); } else { let macAddress; let match; let result = null; while (true) { match = macRegex.exec(stdout); if (!match) { break; } macAddress = match[0]; if (!zeroRegex.test(macAddress) && !result) { result = macAddress; } } if (result === null) { callback(new Error('could not determine the mac address from:\n' + stdout)); } else { callback(null, result.replace(/-/g, ':').toLowerCase()); } } }); } // Is docker environment function isDocker() { let _isDocker = false; try { fs.statSync('/.dockerenv'); _isDocker = true; } catch { // ignore error } let _isDockerGroup = false; try { _isDockerGroup = fs.readFileSync('/proc/self/cgroup', 'utf8').includes('docker'); } catch { // ignore error } return _isDocker || _isDockerGroup; } // Build unique uuid based on MAC address if possible function uuid(givenMac, callback) { if (typeof givenMac === 'function') { callback = givenMac; givenMac = ''; } const _isDocker = isDocker(); // return constant UUID for all CI environments to keep the statistics clean if (require('ci-info').isCI) { return callback('55travis-pipe-line-cior-githubaction'); } let mac = givenMac !== null ? (givenMac || '') : null; let u; if (!_isDocker && mac === '') { const ifaces = require('os').networkInterfaces(); // Find first not empty MAC for (const n of Object.keys(ifaces)) { for (let c = 0; c < ifaces[n].length; c++) { if (ifaces[n][c].mac && ifaces[n][c].mac !== '00:00:00:00:00:00') { mac = ifaces[n][c].mac; break; } } if (mac) { break; } } } if (!_isDocker && mac === '') { return getMac((_err, mac) => uuid(mac || null, callback)); } if (!_isDocker && mac) { const md5sum = require('crypto').createHash('md5'); md5sum.update(mac); mac = md5sum.digest('hex'); u = mac.substring(0, 8) + '-' + mac.substring(8, 12) + '-' + mac.substring(12, 16) + '-' + mac.substring(16, 20) + '-' + mac.substring(20); } else { // Returns a RFC4122 compliant v4 UUID https://gist.github.com/LeverOne/1308368 (DO WTF YOU WANT TO PUBLIC LICENSE) /** @type {any} */ let a; let b; b = a = ''; while (a++ < 36) { b += ((a * 51) & 52) ? (a ^ 15 ? 8 ^ Math.random() * (a ^ 20 ? 16 : 4) : 4).toString(16) : '-'; } u = b; } callback(u); } function updateUuid(newUuid, _objects, callback) { uuid('', _uuid => { _uuid = newUuid || _uuid; // Add vendor prefix to UUID if (fs.existsSync(VENDOR_FILE)) { try { const vendor = require(VENDOR_FILE); if (vendor.vendor && vendor.vendor.uuidPrefix && vendor.vendor.uuidPrefix.length === 2) { _uuid = vendor.vendor.uuidPrefix + _uuid; } } catch { console.error(`Cannot parse ${VENDOR_FILE}`); } } _objects.setObject('system.meta.uuid', { type: 'meta', common: { name: 'uuid', type: 'uuid' }, ts: new Date().getTime(), from: 'system.host.' + getHostName() + '.tools', native: { uuid: _uuid } }, err => { if (err) { console.error('object system.meta.uuid cannot be updated: ' + err); callback(); } else { _objects.getObject('system.meta.uuid', (err, obj) => { if (obj.native.uuid !== _uuid) { console.error('object system.meta.uuid cannot be updated: write protected'); } else { console.log('object system.meta.uuid created: ' + _uuid); } callback(_uuid); }); } }); }); } function createUuid(_objects, callback) { const promiseCheckPassword = new Promise(resolve => _objects.getObject('system.user.admin', (err, obj) => { if (err || !obj) { password = password || require('./password'); // Default Password for user 'admin' is application name in lower case password(getAppName()).hash(null, null, (err, res) => { err && console.error(err); // Create user here and not in io-package.js because of hash password _objects.setObject('system.user.admin', { type: 'user', common: { name: 'admin', password: res, dontDelete: true, enabled: true }, ts: new Date().getTime(), from: 'system.host.' + getHostName() + '.tools', native: {} }, () => { console.log('object system.user.admin created'); resolve(); }); }); } else { resolve(); } }) ); const promiseCheckUuid = new Promise(resolve => _objects.getObject('system.meta.uuid', (err, obj) => { if (!err && obj && obj.native && obj.native.uuid) { const PROBLEM_UUIDS = [ 'ab265f4a-67f9-a46a-c0b2-61e4b95cefe5', '7abd3182-d399-f7bd-da19-9550d8babede', 'deb6f2a8-fe69-5491-0a50-a9f9b8f3419c', 'ec66c85e-fc36-f6f9-f1c9-f5a2882d23c7', 'e6203b03-f5f4-253a-e4f6-b295fc543ab7', 'd659fa3d-7ef9-202a-ea23-acd0aff67b24' ]; // if COMMON invalid docker uuid if (PROBLEM_UUIDS.find(u => u === obj.native.uuid)) { // Read vis license _objects.getObject('system.adapter.vis.0', (err, licObj) => { if (!licObj || !licObj.native || !licObj.native.license) { // generate new UUID updateUuid('', _objects, _uuid => resolve(_uuid)); } else { // decode obj.native.license let data; try { data = jwt.decode(licObj.native.license); } catch { data = null; } if (!data || !data.uuid) { // generate new UUID updateUuid('', _objects, __uuid => resolve(__uuid)); } else { if (data.uuid !== obj.native.uuid) { updateUuid(data.correct ? data.uuid : '', _objects, _uuid => resolve(_uuid)); } else { // Show error console.warn(`Your iobroker.vis license must be updated. Please contact info@iobroker.net to get a new license!`); console.warn(`Provide following information in email: ${data.email}, invoice: ${data.invoice}`); resolve(); } } } }); } else { resolve(); } } else { // generate new UUID updateUuid('', _objects, _uuid => resolve(_uuid)); } }) ); return Promise.all([promiseCheckPassword, promiseCheckUuid]) .then(result => callback && callback(result[1])); } // Download file to tmp or return file name directly function getFile(urlOrPath, fileName, callback) { request = request || require('request'); // If object was read if (urlOrPath.substring(0, 'http://'.length) === 'http://' || urlOrPath.substring(0, 'https://'.length) === 'https://') { const tmpFile = `${__dirname}/../tmp/${fileName || Math.floor(Math.random() * 0xFFFFFFE) + '.zip'}`; // Add some information to user-agent, like chrome, IE and Firefox do request({url: urlOrPath, gzip: true, headers: {'User-Agent': `${module.exports.appName}, RND: ${randomID}, N: ${process.version}`}}).on('error', error => { console.log(`Cannot download "${tmpFile}": ${error}`); if (callback) { callback(tmpFile); } }).pipe(fs.createWriteStream(tmpFile)).on('close', () => { console.log('downloaded ' + tmpFile); if (callback) { callback(tmpFile); } }); } else { if (fs.existsSync(urlOrPath)) { if (callback) { callback(urlOrPath); } } else if (fs.existsSync(__dirname + '/../' + urlOrPath)) { if (callback) { callback(__dirname + '/../' + urlOrPath); } } else if (fs.existsSync(__dirname + '/../tmp/' + urlOrPath)) { if (callback) { callback(__dirname + '/../tmp/' + urlOrPath); } } else { console.log('File not found: ' + urlOrPath); process.exit(1); } } } // Return content of the json file. Download it or read directly function getJson(urlOrPath, agent, callback) { if (typeof agent === 'function') { callback = agent; agent = ''; } agent = agent || ''; request = request || require('request'); let sources = {}; // If object was read if (urlOrPath && typeof urlOrPath === 'object') { if (callback) { callback(urlOrPath); } } else if (!urlOrPath) { console.log('Empty url!'); if (callback) { callback(null); } } else { if (urlOrPath.substring(0, 'http://'.length) === 'http://' || urlOrPath.substring(0, 'https://'.length) === 'https://') { request({url: urlOrPath, timeout: 10000, gzip: true, headers: {'User-Agent': agent}}, (error, response, body) => { if (error || !body || response.statusCode !== 200) { console.warn('Cannot download json from ' + urlOrPath + '. Error: ' + (error || body)); if (callback) { callback(null, urlOrPath); } return; } try { sources = JSON.parse(body); } catch { console.error('Json file is invalid on ' + urlOrPath); if (callback) { callback(null, urlOrPath); } return; } if (callback) { callback(sources, urlOrPath); } }).on('error', _error => { //console.log('Cannot download json from ' + urlOrPath + '. Error: ' + error); //if (callback) callback(null, urlOrPath); }); } else { if (fs.existsSync(urlOrPath)) { try { sources = fs.readJSONSync(urlOrPath); } catch (e) { console.log('Cannot parse json file from ' + urlOrPath + '. Error: ' + e.message); if (callback) { callback(null, urlOrPath); } return; } if (callback) { callback(sources, urlOrPath); } } else if (fs.existsSync(__dirname + '/../' + urlOrPath)) { try { sources = fs.readJSONSync(__dirname + '/../' + urlOrPath); } catch (e) { console.log('Cannot parse json file from ' + __dirname + '/../' + urlOrPath + '. Error: ' + e.message); if (callback) { callback(null, urlOrPath); } return; } if (callback) { callback(sources, urlOrPath); } } else if (fs.existsSync(__dirname + '/../tmp/' + urlOrPath)) { try { sources = fs.readJSONSync(__dirname + '/../tmp/' + urlOrPath); } catch (e) { console.log('Cannot parse json file from ' + __dirname + '/../tmp/' + urlOrPath + '. Error: ' + e.message); if (callback) { callback(null, urlOrPath); } return; } if (callback) { callback(sources, urlOrPath); } } else { //if (urlOrPath.indexOf('/example/') === -1) console.log('Json file not found: ' + urlOrPath); if (callback) { callback(null, urlOrPath); } } } } } function scanDirectory(dirName, list, regExp) { if (fs.existsSync(dirName)) { let dirs; try { dirs = fs.readdirSync(dirName); } catch (e) { console.log(`Cannot read or parse ${dirName}: ${e.message}`); return; } for (let i = 0; i < dirs.length; i++) { try { const fullPath = path.join(dirName, dirs[i]); const fileIoName = path.join(fullPath, 'io-package.json'); const fileName = path.join(fullPath, 'package.json'); if (regExp.test(dirs[i]) && fs.existsSync(fileIoName)) { const ioPackage = fs.readJSONSync(fileIoName); const package_ = fs.existsSync(fileName) ? fs.readJSONSync(fileName) : {}; const localIcon = (ioPackage.common.icon ? `/adapter/${dirs[i].substring(module.exports.appName.length + 1)}/${ioPackage.common.icon}` : ''); //noinspection JSUnresolvedVariable list[ioPackage.common.name] = { controller: ioPackage.common.controller || false, version: ioPackage.common.version, icon: ioPackage.common.extIcon || localIcon, localIcon, title: ioPackage.common.title, // deprecated 2021.04.18 BF titleLang: ioPackage.common.titleLang, desc: ioPackage.common.desc, platform: ioPackage.common.platform, keywords: ioPackage.common.keywords, readme: ioPackage.common.readme, type: ioPackage.common.type, license: ioPackage.common.license ? ioPackage.common.license : ((package_.licenses && package_.licenses.length) ? package_.licenses[0].type : ''), licenseUrl: (package_.licenses && package_.licenses.length) ? package_.licenses[0].url : '' }; } } catch (e) { console.log(`Cannot read or parse ${__dirname}/../node_modules/${dirs[i]}/io-package.json: ${e.message}`); } } } } /** * Get list of all installed adapters and controller version on this host * @param {string} [hostRunningVersion] Version of the running js-controller, will be included in the returned information if provided * @returns {object} object containing information about installed host */ function getInstalledInfo(hostRunningVersion) { const result = {}; const fullPath = path.join(__dirname, '..'); // Get info about host let ioPackage; try { ioPackage = fs.readJSONSync(path.join(fullPath, 'io-package.json')); } catch (e) { console.error(`Cannot get installed host information: ${e.message}`); } const package_ = fs.existsSync(path.join(fullPath, 'package.json')) ? fs.readJSONSync(path.join(fullPath, 'package.json')) : {}; const regExp = new RegExp(`^${module.exports.appName}\\.`, 'i'); if (ioPackage) { result[ioPackage.common.name] = { controller: true, version: ioPackage.common.version, icon: ioPackage.common.extIcon || ioPackage.common.icon, title: ioPackage.common.title, // deprecated 2021.04.18 BF titleLang: ioPackage.common.titleLang, desc: ioPackage.common.desc, platform: ioPackage.common.platform, keywords: ioPackage.common.keywords, readme: ioPackage.common.readme, runningVersion: hostRunningVersion, license: ioPackage.common.license ? ioPackage.common.license : ((package_.licenses && package_.licenses.length) ? package_.licenses[0].type : ''), licenseUrl: (package_.licenses && package_.licenses.length) ? package_.licenses[0].url : '' }; } scanDirectory(path.join(__dirname, '../node_modules'), result, regExp); scanDirectory(path.join(__dirname, '../../node_modules'), result, regExp); if ( fs.existsSync(path.join(__dirname, `../../../node_modules/${module.exports.appName.toLowerCase()}.js-controller`)) || fs.existsSync(path.join(__dirname, `../../../node_modules/${module.exports.appName}.js-controller`)) ) { scanDirectory(path.join(__dirname, '../..'), result, regExp); } return result; } /** * Reads an adapter's npm version * @param {string | null} adapter The adapter to read the npm version from. Null for the root ioBroker packet * @param {(err: Error | null, version?: string) => void} [callback] */ function getNpmVersion(adapter, callback) { adapter = adapter ? module.exports.appName + '.' + adapter : module.exports.appName; adapter = adapter.toLowerCase(); const cliCommand = `npm view ${adapter}@latest version`; const exec = require('child_process').exec; exec(cliCommand, {timeout: 2000, windowsHide: true}, (error, stdout, _stderr) => { let version; if (error) { // command failed if (typeof callback === 'function') { callback(error); return; } } else if (stdout) { version = semver.valid(stdout.trim()); } if (typeof callback === 'function') { callback(null, version); } }); } function getIoPack(sources, name, callback) { getJson(sources[name].meta, ioPack => { const packUrl = sources[name].meta.replace('io-package.json', 'package.json'); if (!ioPack) { if (sources._helper) { sources._helper.failCounter.push(name); } if (callback) { callback(sources, name); } } else { setImmediate(() => { getJson(packUrl, pack => { const version = sources[name].version; const type = sources[name].type; // If installed from git or something else // js-controller is exception, because can be installed from npm and from git if (sources[name].url && name !== 'js-controller') { if (ioPack && ioPack.common) { sources[name] = extend(true, sources[name], ioPack.common); // overwrite type of adapter from repository if (type) { sources[name].type = type; } if (pack && pack.licenses && pack.licenses.length) { if (!sources[name].license) { sources[name].license = pack.licenses[0].type; } if (!sources[name].licenseUrl) { sources[name].licenseUrl = pack.licenses[0].url; } } } if (callback) { callback(sources, name); } } else { if (ioPack && ioPack.common) { sources[name] = extend(true, sources[name], ioPack.common); if (pack && pack.licenses && pack.licenses.length) { if (!sources[name].license) { sources[name].license = pack.licenses[0].type; } if (!sources[name].licenseUrl) { sources[name].licenseUrl = pack.licenses[0].url; } } } // overwrite type of adapter from repository if (type) { sources[name].type = type; } if (version) { sources[name].version = version; if (callback) { callback(sources, name); } } else { if (sources[name].meta.substring(0, 'http://'.length) === 'http://' || sources[name].meta.substring(0, 'https://'.length) === 'https://') { //installed from npm getNpmVersion(name, (_err, version) => { if (version) { sources[name].version = version; } else { sources[name].version = 'npm error'; } if (callback) { callback(sources, name); } }); } else { if (callback) { callback(sources, name); } } } } }); }); } }); } function _getRepositoryFile(sources, path, callback) { if (!sources._helper) { let count = 0; for (const _name in sources) { if (!Object.prototype.hasOwnProperty.call(sources, _name)) { continue; } count++; } sources._helper = {failCounter: []}; sources._helper.timeout = setTimeout(() => { if (sources._helper) { delete sources._helper; for (const __name of Object.keys(sources)) { if (sources[__name].processed !== undefined) { delete sources[__name].processed; } } if (callback) { callback('Timeout by read all package.json (' + count + ') seconds', sources); } callback = null; } }, count * 1000); } for (const name of Object.keys(sources)) { if (sources[name].processed || name === '_helper') { continue; } sources[name].processed = true; if (sources[name].url) { sources[name].url = findPath(path, sources[name].url); } if (sources[name].meta) { sources[name].meta = findPath(path, sources[name].meta); } if (sources[name].icon) { sources[name].icon = findPath(path, sources[name].icon); } if (!sources[name].name && sources[name].meta) { getIoPack(sources, name, _ignore => { if (sources._helper) { if (sources._helper.failCounter.length > 10) { clearTimeout(sources._helper.timeout); delete sources._helper; for (const _name of Object.keys(sources)) { if (sources[_name].processed !== undefined) { delete sources[_name].processed; } } if (callback) { callback('Looks like there is no internet.', sources); } callback = null; } else { // process next setImmediate(() => _getRepositoryFile(sources, path, callback)); } } }); return; } } // all packages are processed if (sources._helper) { let err; if (sources._helper.failCounter.length) { err = 'Following packages cannot be read: ' + sources._helper.failCounter.join(', '); } clearTimeout(sources._helper.timeout); delete sources._helper; for (const __name of Object.keys(sources)) { if (sources[__name].processed !== undefined) { delete sources[__name].processed; } } if (callback) { callback(err, sources); } callback = null; } } function _checkRepositoryFileHash(urlOrPath, additionalInfo, callback) { request = request || require('request'); // read hash of file if (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://')) { urlOrPath = urlOrPath.replace(/\.json$/, '-hash.json'); let json = null; request({url: urlOrPath, timeout: 10000, gzip: true}, (error, response, body) => { if (error || !body || response.statusCode !== 200) { console.warn('Cannot download json from ' + urlOrPath + '. Error: ' + (error || body)); } else { try { json = JSON.parse(body); } catch { console.error('Json file is invalid on ' + urlOrPath); } } if (json && json.hash) { // The hash download was successful if (additionalInfo && additionalInfo.sources && json.hash === additionalInfo.hash) { // The hash is the same as for the cached sources console.log('hash unchanged, use cached sources'); callback(null, additionalInfo.sources, json.hash); } else { // Either we have no sources cached or the hash changed // => force download of new sources console.log('hash changed or no sources cached => force download of new sources'); callback(null, null, json.hash); } } else { // Could not download new sources, use the old ones console.log('failed to download new sources, use cached sources'); callback(null, additionalInfo.sources, ''); } }).on('error', _error => { //console.log('Cannot download json from ' + urlOrPath + '. Error: ' + error); //if (callback) callback(null, urlOrPath); }); } else { // it is a file and file has not hash callback(null, null, 0); } } /** * Get list of all adapters and controller in some repository file or in /conf/source-dist.json * * @alias getRepositoryFile * @memberof tools * @param {string} urlOrPath URL stargin with http:// or https:// or local file link * @param {object} additionalInfo destination object * @param {function} callback function (err, sources, actualHash) { } * */ function getRepositoryFile(urlOrPath, additionalInfo, callback) { let sources = {}; let path = ''; if (typeof additionalInfo === 'function') { callback = additionalInfo; additionalInfo = {}; } additionalInfo = additionalInfo || {}; extend = extend || require('node.extend'); if (urlOrPath) { const parts = urlOrPath.split('/'); path = parts.splice(0, parts.length - 1).join('/') + '/'; } // If object was read if (urlOrPath && typeof urlOrPath === 'object') { if (typeof callback === 'function') { callback(null, urlOrPath); } } else if (!urlOrPath) { try { sources = fs.readJSONSync(getDefaultDataDir() + 'sources.json'); } catch { sources = {}; } try { const sourcesDist = fs.readJSONSync(__dirname + '/../conf/sources-dist.json'); sources = extend(true, sourcesDist, sources); } catch { // continue regardless of error } for (const s of Object.keys(sources)) { if (additionalInfo[s] && additionalInfo[s].published) { sources[s].published = additionalInfo[s].published; } } _getRepositoryFile(sources, path, err => { if (err) { console.error(`[${new Date()}] ${err}`); } if (typeof callback === 'function') { callback(err, sources); } }); } else { let agent = ''; if (additionalInfo) { // Add some information to user-agent, like chrome, IE and Firefox do agent = `${additionalInfo.name}, RND: ${additionalInfo.randomID || randomID}, Node:${additionalInfo.node}, V:${additionalInfo.controller}`; } // load hash of file first to not load the whole 1MB of sources _checkRepositoryFileHash(urlOrPath, additionalInfo, (err, sources, actualSourcesHash) => { if (!err && sources) { // Source file was not changed typeof callback === 'function' && callback(err, sources, actualSourcesHash); } else { getJson(urlOrPath, agent, sources => { if (sources) { for (const s of Object.keys(sources)) { if (additionalInfo[s] && additionalInfo[s].published) { sources[s].published = additionalInfo[s].published; } } setImmediate(() => _getRepositoryFile(sources, path, err => { err && console.error(`[${new Date()}] ${err}`); typeof callback === 'function' && callback(err, sources, actualSourcesHash); })); } else { // return cached sources, because no sources found console.log(`failed to download new sources, ${additionalInfo.sources ? 'use cached sources' : 'no cached sources available'}`); return maybeCallbackWithError(callback, `Cannot read "${urlOrPath}"`, additionalInfo.sources, ''); } }); } }); } } function sendDiagInfo(obj, callback) { request = request || require('request'); console.log(`Send diag info: ${JSON.stringify(obj)}`); request.post({ url: 'http://download.' + module.exports.appName + '.net/diag.php', method: 'POST', gzip: true, headers: {'content-type': 'application/x-www-form-urlencoded'}, body: 'data=' + JSON.stringify(obj), timeout: 2000 }, (_err, _response, _body) => { /*if (err || !body || response.statusCode !== 200) { }*/ if (typeof callback === 'function') { callback(); } }).on('error', error => { console.log('Cannot send diag info: ' + error.message); if (typeof callback === 'function') { callback(error); } }); } /** * Finds the adapter directory of a given adapter * * @alias getAdapterDir * @memberof tools * @param {string} adapter name of the adapter, e.g. hm-rpc * @returns {string|null} path to adapter directory or null if no directory found */ function getAdapterDir(adapter) { const appName = module.exports.appName; // snip off 'iobroker.' if (adapter.startsWith(appName + '.')) { adapter = adapter.substring(appName.length + 1); } // snip off instance id if (/\.\d+$/.test(adapter)) { adapter = adapter.substr(0, adapter.lastIndexOf('.')); } const possibilities = [ `${appName.toLowerCase()}.${adapter}/package.json`, `${appName}.${adapter}/package.json` ]; /** @type {string} */ let adapterPath; for (const possibility of possibilities) { // special case to not read adapters from js-controller/node_module/adapter adn check first in parent directory if (fs.existsSync(`${__dirname}/../../${possibility}`)) { adapterPath = `${__dirname}/../../${possibility}`; } else { try { adapterPath = require.resolve(possibility); break; } catch { /* not found */ } } } if (!adapterPath) { return null; // inactive } else { const parts = path.normalize(adapterPath).split(/[\\/]/g); parts.pop(); return parts.join('/'); } } /** * Returns the hostname of this host * @alias getHostName * @returns {string} */ function getHostName() { // for tests purposes if (process.env.IOB_HOSTNAME) { return process.env.IOB_HOSTNAME; } try { const configName = getConfigFileName(); const config = fs.readJSONSync(configName); return config.system ? config.system.hostname || require('os').hostname() : require('os').hostname(); } catch { return require('os').hostname(); } } /** * Read version of system npm * * @alias getSystemNpmVersion * @memberof Tools * @param {function} callback return result * <pre><code> * function (err, version) { * adapter.log.debug('NPM version is: ' + version); * } * </code></pre> */ function getSystemNpmVersion(callback) { const exec = require('child_process').exec; // remove local node_modules\.bin dir from path // or we potentially get a wrong npm version const newEnv = Object.assign({}, process.env); newEnv.PATH = (newEnv.PATH || newEnv.Path || newEnv.path) .split(path.delimiter) .filter(dir => { dir = dir.toLowerCase(); return !(dir.indexOf('iobroker') > -1 && dir.indexOf(path.join('node_modules', '.bin')) > -1); }) .join(path.delimiter); try { let timeout = setTimeout(() => { timeout = null; if (callback) { callback('timeout'); callback = null; } }, 10000); exec('npm -v', {encoding: 'utf8', env: newEnv, windowsHide: true}, (error, stdout) => {//, stderr) { if (timeout) { clearTimeout(timeout); timeout = null; } if (stdout) { stdout = semver.valid(stdout.trim()); } if (callback) { callback(error, stdout); callback = null; } }); } catch (e) { if (callback) { callback(e); callback = null; } } } /** * Read disk free space * * @alias getDiskInfo * @memberof Tools * @param {string} platform result of os.platform() (win32 => Windows, darwin => OSX) * @param {function} callback return result * <pre><code> * function (err, infos) { * adapter.log.debug('Disks sizes is: ' + info['Disk size'] + ' - ' + info['Disk free']); * } * </code></pre> */ function getDiskInfo(platform, callback) { platform = platform || require('os').platform(); if (diskusage) { try { const path = platform === 'win32' ? __dirname.substring(0, 2) : '/'; const info = diskusage.checkSync(path); return callback && callback(null, {'Disk size': info.total, 'Disk free': info.free}); } catch (err) { console.log(err); } } else { const exec = require('child_process').exec; try { if (platform === 'Windows' || platform === 'win32') { // Caption FreeSpace Size // A: // C: 66993807360 214640357376 // D: // Y: 116649795584 148368257024 // Z: 116649795584 148368257024 const disk = __dirname.substring(0, 2).toUpperCase(); exec('wmic logicaldisk get size,freespace,caption', {encoding: 'utf8', windowsHide: true}, (error, stdout) => {//, stderr) { if (stdout) { const lines = stdout.split('\n'); const line = lines.find(line => { const parts = line.split(/\s+/); return parts[0].toUpperCase() === disk; }); if (line) { const parts = line.split(/\s+/); return callback && callback(error, {'Disk size': parseInt(parts[2]), 'Disk free': parseInt(parts[1])}); } } callback && callback(error, null); }); } else { exec('df -k /', {encoding: 'utf8', windowsHide: true}, (error, stdout) => {//, stderr) { // Filesystem 1K-blocks Used Available Use% Mounted on // /dev/mapper/vg00-lv01 162544556 9966192 145767152 7% / try { if (stdout) { const parts = stdout.split('\n')[1].split(/\s+/); return callback && callback(error, {'Disk size': parseInt(parts[1]) * 1024, 'Disk free': parseInt(parts[3]) * 1024}); } } catch { // continue regardless of error } callback && callback(error, null); }); } } catch (e) { callback && callback(e, null); } } } /** * Returns information about a certificate * * * Following info will be returned: * - certificate: the certificate itself * - serialNumber: serial number * - signature: type of signature as text like "RSA", * - keyLength: bits used for encryption key like 2048 * - issuer: issuer of the certificate * - subject: subject that is signed * - dnsNames: server name this certificate belong to * - keyUsage: this certificate can be used for the followinf puposes * - extKeyUsage: usable or client, server or ... * - validityNotBefore: certificate validity start datetime * - validityNotAfter: certificate validity end datetime * * @alias getCertificateInfo * @memberof Tools * @param {string} cert * @return certificate information object */ function getCertificateInfo(cert) { let info = null; if (!cert) { return null; } // https://github.com/digitalbazaar/forge forge.options.usePureJavaScript = false; const pki = forge.pki; let certFile = null; try { if (typeof cert === 'string' && cert.length < 1024 && fs.existsSync(cert)) { certFile = cert; cert = fs.readFileSync(cert, 'utf8'); } const crt = pki.certificateFromPem(cert); info = { certificateFilename: certFile, certificate: cert, serialNumber: crt.serialNumber, signature: pki.oids[crt.signatureOid], keyLength: crt.publicKey.n.toString(2).length, issuer: crt.issuer, subject: crt.subject, dnsNames: crt.getExtension('subjectAltName').altNames, keyUsage: crt.getExtension('keyUsage'), extKeyUsage: crt.getExtension('extKeyUsage'), validityNotBefore: crt.validity.notBefore, validityNotAfter: crt.validity.notAfter }; // do not return info about values delete info.keyUsage.value; delete info.extKeyUsage.value; return info; } catch { return null; } } /** * Returns default SSL certificates (private and public) * * * Following info will be returned: * - defaultPrivate: private RSA key * - defaultPublic: public certificate * * @alias generateDefaultCertificates * @memberof Tools * @returns {{ defaultPrivate: any[];defaultPublic: any[] }} * <pre><code> * const certificates = tools.generateDefaultCertificates(); * </code></pre> */ function generateDefaultCertificates() { // If at any time you wish to disable the use of native code, where available, for particular forge features // like its secure random number generator, you may set the forge.options.usePureJavaScript flag to true. It // is not recommended that you set this flag as native code is typically more performant and may have stronger // security properties. It may be useful to set this flag to test certain features that you plan to run in // environments that are different from your testing environment. // https://github.com/digitalbazaar/forge forge.options.usePureJavaScript = false; const pki = forge.pki; const keys = pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 }); const cert = pki.createCertificate(); cert.publicKey = keys.publicKey; cert.serialNumber = '0' + makeid(17); cert.validity.notBefore = new Date(); cert.validity.notAfter = new Date(); cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); const subAttrs = [ { name: 'commonName', value: getHostName() }, { name: 'organizationName', value: 'ioBroker GmbH' }, { shortName: 'OU', value: 'iobroker' } ]; const issAttrs = [ { name: 'commonName', value: 'iobroker' }, { name: 'organizationName', value: 'ioBroker GmbH' }, { shortName: 'OU', value: 'iobroker' } ]; cert.setSubject(subAttrs); cert.setIssuer(issAttrs); cert.setExtensions([ { name: 'basicConstraints', critical: true, cA: false }, { name: 'keyUsage', critical: true, digitalSignature: true, contentCommitment: true, keyEncipherment: true, dataEncipherment: true, keyAgreement: true, keyCertSign: true, cRLSign: true, encipherOnly: true, decipherOnly: true }, { name: 'subjectAltName', altNames: [{ type: 2, value: os.hostname() }] }, { name: 'subjectKeyIdentifier' }, { name: 'extKeyUsage', serverAuth: true, clientAuth: true, codeSigning: false, emailProtection: false, timeStamping: false }, { name: 'authorityKeyIdentifier' } ]); cert.sign(keys.privateKey, forge.md.sha256.create()); const pem_pkey = pki.privateKeyToPem(keys.privateKey); const pem_cert = pki.certificateToPem(cert); //console.log(pem_pkey); //console.log(pem_cert); return { defaultPrivate: pem_pkey, defaultPublic: pem_cert }; } function makeid(length) { let result = ''; const characters = 'abcdef0123456789'; const charactersLength = characters.length; for ( let i = 0; i < length; i++ ) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); } return result; } /** * Collects information about host and available adapters * * Following info will be collected: * - available adapters * - node.js --version * - npm --version * * @alias getHostInfo * @memberof Tools * @param {object} objects * @param {function} callback return result * <pre><code> * function (err, result) { * adapter.log.debug('Info about host: ' + JSON.stringify(result, null, 2); * } * </code></pre> */ function getHostInfo(objects, callback) { const os = require('os'); if (diskusage !== false) { try { diskusage = diskusage || require('diskusage'); } catch { diskusage = false; } } const cpus = os.cpus(); const data = { Platform: os.platform(), os: process.platform, Architecture: os.arch(), CPUs: cpus && Array.isArray(cpus) ? cpus.length : null, Speed: cpus && Array.isArray(cpus) ? cpus[0].speed : null, Model: cpus && Array.isArray(cpus) ? cpus[0].model : null, RAM: os.totalmem(), 'System uptime': Math.round(os.uptime()), 'Node.js': process.version }; if (data.Platform === 'win32') { data.Platform = 'Windows'; } else if (data.Platform === 'darwin') { data.Platform = 'OSX'; } let task = 0; task++; objects.getObject('system.config', (_err, systemConfig) => { objects.getObject('system.repositories', (err, repos) => { // Check if repositories exists if (!err && repos && repos.native && repos.native.repositories) { const repo = repos.native.repositories[systemConfig.common.activeRepo]; if (repo && repo.json) { data['adapters count'] = Object.keys(repo.json).length; } } if (!--task) { callback(err, data); } }); }); if (!npmVersion) { task++; getSystemNpmVersion((err, version) => { data['NPM'] = 'v' + (version || ' ---'); npmVersion = version; if (!--task) { callback(err, data); } }); } else { data['NPM'] = npmVersion; if (!task) { callback(null, data); } } task++; getDiskInfo(data.Platform, (err, info) => { if (info) { Object.assign(data, info); } if (!--task) { callback(err, data); } }); } // All paths are returned always relative to /node_modules/' + module.exports.appName + '.js-controller // the result has always "/" as last symbol function getDefaultDataDir() { //var dataDir = __dirname.replace(/\\/g, '/'); //dataDir = dataDir.split('/'); const appName = module.exports.appName.toLowerCase(); // if debugging with npm5 if (fs.existsSync(__dirname + '/../../node_modules/' + appName + '.js-controller')) { return '../' + appName + '-data/'; } else // If installed with npm if (fs.existsSync(__dirname + '/../../../node_modules/' + appName + '.js-controller')) { return '../../' + appName + '-data/'; } else { //dataDir.splice(dataDir.length - 1, 1); //dataDir = dataDir.join('/'); return './data/'; } } /** * Returns the path of the config file * * @returns {string} */ function getConfigFileName() { /** @type {string|string[]} */ let configDir = __dirname.replace(/\\/g, '/'); configDir = configDir.split('/'); const appName = module.exports.appName.toLowerCase(); // If installed with npm if (fs.existsSync(__dirname + '/../../../node_modules/' + appName.toLowerCase() + '.js-controller') || fs.existsSync(__dirname + '/../../../node_modules/' + appName + '.js-controller')) { // remove /node_modules/' + appName + '.js-controller/lib configDir.splice(configDir.length - 3, 3); configDir = configDir.join('/'); return configDir + '/' + appName + '-data/' + appName + '.json'; } else // if debugging with npm5 if (fs.existsSync(__dirname + '/../../node_modules/' + appName.toLowerCase() + '.js-controller') || fs.existsSync(__dirname + '/../../node_modules/' + appName + '.js-controller')) { // remove /node_modules/' + appName + '.js-controller/lib configDir.splice(configDir.length - 2, 2); configDir = configDir.join('/'); return configDir + '/' + appName + '-data/' + appName + '.json'; } else { // Remove /lib configDir.splice(configDir.length - 1, 1); configDir = configDir.join('/'); if (fs.existsSync(__dirname + '/../conf/' + appName + '.json')) { return configDir + '/conf/' + appName + '.json'; } else { return configDir + '/data/' + appName + '.json'; } } } /** * Puts all values from an `arguments` object into an array, starting at the given index * @param {IArguments} argsObj An `arguments` object as passed to a function * @param {number} [startIndex=0] The optional index to start taking the arguments from */ function sliceArgs(argsObj, startIndex) { if (startIndex === null || startIndex === undefined) { startIndex = 0; } const ret = []; for (let i = startIndex; i < argsObj.length; i++) { ret.push(argsObj[i]); } return ret; } /** * Promisifies a function which returns an error as the first argument in its callback * @param {Function} fn The function to promisify * @param {any} [context=this] (optional) The context (value of `this` to bind the function to) * @param {string[]} [returnArgNames] (optional) If the callback contains multiple arguments, * you can combine them into one object by passing the names as an array. * Otherwise the Promise will resolve with an array * @returns {(...args: any[]) => Promise<any>} */ function promisify(fn, context, returnArgNames) { return function () { const args = sliceArgs(arguments); // @ts-ignore we cannot know the type of `this` context = context || this; return new Promise((resolve, reject) => { fn.apply(context, args.concat([ function (error, result) { if (error) { return reject(error instanceof Error ? error : new Error(error)); } else { // decide on how we want to return the callback arguments switch (arguments.length) { case 1: // only an error was given return resolve(); // Promise<void> case 2: // a single value (result) was returned return resolve(result); default: {// multiple values should be returned /** @type {{} | any[]} */ let ret; const extraArgs = sliceArgs(arguments, 1); if (returnArgNames && returnArgNames.length === extraArgs.length) { // we can build an object ret = {}; for (let i = 0; i < returnArgNames.length; i++) { ret[returnArgNames[i]] = extraArgs[i]; } } else { // we return the raw array ret = extraArgs; } return resolve(ret); } } } } ])); }); }; } /** * Promisifies a function which does not provide an error as the first argument in its callback * @param {Function} fn The function to promisify * @param {any} [context] (optional) The context (value of `this` to bind the function to) * @param {string[]} [returnArgNames] (optional) If the callback contains multiple arguments, * you can combine them into one object by passing the names as an array. * Otherwise the Promise will resolve with an array * @returns {(...args: any[]) => Promise<any>} */ function promisifyNoError(fn, context, returnArgNames) { return function () { const args = sliceArgs(arguments); // @ts-ignore we cannot know the type of `this` context = context || this; return new Promise((resolve, _reject) => { fn.apply(context, args.concat([ function (result) { // decide on how we want to return the callback arguments switch (arguments.length) { case 0: // no arguments were given return resolve(); // Promise<void> case 1: // a single value (result) was returned return resolve(result); default: {// multiple values should be returned /** @type {{} | any[]} */ let ret; const extraArgs = sliceArgs(arguments, 0); if (returnArgNames && returnArgNames.length === extraArgs.length) { // we can build an object ret = {}; for (let i = 0; i < returnArgNames.length; i++) { ret[returnArgNames[i]] = extraArgs[i]; } } else { // we return the raw array ret = extraArgs; } return resolve(ret); } } } ])); }); }; } /** * Creates and executes an array of promises in sequence * @param {((...args: any[]) => Promise<any>)[]} promiseFactories An array of promise-returning functions */ function promiseSequence(promiseFactories) { return promiseFactories.reduce((promise, factory) => { return promise.then(result => factory().then(Array.prototype.concat.bind(result))); }, Promise.resolve([])); } function _setQualityForStates(states, keys, quality, cb) { if (!keys || !states || !keys.length) { cb(); } else { states.setState(keys.shift(), {ack: null, q: quality}, () => setImmediate(_setQualityForStates, states, keys, quality, cb)); } } function setQualityForInstance(objects, states, namespace, q) { return new Promise((resolve, reject) => { objects.getObjectView('system', 'state', {startkey: namespace + '.', endkey: namespace + '.\u9999', include_docs: false}, (err, _states) => { if (err) { reject(err); } else { let keys = []; if (_states && _states.rows) { for (let s = 0; s < _states.rows.length; s++) { const id = _states.rows[s].id; // if instance still active, but device is offline if (!(q & 0x10) && id.match(/\.info\.connection$/)) { continue; } keys.push(id); } } // read all values for IDs states.getStates(keys, (_err, values) => { // Get only states, that have ack = true keys = keys.filter((_id, i) => values[i] && values[i].ack); // update quality code of the states to new one _setQualityForStates(states, keys, q, err => err ? reject(err) : resolve()); }); } }); }); } /** * Converts ioB pattern into regex. * @param {string} pattern - Regex string to use it in new RegExp(pattern) * @returns {string} */ function pattern2RegEx(pattern) { pattern = (pattern || '').toString(); const startsWithWildcard = pattern[0] === '*'; const endsWithWildcard = pattern[pattern.length - 1] === '*'; pattern = pattern.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*'); return (startsWithWildcard ? '' : '^') + pattern + (endsWithWildcard ? '' : '$'); } /** * Generates a stack trace that can be added to log outputs to trace their source * @param {string} [wrapperName = 'captureStackTrace'] The wrapper function after which the stack trace should begin * @returns {string} */ function captureStackTrace(wrapperName) { if (typeof wrapperName !== 'string') { wrapperName = 'captureStackTrace'; } const ret = new Error(); if (ret.stack) { let foundSelf = false; const lines = ret.stack.split('\n') .filter(line => { // keep all lines after this function's if (foundSelf) { return true; } if (line.indexOf(wrapperName) > -1) { foundSelf = true; } return false; }); return lines.join('\n'); } return ''; } /** * Appends the stack trace generated by `captureStackTrace` to the given string * @param {string} str - The string to append the stack trace to * @returns {string} */ function appendStackTrace(str) { // Convert anything that isn't a string into a string if (typeof str !== 'string') { str = String(str); } if (str.substr(-1) !== '\n') { str += '\n'; } return str + captureStackTrace('appendStackTrace'); } /** * @template T * @typedef {{new (...args: any[]): T}} ES6Class<T> */ /** * @template T * @typedef {{new (...args: any[]): T; (...args: any[]): T; prototype: Function}} Class<T> */ /** * Wraps an ES6 class so it can be called from legacy code without the new keyword. * Usage e.g.: * ```js // filename: foo.js class Foo { constructor() { this.bar = 1; } } module.exports = wrapES6Class(Foo); // filename: index.js const Foo = require("./foo"); var x = new Foo(); // works! var y = Foo(); // works too! ``` * @template T * @param {ES6Class<T>} Class * @returns {Class<T>} */ function wrapES6Class(Class) { const _Class = function _Class() { const args = sliceArgs(arguments); return new (Function.prototype.bind.apply(Class, [null].concat(args)))(); }; _Class.prototype = Class.prototype; // @ts-ignore return _Class; } /** * Encrypt the password/value with given key * @param {string} key - Secret key * @param {string} value - value to encrypt * @returns {string} */ function encryptLegacy(key, value) { let result = ''; for(let i = 0; i < value.length; i++) { result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i)); } return result; } /** * Decrypt the password/value with given key * @param {string} key - Secret key * @param {string} value - value to decrypt * @returns {string} */ function decryptLegacy(key, value) { let result = ''; for(let i = 0; i < value.length; i++) { result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i)); } return result; } /** * encrypts a value by a given key via AES-192-CBC * * @param {string} key - Secret key * @param {string} value - value to decrypt * @returns {string} */ function encrypt(key, value) { if (!/^[0-9a-f]{48}$/.test(key)) { // key length is not matching for AES-192-CBC or key is no valid hex - fallback to old encryption return encryptLegacy(key, value); } crypto = crypto || require('crypto'); const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-192-cbc', Buffer.from(key, 'hex'), iv); const encrypted = Buffer.concat([cipher.update(value), cipher.final()]); return `$/aes-192-cbc:${iv.toString('hex')}:${encrypted.toString('hex')}`; } /** * encrypts a value by a given key via AES-192-CBC * * @param {string} key - Secret key * @param {string} value - value to decrypt * @returns {string} */ function decrypt(key, value) { // if not encrypted as aes-192 or key not a valid 48 digit hex -> fallback if (!value.startsWith(`$/aes-192-cbc:`) || !/^[0-9a-f]{48}$/.test(key)) { return decryptLegacy(key, value); } crypto = crypto || require('crypto'); const textParts = value.split(':'); const iv = Buffer.from(textParts[1], 'hex'); const encryptedText = Buffer.from(textParts.pop(), 'hex'); const decipher = crypto.createDecipheriv('aes-192-cbc', Buffer.from(key, 'hex'), iv); const decrypted = Buffer.concat([decipher.update(encryptedText), decipher.final()]); return decrypted.toString(); } /** * Tests whether the given variable is a real object and not an Array * @param {any} it The variable to test * @returns {it is Record<string, any>} */ function isObject(it) { // This is necessary because: // typeof null === 'object' // typeof [] === 'object' // [] instanceof Object === true return Object.prototype.toString.call(it) === '[object Object]'; // this code is 25% faster then below one // return it && typeof it === 'object' && !(it instanceof Array); } /** * Tests whether the given variable is really an Array * @param {any} it The variable to test */ function isArray(it) { return Array.isArray(it); // from node 0.1 is a part of engine } /** * Measure the Node.js event loop lag and repeatedly call the provided callback function with the updated results * @param {number} ms The number of milliseconds for monitoring * @param {function} cb Callback function to call for each new value */ function measureEventLoopLag(ms, cb) { let start = hrtime(); let timeout = setTimeout(check, ms); timeout.unref(); function check() { // workaround for https://github.com/joyent/node/issues/8364 clearTimeout(timeout); // how much time has actually elapsed in the loop beyond what // setTimeout says is supposed to happen. we use setTimeout to // cover multiple iterations of the event loop, getting a larger // sample of what the process is working on. const t = hrtime(); // we use Math.max to handle case where timers are running efficiently // and our callback executes earlier than `ms` due to how timers are // implemented. this is ok. it means we're healthy. cb && cb(Math.max(0, t - start - ms)); start = t; timeout = setTimeout(check, ms); timeout.unref(); } function hrtime() { const t = process.hrtime(); return (t[0] * 1e3) + (t[1] / 1e6); } } /** * This function convert state values by read and write of aliases. Function is synchron. * * @param {object} sourceObj * @param {object} targetObj * @param {object} state Object with val, ack and so on * @param {object} logger Logging object * @param {string} logNamespace optional Logging namespace */ function formatAliasValue(sourceObj, targetObj, state, logger, logNamespace) { logNamespace = logNamespace ? logNamespace + ' ' : ''; if (!state) { return; } if (state.val === undefined) { state.val = null; return state; } if (targetObj && targetObj.alias && targetObj.alias.read) { try { // process the value here const func = new Function('val', 'type', 'min', 'max', 'sType', 'sMin', 'sMax', 'return ' + targetObj.alias.read); state.val = func(state.val, targetObj.type, targetObj.min, targetObj.max, sourceObj.type, sourceObj.min, sourceObj.max); } catch (e) { logger.error(`${logNamespace}Invalid read function for ${targetObj._id}: ${targetObj.alias.read} => ${e}`); return null; } } if (sourceObj && sourceObj.alias && sourceObj.alias.write) { try { // process the value here const func = new Function('val', 'type', 'min', 'max', 'tType', 'tMin', 'tMax', 'return ' + sourceObj.alias.write); state.val = func(state.val, sourceObj.type, sourceObj.min, sourceObj.max, targetObj.type, targetObj.min, targetObj.max); } catch (e) { logger.error(`${logNamespace}Invalid write function for ${sourceObj._id}: ${sourceObj.alias.write} => ${e}`); return null; } } if (targetObj && typeof state.val !== targetObj.type && state.val !== null) { if (targetObj.type === 'boolean') { const lowerVal = typeof state.val === 'string' ? state.val.toLowerCase() : state.val; if (lowerVal === 'off' || lowerVal === 'aus' || state.val === '0') { state.val = false; } else { // this also handles strings like "EIN" or such that will be true state.val = !!state.val; } } else if (targetObj.type === 'number') { state.val = parseFloat(state.val); } else if (targetObj.type === 'string') { state.val = state.val.toString(); } } // auto-scaling, only if val not null and unit for target (x)or source is % if (((targetObj && targetObj.alias && !targetObj.alias.read) || (sourceObj && sourceObj.alias && !sourceObj.alias.write)) && state.val !== null) { if (targetObj && targetObj.type === 'number' && targetObj.unit === '%' && sourceObj && sourceObj.type === 'number' && sourceObj.unit !== '%' && sourceObj.min !== undefined && sourceObj.max !== undefined) { // scale target between 0 and 100 % based on sources min/max state.val = (state.val - sourceObj.min) / (sourceObj.max - sourceObj.min) * 100; } else if (sourceObj && sourceObj.type === 'number' && sourceObj.unit === '%' && targetObj && targetObj.unit !== '%' && targetObj.type === 'number' && targetObj.min !== undefined && targetObj.max !== undefined) { // scale target based on its min/max by its source (assuming source is meant to be 0 - 100 %) state.val = ((targetObj.max - targetObj.min) * state.val / 100) + targetObj.min; } } return state; } /** * remove given id from all enums * * @alias removeIdFromAllEnums * @memberof tools * @param {object} objects object to access objects db * @param {string} id the object id which will be deleted from enums * @returns {Promise} * */ function removeIdFromAllEnums(objects, id) { return new Promise((resolve, reject) => { objects.getObjectView('system', 'enum', {startkey: '', endkey: '\u9999'}, (err, res) => { if (err) { reject(err); } else { const promises = []; for (const obj of res.rows) { const idx = obj.value && obj.value.common && obj.value.common.members ? obj.value.common.members.indexOf(id) : -1; if (idx !== -1) { // the id is in the enum now we have to remove it obj.value.common.members.splice(idx, 1); promises.push(new Promise(resolve => { objects.setObject(obj.value._id, obj.value, err => { err ? reject(err) : resolve(); }); })); } // endIf } // endFor Promise.all(promises).then(resolve); } // endElse }); }); } /** * Parses dependencies to standardized object of form * * @alias parseDependencies * @memberof tools * @param {string[]|object[]|string} dependencies dependencies array or single dependency * @returns {object} parsed dependencies */ function parseDependencies(dependencies) { let adapters = {}; if (Array.isArray(dependencies)) { dependencies.forEach(rule => { if (typeof rule === 'string') { // No version given, all are okay adapters[rule] = '*'; } else if (isObject(rule)) { // can be object containing single adapter or multiple Object.keys(rule).filter(adapter => !adapters[adapter]).forEach(adapter => adapters[adapter] = rule[adapter]); } }); } else if (typeof dependencies === 'string') { // its a single string without version requirement adapters[dependencies] = '*'; } else if (isObject(dependencies)) { // if dependencies is already an object, just use it adapters = dependencies; } return adapters; } /** * Validates types of obj.common properties and object.type, if invalid types are used, an error is thrown. * If attributes of obj.common are not provided, no error is thrown. obj.type has to be there and has to be valid. * * @param {object} obj an object which will be validated * @param {boolean} [extend] (optional) if true checks will allow more optional cases for extendObject calls * @throws Error if a property has the wrong type or obj.type is non existing */ function validateGeneralObjectProperties(obj, extend) { // designs have no type but have attribute views if (obj && obj.type === undefined && obj.views !== undefined) { return; } if (!obj || (obj.type === undefined && !extend)) { throw new Error(`obj.type has to exist`); } if (obj.type !== undefined && typeof obj.type !== 'string') { throw new Error(`obj.type has an invalid type! Expected "string", received "${typeof obj.type}"`); } const allowedObjectTypes = ['state', 'channel', 'device', 'enum', 'host', 'adapter', 'instance', 'meta', 'config', 'script', 'user', 'group', 'chart', 'folder']; if (obj.type !== undefined && !allowedObjectTypes.includes(obj.type)) { throw new Error(`obj.type has an invalid value (${obj.type}) but has to be one of ${allowedObjectTypes.join(', ')}`); } // obj.common is optional in general check if (!obj.common) { return; } if (obj.common.name !== undefined && typeof obj.common.name !== 'string' && typeof obj.common.name !== 'object') { throw new Error(`obj.common.name has an invalid type! Expected "string" or "object", received "${typeof obj.common.name}"`); } if (obj.common.type !== undefined) { if (typeof obj.common.type !== 'string') { throw new Error(`obj.common.type has an invalid type! Expected "string", received "${typeof obj.common.type}"`); } // if object type indicates a state, check that common.type matches const allowedStateTypes = ['number', 'string', 'boolean', 'array', 'object', 'mixed', 'file', 'json']; if (obj.type === 'state' && !allowedStateTypes.includes(obj.common.type)) { throw new Error(`obj.common.type has an invalid value (${obj.common.type}) but has to be one of ${allowedStateTypes.join(', ')}`); } } if (obj.common.read !== undefined && typeof obj.common.read !== 'boolean') { throw new Error(`obj.common.read has an invalid type! Expected "boolean", received "${typeof obj.common.read}"`); } if (obj.common.write !== undefined && typeof obj.common.write !== 'boolean') { throw new Error(`obj.common.write has an invalid type! Expected "boolean", received "${typeof obj.common.write}"`); } if (obj.common.role !== undefined && typeof obj.common.role !== 'string') { throw new Error(`obj.common.role has an invalid type! Expected "string", received "${typeof obj.common.role}"`); } if (obj.common.desc !== undefined && typeof obj.common.desc !== 'string' && typeof obj.common.desc !== 'object') { throw new Error(`obj.common.desc has an invalid type! Expected "string" or "object", received "${typeof obj.common.desc}"`); } if (obj.type === 'state' && obj.common.custom !== undefined && obj.common.custom !== null && !isObject(obj.common.custom)) { throw new Error(`obj.common.custom has an invalid type! Expected "object", received "${typeof obj.common.custom}"`); } } /** * get all instances of all adapters in the list * * @alias getAllInstances * @memberof tools * @param {string[]} adapters list of adapter names to get instances for * @param {function} callback callback to be executed */ function getAllInstances(adapters, objects, callback) { const instances = []; let count = 0; for (let k = 0; k < adapters.length; k++) { if (!adapters[k]) { continue; } if (adapters[k].indexOf('.') === -1) { count++; } } for (let i = 0; i < adapters.length; i++) { if (!adapters[i]) { continue; } if (adapters[i].indexOf('.') === -1) { getInstances(adapters[i], objects, false, (err, inst) => { for (let j = 0; j < inst.length; j++) { if (instances.indexOf(inst[j]) === -1) { instances.push(inst[j]); } } if (!--count && callback) { callback(null, instances); callback = null; } }); } else { if (instances.indexOf(adapters[i]) === -1) { instances.push(adapters[i]); } } } if (!count && callback) { callback(null, instances); callback = null; } } /** * Promise-version of getAllInstances */ const getAllInstancesAsync = promisify(getAllInstances); /** * get all instances of one adapter * * @alias getInstances * @param {string }adapter name of the adapter * @param {object }objects objects DB * @param {boolean} withObjects return objects instead of only ids * @param {function} callback callback to be executed */ function getInstances(adapter, objects, withObjects, callback) { if (typeof withObjects === 'function') { callback = withObjects; withObjects = false; } objects.getObjectList({startkey: 'system.adapter.' + adapter + '.', endkey: 'system.adapter.' + adapter + '.\u9999'}, (err, arr) => { const instances = []; if (!err && arr && arr.rows) { for (let i = 0; i < arr.rows.length; i++) { if (arr.rows[i].value.type !== 'instance') { continue; } if (withObjects) { instances.push(arr.rows[i].value); } else { instances.push(arr.rows[i].value._id); } } } callback(null, instances); }); } /** * Checks if the given callback is a function and if so calls it with the given parameter immediately, else a resolved Promise is returned * * @param {(...args: any[]) => void | null | undefined} callback - callback function to be executed * @param {any[]} args - as many arguments as needed, which will be returned by the callback function or by the Promise * @returns {Promise<any>} - if Promise is resolved with multiple arguments, an array is returned */ function maybeCallback(callback, ...args) { if (typeof callback === 'function') { // if function we call it with given param setImmediate(callback, ...args); } else { return Promise.resolve(args.length > 1 ? args : args[0]); } } /** * Checks if the given callback is a function and if so calls it with the given error and parameter immediately, else a resolved or rejected Promise is returned. Error ERROR_DB_CLOSED are not rejecting the promise * * @param {((error: Error | null | undefined, ...args: any[]) => void) | null | undefined} callback - callback function to be executed * @param {Error | string | null | undefined} error - error which will be used by the callback function. If callback is not a function and * error is given, a rejected Promise is returned. If error is given but it is not an instance of Error, it is converted into one. * @param {any[]} args - as many arguments as needed, which will be returned by the callback function or by the Promise * @returns {Promise<any>} - if Promise is resolved with multiple arguments, an array is returned */ function maybeCallbackWithError(callback, error, ...args) { if (error !== undefined && error !== null && !(error instanceof Error)) { // if it's not a real Error, we convert it into one error = new Error(error); } const isDbError = error ? error.message === ERROR_DB_CLOSED : false; if (typeof callback === 'function') { setImmediate(callback, error, ...args); } else if (error && !isDbError) { return Promise.reject(error); } else { return Promise.resolve(args.length > 1 ? args : args[0]); } } /** * Executes a command asynchronously. On success, the promise resolves with stdout and stderr. * On error, the promise rejects with the exit code or signal, as well as stdout and stderr. * @param {string} command The command to execute * @param {import("child_process").ExecOptions} [execOptions] The options for child_process.exec * @returns {import("child_process").ChildProcess & Promise<{stdout?: string; stderr?: string}>} */ function execAsync(command, execOptions) { const defaultOptions = { // we do not want to show the node.js window on Windows windowsHide: true, // And we want to capture stdout/stderr encoding: 'utf8' }; // @ts-ignore We set the encoding, so stdout/stdrr must be a string return cpPromise.exec(command, { ...defaultOptions, ...execOptions }); } /** * Takes input from one stream and writes it to another as soon as a complete line was read. * @param {NodeJS.ReadableStream} input The stream to read from * @param {NodeJS.WritableStream} output The stream to write into */ function pipeLinewise(input, output) { const rl = createInterface({ input, crlfDelay: Infinity }); rl.on('line', line => output.write(line + os.EOL)); } /** * Find the adapter main file as full path * * @memberof tools * @param {string} adapter - adapter name of the adapter, e.g. hm-rpc * @returns {Promise<string>} */ async function resolveAdapterMainFile(adapter) { const adapterDir = getAdapterDir(adapter); if (!adapterDir) { throw new Error(`Could not find adapter dir of ${adapter}`); } const possibleMainFiles = [ 'main.js', `${adapter}.js` ]; // Add package.json -> main as the 2nd choice try { const pack = JSON.parse( await fs.readFile( path.join(adapterDir, 'package.json'), 'utf8' ) ); if (pack && typeof pack.main === 'string') { possibleMainFiles.unshift(pack.main); } } catch { // Ignore, we have fallback solutions } // Add io-package.json -> common.main as the preferred choice try { const ioPack = JSON.parse( await fs.readFile( path.join(adapterDir, 'io-package.json'), 'utf8' ) ); if (ioPack && ioPack.common && typeof ioPack.common.main === 'string') { possibleMainFiles.unshift(ioPack.common.main); } } catch { // Ignore, we have fallback solutions } // Try all possible main files for (const mainFile of possibleMainFiles) { const fullFileName = path.join(adapterDir, mainFile); if (await fs.pathExists(fullFileName)) { return fullFileName; } } throw new Error(`Could not find main file of ${adapter}`); } /** * Returns the default nodeArgs required to execute the main file, e.g. transpile hooks for TypeScript * @param {string} mainFile * @returns {string[]} */ function getDefaultNodeArgs(mainFile) { if (mainFile.endsWith('.ts')) { return ['-r', '@alcalzone/esbuild-register']; } return []; } /** This is used for the short github URL format that NPM accepts (<githubname>/<githubrepo>[#<commit-ish>]) */ const shortGithubUrlRegex = /^(?<user>[^/]+)\/(?<repo>[^#]+)(?:#(?<commit>.+))?$/; /** * Tests if the given URL matches the format <githubname>/<githubrepo>[#<commit-ish>] * @param {string} url The URL to parse */ function isShortGithubUrl(url) { return shortGithubUrlRegex.test(url); } /** * Tries to parse an URL in the format <githubname>/<githubrepo>[#<commit-ish>] into its separate parts * @param {string} url The URL to parse * @returns {{user: string; repo: string; commit?: string} | null} */ function parseShortGithubUrl(url) { const match = shortGithubUrlRegex.exec(url); if (!match || !match.groups) { return null; } return { user: match.groups.user, repo: match.groups.repo, commit: match.groups.commit }; } /** This is used to parse the pathname of a github URL */ const githubPathnameRegex = /^\/(?<user>[^/]+)\/(?<repo>[^/]*?)(?:\.git)?(?:\/(?:tree|tarball|archive)\/(?<commit>.*?)(?:\.(?:zip|gz|tar\.gz))?)?$/; /** * Tests if the given pathname matches the format /<githubname>/<githubrepo>[.git][/<tarball|tree|archive>/<commit-ish>[.zip|.gz]] * @param {string} pathname The pathname part of a Github URL */ function isGithubPathname(pathname) { return githubPathnameRegex.test(pathname); } /** * Tries to a github pathname format /<githubname>/<githubrepo>[.git][/<tarball|tree|archive>/<commit-ish>[.zip|.gz|.tar.gz]] into its separate parts * @param {string} pathname The pathname part of a Github URL * @returns {{user: string; repo: string; commit: string} | null} */ function parseGithubPathname(pathname) { const match = githubPathnameRegex.exec(pathname); if (!match || !match.groups) { return null; } return { user: match.groups.user, repo: match.groups.repo, commit: match.groups.commit }; } /** * Removes properties which are given by preserve * @param {object} preserve - object which has true entries (or array of selected attributes) for all attributes which should be removed from currObj * @param {object} oldObj - old object * @param {object} newObj - new object */ function removePreservedProperties(preserve, oldObj, newObj) { for (const prop of Object.keys(preserve)) { if (isObject(preserve[prop]) && isObject(newObj[prop])) { // we have to go one step deeper removePreservedProperties(preserve[prop], oldObj[prop], newObj[prop]); } else if (newObj && newObj[prop] !== undefined && (oldObj && oldObj[prop] !== undefined)) { // we only need to remove something if its in the old object and in the new one if (typeof preserve[prop] === 'boolean') { delete newObj[prop]; } else if (Array.isArray(preserve[prop])) { // array, only rm selected subattributes instead of whole attribute for (const rmProp of preserve[prop]) { if (oldObj[prop][rmProp] !== undefined && newObj[prop][rmProp] !== undefined) { // only delete if conflicting delete newObj[prop][rmProp]; } } } } } } /** * Returns the array of system.adapter.<namespace>.* objects which are created for every instance * * @param {string} namespace - adapter namespace + id, e.g. hm-rpc.0 * @param {boolean} createWakeup - indicator to create wakeup object too * @returns {object[]} */ function getInstanceIndicatorObjects(namespace, createWakeup) { const id = `system.adapter.${namespace}`; const objs = [ { _id: `${id}.alive`, type: 'state', common: { name: `${namespace} alive`, type: 'boolean', read: true, write: true, role: 'indicator.state' }, native: {} }, { _id: `${id}.connected`, type: 'state', common: { name: `${namespace} is connected`, type: 'boolean', read: true, write: false, role: 'indicator.state' }, native: {} }, { _id: `${id}.compactMode`, type: 'state', common: { name: `${namespace}.compactMode`, type: 'boolean', read: true, write: false, role: 'indicator.state' }, native: {} }, { _id: `${id}.cpu`, type: 'state', common: { name: `${namespace}.cpu`, type: 'number', read: true, write: false, role: 'indicator.state', unit: '% of one core' }, native: {} }, { _id: `${id}.cputime`, type: 'state', common: { name: namespace + '.cputime', type: 'number', read: true, write: false, role: 'indicator.state', unit: 'seconds' }, native: {} }, { _id: `${id}.memHeapUsed`, type: 'state', common: { name: `${namespace} heap actually Used`, type: 'number', read: true, write: false, role: 'indicator.state', unit: 'MB' }, native: {} }, { _id: `${id}.memHeapTotal`, type: 'state', common: { name: `${namespace} total Size of the Heap`, read: true, write: false, type: 'number', role: 'indicator.state', unit: 'MB' }, native: {} }, { _id: `${id}.memRss`, type: 'state', common: { name: `${namespace} resident Set Size`, desc: 'Resident set size', read: true, write: false, type: 'number', role: 'indicator.state', unit: 'MB' }, native: {} }, { _id: `${id}.uptime`, type: 'state', common: { name: `${namespace} uptime`, type: 'number', read: true, write: false, role: 'indicator.state', unit: 'seconds' }, native: {} }, { _id: `${id}.inputCount`, type: 'state', common: { name: `${namespace} events input counter`, desc: 'State\'s inputs in 15 seconds', type: 'number', read: true, write: false, role: 'state', unit: 'events/15 seconds' }, native: {} }, { _id: `${id}.outputCount`, type: 'state', common: { name: `${namespace} events output counter`, desc: 'State\'s outputs in 15 seconds', type: 'number', read: true, write: false, role: 'state', unit: 'events/15 seconds' }, native: {} }, { _id: `${id}.eventLoopLag`, type: 'state', common: { name: `${namespace} Node.js event loop lag`, desc: 'Node.js event loop lag in ms averaged over 15 seconds', type: 'number', read: true, write: false, role: 'state', unit: 'ms' }, native: {} }, { _id: `${id}.sigKill`, type: 'state', common: { name: `${namespace} kill signal`, type: 'number', read: true, write: false, desc: 'Process id that must survive. All other IDs must terminate itself', role: 'state' }, native: {} }, { _id: `${id}.logLevel`, type: 'state', common: { name: `${namespace} loglevel`, type: 'string', read: true, write: true, desc: 'Loglevel of the adapter. Will be set on start with defined value but can be overridden during runtime', role: 'state' }, native: {} } ]; if (createWakeup) { objs.push({ _id: `${id}.wakeup`, type: 'state', common: { name: `${namespace}.wakeup`, read: true, write: true, type: 'boolean', role: 'adapter.wakeup' }, native: {} }); } return objs; } function getLogger(log) { if (!log) { log = { silly: function (_msg) {/*console.log(msg);*/}, debug: function (_msg) {/*console.log(msg);*/}, info: function (_msg) {/*console.log(msg);*/}, warn: function (msg) { console.log(msg); }, error: function (msg) { console.log(msg); } }; } else if (!log.silly) { log.silly = log.debug; } return log; } /** * Allows to find out if a given objects dbType offers a server or not * @param dbType {string} database type * @returns {boolean} true if a server class is available */ function objectsDbHasServer(dbType) { try { const path = require.resolve(`@iobroker/db-objects-${dbType}`); return !!require(path).Server; } catch { throw new Error(`Installation error or unknown objects database type: ${dbType}`); } } /** * Allows to find out if a given objects dbType offers a server which runs on this host and listens (locally or globally/by IP) * @param dbType {string} database type * @param host {string} configured db host * @param [checkIfLocalOnly=false] {boolean} optional if try the method checks if the server listens to local connections only; else also external connection options are checked * @returns {boolean} true if a server listens on this host (locally or globally/by IP) */ function isLocalObjectsDbServer(dbType, host, checkIfLocalOnly) { const ownIps = findIPs(); if (!objectsDbHasServer(dbType)) { return false; // if no server it can not be a local server } let result = host === 'localhost' || host === '127.0.0.1'; // reachable locally only if (!checkIfLocalOnly) { result = result || host === '0.0.0.0' || ownIps.includes(host); } return result; } /** * Allows to find out if a given states dbType offers a server or not * @param dbType {string} database type * @returns {boolean} true if a server class is available */ function statesDbHasServer(dbType) { try { const path = require.resolve(`@iobroker/db-states-${dbType}`); return !!require(path).Server; } catch { throw new Error(`Installation error or unknown states database type: ${dbType}`); } } /** * Allows to find out if a given states dbType offers a server which runs on this host and listens (locally or globally/by IP) * @param dbType {string} database type * @param host {string} configured db host * @param [checkIfLocalOnly=false] {boolean} if try the method checks if the server listens to local connections only; else also external connection options are checked * @returns {boolean} true if a server listens on this host (locally or globally/by IP) */ function isLocalStatesDbServer(dbType, host, checkIfLocalOnly) { const ownIps = findIPs(); if (!statesDbHasServer(dbType)) { return false; // if no server it can not be a local server } let result = host === 'localhost' || host === '127.0.0.1'; // reachable locally only if (!checkIfLocalOnly) { result = result || host === '0.0.0.0' || ownIps.includes(host); } return result; } /** * Get ordered instances according to tier level * * @param {object} objects - Objects DB * @param {object} logger - logger object * @param {string} [logPrefix] - prefix for logging * @return {Promise<object[]>} */ async function getInstancesOrderedByStartPrio(objects, logger, logPrefix='') { const instances = {'1': [], '2': [], '3': [], 'admin': []}; const allowedTiers = [1, 2, 3]; if (logPrefix) { // append space if we have a prefix logPrefix += ' '; } let doc = {}; try { doc = await objects.getObjectViewAsync('system', 'instance'); } catch (e) { if (e.message.startsWith('Cannot find ')) { logger.error(`${logPrefix}_design/system missing - call node ${getAppName()}.js setup`); } else { logger.error(`${logPrefix}Can not get instances: ${e.message}`); } } if (!doc.rows || doc.rows.length === 0) { logger.info(`${logPrefix}no instances found`); } for (const row of doc.rows) { if (row && row.value) { if (row.value._id.startsWith('system.adapter.admin')) { instances.admin.push(row.value); } else if (row.value.common && allowedTiers.includes(parseInt(row.value.common.tier))) { instances[row.value.common.tier].push(row.value); } else { // no valid tier so put it in the last one instances['3'].push(row.value); } } } return [...instances.admin, ...instances['1'], ...instances['2'], ...instances['3']]; } const ERROR_NOT_FOUND = 'Not exists'; const ERROR_EMPTY_OBJECT = 'null object'; const ERROR_NO_OBJECT = 'no object'; const ERROR_DB_CLOSED = 'DB closed'; module.exports = { appName: getAppName(), createUuid, decryptPhrase, execAsync, findIPs, generateDefaultCertificates, getAdapterDir, getInstances, getAllInstances, getAllInstancesAsync, getCertificateInfo, getConfigFileName, getDefaultDataDir, getDefaultNodeArgs, getFile, getHostInfo, getHostName, getInstalledInfo, getInstanceIndicatorObjects, getIoPack, getJson, getInstancesOrderedByStartPrio, getRepositoryFile, getSystemNpmVersion, isObject, isArray, maybeCallback, maybeCallbackWithError, promisify, promisifyNoError, promiseSequence, removeIdFromAllEnums, resolveAdapterMainFile, rmdirRecursiveSync, parseDependencies, sendDiagInfo, upToDate, validateGeneralObjectProperties, checkNonEditable, copyAttributes, getDiskInfo, wrapES6Class, setQualityForInstance, appendStackTrace, captureStackTrace, pattern2RegEx, encrypt, decrypt, measureEventLoopLag, formatAliasValue, pipeLinewise, isShortGithubUrl, parseShortGithubUrl, isGithubPathname, parseGithubPathname, removePreservedProperties, FORBIDDEN_CHARS, getLogger, objectsDbHasServer, isLocalObjectsDbServer, statesDbHasServer, isLocalStatesDbServer, ERRORS: { ERROR_NOT_FOUND: ERROR_NOT_FOUND, ERROR_EMPTY_OBJECT: ERROR_EMPTY_OBJECT, ERROR_NO_OBJECT: ERROR_NO_OBJECT, ERROR_DB_CLOSED: ERROR_DB_CLOSED } };