From 4b06b320c64fd7e3ac8c899a918f5379ef3c9e3d Mon Sep 17 00:00:00 2001 From: Mathias Wagner Date: Tue, 20 Jan 2026 22:38:26 +0100 Subject: [PATCH 1/3] Add server-side logic for LibreSpeed --- server/controller/config.js | 9 +++++++++ server/routes/config.js | 2 +- server/tasks/speedtest.js | 21 ++++++++++++++------- server/util/speedtest.js | 22 ++++++++++++++++++++-- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/server/controller/config.js b/server/controller/config.js index f4e5b398..8517278b 100644 --- a/server/controller/config.js +++ b/server/controller/config.js @@ -20,6 +20,7 @@ const configDefaults = { provider: "none", ooklaId: "none", libreId: "none", + libreUrl: "none", password: "none", passwordLevel: "none", interface: "none" @@ -89,6 +90,14 @@ module.exports.validateInput = async (key, value) => { if ((key === "ooklaId" || key === "libreId") && (/[^0-9]/.test(value) && value !== "none")) return "You need to provide a number in order to change this"; + if (key === "libreUrl" && value !== "none") { + try { + new URL(value); + } catch (e) { + return "You need to provide a valid URL"; + } + } + if (key === "passwordLevel" && !["none", "read"].includes(value)) return "You need to provide either none or read-access"; diff --git a/server/routes/config.js b/server/routes/config.js index 7294330f..4ebeaaf0 100644 --- a/server/routes/config.js +++ b/server/routes/config.js @@ -6,7 +6,7 @@ const password = require('../middlewares/password'); app.get("/", password(true), async (req, res) => { let configValues = {}; (await config.listAll()).forEach(row => { - if (row.key !== "password" && !(req.viewMode && ["ooklaId", "libreId", "cron", "passwordLevel"].includes(row.key))) + if (row.key !== "password" && !(req.viewMode && ["ooklaId", "libreId", "libreUrl", "cron", "passwordLevel"].includes(row.key))) configValues[row.key] = row.value; }); configValues['viewMode'] = req.viewMode; diff --git a/server/tasks/speedtest.js b/server/tasks/speedtest.js index a7df06ac..412edae1 100644 --- a/server/tasks/speedtest.js +++ b/server/tasks/speedtest.js @@ -43,24 +43,31 @@ module.exports.run = async (retryAuto = false) => { } let serverId = mode === "cloudflare" ? 0 : await config.getValue(mode + "Id"); + let serverUrl = mode === "libre" ? await config.getValue("libreUrl") : undefined; if (serverId === "none") serverId = undefined; + + if (serverUrl === "none") + serverUrl = undefined; - let speedtest = await (retryAuto ? speedTest(mode) : speedTest(mode, serverId)); + if (mode === "libre" && serverUrl) + serverId = undefined; + + let speedtest = await (retryAuto ? speedTest(mode) : speedTest(mode, serverId, serverUrl)); if (mode === "ookla" && speedtest.server) { if (serverId === undefined) await config.updateValue("ooklaId", speedtest.server?.id); serverId = speedtest.server?.id; } - if (mode === "libre" && speedtest.server) { - let server = Object.entries(serverController.getLibreServers()) - .filter(([, value]) => value === speedtest.server.name)[0][0]; + if (mode === "libre" && speedtest.server && !serverUrl) { + let serverEntry = Object.entries(serverController.getLibreServers()) + .filter(([, value]) => value === speedtest.server.name)[0]; - if (server) { - if (serverId === undefined) await config.updateValue("libreId", server); - serverId = parseInt(server); + if (serverEntry) { + if (serverId === undefined) await config.updateValue("libreId", serverEntry[0]); + serverId = parseInt(serverEntry[0]); } } diff --git a/server/util/speedtest.js b/server/util/speedtest.js index 91dfd072..53cf13c7 100644 --- a/server/util/speedtest.js +++ b/server/util/speedtest.js @@ -1,8 +1,10 @@ const {spawn} = require('child_process'); const interfaces = require('../util/loadInterfaces'); const config = require('../controller/config'); +const fs = require('fs'); +const path = require('path'); -module.exports = async (mode, serverId) => { +module.exports = async (mode, serverId, serverUrl) => { const binaryPath = mode === "ookla" ? './bin/speedtest' + (process.platform === "win32" ? ".exe" : "") : mode === "libre" ? './bin/librespeed-cli' + (process.platform === "win32" ? ".exe" : "") : './bin/cfspeedtest' + (process.platform === "win32" ? ".exe" : ""); @@ -27,7 +29,23 @@ module.exports = async (mode, serverId) => { if (serverId) args.push(`--server-id=${serverId}`); } else if (mode === "libre") { args = ['--json', '--duration=5', '--source=' + interfaceIp]; - if (serverId) args.push(`--server=${serverId}`); + if (serverUrl) { + const customServerConfig = [{ + id: 1, + name: "Custom Server", + server: serverUrl, + dlURL: "garbage.php", + ulURL: "empty.php", + pingURL: "empty.php", + getIpURL: "getIP.php" + }]; + const tempJsonPath = path.join('data', 'servers', 'libre_custom.json'); + fs.writeFileSync(tempJsonPath, JSON.stringify(customServerConfig)); + args.push(`--local-json=${tempJsonPath}`); + args.push('--server=1'); + } else if (serverId) { + args.push(`--server=${serverId}`); + } } else if (mode === "cloudflare") { args = ['--output-format=json']; From e2b6a2aa8f3ab6941047c891d1c2a59fff581c2a Mon Sep 17 00:00:00 2001 From: Mathias Wagner Date: Tue, 20 Jan 2026 22:42:42 +0100 Subject: [PATCH 2/3] Add custom server URL support for LibreSpeed in Provider Dialog --- client/public/assets/locales/en.json | 2 + .../ProviderDialog/ProviderDialog.jsx | 46 ++++++++++++++++--- .../components/ProviderDialog/styles.sass | 6 ++- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/client/public/assets/locales/en.json b/client/public/assets/locales/en.json index 328048bd..3c31351b 100644 --- a/client/public/assets/locales/en.json +++ b/client/public/assets/locales/en.json @@ -39,6 +39,8 @@ "interface": "Interface", "server": "Server", "server_id": "Server ID", + "custom_url": "Custom Server URL", + "custom_url_placeholder": "https://your-librespeed-server.com/", "choose_automatically": "Choose automatically", "ookla_license": "I have read and accept the EULA, privacy policy and terms of service of Ookla.", "ookla_notice": "By using Ookla, you agree to their EULA, privacy policy and terms of service.", diff --git a/client/src/common/components/ProviderDialog/ProviderDialog.jsx b/client/src/common/components/ProviderDialog/ProviderDialog.jsx index 784b8767..b12778a5 100644 --- a/client/src/common/components/ProviderDialog/ProviderDialog.jsx +++ b/client/src/common/components/ProviderDialog/ProviderDialog.jsx @@ -1,7 +1,7 @@ import {Dialog, DialogHeader, DialogBody, DialogFooter} from "@/common/contexts/Dialog"; import {t} from "i18next"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faCheck, faServer, faNetworkWired} from "@fortawesome/free-solid-svg-icons"; +import {faCheck, faServer, faNetworkWired, faLink} from "@fortawesome/free-solid-svg-icons"; import "./styles.sass"; import React, {useContext, useEffect, useState} from "react"; import OoklaImage from "./assets/img/ookla.webp"; @@ -27,6 +27,7 @@ export const ProviderDialog = ({open, onClose}) => { const [ooklaServers, setOoklaServers] = useState({}); const [libreServers, setLibreServers] = useState({}); const [serverId, setServerId] = useState("none"); + const [libreUrl, setLibreUrl] = useState(config.libreUrl || "none"); useEffect(() => { if (!open) return; @@ -37,17 +38,35 @@ export const ProviderDialog = ({open, onClose}) => { useEffect(() => { if (config[provider + "Id"]) setServerId(config[provider + "Id"]); + if (config.libreUrl) setLibreUrl(config.libreUrl); }, [provider, config]); useEffect(() => { if (serverId === "") setServerId("none"); }, [serverId]); + useEffect(() => { + if (libreUrl === "") setLibreUrl("none"); + }, [libreUrl]); + + const handleLibreUrlChange = (value) => { + setLibreUrl(value); + if (value && value !== "none") setServerId("none"); + }; + + const handleServerIdChange = (value) => { + setServerId(value); + if (provider === "libre" && value && value !== "none") setLibreUrl("none"); + }; + const update = async (close) => { await patchRequest("/config/provider", {value: provider}); if (serverId !== config[provider + "Id"] && provider !== "cloudflare") { await patchRequest("/config/" + provider + "Id", {value: serverId}); } + if (provider === "libre" && libreUrl !== config.libreUrl) { + await patchRequest("/config/libreUrl", {value: libreUrl}); + } if (currentInterface !== config.interface) { await patchRequest("/config/interface", {value: currentInterface}); } @@ -56,8 +75,10 @@ export const ProviderDialog = ({open, onClose}) => { close(); }; + const isUsingCustomUrl = provider === "libre" && libreUrl && libreUrl !== "none"; + return ( - + {({close}) => ( <> {t("update.provider_title")} @@ -87,14 +108,14 @@ export const ProviderDialog = ({open, onClose}) => { - {provider !== "cloudflare" && ( + {provider !== "cloudflare" && !isUsingCustomUrl && (

{t("dialog.provider.server")}

setServerId(e.target.value)}/> + onChange={(e) => handleServerIdChange(e.target.value)}/> +
+ )} + + {provider === "libre" && ( +
+
+ +

{t("dialog.provider.custom_url")}

+
+ handleLibreUrlChange(e.target.value || "none")}/>
)} diff --git a/client/src/common/components/ProviderDialog/styles.sass b/client/src/common/components/ProviderDialog/styles.sass index 8ded03d9..6f31d774 100644 --- a/client/src/common/components/ProviderDialog/styles.sass +++ b/client/src/common/components/ProviderDialog/styles.sass @@ -1,6 +1,6 @@ @use "@/common/styles/colors" as * -.provider-dialog +.provider-dialog-wrapper width: 28rem max-width: 95vw @@ -13,12 +13,14 @@ .provider-list display: flex + flex-wrap: wrap gap: 0.5rem margin-bottom: 1rem .provider-item display: flex - flex: 1 + flex: 1 1 auto + min-width: 7rem align-items: center justify-content: center gap: 0.5rem From eec2d27cc66ed0e94d75965caf024c503bfe0c9e Mon Sep 17 00:00:00 2001 From: Mathias Wagner Date: Tue, 20 Jan 2026 22:54:11 +0100 Subject: [PATCH 3/3] Update localization files for custom server URL placeholder adjustments --- client/public/assets/locales/de.json | 2 ++ client/public/assets/locales/en.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/client/public/assets/locales/de.json b/client/public/assets/locales/de.json index 90a756ab..40a9178a 100644 --- a/client/public/assets/locales/de.json +++ b/client/public/assets/locales/de.json @@ -37,6 +37,8 @@ "interface": "Schnittstelle", "server": "Server", "server_id": "Server-ID", + "custom_url": "Benutzerdefinierte Server-URL", + "custom_url_placeholder": "https://speed.test/backend/", "choose_automatically": "Automatisch wählen", "ookla_license": "Ich habe die EULA, die Datenschutzrichtlinie und die Nutzungsbedingungen von Ookla gelesen und akzeptiere sie." } diff --git a/client/public/assets/locales/en.json b/client/public/assets/locales/en.json index 3c31351b..c4ecc19a 100644 --- a/client/public/assets/locales/en.json +++ b/client/public/assets/locales/en.json @@ -40,7 +40,7 @@ "server": "Server", "server_id": "Server ID", "custom_url": "Custom Server URL", - "custom_url_placeholder": "https://your-librespeed-server.com/", + "custom_url_placeholder": "https://speed.test/backend/", "choose_automatically": "Choose automatically", "ookla_license": "I have read and accept the EULA, privacy policy and terms of service of Ookla.", "ookla_notice": "By using Ookla, you agree to their EULA, privacy policy and terms of service.",