UNPKG

domotz-remote-pawn

Version:

Domotz Agent

227 lines (200 loc) 8.66 kB
/** This file is part of Domotz Agent. * Copyright (C) 2016 Domotz UK LLP * * 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/>. **/ /** * * Created by Massimiliano Cuzzoli <massi@domotz.com> on 26/08/16. * */ const PROTOCOL_NAME = 'upnp'; const UPNP_SEMAPHORE = 'UPNP_SEMAPHORE'; const UPNP_INSPECTION_TIMEOUT = 15000; // milliseconds const UPNP_DISCOVERY_TIMEOUT = 5000; // milliseconds const SEARCH_ATTEMPT_INTERVAL = 500; // milliseconds const MAX_SEARCH_ATTEMPTS = 5; var upnpDiscoveryFactory = function (resourceLocator, messageBuilder, responseParser, deferredRequestHandler) { 'use strict'; var q = resourceLocator.q; var ssdp = resourceLocator.ssdp; var mutex = resourceLocator.utils.mutex; var isLoopbackLocation = resourceLocator.utils.web.isLoopbackUrl; var ssdpClient; // A new client is instantiated when the discovery starts var upnpDiscoveryWorkingData; // This data structure is cleaned up when the discovery starts var upnpDiscoverySnapshotData; var myConsole = resourceLocator.log.decorateLogs(); var interfaceBidingStorage = resourceLocator.interfacesBindingStorage; var indexedByIpFresh = null; var ipConflict = resourceLocator.networkTools.ipConflictDetection; var lockDiscovery = function () { return mutex.lock(UPNP_SEMAPHORE, 1); }; var unlockDiscovery = function () { mutex.unlock(UPNP_SEMAPHORE, 1); }; var getDeviceServices = function (deviceAddress) { var promiseList = []; var locationList = upnpDiscoveryWorkingData[deviceAddress]; myConsole.debug('Inspecting upnp device ' + deviceAddress); var addLocation = function (locationArg) { var resultHandler = function (data) { locationList[locationArg].result = data; return deviceAddress; }; var errorHandler = resultHandler; myConsole.debug('Inspecting upnp location ' + locationArg); promiseList.push( deferredRequestHandler .inspect_upnp_resource({ url: locationArg, method: 'get', timeout: UPNP_INSPECTION_TIMEOUT, }) .then(responseParser.parse_xml_response) .then(resultHandler) .catch(errorHandler) ); }; for (var location in locationList) { if (locationList.hasOwnProperty(location)) { if (isLoopbackLocation(location)) { myConsole.debug('Device %s has a loopback location %s', deviceAddress, location); } else { var url = require('url'); try { url.parse(location); // Validate URL addLocation(location); } catch (e) { myConsole.warn('Invalid URL: ' + location); } } } } if (promiseList.length === 0) { myConsole.warn('Device %s has not viable UPNP discovery locations', deviceAddress); } return promiseList; }; var storeUpnpInformationForDevice = function (data) { if (data === undefined || data === null || data === []) { myConsole.warn('Upnp: Empty device address list'); return; } var deviceAddress = data[0]; if (deviceAddress === undefined) { return; } var binding = indexedByIpFresh[deviceAddress]; if (binding === undefined) { myConsole.warn('Device with IP %s not discovered yet, ignoring UPNP sample', deviceAddress); return; } var mac = binding.mac; upnpDiscoverySnapshotData[mac] = messageBuilder.buildUpnpDeviceMessageForEngine(upnpDiscoveryWorkingData[deviceAddress]); }; var collectUpnpInformation = function (callback, errCallback) { var dataCompletionPromises = []; ssdpClient.stop(); var logError = function (err) { myConsole.error('Upnp Discovery ERROR: ' + JSON.stringify(err)); }; for (var deviceAddress in upnpDiscoveryWorkingData) { if (upnpDiscoveryWorkingData.hasOwnProperty(deviceAddress)) { if (!ipConflict.checkIfIpInConflict(deviceAddress)) { var dataGatherPromise, devicePromises; devicePromises = getDeviceServices(deviceAddress); dataGatherPromise = q.all(devicePromises).then(storeUpnpInformationForDevice).catch(logError); dataCompletionPromises.push(dataGatherPromise); } else { myConsole.warn('Skip Upnp Discovery for conflicting IP address %s', deviceAddress); } } } q.all(dataCompletionPromises) .then(function () { unlockDiscovery(); if (callback) { myConsole.debug('Sending discovery data to callback'); callback(upnpDiscoverySnapshotData); } }) .catch(function (err) { unlockDiscovery(); if (errCallback) { errCallback(err); } }); }; var onSsdpResponse = function (headers, statusCode, rinfo) { myConsole.debug('SSDP response arrived: (location=' + headers.LOCATION + ', uuid=' + headers.USN + ')'); if (upnpDiscoveryWorkingData[rinfo.address] === undefined) { upnpDiscoveryWorkingData[rinfo.address] = {}; } if (upnpDiscoveryWorkingData[rinfo.address][headers.LOCATION] === undefined) { upnpDiscoveryWorkingData[rinfo.address][headers.LOCATION] = { uuid: {} }; } upnpDiscoveryWorkingData[rinfo.address][headers.LOCATION].uuid[headers.USN] = true; }; var discover = function (callback, errCallback, timeout) { if (!lockDiscovery()) { myConsole.warn('WARNING: UPNP Discovery service ALREADY STARTED, ignoring call !'); return; } indexedByIpFresh = interfaceBidingStorage.indexedByIpFresh(); ssdpClient = new ssdp.Client(); upnpDiscoveryWorkingData = {}; upnpDiscoverySnapshotData = {}; ssdpClient.on('response', onSsdpResponse); var searchFunction = function () { myConsole.debug('Broadcasting SSDP search for all'); ssdpClient.search('ssdp:all'); }; for (var i = 0; i < MAX_SEARCH_ATTEMPTS; i++) { setTimeout(searchFunction, SEARCH_ATTEMPT_INTERVAL * i); } setTimeout(function () { myConsole.debug('Collecting UPNP information'); collectUpnpInformation(callback, errCallback); }, timeout || UPNP_DISCOVERY_TIMEOUT); }; var applyFunctionToSnapshotEntries = function (functionToApply) { for (var deviceAddress in upnpDiscoverySnapshotData) { if (upnpDiscoverySnapshotData.hasOwnProperty(deviceAddress)) { functionToApply(deviceAddress, upnpDiscoverySnapshotData[deviceAddress], PROTOCOL_NAME); } } }; var searchEntryIntoSnapshot = function (deviceAddress) { return upnpDiscoverySnapshotData[deviceAddress]; }; return { discover: discover, isDiscoveryInProgress: function () { return mutex.isLocked(UPNP_SEMAPHORE); }, applyFunctionToSnapshotEntries: applyFunctionToSnapshotEntries, searchEntryIntoSnapshot: searchEntryIntoSnapshot, _getUpnpWorkingData: function () { return upnpDiscoveryWorkingData; }, getProtocolName: function () { return PROTOCOL_NAME; }, getProtocolNameUpperCase: function () { return PROTOCOL_NAME.toUpperCase(); }, }; }; module.exports.factory = upnpDiscoveryFactory;