diff --git a/src/GameOptions/ui/RemoteAPIPage.tsx b/src/GameOptions/ui/RemoteAPIPage.tsx index b46735f1ba..e0056f0cde 100644 --- a/src/GameOptions/ui/RemoteAPIPage.tsx +++ b/src/GameOptions/ui/RemoteAPIPage.tsx @@ -1,28 +1,46 @@ import React, { useState } from "react"; import { Button, Link, TextField, Tooltip, Typography } from "@mui/material"; import { GameOptionsPage } from "./GameOptionsPage"; -import { Settings } from "../../Settings/Settings"; +import { isValidConnectionHostname, isValidConnectionPort, Settings } from "../../Settings/Settings"; import { ConnectionBauble } from "./ConnectionBauble"; import { isRemoteFileApiConnectionLive, newRemoteFileApiConnection } from "../../RemoteFileAPI/RemoteFileAPI"; export const RemoteAPIPage = (): React.ReactElement => { + const [remoteFileApiHostname, setRemoteFileApiHostname] = useState(Settings.RemoteFileApiAddress); const [remoteFileApiPort, setRemoteFileApiPort] = useState(Settings.RemoteFileApiPort); - const [remoteFileApiAddress, setRemoteFileApiAddress] = useState(Settings.RemoteFileApiAddress); - function handleRemoteFileApiPortChange(event: React.ChangeEvent): void { - setRemoteFileApiPort(Number(event.target.value)); - Settings.RemoteFileApiPort = Number(event.target.value); + function handleRemoteFileApiHostnameChange(event: React.ChangeEvent): void { + let newValue = event.target.value.trim(); + // Empty string will be automatically changed to "localhost". + if (newValue === "") { + newValue = "localhost"; + } + if (!isValidConnectionHostname(newValue)) { + return; + } + setRemoteFileApiHostname(newValue); + Settings.RemoteFileApiAddress = newValue; } - function handleRemoteFileApiAddressChange(event: React.ChangeEvent): void { - setRemoteFileApiAddress(String(event.target.value)); - Settings.RemoteFileApiAddress = String(event.target.value); + function handleRemoteFileApiPortChange(event: React.ChangeEvent): void { + let newValue = event.target.value.trim(); + // Empty string will be automatically changed to "0". + if (newValue === "") { + newValue = "0"; + } + const port = Number.parseInt(newValue); + // Disallow invalid ports but still allow the player to set port to 0. Setting it to 0 means that RFA is disabled. + if (port !== 0 && !isValidConnectionPort(port)) { + return; + } + setRemoteFileApiPort(port); + Settings.RemoteFileApiPort = port; } return ( - These settings control the Remote API for bitburner. This is typically used to write scripts using an external + These settings control the Remote API for Bitburner. This is typically used to write scripts using an external text editor and then upload files to the home server. @@ -37,18 +55,17 @@ export const RemoteAPIPage = (): React.ReactElement => { - This address is used to connect to a Remote API, please ensure that it matches with your Remote API address. - Default localhost. + This hostname is used to connect to a Remote API, please ensure that it matches with your Remote API + hostname. Default: localhost. } > Address: , + startAdornment: Hostname: , }} - value={remoteFileApiAddress} - onChange={handleRemoteFileApiAddressChange} + value={remoteFileApiHostname} + onChange={handleRemoteFileApiHostnameChange} placeholder="localhost" size={"medium"} /> @@ -63,10 +80,9 @@ export const RemoteAPIPage = (): React.ReactElement => { } > 0 && remoteFileApiPort <= 65535 ? "success" : "error"}> + Port:  ), diff --git a/src/Settings/Settings.ts b/src/Settings/Settings.ts index 97cb68991e..c2f2357ab5 100644 --- a/src/Settings/Settings.ts +++ b/src/Settings/Settings.ts @@ -4,6 +4,45 @@ import { defaultStyles } from "../Themes/Styles"; import { CursorStyle, CursorBlinking, WordWrapOptions } from "../ScriptEditor/ui/Options"; import { defaultMonacoTheme } from "../ScriptEditor/ui/themes"; +/** + * This function won't be able to catch **all** invalid hostnames, and it's still fine. In order to validate a hostname + * properly, we need to import a good validation library or write one by ourselves. I think that it's unnecessary. + * + * Some invalid hostnames that we don't catch: + * - Invalid/missing TLD: "abc". + * - Use space character: "a a.com" + * - Use non-http schemes in the hostname: "ftp://a.com" + * - etc. + */ +export function isValidConnectionHostname(hostname: string): boolean { + /** + * We expect a hostname, but the player may mistakenly put other unexpected things. We will try to catch common mistakes: + * - Specify a scheme: http or https. + * - Specify a port. + * - Specify a pathname or search params. + */ + try { + // Check scheme. + if (hostname.startsWith("http://") || hostname.startsWith("https://")) { + return false; + } + // Parse to a URL with a default scheme. + const url = new URL(`http://${hostname}`); + // Check port, pathname, and search params. + if (url.port !== "" || url.pathname !== "/" || url.search !== "") { + return false; + } + } catch (e) { + console.error(`Invalid hostname: ${hostname}`, e); + return false; + } + return true; +} + +export function isValidConnectionPort(port: number): boolean { + return Number.isFinite(port) && port > 0 && port <= 65535; +} + /** The current options the player has customized to their play style. */ export const Settings = { /** How many servers per page */ @@ -125,5 +164,15 @@ export const Settings = { save.EditorTheme && Object.assign(Settings.EditorTheme, save.EditorTheme); delete save.theme, save.styles, save.overview, save.EditorTheme; Object.assign(Settings, save); + /** + * The hostname and port of RFA have not been validated properly, so the save data may contain invalid data. In that + * case, we set them to the default value. + */ + if (!isValidConnectionHostname(Settings.RemoteFileApiAddress)) { + Settings.RemoteFileApiAddress = "localhost"; + } + if (!isValidConnectionPort(Settings.RemoteFileApiPort)) { + Settings.RemoteFileApiPort = 0; + } }, };