import * as plugins from './smartnetwork.plugins.js'; import * as stats from './helpers/stats.js'; export class CloudflareSpeed { constructor() {} public async speedTest() { const latency = await this.measureLatency(); const serverLocations = await this.fetchServerLocations(); const cgiData = await this.fetchCfCdnCgiTrace(); // lets test the download speed const testDown1 = await this.measureDownload(101000, 10); const testDown2 = await this.measureDownload(1001000, 8); const testDown3 = await this.measureDownload(10001000, 6); const testDown4 = await this.measureDownload(25001000, 4); const testDown5 = await this.measureDownload(100001000, 1); const downloadTests = [...testDown1, ...testDown2, ...testDown3, ...testDown4, ...testDown5]; const speedDownload = stats.quartile(downloadTests, 0.9).toFixed(2); // lets test the upload speed const testUp1 = await this.measureUpload(11000, 10); const testUp2 = await this.measureUpload(101000, 10); const testUp3 = await this.measureUpload(1001000, 8); const uploadTests = [...testUp1, ...testUp2, ...testUp3]; const speedUpload = stats.quartile(uploadTests, 0.9).toFixed(2); return { ...latency, ip: cgiData.ip, serverLocation: { shortId: cgiData.colo, name: serverLocations[cgiData.colo], availableLocations: serverLocations, }, downloadSpeed: speedDownload, uploadSpeed: speedUpload, }; } 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 measureDownload(bytes: number, iterations: number) { const measurements: number[] = []; for (let i = 0; i < iterations; i += 1) { await this.download(bytes).then( async (response) => { const transferTime = response[5] - response[4]; measurements.push(await this.measureSpeed(bytes, transferTime)); }, (error) => { console.log(`Error: ${error}`); } ); } return measurements; } public async measureUpload(bytes: number, iterations: number) { const measurements: number[] = []; for (let i = 0; i < iterations; i += 1) { await this.upload(bytes).then( async (response) => { const transferTime = response[6]; measurements.push(await this.measureSpeed(bytes, transferTime)); }, (error) => { console.log(`Error: ${error}`); } ); } return measurements; } public async measureSpeed(bytes: number, duration: number) { return (bytes * 8) / (duration / 1000) / 1e6; } public async fetchServerLocations(): Promise<{ [key: string]: string }> { const res = JSON.parse(await this.get('speed.cloudflare.com', '/locations')); return res.reduce((data: any, optionsArg: { iata: string; city: string }) => { // Bypass prettier "no-assign-param" rules const data1 = data; data1[optionsArg.iata] = optionsArg.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: Array = []; 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: number) { const options = { hostname: 'speed.cloudflare.com', path: `/__down?bytes=${bytes}`, method: 'GET', }; return this.request(options); } public async upload(bytes: number) { const data = '0'.repeat(bytes); const options = { hostname: 'speed.cloudflare.com', path: '/__up', method: 'POST', headers: { 'Content-Length': Buffer.byteLength(data), }, }; return this.request(options, data); } public async request(options: plugins.https.RequestOptions, data = ''): Promise { let started: number; let dnsLookup: number; let tcpHandshake: number; let sslHandshake: number; let ttfb: number; let ended: number; 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: string) => text .split('\n') .map((i) => { const j = i.split('='); return [j[0], j[1]]; }) .reduce((data: any, [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); } }