UNPKG

iobroker.js-controller

Version:

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

1,342 lines (1,241 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.getExten