Merge pull request #1083 from gnmyt/fixes/cloudflare-cli

☁️ Fix Cloudflare Speed
This commit is contained in:
Mathias Wagner
2026-01-20 00:33:19 +01:00
committed by GitHub
7 changed files with 155 additions and 261 deletions

View File

@@ -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'}
]

View File

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

View File

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

View File

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

View 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();
}
};

View File

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

View File

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