mirror of
https://github.com/gnmyt/myspeed.git
synced 2026-02-10 07:39:08 -06:00
Merge pull request #1083 from gnmyt/fixes/cloudflare-cli
☁️ Fix Cloudflare Speed
This commit is contained in:
@@ -38,4 +38,19 @@ module.exports.libreList = [
|
||||
{os: 'freebsd', arch: 'ia32', suffix: 'freebsd_386.tar.gz'},
|
||||
{os: 'freebsd', arch: 'arm', suffix: 'freebsd_armv7.tar.gz'},
|
||||
{os: 'freebsd', arch: 'arm64', suffix: 'freebsd_arm64.tar.gz'}
|
||||
];
|
||||
|
||||
module.exports.cloudflareVersion = "2.1.0";
|
||||
module.exports.cloudflareList = [
|
||||
// MacOS
|
||||
{os: 'darwin', arch: 'x64', suffix: 'cfspeedtest-x86_64-apple-darwin.tar.gz'},
|
||||
{os: 'darwin', arch: 'arm64', suffix: 'cfspeedtest-aarch64-apple-darwin.tar.gz'},
|
||||
{os: 'darwin', arch: 'universal', suffix: 'cfspeedtest-universal-apple-darwin.tar.gz'},
|
||||
|
||||
// Windows
|
||||
{os: 'win32', arch: 'x64', suffix: 'cfspeedtest-x86_64-pc-windows-msvc.zip'},
|
||||
|
||||
// Linux
|
||||
{os: 'linux', arch: 'x64', suffix: 'cfspeedtest-x86_64-unknown-linux-gnu.tar.gz'},
|
||||
{os: 'linux', arch: 'arm64', suffix: 'cfspeedtest-aarch64-unknown-linux-gnu.tar.gz'}
|
||||
]
|
||||
@@ -1,234 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
@@ -5,7 +5,6 @@ const controller = require("../controller/recommendations");
|
||||
const parseData = require('../util/providers/parseData');
|
||||
let {setState, sendRunning, sendError, sendFinished} = require("./integrations");
|
||||
const serverController = require("../controller/servers");
|
||||
const cloudflareTask = require("./cloudflare");
|
||||
|
||||
let isRunning = false;
|
||||
|
||||
@@ -48,14 +47,7 @@ module.exports.run = async (retryAuto = false) => {
|
||||
if (serverId === "none")
|
||||
serverId = undefined;
|
||||
|
||||
let speedtest;
|
||||
if (mode === "cloudflare") {
|
||||
const startTime = new Date().getTime();
|
||||
speedtest = await cloudflareTask();
|
||||
speedtest = {...speedtest, elapsed: (new Date().getTime() - startTime) / 1000};
|
||||
} else {
|
||||
speedtest = await (retryAuto ? speedTest(mode) : speedTest(mode, serverId));
|
||||
}
|
||||
let speedtest = await (retryAuto ? speedTest(mode) : speedTest(mode, serverId));
|
||||
|
||||
if (mode === "ookla" && speedtest.server) {
|
||||
if (serverId === undefined) await config.updateValue("ooklaId", speedtest.server?.id);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
const libreProvider = require('./providers/loadLibre');
|
||||
const ooklaProvider = require('./providers/loadOokla');
|
||||
const cloudflareProvider = require('./providers/loadCloudflare');
|
||||
|
||||
module.exports.load = async () => {
|
||||
await libreProvider.load();
|
||||
await ooklaProvider.load();
|
||||
await cloudflareProvider.load();
|
||||
}
|
||||
78
server/util/providers/loadCloudflare.js
Normal file
78
server/util/providers/loadCloudflare.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { get } = require('https');
|
||||
const decompress = require("decompress");
|
||||
const decompressTarGz = require('decompress-targz');
|
||||
const decompressUnzip = require('decompress-unzip');
|
||||
const { file } = require("tmp");
|
||||
const binaries = require('../../config/binaries');
|
||||
|
||||
const binaryName = `cfspeedtest${process.platform === "win32" ? ".exe" : ""}`;
|
||||
const binaryDirectory = path.join(__dirname, "../../../bin");
|
||||
const binaryPath = path.join(binaryDirectory, binaryName);
|
||||
const downloadBaseURL = `https://github.com/code-inflation/cfspeedtest/releases/download/v${binaries.cloudflareVersion}/`;
|
||||
|
||||
const binaryRegex = /cfspeedtest(.exe)?$/;
|
||||
|
||||
module.exports.fileExists = async () => fs.existsSync(binaryPath);
|
||||
|
||||
const downloadToFile = (url, destinationPath) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
get(url, (res) => {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
return resolve(downloadToFile(res.headers.location, destinationPath));
|
||||
}
|
||||
|
||||
const writeStream = fs.createWriteStream(destinationPath);
|
||||
res.pipe(writeStream);
|
||||
|
||||
writeStream.on('finish', () => resolve());
|
||||
writeStream.on('error', reject);
|
||||
res.on('error', reject);
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
const decompressBinary = async (archivePath) => {
|
||||
await decompress(archivePath, binaryDirectory, {
|
||||
plugins: [decompressTarGz(), decompressUnzip()],
|
||||
filter: file => binaryRegex.test(file.path),
|
||||
map: file => {
|
||||
file.path = binaryName;
|
||||
return file;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.downloadFile = async () => {
|
||||
let binary = binaries.cloudflareList.find(b => b.os === process.platform && b.arch === process.arch);
|
||||
|
||||
if (!binary && process.platform === 'darwin') {
|
||||
binary = binaries.cloudflareList.find(b => b.os === 'darwin' && b.arch === 'universal');
|
||||
}
|
||||
|
||||
if (!binary) {
|
||||
throw new Error(`Your platform (${process.platform}-${process.arch}) is not supported by the Cloudflare CLI`);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
file({ postfix: binary.suffix }, async (err, tempPath) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
try {
|
||||
const fullUrl = downloadBaseURL + binary.suffix;
|
||||
await downloadToFile(fullUrl, tempPath);
|
||||
await decompressBinary(tempPath);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(new Error(`Failed to download and extract binary: ${error.message}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.load = async () => {
|
||||
if (!await module.exports.fileExists()) {
|
||||
await module.exports.downloadFile();
|
||||
}
|
||||
};
|
||||
@@ -14,7 +14,27 @@ module.exports.parseOokla = (test) => {
|
||||
module.exports.parseLibre = (test) => ({...test, ping: Math.round(test.ping), time: Math.round(test.elapsed / 1000),
|
||||
resultId: null});
|
||||
|
||||
module.exports.parseCloudflare = (test) => ({...test, time: Math.round(test.elapsed), resultId: null});
|
||||
module.exports.parseCloudflare = (test) => {
|
||||
if (test && test.latency_measurement && test.speed_measurements) {
|
||||
const downloadTests = test.speed_measurements.filter(t => t.test_type === "Download");
|
||||
const uploadTests = test.speed_measurements.filter(t => t.test_type === "Upload");
|
||||
|
||||
const downloadSpeeds = downloadTests.map(t => t.max || t.median || 0);
|
||||
const download = downloadSpeeds.length > 0 ? Math.max(...downloadSpeeds) : 0;
|
||||
|
||||
const uploadSpeeds = uploadTests.map(t => t.max || t.median || 0);
|
||||
const upload = uploadSpeeds.length > 0 ? Math.max(...uploadSpeeds) : 0;
|
||||
|
||||
const ping = Math.round(test.latency_measurement.avg_latency_ms || 0);
|
||||
|
||||
const time = Math.round((test.elapsed || 30000) / 1000);
|
||||
|
||||
return {ping, download: parseFloat(download.toFixed(2)),
|
||||
upload: parseFloat(upload.toFixed(2)), time, resultId: null};
|
||||
}
|
||||
|
||||
return {ping: 0, download: 0, upload: 0, time: 0, resultId: null};
|
||||
};
|
||||
|
||||
module.exports.parseData = (provider, data) => {
|
||||
switch (provider) {
|
||||
|
||||
@@ -4,7 +4,8 @@ const config = require('../controller/config');
|
||||
|
||||
module.exports = async (mode, serverId) => {
|
||||
const binaryPath = mode === "ookla" ? './bin/speedtest' + (process.platform === "win32" ? ".exe" : "")
|
||||
: './bin/librespeed-cli' + (process.platform === "win32" ? ".exe" : "");
|
||||
: mode === "libre" ? './bin/librespeed-cli' + (process.platform === "win32" ? ".exe" : "")
|
||||
: './bin/cfspeedtest' + (process.platform === "win32" ? ".exe" : "");
|
||||
|
||||
if (!interfaces.interfaces) throw new Error("No interfaces found");
|
||||
|
||||
@@ -24,12 +25,21 @@ module.exports = async (mode, serverId) => {
|
||||
}
|
||||
|
||||
if (serverId) args.push(`--server-id=${serverId}`);
|
||||
} else {
|
||||
} else if (mode === "libre") {
|
||||
args = ['--json', '--duration=5', '--source=' + interfaceIp];
|
||||
if (serverId) args.push(`--server=${serverId}`);
|
||||
} else if (mode === "cloudflare") {
|
||||
args = ['--output-format=json'];
|
||||
|
||||
if (interfaceIp.includes(':')) {
|
||||
args.push('--ipv6=' + interfaceIp);
|
||||
} else {
|
||||
args.push('--ipv4=' + interfaceIp);
|
||||
}
|
||||
}
|
||||
|
||||
let result = {};
|
||||
let stdout = '';
|
||||
|
||||
const testProcess = spawn(binaryPath, args, {windowsHide: true});
|
||||
|
||||
@@ -41,25 +51,36 @@ module.exports = async (mode, serverId) => {
|
||||
});
|
||||
|
||||
testProcess.stdout.on('data', (buffer) => {
|
||||
const line = buffer.toString().replace("\n", "");
|
||||
if (!(line.startsWith("{") || line.startsWith("["))) return;
|
||||
|
||||
let data = {};
|
||||
try {
|
||||
data = JSON.parse(line);
|
||||
if (line.startsWith("[")) data = data[0];
|
||||
} catch (e) {
|
||||
data.error = e.message;
|
||||
}
|
||||
|
||||
if (data.error) result.error = data.error;
|
||||
|
||||
if ((mode === "ookla" && data.type === "result") || mode === "libre") result = data;
|
||||
stdout += buffer.toString();
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
testProcess.on('error', e => reject({message: e}));
|
||||
testProcess.on('exit', resolve);
|
||||
testProcess.on('exit', () => {
|
||||
if (stdout.trim()) {
|
||||
const lines = stdout.trim().split('\n');
|
||||
for (const line of lines) {
|
||||
if (!(line.startsWith("{") || line.startsWith("["))) continue;
|
||||
|
||||
let data = {};
|
||||
try {
|
||||
data = JSON.parse(line);
|
||||
if (line.startsWith("[") && mode !== "cloudflare") data = data[0];
|
||||
} catch (e) {
|
||||
data.error = e.message;
|
||||
console.error("JSON parse error:", e.message, "Line:", line);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data.error) result.error = data.error;
|
||||
|
||||
if ((mode === "ookla" && data.type === "result") || mode === "libre" || mode === "cloudflare") {
|
||||
result = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
if (result.error) throw new Error(result.error);
|
||||
|
||||
Reference in New Issue
Block a user