189 lines
4.5 KiB
TypeScript
189 lines
4.5 KiB
TypeScript
|
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<string> {
|
||
|
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);
|
||
|
}
|
||
|
}
|