domotz-remote-pawn
Version:
Domotz Agent
415 lines (373 loc) • 16.1 kB
JavaScript
/*jslint node: true */
var CONFIG_URL = 'http://www.speedtest.net/speedtest-config.php';
var SERVERS_URL = '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 FETCH_CACHE_SCOPE = 'fetch-ookla';
var MAX_SERVERS_TO_PING = 4;
var MAX_PING_PER_SERVER = 10;
var DEFAULT_TIMEOUT = 30000;
var PING_TIMEOUT = 10000;
function factory(resourceLocator, resetCache) {
var myConsole = resourceLocator.log.decorateLogs();
var lodash = resourceLocator.lodash;
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 fileUtils = resourceLocator.utils.file;
var configuration = resourceLocator.configuration;
var distanceCalculator = resourceLocator.distanceCalculator;
var blacklistFilePath = process.env.DOMOTZ_CONF_DIR + '/ookla_blacklist.conf';
var preferredServer = null;
var clientConfig = null;
var serversCache = cacheUtils.createCache(SERVER_CACHE_SCOPE);
var day = new Date().getDay();
var customBlacklist = [];
resetCache = typeof resetCache !== 'undefined' ? resetCache : true;
if (resetCache && day === 0) {
myConsole.info("(cache): It's sunday, resetting cache, scope = %s", SERVER_CACHE_SCOPE);
serversCache.reset();
}
fileUtils.readFile(blacklistFilePath, function (content) {
content.split('\n').forEach(function contentFilter(e) {
myConsole.silly(' Processed elem: %s', content);
if (e) {
customBlacklist.push(parseInt(e));
}
});
myConsole.info('(blacklist): Loaded custom blacklist: %s', JSON.stringify(customBlacklist));
});
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) {
myConsole.alert('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,
};
myConsole.alert('error getting XML data' + response.statusCode);
deferred.reject(error);
return;
}
myConsole.debug('getXMLRequest: Parsing xml ' + uri);
parseXML(body, function (err, xml) {
if (err) {
myConsole.alert('error parsing XML data' + JSON.stringify(err));
deferred.reject(err);
return;
}
myConsole.info('getXMLRequest: Resolving parsed xml ' + uri);
deferred.resolve(xml);
});
} catch (exception) {
myConsole.alert('error getting XML data' + JSON.stringify(exception));
deferred.reject(exception);
}
});
return deferred.promise;
}
function getConfiguration() {
var deferred = q.defer();
getXMLRequest(CONFIG_URL)
.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) {
myConsole.alert('error getting configuration' + JSON.stringify(exception));
deferred.reject(exception);
}
})
.catch(function (error) {
myConsole.error('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() {
myConsole.info('(fetch): Fetching server list');
var deferred = q.defer();
serversCache.get(
FETCH_CACHE_SCOPE,
function () {
getXMLRequest(SERVERS_URL)
.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++) {
var serverDescriptor = serversObject[i].$;
if (
config.ignore_ids.indexOf(serverDescriptor.id) === -1 &&
customBlacklist.indexOf(parseInt(serverDescriptor.id)) === -1
) {
serversList.push(serverDescriptor);
} else {
myConsole.verbose('(fetch): Server %d blacklisted.', serverDescriptor.id);
}
}
serversCache.set(FETCH_CACHE_SCOPE, serversList);
myConsole.info('(fetch): Removed %d invalid servers from the list', serversObject.length - serversList.length);
deferred.resolve(serversList);
})
.catch(function (error) {
myConsole.error('(fetch): Error retrieving xml configuration file');
deferred.reject(error);
});
})
.catch(function (error) {
myConsole.error('(fetch): Error getting servers from XML list: %s', JSON.stringify(error));
deferred.reject(error);
});
},
function (servers) {
myConsole.info('(fetch): Fetching ookla server list, cache resolved');
deferred.resolve(servers);
}
);
return deferred.promise;
}
function blacklistServers(blacklist) {
var blacklistIds = lodash.map(blacklist, 'id');
myConsole.info('blacklist server function: %s', JSON.stringify(blacklistIds));
serversCache.get(
FETCH_CACHE_SCOPE,
function onMiss() {},
function onHit(servers) {
var filteredServers = lodash.filter(servers, function (ooklaServer) {
return blacklistIds.indexOf(ooklaServer.id) === -1;
});
serversCache.set(FETCH_CACHE_SCOPE, filteredServers);
setServerAsPreferred(null);
myConsole.info('Blacklisted %d servers', blacklist.length);
}
);
}
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) {
myConsole.alert('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' });
})
.catch(function () {
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);
myConsole.silly('getBestServers, searching in:' + JSON.stringify(servers));
async.mapSeries(servers, pingServer, function (err, results) {
if (err !== null) {
deferred.reject(err);
} else {
myConsole.silly('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) {
myConsole.info('getBestServer: selected ' + JSON.stringify(servers[minIndex]));
deferred.resolve(servers[minIndex]);
} else {
myConsole.alert('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) {
myConsole.info('Sorting servers, using Agent geolocation');
client.lat = configuration.location.latitude;
client.lon = configuration.location.longitude;
} else if (clientConfig) {
myConsole.info('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) {
myConsole.info('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);
myConsole.info('getNearestServers: Resolving nearest servers. Got restricted server list.');
deferred.resolve(restrictedList);
})
.catch(function (error) {
myConsole.alert('getNearestServers: unable to resolving nearest servers.');
deferred.reject(error);
});
return deferred.promise;
}
function findNearestResponding(serverList) {
var deferred = q.defer();
if (!preferredServer) {
myConsole.info('findNearestResponding: preferred server not available.');
serverList = sortServers(serverList);
getBestServer(serverList)
.then(function (bestServer) {
bestServer.location = { siteName: bestServer.sponsor };
myConsole.info('findNearestResponding: Preferred server is ' + bestServer.sponsor);
//Do not save it in the module variable.
deferred.resolve(bestServer);
})
.catch(function (error) {
myConsole.alert('findNearestResponding: Unable to find the nearest responding server ' + JSON.stringify(error));
deferred.reject('unable to find the nearest responding server');
});
} else {
myConsole.info('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 = factory;