UNPKG

domotz-remote-pawn

Version:

Domotz Agent

454 lines (403 loc) 16 kB
/** 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, }; };