domotz-remote-pawn
Version:
Domotz Agent
454 lines (403 loc) • 16 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/>.
*
* InterfacesBinding provides functionality to project the network
* discovery onto a map of mac - ip tuple, hereafter called as "i_binding"
*
* This class is stateful (remembers the binding discovered at each iteration).
*
* Created by fausto on 18/05/16.
*/
module.exports.factory = function (q, util, fileUtils, engineRequest, hostInfo, macAddressesMapFile, resourceLocator) {
const CUSTOM_NETWORK_PREFIX = 'CS:TM';
const EXTERNAL_HOST_PREFIX = 'EX:TN';
const FRESH_STATE = 'FRESH';
const STALE_STATE = 'STALE';
('use strict');
var myConsole = resourceLocator.log.decorateLogs();
var lodash = resourceLocator.lodash;
var settingsManager = resourceLocator.settingsManager;
var freshBindingExpirationSeconds = settingsManager.getSetting('configuration.settings.discovery.fresh_binding_expiration_seconds', 90);
function currentTimestamp() {
return Math.floor(Date.now() / 1000);
}
function cloneStateRecord(ibinding) {
return {
mac: ibinding.mac,
ip: ibinding.ip,
hostname: ibinding.hostname,
state: ibinding.state,
};
}
function createStateRecord(mac, ip, hostname, state) {
return {
mac: mac,
ip: ip,
hostname: hostname,
state: state,
ts: currentTimestamp(),
};
}
function createReportRecord(ibinding) {
return {
mac: ibinding.mac,
ip: ibinding.ip,
hostname: ibinding.hostname,
ts: ibinding.ts,
};
}
function getDevice(ip, mac, hostname, forcedMacAddress) {
if (mac) {
deviceMap[ip] = mac;
}
var bindingMacAddress = deviceMap[ip] || createCustomMacFromIp(ip);
if (settingsManager.isLayer3ScanEnabled() && !forcedMacAddress) {
bindingMacAddress = createCustomMacFromIp(ip, true);
}
return {
ip: ip,
mac: bindingMacAddress,
hostname: hostname === '\n' ? '' : hostname,
};
}
function deviceProjection(device) {
return {
ip: device.ip,
host_name: device.hostname,
};
}
function asBindingId(mac, ip) {
return '(' + mac + ' - ' + ip + ')';
}
function bindingToId(ibinding) {
return asBindingId(ibinding.mac, ibinding.ip);
}
function setState(mac, ip, hostname, state) {
var bindingId = asBindingId(mac, ip);
iBindingsCurrentSnapshot[bindingId] = createStateRecord(mac, ip, hostname, state);
}
function getMacFromIp(ip) {
var findMac = function (ip, list) {
for (var bindingId in list) {
if (list.hasOwnProperty(bindingId)) {
var tmp = list[bindingId];
if (tmp.ip === ip) {
return tmp.mac;
}
}
}
};
return findMac(ip, iBindingsCurrentSnapshot) || findMac(ip, iBindingsPreviousSnapshot);
}
function getIpsFromMac(mac) {
var findIp = function (mac, list) {
var ips = [];
for (var bindingId in list) {
if (list.hasOwnProperty(bindingId)) {
if (list[bindingId].mac === mac) {
ips.push(list[bindingId].ip);
}
}
}
return ips.length === 0 ? null : ips;
};
return findIp(mac, iBindingsCurrentSnapshot) || findIp(mac, iBindingsPreviousSnapshot) || [];
}
/**
* ------------------- CURRENT AND PREVIOUS SNAPSHOT -----------------------
*
* Current and Previous Snapshot of discovery
* {
* <mac_address> : <ip_address>
* }
*/
var iBindingsCurrentSnapshot = {};
var iBindingsPreviousSnapshot = {};
var lastSnapshotFreshRecords = [];
var counterCycle = 0;
function cleanAll() {
iBindingsCurrentSnapshot = {};
iBindingsPreviousSnapshot = {};
}
function setFresh(mac, ip, hostname) {
if (!ip) {
throw new Error('Cannot set as fresh binding for mac ' + mac + ' if ip is empty');
}
myConsole.verbose('setFresh called with bindings ' + asBindingId(mac, ip) + ' (icycle: %s) ', counterCycle);
setState(mac, ip, hostname, FRESH_STATE);
}
function foundInPreviousSnapshot(freshBinding) {
var bid = bindingToId(freshBinding);
return iBindingsPreviousSnapshot[bid] !== undefined;
}
function listByState(state, excludeExpired) {
excludeExpired = typeof excludeExpired !== 'undefined' ? excludeExpired : false;
var filteredBindingList = [];
lodash.forOwn(iBindingsCurrentSnapshot, function (value) {
if (value.state !== state) {
return;
}
var record = createReportRecord(value);
if (freshBindingExpirationSeconds && excludeExpired && record.ts) {
var bindingAge = currentTimestamp() - record.ts;
myConsole.verbose('Binding MAC %s -> IP: %s. Age %s s', record.mac, record.ip, bindingAge);
if (bindingAge > freshBindingExpirationSeconds) {
myConsole.info('Ignoring Binding MAC %s -> IP: %s. Age %s s', record.mac, record.ip, bindingAge);
return;
}
}
if (resourceLocator.nmapBlackList.isBlackListedMac(record.mac)) {
myConsole.debug('(listByState): Skip blacklisted device %s', record.mac);
return;
}
filteredBindingList.push(record);
});
return filteredBindingList;
}
function listNew() {
myConsole.verbose('listNew called (icycle: %s ) ...', counterCycle);
var newBindings = [];
var currentlyFreshBindings = listFresh();
for (var i = 0; i < currentlyFreshBindings.length; i++) {
if (!foundInPreviousSnapshot(currentlyFreshBindings[i])) {
var record = createReportRecord(currentlyFreshBindings[i]);
newBindings.push(record);
}
}
for (var j = 0; j < newBindings.length; j++) {
myConsole.info('Found new binding: %s (icycle: %s)', JSON.stringify(newBindings[j]), counterCycle);
}
return newBindings;
}
function indexedByIpFresh() {
var freshByIpList = {};
listFresh(true).forEach(function (binding) {
freshByIpList[binding.ip] = binding;
});
return freshByIpList;
}
function listFresh(excludeExpired) {
excludeExpired = typeof excludeExpired !== 'undefined' ? excludeExpired : false;
return listByState(FRESH_STATE, excludeExpired);
}
function listStale() {
return listByState(STALE_STATE);
}
function staleAll() {
myConsole.verbose('staleAll START - All bindings set to stale (icycle: %s)', counterCycle);
iBindingsPreviousSnapshot = {};
for (var bindingID in iBindingsCurrentSnapshot) {
if (iBindingsCurrentSnapshot.hasOwnProperty(bindingID)) {
// Copy i_bindings to 'when at last staleAll call' i_bindings state for later use
iBindingsPreviousSnapshot[bindingID] = cloneStateRecord(iBindingsCurrentSnapshot[bindingID]);
iBindingsCurrentSnapshot[bindingID].state = STALE_STATE;
}
}
myConsole.verbose('staleAll END - All bindings set to stale (icycle: %s)', counterCycle);
}
function saveLastSnapshot() {
lastSnapshotFreshRecords = listFresh();
}
function getLastSnapshot() {
return lastSnapshotFreshRecords;
}
function purgeStale() {
if (counterCycle > 100000) {
counterCycle = 0;
}
counterCycle += 1;
myConsole.verbose('Purge Stale called. Generate new icycle %s ', counterCycle);
setInterfaceInfo();
var staleBindings = listStale();
var toRemove = [];
for (var i = 0; i < staleBindings.length; i++) {
toRemove.push(bindingToId(staleBindings[i]));
}
for (var j = 0; j < toRemove.length; j++) {
myConsole.verbose('purging binding: %s (icycle: %s)', JSON.stringify(toRemove[j]), counterCycle);
delete iBindingsCurrentSnapshot[toRemove[j]];
}
}
/**
* ---------------------------- INITIALIZER --------------------------------
*
* It assures that the current status cache is as the last sent to the
* engine.
*
*/
var initialized = false;
function assureInitialized() {
if (initialized) {
return q.resolve(null);
}
myConsole.info('INITIALIZING Devices list (sending request to Engine) ....');
return engineRequest.sendWithPromise('/device', 'GET').then(function (response) {
var fullIpList = [];
myConsole.verbose('GOT RESPONSE, analyzing ... ');
for (var i = 0; i < response.length; i++) {
var device = response[i];
var mac = device.mac,
hostname = device.hostname,
status = device.status;
if (!device.ip_addresses) {
continue;
}
for (var j = 0; j < device.ip_addresses.length; j++) {
var ip = device.ip_addresses[j];
fullIpList.push(ip);
deviceMap[ip] = mac;
myConsole.verbose(
'Device mac: %s. IP: %s. Hostname: %s -- is %s, adding to list of previously known devices ..',
mac,
ip,
hostname,
status
);
if (status === 'ONLINE') {
setState(mac, ip, hostname, FRESH_STATE);
} else {
myConsole.verbose('Device mac: %s ip: %s hostname: %s -- is %s', mac, device.ip, hostname, status);
}
}
}
initialized = true;
hostInfo.setFullIpList(fullIpList);
});
}
/**
* ------------------- PERSISTENCE INTERFACE INFO --------------------------
*
* It's an object that contains the correspondence between mac address and
* ip address. It is exploited by a lot of services that need an IP address
* to reach the device from the MAC.
* {
* <mac_address> : <ip_address>
* }
*/
var persistentInterfaceInfo = {};
function setInterfaceInfo() {
persistentInterfaceInfo = {};
for (var interfaceIndex in iBindingsCurrentSnapshot) {
if (iBindingsCurrentSnapshot.hasOwnProperty(interfaceIndex)) {
var interfaceInfo = iBindingsCurrentSnapshot[interfaceIndex];
if (persistentInterfaceInfo[interfaceInfo.mac] === undefined) {
persistentInterfaceInfo[interfaceInfo.mac] = interfaceInfo.ip;
}
}
}
}
function getInterfaceInfo() {
return persistentInterfaceInfo;
}
/**
* ------------------- PERSISTENCE INTERFACE INFO --------------------------
*/
/**
* ----------------------- DEVICE REVERSE MAP ------------------------------
* For layer-3 monitoring we keep a reverse mapping IP -> MAC address
* initialized from a file:
* <ip> <mac>
*/
function _toHexWith2Digits(byte) {
return ('0' + parseInt(byte).toString(16)).slice(-2);
}
var deviceMap =
fileUtils.readFile(macAddressesMapFile, function (content) {
var rows = content.split('\n');
var tmp = {};
for (var i = 0; i < rows.length; i++) {
var row = rows[i],
fields = row.split(/\s+/);
if (row && fields) {
var ip = fields[0];
var mac = fields[1];
tmp[ip] = mac;
myConsole.info('Add mapping %s --> %s for custom external IPs', ip, mac);
}
}
return tmp;
}) || {};
/**
* ID network + last 16 bits are unique ID (netmask is max 22...)
* N.B. The interface exists because we have just scanned it
*/
function createCustomMacFromIp(ip, force) {
if (!force && !hostInfo.getInterfaceType(ip)) {
// true = custom, false = real
return null;
}
var bytes = ip.split('.');
var ipB0 = _toHexWith2Digits(bytes[0]);
var ipB1 = _toHexWith2Digits(bytes[1]);
var ipB2 = _toHexWith2Digits(bytes[2]);
var ipB3 = _toHexWith2Digits(bytes[3]);
var mac = util.format('%s:%s:%s:%s:%s', CUSTOM_NETWORK_PREFIX, ipB0, ipB1, ipB2, ipB3).toUpperCase();
myConsole.info('Create unique ID fo ip %s - byte ' + 'from ip: %s.%s.%s.%s Caching [%s] --> [%s].', ip, ipB0, ipB1, ipB2, ipB3, ip, mac);
deviceMap[ip] = mac;
return mac;
}
function invalidateList() {
initialized = false;
cleanAll();
}
function setFreshDiscoveredBindings(bindings) {
lodash.forOwn(bindings, function (interfaceInfo) {
lodash.forOwn(interfaceInfo, function (addresses, mac) {
for (var i = 0; i < addresses.length; i++) {
var entry = addresses[i];
setFresh(mac, entry.ip, entry.host_name);
}
});
});
myConsole.verbose('setFreshDiscoveredBindings END - All bindings set to fresh (icycle: %s)', counterCycle);
return bindings;
}
function invalidateBindingByMac(mac) {
lodash.forOwn(iBindingsCurrentSnapshot, function (value, key) {
if (key.indexOf(mac) > 0 && value.state === FRESH_STATE) {
myConsole.info('Removing fresh binding %s', key);
delete iBindingsCurrentSnapshot[key];
}
});
}
function _setBindingTimestamp(binding, ts) {
iBindingsCurrentSnapshot[binding].ts = ts;
}
return {
staleAll: staleAll,
cleanAll: cleanAll,
setFresh: setFresh,
listFresh: listFresh,
indexedByIpFresh: indexedByIpFresh,
listNew: listNew,
listStale: listStale,
purgeStale: purgeStale,
getDevice: getDevice,
deviceProjection: deviceProjection,
assureInitialized: assureInitialized,
getInterfaceInfo: getInterfaceInfo,
invalidateList: invalidateList,
getMacFromIp: getMacFromIp,
getIpsFromMac: getIpsFromMac,
setFreshDiscoveredBindings: setFreshDiscoveredBindings,
saveLastSnapshot: saveLastSnapshot,
getLastSnapshot: getLastSnapshot,
invalidateBindingByMac: invalidateBindingByMac,
CUSTOM_NETWORK_PREFIX: CUSTOM_NETWORK_PREFIX,
EXTERNAL_HOST_PREFIX: EXTERNAL_HOST_PREFIX,
_setBindingTimestamp: _setBindingTimestamp,
};
};