domotz-remote-pawn
Version:
Domotz Agent
327 lines (290 loc) • 13.7 kB
JavaScript
/** This file is part of Domotz Agent.
* Copyright (C) 2016 Domotz Ltd
*
* Domotz Agent is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Domotz Agent is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Domotz Agent. If not, see <http://www.gnu.org/licenses/>.
**/
/**
* This module wraps the nmap commands used for IP devices' interfaces binds (mac-ip-hostname)
* Created by Iacopo Papalini <iacopo@domotz.com> on 18/05/16.
*/
module.exports.BindDiscoveryFactory = function (nmapCommands, nmapXml2Js, fastParser, resourceLocator) {
'use strict';
var log = resourceLocator.log;
var q = resourceLocator.q;
var lodash = resourceLocator.lodash;
var proc = resourceLocator.utils.proc;
var hostInfo = resourceLocator.discovery.host;
var settingsManager = resourceLocator.settingsManager;
var async = resourceLocator.async;
var platform = resourceLocator.utils.platform;
var nmap_max_parallel_processes = lodash.get(
resourceLocator,
'configuration.settings.discovery.nmap_max_parallel_processes',
platform.isUbuntuCorePrivate() ? 3 : 10
);
var iteration = 0;
var baseConsole = log.decorateLogs();
var myConsole = baseConsole;
var SPAWN_HARD_TIMEOUT = 180000;
function setIteration(i) {
iteration = i;
myConsole = baseConsole.decorate(String(iteration));
}
function executeDiscoveryCommands(commandList, iteration) {
myConsole.verbose('executeDiscoveryCommands START ');
var tasks = commandList.map(function (command) {
return function (callback) {
proc.exec(command.nmap_command, { timeout: SPAWN_HARD_TIMEOUT, suffix_comment: iteration - 1 })
.then(nmapXml2Js.toJSON)
.then(function (result) {
return fastParser.parse(result);
})
.then(function (parsedResult) {
callback(null, parsedResult);
})
.catch(function (err) {
myConsole.error('Nmap error %s', err.toString());
if (err && err.code === 'EMFILE') {
proc.gracefulShutdown();
}
callback(null, { error: err });
});
};
});
var deferred = q.defer();
var rets = [];
async.parallelLimit(tasks, nmap_max_parallel_processes, function (err, results) {
if (err) {
return deferred.reject(err);
}
for (var i = 0; i < results.length; i++) {
var result = results[i];
var deferredResult = q.defer();
if (result && result.error) {
myConsole.debug('Error during discovery command execution: %s', JSON.stringify(result));
deferredResult.reject(result.error);
} else if (result instanceof Error) {
myConsole.debug('Error during discovery command execution. Message: %s, Stacktrace: %s', result.message, result.stack);
deferredResult.reject(result);
} else {
deferredResult.resolve(result);
}
rets.push(deferredResult.promise);
}
return deferred.resolve(q.allSettled(rets));
});
return deferred.promise;
}
function getFulfilledPromisesResults(results) {
/**
* see
* https://github.com/kriskowal/q/wiki/API-Reference#promiseallsettled
* for information about the structure returned by allSettled
* This function returns only the device bindings for the discoveries that
* actually succeeded.
*/
var fulfilled = lodash.map(results, function (netScanResult) {
if (netScanResult.state === 'fulfilled') {
return netScanResult.value;
}
});
return lodash.filter(fulfilled, function (res) {
return res !== null;
});
}
function _checkAllBindingsAreEmpties(bindings) {
var areBindingsAllEmpties = lodash.every(bindings, lodash.isEmpty);
if (areBindingsAllEmpties) {
var err = new Error('All bindings from discovery commands empties');
err.code = 'DISCOVERY_EMPTY';
throw err;
}
return;
}
function getAgentHostname() {
var tenant = process.env.TENANT;
if (tenant === 'adi') {
return 'Capture Box';
}
return 'Domotz';
}
function isNetworkScanEnabled(broadCastDisabled, bigNetwork, iface) {
return !broadCastDisabled && !bigNetwork && !iface.forced_ip_list && !iface.forced_ip_range;
}
function scanAllHostsInNetwork(mac, name, ip, netmask, nmapType, iface, commandList) {
if (mac !== null) {
myConsole.info('Scanning all layer 2 network: %s %s/%s with nmap type: %s', name, ip, netmask, nmapType);
} else {
myConsole.info('Scanning all layer 3 network: %s %s/%s with nmap type: %s', name, ip, netmask, nmapType);
}
var commands = nmapCommands.getFastNmapCmd(ip, netmask, nmapType, name, iface);
commandList = commandList.concat(commands);
return commandList;
}
function _calculateIps(ipList, ipRange) {
var retValue = [];
if (ipList && ipList.length > 0) {
retValue = retValue.concat(ipList);
}
if (ipRange && ipRange.length > 0) {
retValue = retValue.concat(ipRange);
}
return retValue;
}
function scanAllKnownIPAddresses(iface, name, ip, netmask, commandList, nmapType) {
// When ip sweep scan is disabled, or we discover IPs slowly with a Big Network Scan,
// we must ping individually all the known IPs to detect if they are online
return commandList.concat([
{
iface_name: name,
iface: iface,
nmap_command: nmapCommands.getRetryCmd(iface, _calculateIps(iface.ipList, iface.ipRange), nmapType),
},
]);
}
function addLocalhostInterface(myInterfacesBindings) {
var defaultInterface = hostInfo.getDefaultInterface();
var macUpperCase = defaultInterface.mac.toUpperCase();
var defaultInterfaceIP = defaultInterface.ip;
myConsole.debug(
'Internal Interface %s/%s. Add Binding: %s->%s',
defaultInterfaceIP,
defaultInterface.netmask,
macUpperCase,
defaultInterfaceIP
);
myInterfacesBindings[macUpperCase] = [{ ip: defaultInterfaceIP, host_name: getAgentHostname() }];
}
/**
* Discovers all the IP devices in the given interfaces. When done the callback is invoked with, as argument,
* an Array of Object similar to:
* {
* 'mac': '[the mac address]',
* 'ip': '[the ip address]',
* 'host_name': '[the host name]'
* }
*/
function discoverAll() {
var interfaces = hostInfo.getAllowedInterfacesList();
var name;
var commandList = [];
var myInterfacesBindings = {};
var broadCastDisabled = settingsManager.isBroadcastDiscoveryDisabled();
var allowMultipleLocalhostInterfaces = settingsManager.getSetting(
'configuration.settings.discovery.allow_multiple_localhost_interfaces',
true
);
var monitorLocalhostInterfaces = settingsManager.getSetting('configuration.settings.discovery.monitor_localhost_interfaces', true);
var bigNetworkScanner = resourceLocator.discovery.bigNetworkScanner;
myConsole.info('Discover ALL START - Interfaces: %s', JSON.stringify(interfaces));
for (name in interfaces) {
if (interfaces.hasOwnProperty(name)) {
var iface = interfaces[name];
if (!iface.ip && !iface.forced_ip_list && !iface.forced_ip_range) {
myConsole.warn('Discover ALL, missing ip address for interface: %s', name);
continue;
}
var ip = iface.ip;
var mac = iface.mac;
var netmask = iface.netmask;
var nmapType = mac !== null ? nmapCommands.NMAP_TYPE.DEFAULT : nmapCommands.NMAP_TYPE.EXTERNAL_SUBNET;
var externalDevice = [
{
ip: ip,
},
];
if (hostInfo.deviceBelongsToExternalHosts(externalDevice)) {
nmapType = nmapCommands.NMAP_TYPE.EXTERNAL_HOST;
}
var bigNetwork = bigNetworkScanner.isBigNetwork(iface);
var enableNetworkScan = isNetworkScanEnabled(broadCastDisabled, bigNetwork, iface);
if (bigNetwork) {
myConsole.debug('Network %s (%s/%s) is a big network', name, ip, netmask);
}
if (
nmapType === nmapCommands.NMAP_TYPE.EXTERNAL_HOST ||
nmapType === nmapCommands.NMAP_TYPE.EXTERNAL_SUBNET ||
nmapType === nmapCommands.NMAP_TYPE.EXTERNAL_SUBNET_PING_ONLY
) {
myConsole.debug('Custom Interface for Monitoring External Network: %s/%s', ip, netmask);
} else if (iface.forced_ip_list) {
myConsole.debug('Virtual interface %s to scan %s ip addresses', name, iface.ipList.length);
} else if (iface.forced_ip_range) {
myConsole.debug('Virtual interface %s to scan %s ip address ranges', name, iface.ipRange.length);
} else if (allowMultipleLocalhostInterfaces && monitorLocalhostInterfaces) {
var macUpperCase = mac.toUpperCase();
myConsole.debug('Real Interface for Monitoring Internal Network %s/%s. Add Binding: %s->%s', ip, netmask, mac, ip);
if (!myInterfacesBindings.hasOwnProperty(macUpperCase)) {
myInterfacesBindings[macUpperCase] = [];
}
myInterfacesBindings[macUpperCase].push({ ip: ip, host_name: getAgentHostname() });
}
if (enableNetworkScan || netmask === 32) {
commandList = scanAllHostsInNetwork(mac, name, ip, netmask, nmapType, iface, commandList);
} else if ((iface.ipList && iface.ipList.length > 0) || (iface.ipRange && iface.ipRange.length > 0)) {
commandList = scanAllKnownIPAddresses(iface, name, ip, netmask, commandList, nmapType);
} else {
myConsole.info('Network scan disabled and no known IP addresses for network %s (%s/%s), doing nothing', name, ip, netmask);
}
}
}
if (!allowMultipleLocalhostInterfaces && monitorLocalhostInterfaces) {
addLocalhostInterface(myInterfacesBindings);
}
return executeDiscoveryCommands(commandList, iteration).then(function (results) {
myConsole.verbose('executeDiscoveryCommands END');
var bindings = getFulfilledPromisesResults(results);
_checkAllBindingsAreEmpties(bindings);
bindings.push(myInterfacesBindings);
myConsole.info('Discover ALL END - Interfaces: %s', JSON.stringify(interfaces));
return bindings;
});
}
function discover(ipList, nmapType, hostTimeout, iface) {
myConsole.info('Discover RETRY called - IP addresses: %s', JSON.stringify(ipList));
var commandList = [{ nmap_command: nmapCommands.getRetryCmd(iface, ipList, nmapType, hostTimeout) }];
return executeDiscoveryCommands(commandList, iteration).then(function (results) {
myConsole.debug('Discover retry results %s', JSON.stringify(results));
myConsole.verbose('executeDiscoveryCommands END ');
myConsole.info('Discover RETRY END - IP addresses: %s', JSON.stringify(ipList));
return getFulfilledPromisesResults(results);
});
}
function assureInterfaces() {
var deferred = q.defer();
try {
// We must not use getAllowedInterfacesList here, because it is possible to have an empty allowed list
// as a valid state
var interfaces = hostInfo.getInterfacesList();
if (Object.keys(interfaces).length === 0) {
myConsole.info('assureInterfaces - No interfaces available, trying to rediscover host info');
hostInfo.recoverHostInfo().finally(function () {
deferred.resolve();
});
} else {
deferred.resolve();
}
} catch (err) {
myConsole.info('assureInterfaces - Error trying to get interfaces list %s', err);
deferred.resolve();
}
return deferred.promise;
}
return {
setIteration: setIteration,
discoverAll: discoverAll,
discover: discover,
assureInterfaces: assureInterfaces,
};
};