Merge pull request #677 from gnmyt/features/multiple-providers

🗃️ Support für LibreSpeed & Cloudflare Speed
This commit is contained in:
Mathias Wagner
2024-05-19 20:46:01 +02:00
committed by GitHub
30 changed files with 820 additions and 184 deletions

View File

@@ -15,14 +15,16 @@
"wrong": "The password you entered is incorrect",
"unlock": "Unlock"
},
"accept": {
"title": "We need your permission",
"description": "We use services from Ookla. By clicking <Bold>Accept</Bold>, you acknowledge that you have read and agree to Ookla's <EULA>EULA</EULA>, <Privacy>Privacy Statement</Privacy> and <Terms>Terms of Use</Terms>.",
"button": "Accept"
},
"api": {
"title": "API not reachable",
"description": "MySpeed could not reach the API of this instance. Please try again later."
},
"provider": {
"server": "Server",
"server_id": "Server ID",
"choose_automatically": "Choose automatically",
"ookla_license": "I have read and accept the <Eula>EULA</Eula>, <GDPR>privacy policy</GDPR> and <TOS>terms of service</TOS> of Ookla.",
"cloudflare_note": "Cloudflare does not require any additional settings"
}
},
"dropdown": {
@@ -35,7 +37,7 @@
"upload": "Optimal up-speed",
"download": "Optimal down-speed",
"recommendations": "Recommendations",
"server": "Change Server",
"change_provider": "Change provider",
"password": "Change password",
"cron": "Set frequency",
"time": "Set period",
@@ -80,10 +82,8 @@
"download_placeholder": "Down speed (Mbps)",
"recommendations_title": "Optimal recommendations",
"recommendations_set": "Set automatic recommendations?",
"server_title": "Set speedtest server",
"provider_title": "Set speedtest provider",
"manually": "Set manually",
"manual_server_title": "Set speedtest server",
"manual_server_id": "Server ID",
"new_password": "Set a new password",
"password_placeholder": "New password",
"password_removed": "The password lock has been removed and the set password has been removed.",

View File

@@ -16,10 +16,9 @@ import {
faPause,
faPingPongPaddleBall,
faPlay,
faServer,
faWandMagicSparkles,
faCheck,
faExclamationTriangle
faExclamationTriangle, faSliders
} from "@fortawesome/free-solid-svg-icons";
import {ConfigContext} from "@/common/contexts/Config";
import {StatusContext} from "@/common/contexts/Status";
@@ -36,6 +35,7 @@ import {ToastNotificationContext} from "@/common/contexts/ToastNotification";
import {NodeContext} from "@/common/contexts/Node";
import {IntegrationDialog} from "@/common/components/IntegrationDialog";
import LanguageDialog from "@/common/components/LanguageDialog";
import ProviderDialog from "@/common/components/ProviderDialog";
let icon;
@@ -65,6 +65,7 @@ function DropdownComponent() {
const [showViewDialog, setShowViewDialog] = useState(false);
const [showIntegrationDialog, setShowIntegrationDialog] = useState(false);
const [showLanguageDialog, setShowLanguageDialog] = useState(false);
const [showProviderDialog, setShowProviderDialog] = useState(false);
const ref = useRef();
useEffect(() => {
@@ -130,19 +131,6 @@ function DropdownComponent() {
} else setDialog({title: t("update.recommendations_title"), description: t("info.recommendations_error"), buttonText: t("dialog.okay")});
}
const updateServer = () => patchDialog("serverId", async (value) => ({
title: t("update.server_title"),
select: true,
selectOptions: await jsonRequest("/info/server"),
unsetButton: t("update.manually"),
onClear: updateServerManually,
value
}));
const updateServerManually = () => patchDialog("serverId", (value) => ({
title: t("update.manual_server_title"), placeholder: t("update.manual_server_id"), type: "number", value: value,
}));
const updatePassword = async () => {
const passwordSet = currentNode !== 0 ? findNode(currentNode).password : localStorage.getItem("password") != null;
@@ -239,7 +227,7 @@ function DropdownComponent() {
{run: updateDownload, icon: faArrowDown, text: t("dropdown.download")},
{run: recommendedSettings, icon: faWandMagicSparkles, text: t("dropdown.recommendations")},
{hr: true, key: 1},
{run: updateServer, icon: faServer, text: t("dropdown.server")},
{run: () => setShowProviderDialog(true), icon: faSliders, text: t("dropdown.change_provider")},
{run: updatePassword, icon: faKey, text: t("dropdown.password"), previewHidden: true},
{run: updateCron, icon: faClock, text: t("dropdown.cron")},
{run: exportDialog, icon: faFileExport, text: t("dropdown.export")},
@@ -258,6 +246,7 @@ function DropdownComponent() {
{showViewDialog && <ViewDialog onClose={() => setShowViewDialog(false)}/>}
{showIntegrationDialog && <IntegrationDialog onClose={() => setShowIntegrationDialog(false)}/>}
{showLanguageDialog && <LanguageDialog onClose={() => setShowLanguageDialog(false)}/>}
{showProviderDialog && <ProviderDialog onClose={() => setShowProviderDialog(false)}/>}
<div className="dropdown dropdown-invisible" id="dropdown" ref={ref}>
<div className="dropdown-content">
<h2>{t("dropdown.settings")}</h2>

View File

@@ -22,7 +22,7 @@
border-radius: 0.5rem
&:hover
background-color: $light-gray
background-color: $darker-gray
img
width: 2rem
@@ -38,6 +38,9 @@
background-color: $light-gray
color: $white
&:hover
background-color: $light-gray
@media screen and (max-height: 425px)
.language-chooser-dialog
height: 15rem

View File

@@ -0,0 +1,138 @@
import {DialogContext, DialogProvider} from "@/common/contexts/Dialog";
import {t} from "i18next";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faClose} from "@fortawesome/free-solid-svg-icons";
import "./styles.sass";
import React, {useContext, useEffect, useState} from "react";
import OoklaImage from "./assets/img/ookla.webp";
import LibreImage from "./assets/img/libre.webp";
import CloudflareImage from "./assets/img/cloudflare.webp";
import {jsonRequest, patchRequest} from "@/common/utils/RequestUtil";
import {Trans} from "react-i18next";
import {ConfigContext} from "@/common/contexts/Config";
const providers = [
{id: "ookla", name: "Ookla", image: OoklaImage},
{id: "libre", name: "LibreSpeed", image: LibreImage},
{id: "cloudflare", name: "Cloudflare", image: CloudflareImage}
]
export const Dialog = () => {
const close = useContext(DialogContext);
const [config, reloadConfig] = useContext(ConfigContext);
const [provider, setProvider] = useState(config.provider || "ookla");
const [licenseAccepted, setLicenseAccepted] = useState(false);
const [licenseError, setLicenseError] = useState(false);
const [ooklaServers, setOoklaServers] = useState({});
const [libreServers, setLibreServers] = useState({});
const [serverId, setServerId] = useState("none");
useEffect(() => {
jsonRequest("/info/server/ookla").then((response) => {
setOoklaServers(response);
});
jsonRequest("/info/server/libre").then((response) => {
setLibreServers(response);
});
}, []);
useEffect(() => {
if (config[provider + "Id"]) setServerId(config[provider + "Id"]);
}, [provider]);
useEffect(() => {
if (serverId === "") setServerId("none");
}, [serverId]);
const update = async () => {
if (provider === "ookla" && !licenseAccepted) {
setLicenseError(true);
return;
}
await patchRequest("/config/provider", {value: provider});
if (serverId !== config[provider + "Id"] && provider !== "cloudflare") {
await patchRequest("/config/" + provider + "Id", {value: serverId});
}
reloadConfig();
close();
}
return (
<>
<div className="dialog-header">
<h4 className="dialog-text">{t("update.provider_title")}</h4>
<FontAwesomeIcon icon={faClose} className="dialog-text dialog-icon" onClick={() => close()}/>
</div>
<div className="provider-dialog-content">
<div className="provider-header">
{providers.map((current, index) => (
<div className={`provider-item ${current.id === provider ? "provider-item-active" : ""}`}
key={index} onClick={() => setProvider(current.id)}>
<img src={current.image} alt={current.name}/>
<h3>{current.name}</h3>
</div>
))}
</div>
{provider !== "cloudflare" && <div className="provider-content">
<div className="provider-setting">
<h3>{t("dialog.provider.server")}</h3>
<select className="dialog-input provider-input" value={serverId}
onChange={(e) => setServerId(e.target.value)}>
<option value="none">{t("dialog.provider.choose_automatically")}</option>
{provider === "ookla" && Object.keys(ooklaServers).map((current, index) => (
<option key={index} value={current}>{ooklaServers[current]}</option>
))}
{provider === "libre" && Object.keys(libreServers).map((current, index) => (
<option key={index} value={current}>{libreServers[current]}</option>
))}
</select>
</div>
<div className="provider-setting">
<h3>{t("dialog.provider.server_id")}</h3>
<input type="text" className="dialog-input provider-input" value={serverId === "none" ? "" : serverId}
onChange={(e) => setServerId(e.target.value)}/>
</div>
</div>}
{provider === "cloudflare" && <div className="provider-content">
<p className="cloudflare-provider-info">{t("dialog.provider.cloudflare_note")}</p>
</div>}
</div>
<div className="provider-dialog-footer">
<div className="provider-license-box">
{provider === "ookla" && <>
<input type="checkbox" className={licenseError ? "cb-error" : ""} id="license" name="license"
onChange={(e) => setLicenseAccepted(e.target.checked)}/>
<label htmlFor="license"
><Trans components={{
Eula: <a href="https://www.speedtest.net/about/eula" target="_blank"
rel="noreferrer" />,
GDPR: <a href="https://www.speedtest.net/about/privacy" target="_blank"
rel="noreferrer" />,
TOS: <a href="https://www.speedtest.net/about/terms" target="_blank"
rel="noreferrer" />}}>dialog.provider.ookla_license</Trans></label>
</>}
</div>
<button className="dialog-btn" onClick={update}>{t("dialog.update")}</button>
</div>
</>
)
}
export const ProviderDialog = (props) => {
return (
<>
<DialogProvider close={props.onClose}>
<Dialog/>
</DialogProvider>
</>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1 @@
export {ProviderDialog as default} from "./ProviderDialog";

View File

@@ -0,0 +1,95 @@
@import "@/common/styles/colors"
.provider-dialog-content
display: flex
margin: 1rem 0.5rem
user-select: none
flex-direction: column
.provider-header
display: flex
gap: 1rem
.provider-item
display: flex
align-items: center
padding: 0.3rem 0.5rem
gap: 0.5rem
border-radius: 0.8rem
border: 2px solid $light-gray
color: $darker-white
cursor: pointer
img
width: 2.5rem
height: 2.5rem
h3
margin: 0
&:hover
background-color: $darker-gray
.provider-item-active
background-color: $light-gray
&:hover
background-color: $light-gray
.provider-content
display: flex
flex-direction: column
margin-top: 1rem
.provider-setting
display: flex
gap: 1rem
align-items: center
justify-content: space-between
.provider-input
width: 20rem
box-sizing: border-box
margin-top: 0.5rem
margin-bottom: 0.5rem
font-size: 1.3rem
h3
color: $darker-white
.cloudflare-provider-info
color: $subtext
text-align: center
.provider-dialog-footer
display: flex
align-items: center
justify-content: space-between
.provider-license-box
display: flex
align-items: center
gap: 0.5rem
input
border: 2px solid $light-gray
.cb-error
border-color: $red
label
color: $subtext
max-width: 16rem
flex: 1
@media screen and (max-width: 610px)
.provider-dialog-content
.provider-header
flex-direction: column
@media screen and (max-width: 520px)
.provider-dialog-content
.provider-setting .provider-input
width: 60%

View File

@@ -1,14 +1,13 @@
import React, {createContext, useContext, useEffect, useState} from "react";
import {InputDialogContext} from "../InputDialog";
import {request} from "@/common/utils/RequestUtil";
import {acceptDialog, apiErrorDialog, passwordRequiredDialog} from "@/common/contexts/Config/dialog";
import {apiErrorDialog, passwordRequiredDialog} from "@/common/contexts/Config/dialog";
export const ConfigContext = createContext({});
export const ConfigProvider = (props) => {
const [config, setConfig] = useState({});
const [setDialog] = useContext(InputDialogContext);
const [dialogShown, setDialogShown] = useState(false);
const reloadConfig = () => {
request("/config").then(async res => {
@@ -32,13 +31,6 @@ export const ConfigProvider = (props) => {
const checkConfig = async () => (await request("/config")).json();
useEffect(() => {
if (config.acceptOoklaLicense !== undefined && config.acceptOoklaLicense === "false" && !dialogShown) {
setDialogShown(true);
setDialog(acceptDialog());
}
}, [config]);
useEffect(reloadConfig, []);
return (

View File

@@ -1,11 +1,4 @@
import {patchRequest} from "@/common/utils/RequestUtil";
import {t} from "i18next";
import {Trans} from "react-i18next";
const OOKLA_ABOUT_URL = "https://www.speedtest.net/about";
const OOKLA_TERMS_URL = OOKLA_ABOUT_URL + "/terms";
const OOKLA_EULA_URL = OOKLA_ABOUT_URL + "/eula";
const OOKLA_PRIVACY_URL = OOKLA_ABOUT_URL + "/privacy";
export const passwordRequiredDialog = () => ({
title: t("dialog.password.title"),
@@ -26,13 +19,4 @@ export const apiErrorDialog = () => ({
buttonText: t("dialog.retry"),
disableCloseButton: true,
onSuccess: () => window.location.reload()
});
export const acceptDialog = () => ({
title: t("dialog.accept.title"),
description: <Trans components={{Bold: <span className="dialog-value"/>, EULA: <a href={OOKLA_EULA_URL} target="_blank"/>,
Privacy: <a href={OOKLA_PRIVACY_URL} target="_blank"/>, Terms: <a href={OOKLA_TERMS_URL} target="_blank"/> }}>dialog.accept.description</Trans>,
buttonText: t("dialog.accept.button"),
disableCloseButton: true,
onSuccess: () => patchRequest("/config/acceptOoklaLicense", {value: true})
});

258
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "myspeed",
"version": "1.0.8",
"dependencies": {
"@cloudflare/speedtest": "^1.3.0",
"axios": "^1.6.8",
"bcrypt": "^5.1.1",
"cron-validator": "^1.3.1",
@@ -38,6 +39,19 @@
"node": ">=6.9.0"
}
},
"node_modules/@cloudflare/speedtest": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@cloudflare/speedtest/-/speedtest-1.3.0.tgz",
"integrity": "sha512-/uXLCVbKcdj/ueD7/StCO/+RC/aTfHo9pBDO9GSD8kRl7oaIdMs9xC4QkPM8EvYGa3OrbVQLXTp/PLNkwt3gNg==",
"dependencies": {
"d3-scale": "^4.0.2",
"isomorphic-fetch": "^3.0.0",
"lodash.memoize": "^4.1.2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -94,25 +108,6 @@
"node": ">=10"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -811,6 +806,81 @@
"resolved": "https://registry.npmjs.org/cron-validator/-/cron-validator-1.3.1.tgz",
"integrity": "sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A=="
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@@ -1669,6 +1739,14 @@
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"engines": {
"node": ">=12"
}
},
"node_modules/ip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
@@ -1768,11 +1846,25 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"optional": true
},
"node_modules/isomorphic-fetch": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
"dependencies": {
"node-fetch": "^2.6.1",
"whatwg-fetch": "^3.4.1"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="
},
"node_modules/long": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
@@ -2145,6 +2237,25 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz",
"integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA=="
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-gyp": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz",
@@ -3462,6 +3573,11 @@
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"node_modules/whatwg-fetch": {
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
@@ -3593,6 +3709,16 @@
"regenerator-runtime": "^0.13.11"
}
},
"@cloudflare/speedtest": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@cloudflare/speedtest/-/speedtest-1.3.0.tgz",
"integrity": "sha512-/uXLCVbKcdj/ueD7/StCO/+RC/aTfHo9pBDO9GSD8kRl7oaIdMs9xC4QkPM8EvYGa3OrbVQLXTp/PLNkwt3gNg==",
"requires": {
"d3-scale": "^4.0.2",
"isomorphic-fetch": "^3.0.0",
"lodash.memoize": "^4.1.2"
}
},
"@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -3640,14 +3766,6 @@
"wide-align": "^1.1.2"
}
},
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"requires": {
"whatwg-url": "^5.0.0"
}
},
"nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -4181,6 +4299,60 @@
"resolved": "https://registry.npmjs.org/cron-validator/-/cron-validator-1.3.1.tgz",
"integrity": "sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A=="
},
"d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"requires": {
"internmap": "1 - 2"
}
},
"d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="
},
"d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="
},
"d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"requires": {
"d3-color": "1 - 3"
}
},
"d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"requires": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
}
},
"d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"requires": {
"d3-array": "2 - 3"
}
},
"d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"requires": {
"d3-time": "1 - 3"
}
},
"date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@@ -4820,6 +4992,11 @@
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="
},
"ip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
@@ -4898,11 +5075,25 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"optional": true
},
"isomorphic-fetch": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
"requires": {
"node-fetch": "^2.6.1",
"whatwg-fetch": "^3.4.1"
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="
},
"long": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
@@ -5177,6 +5368,14 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz",
"integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA=="
},
"node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"requires": {
"whatwg-url": "^5.0.0"
}
},
"node-gyp": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz",
@@ -6146,6 +6345,11 @@
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
},
"whatwg-fetch": {
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",

View File

@@ -8,6 +8,7 @@
"dev": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\""
},
"dependencies": {
"@cloudflare/speedtest": "^1.3.0",
"axios": "^1.6.8",
"bcrypt": "^5.1.1",
"cron-validator": "^1.3.1",

View File

@@ -1,5 +1,5 @@
module.exports.version = "1.2.0";
module.exports.list = [
module.exports.ooklaVersion = "1.2.0";
module.exports.ooklaList = [
// MacOS
{os: 'darwin', arch: 'x64', suffix: 'macosx-x86_64.tgz'},
@@ -14,4 +14,28 @@ module.exports.list = [
// FreeBSD
{os: 'freebsd', arch: 'x64', suffix: 'freebsd12-x86_64.pkg'}
];
module.exports.libreVersion = "1.0.10";
module.exports.libreList = [
// MacOS
{os: 'darwin', arch: 'x64', suffix: 'darwin_amd64.tar.gz'},
{os: 'darwin', arch: 'arm64', suffix: 'darwin_arm64.tar.gz'},
// Windows
{os: 'win32', arch: 'x64', suffix: 'windows_amd64.zip'},
{os: 'win32', arch: 'ia32', suffix: 'windows_386.zip'},
{os: 'win32', arch: 'arm64', suffix: 'windows_arm64.zip'},
// Linux
{os: 'linux', arch: 'x64', suffix: 'linux_amd64.tar.gz'},
{os: 'linux', arch: 'ia32', suffix: 'linux_386.tar.gz'},
{os: 'linux', arch: 'arm', suffix: 'linux_armv7.tar.gz'},
{os: 'linux', arch: 'arm64', suffix: 'linux_arm64.tar.gz'},
// FreeBSD
{os: 'freebsd', arch: 'x64', suffix: 'freebsd_amd64.tar.gz'},
{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'}
]

View File

@@ -6,10 +6,11 @@ const configDefaults = {
download: "100",
upload: "50",
cron: "0 * * * *",
serverId: "none",
provider: "none",
ooklaId: "none",
libreId: "none",
password: "none",
passwordLevel: "none",
acceptOoklaLicense: "false"
passwordLevel: "none"
}
module.exports.insertDefaults = async () => {
@@ -27,8 +28,7 @@ module.exports.listAll = async () => {
}
module.exports.getValue = async (key) => {
if (process.env.PREVIEW_MODE === "true" && key === "acceptOoklaLicense") return true;
return (await config.findByPk(key)).value;
return (await config.findByPk(key))?.value;
}
module.exports.updateValue = async (key, newValue) => {

View File

@@ -0,0 +1,34 @@
const fs = require("fs");
let ooklaServers;
let libreServers;
module.exports.getLibreServers = () => {
if (libreServers) return libreServers;
if (fs.existsSync("./data/servers/librespeed.json")) {
libreServers = fs.readFileSync("./data/servers/librespeed.json");
libreServers = JSON.parse(libreServers);
return libreServers;
}
return [];
}
module.exports.getOoklaServers = () => {
if (ooklaServers) return ooklaServers;
if (fs.existsSync("./data/servers/ookla.json")) {
ooklaServers = fs.readFileSync("./data/servers/ookla.json");
ooklaServers = JSON.parse(ooklaServers);
return ooklaServers;
}
return [];
}
module.exports.getByMode = (mode) => {
if (mode === "ookla") return this.getOoklaServers();
if (mode === "libre") return this.getLibreServers();
}

View File

@@ -2,8 +2,8 @@ const tests = require('../models/Speedtests');
const {Op, Sequelize} = require("sequelize");
const {mapFixed, mapRounded, calculateTestAverages} = require("../util/helpers");
module.exports.create = async (ping, download, upload, time, type = "auto", error = null) => {
return (await tests.create({ping, download, upload, error, type, time})).id;
module.exports.create = async (ping, download, upload, time, serverId, type = "auto", error = null) => {
return (await tests.create({ping, download, upload, error, serverId, type, time})).id;
}
module.exports.getOne = async (id) => {

View File

@@ -59,8 +59,8 @@ const run = async () => {
}
db.authenticate().then(() => {
console.log("Successfully connected to the database file");
run().then(undefined);
console.log("Successfully connected to the database " + (process.env.DB_TYPE === "mysql" ? "server" : "file"));
run().then(undefined);
}).catch(err => {
console.error("Could not open the database file. Maybe it is damaged?: " + err.message);
process.exit(111);

View File

@@ -7,6 +7,10 @@ module.exports = db.define("speedtests", {
primaryKey: true,
autoIncrement: true,
},
serverId: {
type: Sequelize.INTEGER,
defaultValue: 0
},
ping: {
type: Sequelize.INTEGER,
allowNull: false

View File

@@ -7,10 +7,8 @@ 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 && ["serverId", "cron", "passwordLevel"].includes(row.key)))
if (row.key !== "password" && !(req.viewMode && ["ooklaId", "libreId", "cron", "passwordLevel"].includes(row.key)))
configValues[row.key] = row.value;
if (process.env.PREVIEW_MODE === "true" && row.key === "acceptOoklaLicense")
configValues[row.key] = true;
});
configValues['viewMode'] = req.viewMode;
configValues['previewMode'] = process.env.PREVIEW_MODE === "true";
@@ -28,11 +26,15 @@ app.patch("/:key", password(false), async (req, res) => {
if ((req.params.key === "ping" || req.params.key === "download" || req.params.key === "upload") && isNaN(req.body.value))
return res.status(400).json({message: "You need to provide a number in order to change this"});
if ((req.params.key === "ooklaId" || req.params.key === "libreId") && (isNaN(req.body.value) && req.body.value !== "none"))
return res.status(400).json({message: "You need to provide a number in order to change this"});
if (req.params.key === "passwordLevel" && !["none", "read"].includes(req.body.value))
return res.status(400).json({message: "You need to provide either none or read-access"});
if (req.params.key === "acceptOoklaLicense" && typeof req.body.value !== "boolean")
return res.status(400).json({message: "You need to provide a boolean value"});
if (req.params.key === "provider" && !["ookla", "libre", "cloudflare"].includes(req.body.value))
return res.status(400).json({message: "You need to provide a valid provider"});
if (req.params.key === "ping")
req.body.value = req.body.value.toString().split(".")[0];
@@ -45,9 +47,6 @@ app.patch("/:key", password(false), async (req, res) => {
if (!await config.updateValue(req.params.key, req.body.value.toString()))
return res.status(404).json({message: "The provided key does not exist"});
if (process.env.PREVIEW_MODE === "true" && req.params.key === "acceptOoklaLicense")
return res.status(403).json({message: "You can't change the Ookla license acceptance in preview mode"});
if (process.env.PREVIEW_MODE === "true" && (req.params.key === "password" || req.params.key === "passwordLevel"))
return res.status(403).json({message: "You can't change the password in preview mode"});

View File

@@ -22,8 +22,7 @@ app.get("/statistics", password(true), async (req, res) => {
app.post("/run", password(false), async (req, res) => {
if (pauseController.currentState) return res.status(410).json({message: "The speedtests are currently paused"});
if (await config.getValue("acceptOoklaLicense") === "false")
return res.status(410).json({message: "You need to accept the ookla license first"});
if (await config.getValue("provider") === "none") return res.status(410).json({message: "No provider selected"});
let speedtest = await testTask.create("custom");
if (speedtest !== undefined) return res.status(409).json({message: "An speedtest is already running"});
res.json({message: "Speedtest successfully created"});

View File

@@ -2,10 +2,8 @@ const app = require('express').Router();
const version = require('../../package.json').version;
const remote_url = "https://api.github.com/repos/gnmyt/myspeed/releases/latest";
const axios = require('axios');
const fs = require("fs");
const password = require('../middlewares/password');
let servers;
const serverController = require('../controller/servers');
app.get("/version", password(false), async (req, res) => {
if (process.env.PREVIEW_MODE === "true") return res.json({local: version, remote: "0"});
@@ -17,15 +15,11 @@ app.get("/version", password(false), async (req, res) => {
}
});
app.get("/server", password(false), (req, res) => {
if (servers) return res.json(JSON.parse(servers));
app.get("/server/:provider", password(false), (req, res) => {
if (!["ookla", "libre"].includes(req.params.provider))
return res.status(400).json({message: "Invalid provider"});
if (fs.existsSync("./data/servers.json")) {
servers = fs.readFileSync("./data/servers.json");
return res.json(JSON.parse(servers));
} else {
return res.json([]);
}
res.json(serverController.getByMode(req.params.provider));
});
module.exports = app;

View File

@@ -2,14 +2,12 @@ const speedTest = require('../util/speedtest');
const tests = require('../controller/speedtests');
const config = require('../controller/config');
const controller = require("../controller/recommendations");
const parseData = require('../util/providers/parseData');
let {setState, sendRunning, sendError, sendFinished} = require("./integrations");
const serverController = require("../controller/servers");
let isRunning = false;
const roundSpeed = (bytes, elapsed) => {
return Math.round((bytes * 8 / elapsed) / 10) / 100;
}
const setRunning = (running, sendRequest = true) => {
isRunning = running;
@@ -35,25 +33,70 @@ const createRecommendations = async () => {
}
}
module.exports.executeCloudflare = async () => {
try {
const {default: SpeedTest} = await import('@cloudflare/speedtest');
// This needs to be disabled because of a library issue
// See https://github.com/cloudflare/speedtest/issues/17
console.warn = () => {};
const startTime = new Date().getTime();
return await new Promise(resolve => {
const speedTest = new SpeedTest();
speedTest.onFinish = results => {
resolve({...results.getSummary(), elapsed: new Date().getTime() - startTime});
}
});
} catch (error) {
console.error('Error loading SpeedTest module:', error);
}
}
module.exports.run = async (retryAuto = false) => {
setRunning(true);
let serverId = await config.getValue("serverId");
let mode = await config.getValue("provider");
if (mode === "none") {
setRunning(false);
throw {message: "No provider selected"};
}
let serverId = mode === "cloudflare" ? 0 : await config.getValue(mode + "Id");
if (serverId === "none")
serverId = undefined;
let speedtest = await (retryAuto ? speedTest() : speedTest(serverId));
let speedtest;
if (mode === "cloudflare") {
speedtest = await this.executeCloudflare();
} else {
speedtest = await (retryAuto ? speedTest(mode) : speedTest(mode, serverId));
}
if (serverId === undefined)
await config.updateValue("serverId", speedtest.server.id);
if (mode === "ookla" && speedtest.server) {
if (serverId === undefined) await config.updateValue("ooklaId", speedtest.server?.id);
serverId = speedtest.server?.id;
}
if (Object.keys(speedtest).length === 0) throw {message: "No response, even after trying again, test timed out."};
if (mode === "libre" && speedtest.server) {
let server = Object.entries(serverController.getLibreServers())
.filter(([, value]) => value === speedtest.server.name)[0][0];
return speedtest;
if (server) {
if (serverId === undefined) await config.updateValue("libreId", server);
serverId = parseInt(server);
}
}
if (Object.keys(speedtest).length <= 1) throw {message: "No response, even after trying again, test timed out."};
return {...speedtest, serverId}
}
module.exports.create = async (type = "auto", retried = false) => {
if (await config.getValue("acceptOoklaLicense") === 'false') return;
const mode = await config.getValue("provider");
if (mode === "none") return 400;
if (isRunning && !retried) return 500;
try {
@@ -69,18 +112,16 @@ module.exports.create = async (type = "auto", retried = false) => {
test = await this.run(retried);
}
let ping = Math.round(test.ping.latency);
let download = roundSpeed(test.download.bytes, test.download.elapsed);
let upload = roundSpeed(test.upload.bytes, test.upload.elapsed);
let time = Math.round((test.download.elapsed + test.upload.elapsed) / 1000);
let testResult = await tests.create(ping, download, upload, time, type);
let {ping, download, upload, time} = await parseData.parseData(mode, test);
let testResult = await tests.create(ping, download, upload, time, test.serverId, type);
console.log(`Test #${testResult} was executed successfully in ${time}s. 🏓 ${ping}${download}${upload}`);
createRecommendations().then(() => "");
setRunning(false);
sendFinished({ping, download, upload, time}).then(() => "");
} catch (e) {
if (!retried) return this.create(type, true);
let testResult = await tests.create(-1, -1, -1, null, type, e.message);
let testResult = await tests.create(-1, -1, -1, null, 0, type, e.message);
await sendError(e.message);
setRunning(false, false);
console.log(`Test #${testResult} was not executed successfully. Please try reconnecting to the internet or restarting the software: ` + e.message);

View File

@@ -1,6 +1,6 @@
const fs = require('fs');
const neededFolder = ["data", "bin", "data/logs"];
const neededFolder = ["data", "bin", "data/logs", "data/servers"];
neededFolder.forEach(folder => {
if (!fs.existsSync(folder)) {

View File

@@ -1,45 +1,7 @@
const fs = require('fs');
const {get} = require('https');
const decompress = require("decompress");
const {file} = require("tmp");
const decompressTarGz = require('decompress-targz');
const decompressUnzip = require('decompress-unzip');
const binaries = require('../config/binaries');
const binaryRegex = /speedtest(.exe)?$/;
const binaryDirectory = __dirname + "/../../bin/";
const binaryPath = `${binaryDirectory}/speedtest` + (process.platform === "win32" ? ".exe" : "");
const downloadPath = `https://install.speedtest.net/app/cli/ookla-speedtest-${binaries.version}-`;
module.exports.fileExists = async () => fs.existsSync(binaryPath);
module.exports.downloadFile = async () => {
const binary = binaries.list.find(b => b.os === process.platform && b.arch === process.arch);
if (!binary)
throw new Error(`Your platform (${process.platform}-${process.arch}) is not supported by the Speedtest CLI`);
await new Promise((resolve) => {
file({postfix: binary.suffix}, async (err, path) => {
get(downloadPath + binary.suffix, async resp => {
resp.pipe(fs.createWriteStream(path)).on('finish', async () => {
await decompress(path, binaryDirectory, {
plugins: [decompressTarGz(), decompressUnzip()],
filter: file => binaryRegex.test(file.path),
map: file => {
file.path = "speedtest" + (process.platform === "win32" ? ".exe" : "");
return file;
}
});
resolve();
});
});
});
});
}
const libreProvider = require('./providers/loadLibre');
const ooklaProvider = require('./providers/loadOokla');
module.exports.load = async () => {
if (!await this.fileExists())
await this.downloadFile();
await libreProvider.load();
await ooklaProvider.load();
}

View File

@@ -1,7 +1,8 @@
const axios = require('axios');
const fs = require('fs');
if (!fs.existsSync("data/servers.json")) {
// Load servers from ookla
if (!fs.existsSync("data/servers/ookla.json")) {
let servers = {};
try {
axios.get("https://www.speedtest.net/api/js/servers?limit=20")
@@ -12,13 +13,33 @@ if (!fs.existsSync("data/servers.json")) {
});
try {
fs.writeFileSync("data/servers.json", JSON.stringify(servers, null, 4));
fs.writeFileSync("data/servers/ookla.json", JSON.stringify(servers, null, 4));
} catch (e) {
console.error("Could not save servers file")
console.error("Could not save servers file");
}
});
} catch (e) {
console.error("Could not get servers");
}
}
// Load servers from librespeed
if (!fs.existsSync("data/servers/librespeed.json")) {
let servers = {};
try {
axios.get("https://librespeed.org/backend-servers/servers.php")
.then(res => res.data)
.then(data => {
data?.forEach(row => {
servers[row.id] = row.name;
});
try {
fs.writeFileSync("data/servers/librespeed.json", JSON.stringify(servers, null, 4));
} catch (e) {
console.error("Could not save servers file");
}
});
} catch (e) {
console.error("Could not get servers");
}
}

View File

@@ -0,0 +1,49 @@
const fs = require('fs');
const {get} = require('https');
const decompress = require("decompress");
const {file} = require("tmp");
const decompressTarGz = require('decompress-targz');
const decompressUnzip = require('decompress-unzip');
const binaries = require('../../config/binaries');
const binaryRegex = /librespeed-cli(.exe)?$/;
const binaryDirectory = __dirname + "/../../../bin/";
const binaryPath = `${binaryDirectory}/librespeed-cli` + (process.platform === "win32" ? ".exe" : "");
const downloadPath = `https://github.com/librespeed/speedtest-cli/releases/download/v${binaries.libreVersion}/librespeed-cli_${binaries.libreVersion}_`;
module.exports.fileExists = async () => fs.existsSync(binaryPath);
module.exports.downloadFile = async () => {
const binary = binaries.libreList.find(b => b.os === process.platform && b.arch === process.arch);
if (!binary)
throw new Error(`Your platform (${process.platform}-${process.arch}) is not supported by the LibreSpeed CLI`);
await new Promise((resolve) => {
file({postfix: binary.suffix}, async (err, path) => {
const location = await new Promise((resolve) => get(downloadPath + binary.suffix, (res) => {
resolve(res.headers.location);
}));
get(location, async resp => {
resp.pipe(fs.createWriteStream(path)).on('finish', async () => {
await decompress(path, binaryDirectory, {
plugins: [decompressTarGz(), decompressUnzip()],
filter: file => binaryRegex.test(file.path),
map: file => {
file.path = "librespeed-cli" + (process.platform === "win32" ? ".exe" : "");
return file;
}
});
resolve();
});
});
});
});
}
module.exports.load = async () => {
if (!await this.fileExists())
await this.downloadFile();
}

View File

@@ -0,0 +1,45 @@
const fs = require('fs');
const {get} = require('https');
const decompress = require("decompress");
const {file} = require("tmp");
const decompressTarGz = require('decompress-targz');
const decompressUnzip = require('decompress-unzip');
const binaries = require('../../config/binaries');
const binaryRegex = /speedtest(.exe)?$/;
const binaryDirectory = __dirname + "/../../../bin/";
const binaryPath = `${binaryDirectory}/ookla` + (process.platform === "win32" ? ".exe" : "");
const downloadPath = `https://install.speedtest.net/app/cli/ookla-speedtest-${binaries.ooklaVersion}-`;
module.exports.fileExists = async () => fs.existsSync(binaryPath);
module.exports.downloadFile = async () => {
const binary = binaries.ooklaList.find(b => b.os === process.platform && b.arch === process.arch);
if (!binary)
throw new Error(`Your platform (${process.platform}-${process.arch}) is not supported by the Speedtest CLI`);
await new Promise((resolve) => {
file({postfix: binary.suffix}, async (err, path) => {
get(downloadPath + binary.suffix, async resp => {
resp.pipe(fs.createWriteStream(path)).on('finish', async () => {
await decompress(path, binaryDirectory, {
plugins: [decompressTarGz(), decompressUnzip()],
filter: file => binaryRegex.test(file.path),
map: file => {
file.path = "speedtest" + (process.platform === "win32" ? ".exe" : "");
return file;
}
});
resolve();
});
});
});
});
}
module.exports.load = async () => {
if (!await this.fileExists())
await this.downloadFile();
}

View File

@@ -0,0 +1,38 @@
const roundSpeed = (bytes, elapsed) => {
return Math.round((bytes * 8 / elapsed) / 10) / 100;
}
module.exports.parseOokla = (test) => {
let ping = Math.round(test.ping.latency);
let download = roundSpeed(test.download.bytes, test.download.elapsed);
let upload = roundSpeed(test.upload.bytes, test.upload.elapsed);
let time = Math.round((test.download.elapsed + test.upload.elapsed) / 1000);
return {ping, download, upload, time};
}
module.exports.parseLibre = (test) => {
return {ping: test.ping, upload: test.upload, download: test.download, time: Math.round(test.elapsed / 1000)};
}
module.exports.parseCloudflare = async (test) => {
let ping = Math.round(test.latency);
let download = Math.round(test.download / 10000) / 100;
let upload = Math.round(test.upload / 10000) / 100;
let time = Math.round(test.elapsed / 1000);
return {ping, download, upload, time};
}
module.exports.parseData = (provider, data) => {
switch (provider) {
case "ookla":
return this.parseOokla(data);
case "libre":
return this.parseLibre(data);
case "cloudflare":
return this.parseCloudflare(data);
default:
throw {message: "Invalid provider"};
}
}

View File

@@ -1,34 +1,53 @@
const {spawn} = require('child_process');
module.exports = async (serverId, binary_path = './bin/speedtest' + (process.platform === "win32" ? ".exe" : "")) => {
const args = ['--accept-license', '--accept-gdpr', '--format=jsonl'];
if (serverId) args.push(`--server-id=${serverId}`);
module.exports = async (mode, serverId) => {
const binaryPath = mode === "ookla" ? './bin/speedtest' + (process.platform === "win32" ? ".exe" : "")
: './bin/librespeed-cli' + (process.platform === "win32" ? ".exe" : "");
const startTime = new Date().getTime();
let args;
if (mode === "ookla") {
args = ['--accept-license', '--accept-gdpr', '--format=json'];
if (serverId) args.push(`--server-id=${serverId}`);
} else {
args = ['--json', '--duration=5'];
if (serverId) args.push(`--server=${serverId}`);
}
let result = {};
const process = spawn(binary_path, args, {windowsHide: true});
const testProcess = spawn(binaryPath, args, {windowsHide: true});
process.stdout.on('data', (buffer) => {
testProcess.stderr.on('data', (buffer) => {
result.error = buffer.toString();
if (buffer.toString().includes("Too many requests")) {
result.error = "Too many requests. Please try again later";
}
});
testProcess.stdout.on('data', (buffer) => {
const line = buffer.toString().replace("\n", "");
if (!line.startsWith("{")) return;
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 (data.type === "result") result = data;
if ((mode === "ookla" && data.type === "result") || mode === "libre") result = data;
});
await new Promise((resolve, reject) => {
process.on('error', e => reject({message: e}));
process.on('exit', resolve);
testProcess.on('error', e => reject({message: e}));
testProcess.on('exit', resolve);
});
if (result.error) throw new Error(result.error);
return result;
return {...result, elapsed: new Date().getTime() - startTime};
}