import * as plugins from './smartnetwork.plugins'; import * as stats from './helpers/stats'; export class CloudflareSpeed { constructor() {} public async speedTest() { const latency = await this.measureLatency(); const serverLocations = await this.fetchServerLocations(); const cgiData = await this.fetchCfCdnCgiTrace(); return { ...latency, ip: cgiData.ip, serverLocation: { shortId: cgiData.colo, name: serverLocations[cgiData.colo], availableLocations: serverLocations, } }; } public async measureLatency() { const measurements: number[] = []; for (let i = 0; i < 20; i += 1) { await this.download(1000).then( (response) => { // TTFB - Server processing time measurements.push(response[4] - response[0] - response[6]); }, (error) => { console.log(`Error: ${error}`); } ); } return { maxTime: Math.max(...measurements), minTime: Math.min(...measurements), averageTime: stats.average(measurements), medianTime: stats.median(measurements), jitter: stats.jitter(measurements), } ; } public async fetchServerLocations(): Promise<{[key: string]: string}> { const res = JSON.parse(await this.get('speed.cloudflare.com', '/locations')); return res.reduce((data, { iata, city }) => { // Bypass prettier "no-assign-param" rules const data1 = data; data1[iata] = city; return data1; }, {}); } public async get(hostname: string, path: string): Promise { return new Promise((resolve, reject) => { const req = plugins.https.request( { hostname, path, method: 'GET', }, (res) => { const body = []; res.on('data', (chunk) => { body.push(chunk); }); res.on('end', () => { try { resolve(Buffer.concat(body).toString()); } catch (e) { reject(e); } }); req.on('error', (err) => { reject(err); }); } ); req.end(); }); } public async download(bytes) { const options = { hostname: 'speed.cloudflare.com', path: `/__down?bytes=${bytes}`, method: 'GET', }; return this.request(options); } public async request(options, data = '') { let started; let dnsLookup; let tcpHandshake; let sslHandshake; let ttfb; let ended; return new Promise((resolve, reject) => { started = plugins.perfHooks.performance.now(); const req = plugins.https.request(options, (res) => { res.once('readable', () => { ttfb = plugins.perfHooks.performance.now(); }); res.on('data', () => {}); res.on('end', () => { ended = plugins.perfHooks.performance.now(); resolve([ started, dnsLookup, tcpHandshake, sslHandshake, ttfb, ended, parseFloat(res.headers['server-timing'].slice(22) as any), ]); }); }); req.on('socket', (socket) => { socket.on('lookup', () => { dnsLookup = plugins.perfHooks.performance.now(); }); socket.on('connect', () => { tcpHandshake = plugins.perfHooks.performance.now(); }); socket.on('secureConnect', () => { sslHandshake = plugins.perfHooks.performance.now(); }); }); req.on('error', (error) => { reject(error); }); req.write(data); req.end(); }); } public async fetchCfCdnCgiTrace(): Promise<{ fl: string, h: string, ip: string, ts: string, visit_scheme: string, uag: string, colo: string, http: string, loc: string, tls: string, sni: string, warp: string, gateway: string, }> { const parseCfCdnCgiTrace = (text) => text .split('\n') .map((i) => { const j = i.split('='); return [j[0], j[1]]; }) .reduce((data, [k, v]) => { if (v === undefined) return data; // Bypass prettier "no-assign-param" rules const data1 = data; // Object.fromEntries is only supported by Node.js 12 or newer data1[k] = v; return data1; }, {}); return this.get('speed.cloudflare.com', '/cdn-cgi/trace').then(parseCfCdnCgiTrace); } }