diff --git a/license b/license new file mode 100644 index 0000000..c9bedc7 --- /dev/null +++ b/license @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2015 Lossless GmbH +Copyright (c) 2020 Tomás Arias + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/package-lock.json b/package-lock.json index feab590..2f53822 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11866,11 +11866,6 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "dev": true }, - "speed-cloudflare-cli": { - "version": "2.0.3", - "resolved": "https://verdaccio.lossless.one/speed-cloudflare-cli/-/speed-cloudflare-cli-2.0.3.tgz", - "integrity": "sha512-aYnaj7ZhasoW+zhsVrO0tmGTxAfBY6eYFoIdoBJdJ0yZE1F/g11pt2AJqIuST0Qbe0x/pzwsEed267tCR6HuOA==" - }, "speedtest-net": { "version": "2.1.1", "resolved": "https://verdaccio.lossless.one/speedtest-net/-/speedtest-net-2.1.1.tgz", diff --git a/package.json b/package.json index 8cd935d..dc14980 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@types/default-gateway": "^3.0.1", "isopen": "^1.3.0", "public-ip": "^4.0.3", - "speed-cloudflare-cli": "^2.0.3", "speedtest-net": "^2.1.1", "systeminformation": "^5.6.12" }, @@ -43,4 +42,4 @@ "browserslist": [ "last 1 chrome versions" ] -} +} \ No newline at end of file diff --git a/readme.md b/readme.md index 2561906..62d6c36 100644 --- a/readme.md +++ b/readme.md @@ -31,13 +31,17 @@ const testSmartNetwork = new smartnetwork.SmartNetwork(); const run = async () => { // measure average speed over a period of 5 seconds // the structure of speedResult is self explanatory using TypeScript (or the linked TypeDoc above) - const speedResult = testSmartNetwork.getSpeed(5000); + const speedResult: smartnetwork.SpeedResult = testSmartNetwork.getSpeed(5000); - // - const isLocalPortAvailable: boolean = await testSmartNetwork.isLocalPortAvailable(1234); + // you can check for local ports before trying to bind to it from your nodejs program + const isLocalPortUnused: boolean = await testSmartNetwork.isLocalPortUnused(1234); + + // you can run basic port checks on remote domains. const isRemotePortAvailable: boolean = await testSmartNetwork.isRemotePortAvailable( 'google.com:80' ); + + // just another way to call for the same thing as above const isRemotePortAvailable: boolean = await testSmartNetwork.isRemotePortAvailable( 'google.com', 80 diff --git a/test/test.ts b/test/test.ts index 8942a85..b0e828f 100644 --- a/test/test.ts +++ b/test/test.ts @@ -10,8 +10,7 @@ tap.test('should create a valid instance of SmartNetwork', async () => { tap.test('should perform a speedtest', async () => { const result = await testSmartNetwork.getSpeed(); - // console.log(`Download speed for this instance is ${result.download.bandwidth}`); - // console.log(`Upload speed for this instance is ${result.download.bandwidth}`); + console.log(result); }); tap.test('should determine wether a port is free', async () => { diff --git a/ts/helpers/stats.ts b/ts/helpers/stats.ts new file mode 100644 index 0000000..bc7040a --- /dev/null +++ b/ts/helpers/stats.ts @@ -0,0 +1,43 @@ +export function average(values) { + let total = 0; + + for (let i = 0; i < values.length; i += 1) { + total += values[i]; + } + + return total / values.length; +} + +export function median(values) { + const half = Math.floor(values.length / 2); + + values.sort((a, b) => a - b); + + if (values.length % 2) return values[half]; + + return (values[half - 1] + values[half]) / 2; +} + +export function quartile(values, percentile) { + values.sort((a, b) => a - b); + const pos = (values.length - 1) * percentile; + const base = Math.floor(pos); + const rest = pos - base; + + if (values[base + 1] !== undefined) { + return values[base] + rest * (values[base + 1] - values[base]); + } + + return values[base]; +} + +export function jitter(values) { + // Average distance between consecutive latency measurements... + let jitters = []; + + for (let i = 0; i < values.length - 1; i += 1) { + jitters.push(Math.abs(values[i] - values[i + 1])); + } + + return average(jitters); +} diff --git a/ts/smartnetwork.classes.cloudflarespeed.ts b/ts/smartnetwork.classes.cloudflarespeed.ts new file mode 100644 index 0000000..59355f1 --- /dev/null +++ b/ts/smartnetwork.classes.cloudflarespeed.ts @@ -0,0 +1,188 @@ +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); + } +} diff --git a/ts/smartnetwork.classes.smartnetwork.ts b/ts/smartnetwork.classes.smartnetwork.ts index 8fc615c..b5869d5 100644 --- a/ts/smartnetwork.classes.smartnetwork.ts +++ b/ts/smartnetwork.classes.smartnetwork.ts @@ -1,33 +1,6 @@ import * as plugins from './smartnetwork.plugins'; -export interface ISpeedtestData { - timestamp: Date; - ping: { jitter: number; latency: number }; - download: { bandwidth: number; bytes: number; elapsed: number }; - upload: { bandwidth: number; bytes: number; elapsed: number }; - packetLoss: number; - isp: string; - interface: { - internalIp: string; - name: string; - macAddr: string; - isVpn: false; - externalIp: string; - }; - server: { - id: number; - name: string; - location: string; - country: string; - host: string; - port: number; - ip: string; - }; - result: { - id: string; - url: string; - }; -} +import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed'; /** * SmartNetwork simplifies actions within the network @@ -38,7 +11,8 @@ export class SmartNetwork { * @param measurementTime */ public async getSpeed() { - const test = null; + const cloudflareSpeedInstance = new CloudflareSpeed(); + const test = await cloudflareSpeedInstance.speedTest(); return test; } diff --git a/ts/smartnetwork.plugins.ts b/ts/smartnetwork.plugins.ts index 7a61bf0..08d4360 100644 --- a/ts/smartnetwork.plugins.ts +++ b/ts/smartnetwork.plugins.ts @@ -1,7 +1,9 @@ // native scope import * as os from 'os'; +import * as https from 'https'; +import * as perfHooks from 'perf_hooks'; -export { os }; +export { os, https, perfHooks }; // @pushrocks scope import * as smartpromise from '@pushrocks/smartpromise';