iobroker.js-controller
Version:
Updated by reinstall.js on 2018-06-11T15:19:56.688Z
1,342 lines (1,241 loc) • 106 kB
JavaScript
'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