// ZeroTier distributed HTTP test agent

// ---------------------------------------------------------------------------
// Customizable parameters:

// Time between startup and first test attempt
var TEST_STARTUP_LAG = 10000;

// Maximum interval between test attempts (actual timing is random % this)
var TEST_INTERVAL_MAX = (60000 * 10);

// Test timeout in ms
var TEST_TIMEOUT = 30000;

// Where should I get other agents' IDs and POST results?
var SERVER_HOST = '52.26.196.147';
var SERVER_PORT = 18080;

// Which port do agents use to serve up test data to each other?
var AGENT_PORT = 18888;

// Payload size in bytes
var PAYLOAD_SIZE = 5000;

// ---------------------------------------------------------------------------

var ipaddr = require('ipaddr.js');
var os = require('os');
var http = require('http');
var async = require('async');

var express = require('express');
var app = express();

// Find our ZeroTier-assigned RFC4193 IPv6 address
var thisAgentId = null;
var interfaces = os.networkInterfaces();
if (!interfaces) {
	console.error('FATAL: os.networkInterfaces() failed.');
	process.exit(1);
}
for(var ifname in interfaces) {
	var ifaddrs = interfaces[ifname];
	if (Array.isArray(ifaddrs)) {
		for(var i=0;i<ifaddrs.length;++i) {
			if (ifaddrs[i].family == 'IPv6') {
				try {
					var ipbytes = ipaddr.parse(ifaddrs[i].address).toByteArray();
					if ((ipbytes.length === 16)&&(ipbytes[0] == 0xfd)&&(ipbytes[9] == 0x99)&&(ipbytes[10] == 0x93)) {
						thisAgentId = '';
						for(var j=0;j<16;++j) {
							var tmp = ipbytes[j].toString(16);
							if (tmp.length === 1)
								thisAgentId += '0';
							thisAgentId += tmp;
						}
					}
				} catch (e) {
					console.error(e);
				}
			}
		}
	}
}
if (thisAgentId === null) {
	console.error('FATAL: no ZeroTier-assigned RFC4193 IPv6 addresses found on any local interface!');
	process.exit(1);
}

//console.log(thisAgentId);

// Create a random (and therefore not very compressable) payload
var payload = new Buffer(PAYLOAD_SIZE);
for(var xx=0;xx<PAYLOAD_SIZE;++xx) {
	payload.writeUInt8(Math.round(Math.random() * 255.0),xx);
}

function agentIdToIp(agentId)
{
	var ip = '';
	ip += agentId.substr(0,4);
	ip += ':';
	ip += agentId.substr(4,4);
	ip += ':';
	ip += agentId.substr(8,4);
	ip += ':';
	ip += agentId.substr(12,4);
	ip += ':';
	ip += agentId.substr(16,4);
	ip += ':';
	ip += agentId.substr(20,4);
	ip += ':';
	ip += agentId.substr(24,4);
	ip += ':';
	ip += agentId.substr(28,4);
	return ip;
};

var lastTestResult = null;
var allOtherAgents = {};

function doTest()
{
	var submit = http.request({
		host: SERVER_HOST,
		port: SERVER_PORT,
		path: '/'+thisAgentId,
		method: 'POST'
	},function(res) {
		var body = '';
		res.on('data',function(chunk) { body += chunk.toString(); });
		res.on('end',function() {

			if (body) {
				try {
					var peers = JSON.parse(body);
					if (Array.isArray(peers)) {
						for(var xx=0;xx<peers.length;++xx)
							allOtherAgents[peers[xx]] = true;
					}
				} catch (e) {}
			}

			var agents = Object.keys(allOtherAgents);
			if (agents.length > 1) {

				var target = agents[Math.floor(Math.random() * agents.length)];
				while (target === thisAgentId)
					target = agents[Math.floor(Math.random() * agents.length)];

				var testRequest = null;
				var timeoutId = null;
				timeoutId = setTimeout(function() {
					if (testRequest !== null)
						testRequest.abort();
					timeoutId = null;
				},TEST_TIMEOUT);
				var startTime = Date.now();

				testRequest = http.get({
					host: agentIdToIp(target),
					port: AGENT_PORT,
					path: '/'
				},function(res) {
					var bytes = 0;
					res.on('data',function(chunk) { bytes += chunk.length; });
					res.on('end',function() {
						lastTestResult = {
							source: thisAgentId,
							target: target,
							time: (Date.now() - startTime),
							bytes: bytes,
							timedOut: (timeoutId === null),
							error: null
						};
						if (timeoutId !== null)
							clearTimeout(timeoutId);
						return setTimeout(doTest,Math.round(Math.random() * TEST_INTERVAL_MAX) + 1);
					});
				}).on('error',function(e) {
					lastTestResult = {
						source: thisAgentId,
						target: target,
						time: (Date.now() - startTime),
						bytes: 0,
						timedOut: (timeoutId === null),
						error: e.toString()
					};
					if (timeoutId !== null)
						clearTimeout(timeoutId);
					return setTimeout(doTest,Math.round(Math.random() * TEST_INTERVAL_MAX) + 1);
				});

			} else {
				return setTimeout(doTest,1000);
			}

		});
	}).on('error',function(e) {
		console.log('POST failed: '+e.toString());
		return setTimeout(doTest,1000);
	});
	if (lastTestResult !== null) {
		submit.write(JSON.stringify(lastTestResult));
		lastTestResult = null;
	}
	submit.end();
};

// Agents just serve up a test payload
app.get('/',function(req,res) { return res.status(200).send(payload); });

var expressServer = app.listen(AGENT_PORT,function () {
	// Start timeout-based loop
	setTimeout(doTest(),TEST_STARTUP_LAG);
});