From fab92b078bf001ff911974379ebc083c83af4bf0 Mon Sep 17 00:00:00 2001 From: C-3PO Date: Fri, 22 Jun 2018 12:25:31 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=9A=20Merge=20code=20from=20old=20patc?= =?UTF-8?q?her?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cdn/dns.ts | 192 +++++++++++++++++++++++++++++++++++ src/interfaces/IDnsResult.ts | 25 +++++ src/ssn/readSolidpkg.ts | 3 + src/ssn/releasePaths.ts | 40 ++++++++ 4 files changed, 260 insertions(+) create mode 100644 src/cdn/dns.ts create mode 100644 src/interfaces/IDnsResult.ts create mode 100644 src/ssn/releasePaths.ts diff --git a/src/cdn/dns.ts b/src/cdn/dns.ts new file mode 100644 index 0000000..580f1d6 --- /dev/null +++ b/src/cdn/dns.ts @@ -0,0 +1,192 @@ +/* --- DNS resolver for the CDN storing SWTOR's patch files --- +| +| The SWTOR launcher first connects to cdn-patch.swtor.com, +| there the traffic is split onto Akamai and Level3, and +| distributed to local points of presence. +| The master files appear to be stored on 159.153.92.51, and +| all CDNs are synchronized with that source. +| +| > cdn-patch.swtor.com +| -> gslb-patch.swtor.biowareonline.net +| -> cdn-patch.swtor.com.edgesuite.net (Akamai) +| | -> a56.d.akamai.net +| | -> 2.21.74.90 +| | -> 2.21.74.98 +| -> cdn-patch.swtor.com.c.footprint.net (Level3) +| -> eu.lvlt.cdn.ea.com.c.footprint.net (Europe) +| | -> 8.12.207.125 +| | -> 8.253.37.126 +| -> na.lvlt.cdn.ea.com.c.footprint.net (US) +| -> 205.128.74.252 +| -> 4.23.36.253 +| -> 192.221.105.254 +| +| Examples: +| - http://cdn-patch.swtor.com/patch/assets_swtor_main/assets_swtor_main_-1to0.solidpkg +| - http://159.153.92.51/patch/assets_swtor_main/assets_swtor_main_-1to0.solidpkg +*/ + +import { exec} from 'child_process'; +import * as dns from 'dns'; +import { IDnsResult, IServerEntry } from '../interfaces/IDnsResult'; + +//Time when this script started, for delta time calculations +const startTime = Date.now(); +let lastUpdate = 0; + +//List of servers, sorted by reliability +let servers: IServerEntry[] = []; +//the master source is included by default +servers.push({ ip: '159.153.92.51', type: 'master', lastSeen: Infinity, weight: Infinity }); + +//TODO: send e-mail with the error +const assert = (cond: boolean) => { if (!cond) { console.warn('Assert failed'); } }; + +//Looks up the given domain and returns a list of IP addresses, along with their time-to-live +async function resolveDns(domain: string): Promise { + 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 Level3/North_America separetely + 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| /)); + + //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[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++) { + 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'; + } + + 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; +} + +//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); + + //Remeber 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); + + //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++) { + 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 + } + 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 +heartbeatDns('cdn-patch.swtor.com'); +heartbeatDns('na.lvlt.cdn.ea.com.c.footprint.net'); diff --git a/src/interfaces/IDnsResult.ts b/src/interfaces/IDnsResult.ts new file mode 100644 index 0000000..2c8e486 --- /dev/null +++ b/src/interfaces/IDnsResult.ts @@ -0,0 +1,25 @@ +type NetworkType = 'master' | 'akamai' | 'level3-eu' | 'level3-us' | 'unknown'; + +/** One search result in a DNS query response */ +interface IDnsResult { + /** IP address to which cdn-patch.swtor.com resolves to. */ + address: string; + /** Which network / cloud provider this IP address belongs to. */ + type: NetworkType; + /** Time-to-live, given in seconds, of how long the results last, or when we need to update them. */ + ttl: number; +} + +/** An IP address that was returned by resolved the CDN domain. */ +interface IServerEntry { + /** IP address to which cdn-patch.swtor.com resolves to. */ + ip: string; + /** Which network / cloud provider this IP address belongs to. */ + type: NetworkType; + /** When we were last able to resolve to this IP address. */ + lastSeen: number; + /** A measure of how reliable this IP address is, based on how often and how recently it was resolved. */ + weight: number; +} + +export { IDnsResult, IServerEntry }; diff --git a/src/ssn/readSolidpkg.ts b/src/ssn/readSolidpkg.ts index 789418e..93602a3 100644 --- a/src/ssn/readSolidpkg.ts +++ b/src/ssn/readSolidpkg.ts @@ -135,6 +135,9 @@ export default function readSolidpkg(buffer: Buffer) { const decodedPassword = modifyPassword(encodedPassword); //TODO: read encrypted + compressed file + //pos = start of central dir + // + //fseek(fp, pos - centralDirOffset, SEEK_SET); //... return { lastMod1, lastMod2, fileCrc, comprSize, uncomprSize, fileNameLength, extraFieldLength, fileCommentLength, relOffset, fileName, encodedPassword, decodedPassword }; diff --git a/src/ssn/releasePaths.ts b/src/ssn/releasePaths.ts new file mode 100644 index 0000000..9870d98 --- /dev/null +++ b/src/ssn/releasePaths.ts @@ -0,0 +1,40 @@ +export default function getFroms(product: string, releaseTo: number) { + //The launcher (patcher, patcher2014, patcher2017) is always installing from -1, never from a previous version + if (product.startsWith('patcher')) { + return [-1]; + } else { + const froms: number[] = []; + + //always X-1toX + froms.push(releaseTo - 1); + + //also 0toX, unless X is 0 + if (releaseTo >= 2) { froms.push(0); } + + if ((releaseTo % 5) === 0) { + //also X-5toX if X % 5 + if (releaseTo >= 10) { froms.push(releaseTo - 5); } + //also X-20toX if X % 5 + if (releaseTo >= 25) { froms.push(releaseTo - 20); } + //also downgrade from the following four releases + froms.push(releaseTo + 1); + froms.push(releaseTo + 2); + froms.push(releaseTo + 3); + froms.push(releaseTo + 4); + } else { //for some of the older releases, an update from _5 or _0 is possible + /* + e.g. in asset_swtor_main: + 5to7, 5to8, 5to9, + 10to12, 10to13, 10to14, + 15to17, 15to18, 15to19, + 20to22, 20to23, 20to24, + 25to27, 25to28, 25to29, + 30to32, 30to33, 30to34, + 35to37, etc. , 85to87 + */ + if (releaseTo >= 7 && (releaseTo % 5) > 1) { froms.push(releaseTo - (releaseTo % 5)); } + } + + return froms; + } +}