UNPKG

domotz-remote-pawn

Version:

Domotz Agent

382 lines (336 loc) 13.1 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) { "use strict"; macAddressesMapFile = macAddressesMapFile || process.env.DOMOTZ_CONF_DIR + '/device_map.conf'; console.log('BINDINGS - Initialize device map from: %s', macAddressesMapFile); 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 }; } function createReportRecord(ibinding) { return { 'mac': ibinding.mac, 'ip': ibinding.ip, 'hostname': ibinding.hostname }; } function getDevice(ip, mac, hostname) { if (mac) { deviceMap[ip] = mac; } return { ip: ip, mac: deviceMap[ip] || createCustomMacFromIp(ip), 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); } /** * ------------------- CURRENT AND PREVIOUS SNAPSHOT ----------------------- * * Current and Previous Snapshot of discovery * { * <mac_address> : <ip_address> * } */ var iBindingsCurrentSnapshot = {}; var iBindingsPreviousSnapshot = {}; var counterCycle = 0; function cleanAll() { iBindingsCurrentSnapshot = {}; iBindingsPreviousSnapshot = {}; } function setFresh(mac, ip, hostname) { console.verbose("BINDINGS - setFresh called with bindings " + asBindingId(mac, ip) + " (icycle: %s) ", counterCycle); setState(mac, ip, hostname, "FRESH"); } function foundInPreviousSnapshot(fresh_binding) { var bid = bindingToId(fresh_binding); return iBindingsPreviousSnapshot[bid] !== undefined; } function listByState(state) { var filteredBindingList = []; for (var binding_id in iBindingsCurrentSnapshot) { if (iBindingsCurrentSnapshot.hasOwnProperty(binding_id)) { if (iBindingsCurrentSnapshot[binding_id].state === state) { var record = createReportRecord(iBindingsCurrentSnapshot[binding_id]); filteredBindingList.push(record); } } } return filteredBindingList; } function listNew() { console.verbose("BINDINGS - listNew called (icycle: %s ) ...", counterCycle); var newBindings = []; var currently_fresh_bindings = listFresh(); for (var i = 0; i < currently_fresh_bindings.length; i++) { if (!foundInPreviousSnapshot(currently_fresh_bindings[i])) { var record = createReportRecord(currently_fresh_bindings[i]); newBindings.push(record); } } for (var j = 0; j < newBindings.length; j++) { console.info("BINDINGS - Found new binding: %s (icycle: %s)", JSON.stringify(newBindings[j]), counterCycle); } return newBindings; } function listFreshByMac(mac) { var freshByMacList = []; var freshList = listFresh(); for(var i=0; i<freshList.length; i++){ var dev = freshList[i]; if (dev.mac === mac){ freshByMacList.push(dev); } } return freshByMacList; } function listFresh() { return listByState("FRESH"); } function listStale() { return listByState("STALE"); } function staleAll() { console.verbose("BINDINGS - 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"; } } } function purgeStale() { if(counterCycle > 100000){ counterCycle = 0; } counterCycle += 1; console.verbose("BINDINGS - 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++) { console.verbose("BINDINGS - purging binding: %s (icycle: %s)", JSON.stringify(toRemove[j]), counterCycle); delete iBindingsCurrentSnapshot[toRemove[j]]; } } function rollback() { console.info("BINDINGS - Rollback was called. State before rollback: %s", JSON.stringify(iBindingsCurrentSnapshot)); iBindingsCurrentSnapshot = {}; for (var binding_id in iBindingsPreviousSnapshot) { if (iBindingsPreviousSnapshot.hasOwnProperty(binding_id)) { // Copy back ibindings from 'when at last staleAll call' to ibindings state iBindingsCurrentSnapshot[binding_id] = cloneStateRecord(iBindingsPreviousSnapshot[binding_id]); } } console.debug("BINDINGS - State after rollback: %s", JSON.stringify(iBindingsCurrentSnapshot)); } /** * ---------------------------- 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); } console.info("BINDINGS - INITIALIZING Devices list " + "(sending request to Engine) ...."); return engineRequest.sendWithPromise('/device', 'GET', {}, function(response){ console.verbose("GOT RESPONSE, analyzing ... "); for (var i=0; i < response.body.length; i++){ var device = response.body[i]; var mac = device.mac, hostname = device.hostname, status = device.status; if (status === "ONLINE") { for(var j=0; j< device.ip_addresses.length; j++){ var ip = device.ip_addresses[j]; deviceMap[ip] = mac; console.debug("BINDINGS - Device mac: %s. IP: %s. " + "Hostname: %s -- is %s, adding to list of " + "previously known devices ..", mac, ip, hostname, status); setState(mac, ip, hostname, "FRESH"); } } else { console.verbose("BINDINGS - Device mac: %s ip: %s hostname: %s " + "-- is %s", mac, device.ip, hostname, status); } } initialized = true; }); } /** * ---------------------------- INITIALIZER -------------------------------- */ /** * ---------------------------- FILE_SYSTEM -------------------------------- */ function save(fileToSave) { console.info("BINDINGS - InterfaceBinding persisting state to file: %s", fileToSave); fileUtils.writeFileSafe(fileToSave, JSON.stringify(iBindingsCurrentSnapshot, null, 2)); } function load(fileToRead) { console.info("BINDINGS - InterfaceBinding reloading state from file: %s", fileToRead); cleanAll(); iBindingsCurrentSnapshot = fileUtils.readFile(fileToRead, JSON.parse); } /** * ---------------------------- FILE_SYSTEM -------------------------------- */ /** * ------------------- 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 interface_index in iBindingsCurrentSnapshot) { if (iBindingsCurrentSnapshot.hasOwnProperty(interface_index)) { var interface_info = iBindingsCurrentSnapshot[interface_index]; if (persistentInterfaceInfo[interface_info.mac] === undefined){ persistentInterfaceInfo[interface_info.mac] = interface_info.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], mac = fields[1]; tmp[ip] = mac; console.info('BINDINGS - 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) { if (!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('CS:TM:%s:%s:%s:%s', ipB0, ipB1, ipB2, ipB3).toUpperCase(); console.info('BINDINGS - 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; } /** * ----------------------- DEVICE REVERSE MAP ------------------------------ */ return { staleAll: staleAll, cleanAll: cleanAll, setFresh: setFresh, listFresh: listFresh, listFreshByMac: listFreshByMac, listNew: listNew, listStale: listStale, purgeStale: purgeStale, getDevice: getDevice, deviceProjection: deviceProjection, assureInitialized: assureInitialized, getInterfaceInfo: getInterfaceInfo, save: save, load: load, rollback: rollback }; };