🚧 Partial switch to streams, and add utility endpoints

This commit is contained in:
C-3PO 2018-07-05 00:58:40 +02:00
parent e14a8db179
commit ae2ac6d278
Signed by: c3po
GPG key ID: 62993C4BB4D86F24
17 changed files with 395 additions and 213 deletions

View file

@ -5,7 +5,7 @@ import saveResponse from './funcs/saveResponse';
/** Downloads the given URL and saves it to disk. Throws error if download fails. */
export default function downloadUrlContents({ host, path }: {host: string, path: string}): Promise<fs.ReadStream> {
return new Promise((resolve, reject) => {
//Create HTTP request
const request = http.request({
family: 4,
host,

View file

@ -36,69 +36,69 @@ const assert = (condition: boolean) => { if (!condition) { console.warn('Assert
/** Looks up the given domain and returns a list of IP addresses, along with their time-to-live */
async function resolveDns(domain: string): Promise<IDnsResult[]> {
return new Promise((resolve) => {
//check given string for correctness to prevent injection attacks
if (!domain.match(/^[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,3}$/)) { return resolve([]); }
//check given string for correctness to prevent injection attacks
if (!domain.match(/^[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,3}$/)) { return resolve([]); }
//Check Level3/North_America separately
if (domain !== 'cdn-patch.swtor.com') {
dns.resolve4(domain, { ttl: true }, (err, result) => {
return resolve(result.map(({ address, ttl }) => ({ address, ttl, type: 'level3-us' as IDnsResult['type'] })));
});
} else {
//Use bash so we get more information.
//Also do plenty of asserts to ensure that overall CDN structure has stayed unchanged, and TODO send e-mail if it's different (max. once per hour)
exec('dig +noall +answer "cdn-patch.swtor.com"', { timeout: 10000 }, (error, stdout) => {
//check for error
assert(!error);
if (error) {
return resolve([]);
}
//Check Level3/North_America separately
if (domain !== 'cdn-patch.swtor.com') {
dns.resolve4(domain, { ttl: true }, (err, result) => {
return resolve(result.map(({ address, ttl }) => ({ address, ttl, type: 'level3-us' as IDnsResult['type'] })));
});
} else {
//Use bash so we get more information.
//Also do plenty of asserts to ensure that overall CDN structure has stayed unchanged, and TODO send e-mail if it's different (max. once per hour)
exec('dig +noall +answer "cdn-patch.swtor.com"', { timeout: 10000 }, (error, stdout) => {
//check for error
assert(!error);
if (error) {
return resolve([]);
}
const data = stdout.trim().split('\n').map((line) => line.split(/\t| /));
const data = stdout.trim().split('\n').map((line) => line.split(/\t| /));
//Verify output
assert(data.length > 3);
data.forEach((dataLine) => assert(dataLine.length === 5));
assert(data[0][0] === 'cdn-patch.swtor.com.');
assert(data[0][1].match(/^[0-9]{1,3}$/) !== null); //at least up to 598
assert(data[0][2] === 'IN');
assert(data[0][3] === 'CNAME');
assert(data[0][4] === 'gslb-patch.swtor.biowareonline.net.');
//Verify output
assert(data.length > 3);
data.forEach((dataLine) => assert(dataLine.length === 5));
assert(data[0][0] === 'cdn-patch.swtor.com.');
assert(data[0][1].match(/^[0-9]{1,3}$/) !== null); //at least up to 598
assert(data[0][2] === 'IN');
assert(data[0][3] === 'CNAME');
assert(data[0][4] === 'gslb-patch.swtor.biowareonline.net.');
assert(data[1][0] === 'gslb-patch.swtor.biowareonline.net.');
assert(data[1][1].match(/^[0-9]{1,2}$/) !== null); //up to 60 seconds
assert(data[1][2] === 'IN');
assert(data[1][3] === 'CNAME');
assert(data[1][4] === 'cdn-patch.swtor.com.edgesuite.net.' || data[1][4] === 'cdn-patch.swtor.com.c.footprint.net.');
assert(data[1][0] === 'gslb-patch.swtor.biowareonline.net.');
assert(data[1][1].match(/^[0-9]{1,2}$/) !== null); //up to 60 seconds
assert(data[1][2] === 'IN');
assert(data[1][3] === 'CNAME');
assert(data[1][4] === 'cdn-patch.swtor.com.edgesuite.net.' || data[1][4] === 'cdn-patch.swtor.com.c.footprint.net.');
assert(data[2][0] === data[1][4]);
assert(data[2][1].match(/^[0-9]{1,5}$/) !== null); //at least up to 15092 if Akamai, at least up to 627 if Level3
assert(data[2][2] === 'IN');
assert(data[2][3] === 'CNAME');
assert(
(data[2][4] === 'a56.d.akamai.net.' && data[1][4] === 'cdn-patch.swtor.com.edgesuite.net.') ||
(data[2][4] === 'eu.lvlt.cdn.ea.com.c.footprint.net.' && data[1][4] === 'cdn-patch.swtor.com.c.footprint.net.'),
);
assert(data[2][0] === data[1][4]);
assert(data[2][1].match(/^[0-9]{1,5}$/) !== null); //at least up to 15092 if Akamai, at least up to 627 if Level3
assert(data[2][2] === 'IN');
assert(data[2][3] === 'CNAME');
assert(
(data[2][4] === 'a56.d.akamai.net.' && data[1][4] === 'cdn-patch.swtor.com.edgesuite.net.') ||
(data[2][4] === 'eu.lvlt.cdn.ea.com.c.footprint.net.' && data[1][4] === 'cdn-patch.swtor.com.c.footprint.net.'),
);
for (let i = 3, il = data.length; i < il; i += 1) {
assert(data[i][0] === data[2][4]);
assert(data[i][1].match(/^[0-9]{1,3}$/) !== null); //up to 60 seconds if Akamai, at least up to 218 if Level3
assert(data[i][2] === 'IN');
assert(data[i][3] === 'A');
assert(data[i][4].match(/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) !== null);
}
for (let i = 3, il = data.length; i < il; i += 1) {
assert(data[i][0] === data[2][4]);
assert(data[i][1].match(/^[0-9]{1,3}$/) !== null); //up to 60 seconds if Akamai, at least up to 218 if Level3
assert(data[i][2] === 'IN');
assert(data[i][3] === 'A');
assert(data[i][4].match(/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) !== null);
}
//Prepare return values
let type: IDnsResult['type'];
switch (data[1][4]) {
case 'cdn-patch.swtor.com.edgesuite.net.': type = 'akamai'; break;
case 'cdn-patch.swtor.com.c.footprint.net.': type = 'level3-eu'; break;
default: type = 'unknown';
}
//Prepare return values
let type: IDnsResult['type'];
switch (data[1][4]) {
case 'cdn-patch.swtor.com.edgesuite.net.': type = 'akamai'; break;
case 'cdn-patch.swtor.com.c.footprint.net.': type = 'level3-eu'; break;
default: type = 'unknown';
}
resolve(data.filter((e, index) => index >= 3).map(([, ttl, , , address]) => ({ ttl: Math.min(Number(ttl), Number(data[0][1]), Number(data[1][1]), Number(data[2][1])), address, type })));
});
}
resolve(data.filter((e, index) => index >= 3).map(([, ttl, , , address]) => ({ ttl: Math.min(Number(ttl), Number(data[0][1]), Number(data[1][1]), Number(data[2][1])), address, type })));
});
}
}) as Promise<IDnsResult[]>;
}

View file

@ -4,6 +4,7 @@ import handleResponse from './funcs/handleResponse';
/** Downloads the given URL into memory and returns it as an ArrayBuffer. Throws error if download fails or file is too large to be handled in memory. */
export default function getUrlContents({ host, path }: {host: string, path: string}): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
//Create HTTP request
const request = http.request({
family: 4,
host,

View file

@ -12,78 +12,78 @@ servers.push({ ip: '159.153.92.51', type: 'master', lastSeen: Infinity, weight:
/** Updates the list of servers based on current DNS data */
async function heartbeatDns(domain: string) {
//Get list of current patch servers
const dnsResults = await resolveDns(domain);
//Get list of current patch servers
const dnsResults = await resolveDns(domain);
//Remember time when response came in
const now = Date.now() - startTime;
//Remember time when response came in
const now = Date.now() - startTime;
//Schedule next check based on time-to-live, but never longer than 1 minute
const ttl = Math.min(60, ...(dnsResults.map((obj) => obj.ttl))) + 1;
setTimeout(heartbeatDns.bind(null, domain), ttl * 1000);
//Schedule next check based on time-to-live, but never longer than 1 minute
const ttl = Math.min(60, ...(dnsResults.map((obj) => obj.ttl))) + 1;
setTimeout(heartbeatDns.bind(null, domain), ttl * 1000);
//Update array with new information
dnsResults.forEach(
({ address, type }, index) => {
//Calculate weight:
//on cdn-patch.swtor.com: 3 if first, 2 if second, otherwise 1
let weight = (index < 2) ? (3 - index) : 1;
//on Level3 US: 1.2 is first, 1 if second
if (domain !== 'cdn-patch.swtor.com') {
weight = (index === 0) ? 1.2 : 1;
}
//Update array with new information
dnsResults.forEach(
({ address, type }, index) => {
//Calculate weight:
//on cdn-patch.swtor.com: 3 if first, 2 if second, otherwise 1
let weight = (index < 2) ? (3 - index) : 1;
//on Level3 US: 1.2 is first, 1 if second
if (domain !== 'cdn-patch.swtor.com') {
weight = (index === 0) ? 1.2 : 1;
}
//if ip is already contained
for (let i = 0, il = servers.length; i < il; i += 1) {
const server = servers[i];
if (server.ip === address) {
server.lastSeen = now;
server.weight += weight;
if (server.type !== type) { server.type = type; }
return;
}
}
//if not yet contained, add to array
servers.push({
ip: address,
lastSeen: now,
type,
weight: weight + 1, //give a boost to new values compared to existing values
});
},
);
//Remove old entries - old = not seen for one hour
servers = servers.filter((server) => (now - server.lastSeen) < 3600000);
//Decay weights - reduce them based on update frequency (-50% if full minute, but less if TTL was shorter than a minute)
const decayFactor = 0.5 ** ((now - lastUpdate) / 60000);
lastUpdate = now;
servers.forEach((server) => { server.weight *= decayFactor; });
//Sort the array by weight
servers.sort((a, b) => b.weight - a.weight);
//Output current list
let output = '';
servers.forEach((server) => {
//set colors based on server type, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
//bright color if seen within last 5 minutes
if (now - server.lastSeen < 300000) { output += '\x1b[1m'; } else { output += '\x1b[0m'; }
switch (server.type) {
case 'master': output += '\x1b[37m'; break; //white
case 'akamai': output += '\x1b[35m'; break; //magenta
case 'level3-us': output += '\x1b[32m'; break; //green
case 'level3-eu': output += '\x1b[36m'; break; //cyan
case 'unknown': default: output += '\x1b[31m'; //red
//if ip is already contained
for (let i = 0, il = servers.length; i < il; i += 1) {
const server = servers[i];
if (server.ip === address) {
server.lastSeen = now;
server.weight += weight;
if (server.type !== type) { server.type = type; }
return;
}
output += server.ip;
output += '\t';
});
//Reset color to default
output += '\x1b[0m';
console.log(output);
}
//if not yet contained, add to array
servers.push({
ip: address,
lastSeen: now,
type,
weight: weight + 1, //give a boost to new values compared to existing values
});
},
);
//Remove old entries - old = not seen for one hour
servers = servers.filter((server) => (now - server.lastSeen) < 3600000);
//Decay weights - reduce them based on update frequency (-50% if full minute, but less if TTL was shorter than a minute)
const decayFactor = 0.5 ** ((now - lastUpdate) / 60000);
lastUpdate = now;
servers.forEach((server) => { server.weight *= decayFactor; });
//Sort the array by weight
servers.sort((a, b) => b.weight - a.weight);
//Output current list
let output = '';
servers.forEach((server) => {
//set colors based on server type, see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
//bright color if seen within last 5 minutes
if (now - server.lastSeen < 300000) { output += '\x1b[1m'; } else { output += '\x1b[0m'; }
switch (server.type) {
case 'master': output += '\x1b[37m'; break; //white
case 'akamai': output += '\x1b[35m'; break; //magenta
case 'level3-us': output += '\x1b[32m'; break; //green
case 'level3-eu': output += '\x1b[36m'; break; //cyan
case 'unknown': default: output += '\x1b[31m'; //red
}
output += server.ip;
output += '\t';
});
//Reset color to default
output += '\x1b[0m';
console.log(output);
}
//start loading additional addresses, both from CDN, and specifically from Level3/North_America so we have more than just European servers