UNPKG

domotz-remote-pawn

Version:

Domotz Agent

379 lines (329 loc) 14.3 kB
'use strict'; var configUrl = 'http://www.speedtest.net/speedtest-config.php'; var serversUrl = 'http://www.speedtest.net/speedtest-servers.php'; var USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36'; var SERVER_CACHE_SCOPE = 'ookla-servers'; var MODULE = "OOKLA SERVERS "; function serverSelector(resourceLocator, reset_cache) { var request = resourceLocator.request; var q = resourceLocator.q; var parseXML = resourceLocator.xml2js.parseString; var async = resourceLocator.async; var url = resourceLocator.url; var cacheUtils = resourceLocator.utils.cache; var configuration = resourceLocator.configuration; var _ = resourceLocator._; var distanceCalculator = resourceLocator.distanceCalculator; var preferredServer = null; reset_cache = typeof reset_cache !== 'undefined' ? reset_cache : true; var day = (new Date()).getDay(); if (reset_cache && day === 0) { console.info(MODULE + "It's sunday, resetting cache, scope = " + SERVER_CACHE_SCOPE); cacheUtils.createCache(SERVER_CACHE_SCOPE).reset(); } var clientConfig = null; var serversCache = cacheUtils.createCache(SERVER_CACHE_SCOPE); var MAX_SERVERS_TO_PING = 4; var MAX_PING_PER_SERVER = 10; var DEFAULT_TIMEOUT = 30000; var PING_TIMEOUT = 10000; function fromIntervalToNs(time) { return (time[0] * 1e9) + time[1]; } function getXMLRequest(uri) { var deferred = q.defer(); var options = {}; options.url = uri; options.headers = {}; options.headers['user-agent'] = USER_AGENT; options.timeout = DEFAULT_TIMEOUT; request(options, function callback(error, response, body) { try { if (error) { console.alert(MODULE + 'error getting XML data' + JSON.stringify(error)); deferred.reject(error); return; } var statusCode = response.statusCode; if (statusCode > 299 || statusCode < 200) { error = { message: body, status: statusCode }; console.alert(MODULE + 'error getting XML data' + response.statusCode); deferred.reject(error); return; } console.debug(MODULE + "getXMLRequest: Parsing xml " + uri); parseXML(body, function (err, xml) { if (err) { console.alert(MODULE + 'error parsing XML data' + JSON.stringify(err)); deferred.reject(err); return; } console.info(MODULE + "getXMLRequest: Resolving parsed xml " + uri); deferred.resolve(xml); }); } catch (exception) { console.alert(MODULE + 'error getting XML data' + JSON.stringify(exception)); deferred.reject(exception); } }); return deferred.promise; } function getConfiguration() { var deferred = q.defer(); getXMLRequest(configUrl).then(function (xml) { try { var config = xml.settings; var getField = function (name) { return config[name][0].$; }; var speedTestConfig = { client: getField('client'), times: getField('times'), download: getField('download'), upload: getField('upload'), ignore_ids: getField('server-config')['ignoreids'] }; deferred.resolve(speedTestConfig); } catch (exception) { console.alert(MODULE + 'error getting configuration' + JSON.stringify(exception)); deferred.reject(exception); } }).catch(function (error) { console.error(MODULE + "error getting configuration from XML: " + JSON.stringify(error)); deferred.reject(error); }); return deferred.promise; } /* * Executed at startup after a random delay. Fetches the list of servers and saves it in cache. */ function fetch() { console.info(MODULE + "fetch: Fetching server list"); var deferred = q.defer(); var CACHE_KEY = 'fetch-ookla'; serversCache.get(CACHE_KEY, function () { getXMLRequest(serversUrl).then(function (xml) { var serversObject = xml.settings.servers[0].server; var serversList = []; getConfiguration().then(function (config) { clientConfig = config; for (var i = 0; i < serversObject.length; i++) { if (config.ignore_ids.indexOf(serversObject[i].$.id) == -1) serversList.push(serversObject[i].$); } serversCache.set(CACHE_KEY, serversList); console.info(MODULE + "removed " + (serversObject.length - serversList.length) + " invalid servers from the list"); deferred.resolve(serversList); }).catch(function (error) { console.error(MODULE + "error retrieving xml configuration file"); deferred.reject(error); }); }).catch(function (error) { console.error(MODULE + "error getting servers from XML list: " + JSON.stringify(error)); deferred.reject(error); }); }, function (servers) { console.info(MODULE + "fetch: fetching ookla server list, cache resolved"); deferred.resolve(servers); }); return deferred.promise; } function blacklistServers(blacklist) { var blacklistIds = _.map(blacklist, 'id'); console.info(MODULE + "blacklist server function: " + JSON.stringify(blacklistIds)); var CACHE_KEY = 'fetch-ookla'; serversCache.get(CACHE_KEY, function () { }, function (servers) { var filteredServers = _.filter(servers, function (ooklaServer) { return blacklistIds.indexOf(ooklaServer.id) == -1; }); serversCache.set(CACHE_KEY, filteredServers); setServerAsPreferred(null); console.info(MODULE + "Blacklisted " + servers.length + " servers"); }); } function requestToServer(n, address, cb) { var options = {}; options.url = address; options.headers = {}; options.headers['user-agent'] = USER_AGENT; options.timeout = PING_TIMEOUT; var start = process.hrtime(); request(options, function callback(error, response, body) { try { if (error) { cb(error); return; } var statusCode = response.statusCode; if (statusCode > 299 || statusCode < 200) { error = { message: body, status: statusCode }; cb(error); return; } var nanoSecondsLatency = fromIntervalToNs(process.hrtime(start)); cb(null, nanoSecondsLatency); } catch (exception) { cb(exception); } }); } function pingServer(server, cb, numberOfPings) { var iterations = numberOfPings || MAX_PING_PER_SERVER; var address = url.resolve(server.url, 'latency.txt'); async.timesSeries(iterations, function (n, next) { requestToServer(n, address, function (err, res) { next(err, res) }) }, function (err, samples) { if (err) { console.alert(MODULE + "pingServer: error " + JSON.stringify(err)); cb(null, null); return; } var min = Math.min.apply(null, samples); var max = Math.max.apply(null, samples); var sum = 0; for (var i = 0; i < samples.length; i++) { sum += samples[i]; } var avg = sum / samples.length; var result = { server: address, times: samples, max: max, min: min, avg: avg }; cb(null, result); }); } /* scrolls the servers list and finds the one with the matching ID */ var findById = function (serverId) { var deferred = q.defer(); fetch().then(function (servers) { servers.every(function (server) { if (server.id == serverId) { deferred.resolve(server); return false; } return true; }); deferred.reject({status: 404, message: 'NOT FOUND'}); }); return deferred.promise; }; /* Given the server list, selects the N nearest ones. Then performs a series of PING operations on each of them. * Returns the server with the minimum latency. */ function getBestServer(serverList) { var deferred = q.defer(); var servers = serverList.slice(0, MAX_SERVERS_TO_PING); console.silly(MODULE + 'getBestServers, searching in:' + JSON.stringify(servers)); async.mapSeries(servers, pingServer, function (err, results) { if (err != null) { deferred.reject(err); } else { console.silly(MODULE + 'getBestServers, results is:' + JSON.stringify(results)); var lowest = Number.POSITIVE_INFINITY; var minIndex = null; for (var i = 0; i < results.length; i++) { if (results[i] != null && results[i].min < lowest) { lowest = results[i].min; minIndex = i; } } if (minIndex != null) { console.info(MODULE + "getBestServer: selected " + JSON.stringify(servers[minIndex])); deferred.resolve(servers[minIndex]); } else { console.alert(MODULE + 'Unable to complete any of the requested pings'); deferred.reject('Unable to complete any of the requested pings'); } } }); return deferred.promise; } function sortServers(serverList) { var client = { lat: NaN, lon: NaN }; if (configuration && configuration.location) { console.info(MODULE + "Sorting servers, using Agent geolocation"); client.lat = configuration.location.latitude; client.lon = configuration.location.longitude; } else if (clientConfig) { console.info(MODULE + "Sorting servers, using Ookla geolocation"); client.lat = clientConfig.client.lat; client.lon = clientConfig.client.lon; } for (var n = 0; n < serverList.length; n++) { var server = serverList[n]; server.dist = distanceCalculator.distanceTo(server.lat, server.lon, client.lat, client.lon); } serverList.sort(function (a, b) { return (a.dist - b.dist); }); return serverList; } function getNearestServers(maxServers) { console.info(MODULE + "getNearestServers."); maxServers = typeof maxServers !== 'undefined' ? maxServers : MAX_SERVERS_TO_PING; var deferred = q.defer(); fetch().then(function (serverList) { serverList = sortServers(serverList); var restrictedList = serverList.slice(0, maxServers); console.info(MODULE + "getNearestServers: Resolving nearest servers. Got restricted server list."); deferred.resolve(restrictedList); }).catch(function (error) { console.alert(MODULE + "getNearestServers: unable to resolving nearest servers."); deferred.reject(error); }); return deferred.promise; } function findNearestResponding(serverList, timeout, maxAttempts) { var deferred = q.defer(); if (!preferredServer) { console.info(MODULE + "findNearestResponding: preferred server not available."); serverList = sortServers(serverList); getBestServer(serverList).then(function (bestServer) { bestServer.location = {siteName: bestServer.sponsor}; console.info(MODULE + "findNearestResponding: Preferred server is " + bestServer.sponsor); //Do not save it in the module variable. deferred.resolve(bestServer); }).catch(function (error) { console.alert(MODULE + "findNearestResponding: Unable to find the nearest responding server " + JSON.stringify(error)); deferred.reject("unable to find the nearest responding server"); }); } else { console.info(MODULE + "findNearestResponding: The preferred server is already available"); preferredServer.location = {siteName: preferredServer.sponsor}; deferred.resolve(preferredServer); } return deferred.promise; } function setServerAsPreferred(server) { preferredServer = server; } function getPreferredServer() { return preferredServer; } return { fetch: fetch, findNearestResponding: findNearestResponding, findById: findById, getNearestServers: getNearestServers, getBestServer: getBestServer, setServerAsPreferred: setServerAsPreferred, getPreferredServer: getPreferredServer, blacklistServers: blacklistServers, pingServer: pingServer, getConfiguration: getConfiguration }; } module.exports.factory = serverSelector;