diff --git a/client/public/assets/locales/en.json b/client/public/assets/locales/en.json index 9e5ed747..66adf751 100644 --- a/client/public/assets/locales/en.json +++ b/client/public/assets/locales/en.json @@ -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 Accept, you acknowledge that you have read and agree to Ookla's EULA, Privacy Statement and Terms of Use.", - "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, privacy policy and terms of service 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.", diff --git a/client/src/common/components/Dropdown/DropdownComponent.jsx b/client/src/common/components/Dropdown/DropdownComponent.jsx index b80968fe..15c947ff 100644 --- a/client/src/common/components/Dropdown/DropdownComponent.jsx +++ b/client/src/common/components/Dropdown/DropdownComponent.jsx @@ -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 && setShowViewDialog(false)}/>} {showIntegrationDialog && setShowIntegrationDialog(false)}/>} {showLanguageDialog && setShowLanguageDialog(false)}/>} + {showProviderDialog && setShowProviderDialog(false)}/>} {t("dropdown.settings")} diff --git a/client/src/common/components/LanguageDialog/styles.sass b/client/src/common/components/LanguageDialog/styles.sass index 57d6a60c..462e858b 100644 --- a/client/src/common/components/LanguageDialog/styles.sass +++ b/client/src/common/components/LanguageDialog/styles.sass @@ -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 diff --git a/client/src/common/components/ProviderDialog/ProviderDialog.jsx b/client/src/common/components/ProviderDialog/ProviderDialog.jsx new file mode 100644 index 00000000..bd79c505 --- /dev/null +++ b/client/src/common/components/ProviderDialog/ProviderDialog.jsx @@ -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 ( + <> + + {t("update.provider_title")} + close()}/> + + + + {providers.map((current, index) => ( + setProvider(current.id)}> + + {current.name} + + ))} + + {provider !== "cloudflare" && + + {t("dialog.provider.server")} + setServerId(e.target.value)}> + {t("dialog.provider.choose_automatically")} + {provider === "ookla" && Object.keys(ooklaServers).map((current, index) => ( + {ooklaServers[current]} + ))} + {provider === "libre" && Object.keys(libreServers).map((current, index) => ( + {libreServers[current]} + ))} + + + + {t("dialog.provider.server_id")} + setServerId(e.target.value)}/> + + } + {provider === "cloudflare" && + {t("dialog.provider.cloudflare_note")} + } + + + + {provider === "ookla" && <> + setLicenseAccepted(e.target.checked)}/> + , + GDPR: , + TOS: }}>dialog.provider.ookla_license + >} + + + {t("dialog.update")} + + > + ) +} + +export const ProviderDialog = (props) => { + return ( + <> + + + + > + ) +} \ No newline at end of file diff --git a/client/src/common/components/ProviderDialog/assets/img/cloudflare.webp b/client/src/common/components/ProviderDialog/assets/img/cloudflare.webp new file mode 100644 index 00000000..c72b453e Binary files /dev/null and b/client/src/common/components/ProviderDialog/assets/img/cloudflare.webp differ diff --git a/client/src/common/components/ProviderDialog/assets/img/libre.webp b/client/src/common/components/ProviderDialog/assets/img/libre.webp new file mode 100644 index 00000000..8d18f39e Binary files /dev/null and b/client/src/common/components/ProviderDialog/assets/img/libre.webp differ diff --git a/client/src/common/components/ProviderDialog/assets/img/ookla.webp b/client/src/common/components/ProviderDialog/assets/img/ookla.webp new file mode 100644 index 00000000..39f781a5 Binary files /dev/null and b/client/src/common/components/ProviderDialog/assets/img/ookla.webp differ diff --git a/client/src/common/components/ProviderDialog/index.js b/client/src/common/components/ProviderDialog/index.js new file mode 100644 index 00000000..908e023f --- /dev/null +++ b/client/src/common/components/ProviderDialog/index.js @@ -0,0 +1 @@ +export {ProviderDialog as default} from "./ProviderDialog"; \ No newline at end of file diff --git a/client/src/common/components/ProviderDialog/styles.sass b/client/src/common/components/ProviderDialog/styles.sass new file mode 100644 index 00000000..32a64c5f --- /dev/null +++ b/client/src/common/components/ProviderDialog/styles.sass @@ -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% \ No newline at end of file diff --git a/client/src/common/contexts/Config/ConfigContext.jsx b/client/src/common/contexts/Config/ConfigContext.jsx index bc6e73f4..13af6498 100644 --- a/client/src/common/contexts/Config/ConfigContext.jsx +++ b/client/src/common/contexts/Config/ConfigContext.jsx @@ -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 ( diff --git a/client/src/common/contexts/Config/dialog.jsx b/client/src/common/contexts/Config/dialog.jsx index 168d80a1..bed3ddd3 100644 --- a/client/src/common/contexts/Config/dialog.jsx +++ b/client/src/common/contexts/Config/dialog.jsx @@ -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: , EULA: , - Privacy: , Terms: }}>dialog.accept.description, - buttonText: t("dialog.accept.button"), - disableCloseButton: true, - onSuccess: () => patchRequest("/config/acceptOoklaLicense", {value: true}) }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9e3e316d..93631b98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 483ea82c..02383781 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/config/binaries.js b/server/config/binaries.js index d20223db..29c17a81 100644 --- a/server/config/binaries.js +++ b/server/config/binaries.js @@ -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'} ] \ No newline at end of file diff --git a/server/controller/config.js b/server/controller/config.js index f5878d67..d0a00600 100644 --- a/server/controller/config.js +++ b/server/controller/config.js @@ -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) => { diff --git a/server/controller/servers.js b/server/controller/servers.js new file mode 100644 index 00000000..27d9102e --- /dev/null +++ b/server/controller/servers.js @@ -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(); +} \ No newline at end of file diff --git a/server/controller/speedtests.js b/server/controller/speedtests.js index 1b44554e..78588f56 100644 --- a/server/controller/speedtests.js +++ b/server/controller/speedtests.js @@ -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) => { diff --git a/server/index.js b/server/index.js index 6e6a79d5..d5f38334 100755 --- a/server/index.js +++ b/server/index.js @@ -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); diff --git a/server/models/Speedtests.js b/server/models/Speedtests.js index 4394f7ce..190f3ded 100644 --- a/server/models/Speedtests.js +++ b/server/models/Speedtests.js @@ -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 diff --git a/server/routes/config.js b/server/routes/config.js index 328e0895..89d13a14 100644 --- a/server/routes/config.js +++ b/server/routes/config.js @@ -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"}); diff --git a/server/routes/speedtests.js b/server/routes/speedtests.js index ee5b9560..961c5d88 100644 --- a/server/routes/speedtests.js +++ b/server/routes/speedtests.js @@ -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"}); diff --git a/server/routes/system.js b/server/routes/system.js index da5b3f0f..d00627f1 100644 --- a/server/routes/system.js +++ b/server/routes/system.js @@ -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; \ No newline at end of file diff --git a/server/tasks/speedtest.js b/server/tasks/speedtest.js index 68ef69e7..59fcc852 100644 --- a/server/tasks/speedtest.js +++ b/server/tasks/speedtest.js @@ -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 (mode === "ookla" && speedtest.server) { + if (serverId === undefined) await config.updateValue("ooklaId", speedtest.server?.id); + serverId = speedtest.server?.id; + } - if (serverId === undefined) - await config.updateValue("serverId", speedtest.server.id); + if (mode === "libre" && speedtest.server) { + let server = Object.entries(serverController.getLibreServers()) + .filter(([, value]) => value === speedtest.server.name)[0][0]; - if (Object.keys(speedtest).length === 0) throw {message: "No response, even after trying again, test timed out."}; + if (server) { + if (serverId === undefined) await config.updateValue("libreId", server); + serverId = parseInt(server); + } + } - return speedtest; + 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); diff --git a/server/util/createFolders.js b/server/util/createFolders.js index 7c6e525a..7d26cff7 100644 --- a/server/util/createFolders.js +++ b/server/util/createFolders.js @@ -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)) { diff --git a/server/util/loadCli.js b/server/util/loadCli.js index 80741a72..aaf3a8ba 100644 --- a/server/util/loadCli.js +++ b/server/util/loadCli.js @@ -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(); } \ No newline at end of file diff --git a/server/util/loadServers.js b/server/util/loadServers.js index 9742ec2c..8a3e6e30 100644 --- a/server/util/loadServers.js +++ b/server/util/loadServers.js @@ -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"); + } +} \ No newline at end of file diff --git a/server/util/providers/loadLibre.js b/server/util/providers/loadLibre.js new file mode 100644 index 00000000..68bad02f --- /dev/null +++ b/server/util/providers/loadLibre.js @@ -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(); +} \ No newline at end of file diff --git a/server/util/providers/loadOokla.js b/server/util/providers/loadOokla.js new file mode 100644 index 00000000..a34dc9eb --- /dev/null +++ b/server/util/providers/loadOokla.js @@ -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(); +} \ No newline at end of file diff --git a/server/util/providers/parseData.js b/server/util/providers/parseData.js new file mode 100644 index 00000000..140515b1 --- /dev/null +++ b/server/util/providers/parseData.js @@ -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"}; + } +} \ No newline at end of file diff --git a/server/util/speedtest.js b/server/util/speedtest.js index 3adbd7cb..c2cdbe07 100644 --- a/server/util/speedtest.js +++ b/server/util/speedtest.js @@ -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}); + + 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"; + } + }); - process.stdout.on('data', (buffer) => { + 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}; } \ No newline at end of file
{t("dialog.provider.cloudflare_note")}