Files
myspeed/server/tasks/cloudflare.js
2024-05-21 14:28:28 +02:00

234 lines
7.2 KiB
JavaScript

/**
*
* This file is part of speed-cloudflare-cli (https://github.com/KNawm/speed-cloudflare-cli),
* which is released under the MIT License.
*
* This file has been modified to be used inside MySpeed.
*
* 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.
*/
const { performance } = require("perf_hooks");
const https = require("https");
const interfaces = require("../util/loadInterfaces");
const config = require("../controller/config");
function average(values) {
let total = 0;
for (let i = 0; i < values.length; i += 1) {
total += values[i];
}
return total / values.length;
}
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;
}
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];
}
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);
}
function request(localAddress, options, data = "") {
let started;
let dnsLookup;
let tcpHandshake;
let sslHandshake;
let ttfb;
let ended;
options.localAddress = localAddress;
options.family = localAddress.includes(":") ? 6 : 4;
options.agent = new https.Agent(options);
return new Promise((resolve, reject) => {
started = performance.now();
const req = https.request(options, (res) => {
res.once("readable", () => {
ttfb = performance.now();
});
res.on("data", () => {});
res.on("end", () => {
ended = performance.now();
resolve([started, dnsLookup, tcpHandshake, sslHandshake, ttfb, ended, parseFloat(res.headers["server-timing"]?.slice(22))]);
});
});
req.on("socket", (socket) => {
socket.on("lookup", () => {
dnsLookup = performance.now();
});
socket.on("connect", () => {
tcpHandshake = performance.now();
});
socket.on("secureConnect", () => {
sslHandshake = performance.now();
});
});
req.on("error", (error) => {
reject(error.message);
});
req.write(data);
req.end();
});
}
function download(ip, bytes) {
const options = {
hostname: "speed.cloudflare.com",
path: `/__down?bytes=${bytes}`,
method: "GET",
};
return request(ip, options);
}
function upload(ip, bytes) {
const data = "0".repeat(bytes);
const options = {
hostname: "speed.cloudflare.com",
path: "/__up",
method: "POST",
headers: {
"Content-Length": Buffer.byteLength(data),
},
};
return request(ip, options, data);
}
function measureSpeed(bytes, duration) {
return (bytes * 8) / (duration / 1000) / 1e6;
}
async function measureLatency(ip) {
const measurements = [];
for (let i = 0; i < 20; i += 1) {
await download(ip, 1000).then(
(response) => {
// TTFB - Server processing time
measurements.push(response[4] - response[0] - response[6]);
},
(error) => {
console.log(`Error while pinging: ${error}`);
},
);
}
return [Math.min(...measurements), Math.max(...measurements), average(measurements), median(measurements), jitter(measurements)];
}
async function measureDownload(ip, bytes, iterations) {
const measurements = [];
for (let i = 0; i < iterations; i += 1) {
await download(ip, bytes).then(
(response) => {
const transferTime = response[5] - response[4];
measurements.push(measureSpeed(bytes, transferTime));
},
(error) => {
console.log(`Error while downloading: ${error}`);
},
);
}
return measurements;
}
async function measureUpload(ip, bytes, iterations) {
const measurements = [];
for (let i = 0; i < iterations; i += 1) {
await upload(ip, bytes).then(
(response) => {
const transferTime = response[6];
measurements.push(measureSpeed(bytes, transferTime));
},
(error) => {
console.log(`Error while uploading: ${error}`);
},
);
}
return measurements;
}
module.exports = async function speedTest() {
let result = {};
try {
const currentInterface = await config.getValue("interface");
const interfaceIp = interfaces.interfaces[currentInterface];
if (!interfaceIp) {
throw new Error("Invalid interface");
}
result["ping"] = Math.round((await measureLatency(interfaceIp))[3]);
const testDown1 = await measureDownload(interfaceIp, 101000, 1);
const testDown2 = await measureDownload(interfaceIp, 1001000, 8);
const testDown3 = await measureDownload(interfaceIp, 10001000, 6);
const testDown4 = await measureDownload(interfaceIp, 25001000, 4);
const testDown5 = await measureDownload(interfaceIp, 100001000, 1);
result["download"] = quartile([...testDown1, ...testDown2, ...testDown3, ...testDown4, ...testDown5], 0.9).toFixed(2);
const testUp1 = await measureUpload(interfaceIp, 11000, 10);
const testUp2 = await measureUpload(interfaceIp, 101000, 10);
const testUp3 = await measureUpload(interfaceIp, 1001000, 8);
result["upload"] = quartile([...testUp1, ...testUp2, ...testUp3], 0.9).toFixed(2);
} catch (error) {
console.error("Error while using cloudflare speedtest: " + error.message);
result = {error: error.message};
}
return result;
}