Compare commits

...

16 Commits

Author SHA1 Message Date
9544823401 2.0.6 2021-04-28 13:41:56 +00:00
260f000304 fix(core): update 2021-04-28 13:41:55 +00:00
d8044507ed 2.0.5 2021-04-27 09:46:32 +00:00
b9380be999 fix(core): update 2021-04-27 09:46:31 +00:00
1b9c354d69 2.0.4 2021-04-13 13:05:48 +00:00
a8f4ecf98f fix(core): update 2021-04-13 13:05:47 +00:00
6350088d2a 2.0.3 2021-04-13 10:29:43 +00:00
10ef1d0455 fix(core): update 2021-04-13 10:29:42 +00:00
f709238e50 2.0.2 2021-04-13 10:09:40 +00:00
49940635d5 fix(core): update 2021-04-13 10:09:39 +00:00
ec4121cbcf 2.0.1 2021-04-13 10:09:09 +00:00
ea9a2572f9 fix(core): update 2021-04-13 10:09:09 +00:00
cc0f1c40a6 2.0.0 2021-04-13 10:07:39 +00:00
9da04081e4 BREAKING CHANGE(core): update 2021-04-13 10:07:39 +00:00
4ae0925043 1.1.22 2020-08-13 02:34:06 +00:00
4e862e784b fix(core): update 2020-08-13 02:34:06 +00:00
9 changed files with 4062 additions and 1815 deletions

23
license Normal file
View File

@ -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.

5530
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@pushrocks/smartnetwork",
"version": "1.1.21",
"version": "2.0.6",
"private": false,
"description": "network diagnostics",
"main": "dist_ts/index.js",
@ -9,24 +9,23 @@
"license": "MIT",
"scripts": {
"test": "(tstest test/)",
"build": "(tsbuild)"
"build": "(tsbuild --web)"
},
"devDependencies": {
"@gitzone/tsbuild": "^2.1.25",
"@gitzone/tstest": "^1.0.44",
"@pushrocks/tapbundle": "^3.2.9",
"tslint": "^5.20.1",
"@gitzone/tstest": "^1.0.52",
"@pushrocks/tapbundle": "^3.2.14",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0"
},
"dependencies": {
"@pushrocks/smartpromise": "^3.0.6",
"@pushrocks/smartstring": "^3.0.18",
"@pushrocks/smartpromise": "^3.1.3",
"@pushrocks/smartstring": "^3.0.24",
"@types/default-gateway": "^3.0.1",
"default-gateway": "^5.0.5",
"isopen": "^1.3.0",
"public-ip": "^4.0.2",
"speedtest-net": "^1.6.0",
"systeminformation": "^4.26.10"
"public-ip": "^4.0.3",
"speedtest-net": "^2.1.1",
"systeminformation": "^5.6.12"
},
"files": [
"ts/**/*",

View File

@ -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

View File

@ -9,13 +9,12 @@ tap.test('should create a valid instance of SmartNetwork', async () => {
});
tap.test('should perform a speedtest', async () => {
let result = await testSmartNetwork.getSpeed();
console.log(`Download speed for this instance is ${result.speeds.download}`);
console.log(`Upload speed for this instance is ${result.speeds.upload}`);
const result = await testSmartNetwork.getSpeed();
console.log(result);
});
tap.test('should determine wether a port is free', async () => {
await expect(testSmartNetwork.isLocalPortAvailable(8080)).to.eventually.be.true;
await expect(testSmartNetwork.isLocalPortUnused(8080)).to.eventually.be.true;
});
tap.test('should scan a port', async () => {

43
ts/helpers/stats.ts Normal file
View File

@ -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);
}

View File

@ -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<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);
}
}

View File

@ -1,36 +1,6 @@
import * as plugins from './smartnetwork.plugins';
export interface ISpeedtestData {
speeds: {
download: number;
upload: number;
originalDownload: number;
originalUpload: number;
};
client: {
ip: string;
lat: number;
lon: number;
isp: string;
isprating: string;
rating: number;
ispdlavg: number;
ispulavg: number;
};
server: {
host: string;
lat: number;
lon: number;
location: string;
country: string;
cc: string;
sponsor: string;
distance: number;
distanceMi: number;
ping: number;
id: string;
};
}
import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed';
/**
* SmartNetwork simplifies actions within the network
@ -40,16 +10,10 @@ export class SmartNetwork {
* get network speed
* @param measurementTime
*/
public async getSpeed(measurementTime = 5000): Promise<ISpeedtestData> {
const done = plugins.smartpromise.defer<ISpeedtestData>();
const test = plugins.speedtestNet({ maxTime: measurementTime });
test.on('data', (data) => {
done.resolve(data);
});
test.on('error', (err) => {
done.reject(err);
});
return await done.promise;
public async getSpeed() {
const cloudflareSpeedInstance = new CloudflareSpeed();
const test = await cloudflareSpeedInstance.speedTest();
return test;
}
/**
@ -57,7 +21,7 @@ export class SmartNetwork {
* note: false also resolves with false as argument
* @param port
*/
public async isLocalPortAvailable(port: number): Promise<boolean> {
public async isLocalPortUnused(port: number): Promise<boolean> {
const doneIpV4 = plugins.smartpromise.defer<boolean>();
const doneIpV6 = plugins.smartpromise.defer<boolean>();
const net = await import('net'); // creates only one instance of net ;) even on multiple calls

View File

@ -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';
@ -12,7 +14,6 @@ export { smartpromise, smartstring };
// @third party scope
import isopen from 'isopen';
import publicIp from 'public-ip';
import speedtestNet from 'speedtest-net';
import * as systeminformation from 'systeminformation';
export { isopen, publicIp, speedtestNet, systeminformation };
export { isopen, publicIp, systeminformation };