diff --git a/client/src/components/WelcomeButtons.tsx b/client/src/components/WelcomeButtons.tsx index cae86f48..be732628 100644 --- a/client/src/components/WelcomeButtons.tsx +++ b/client/src/components/WelcomeButtons.tsx @@ -65,6 +65,17 @@ export const WelcomeButtons = ({className}: {className?: string}) => { How-to Guides + {process.env.NODE_ENV === "production" && + location.protocol !== "https" && ( + + Use HTTPS + + )} )} diff --git a/client/src/docs/Troubleshooting/insecure.png b/client/src/docs/Troubleshooting/insecure.png new file mode 100644 index 00000000..7330ae74 Binary files /dev/null and b/client/src/docs/Troubleshooting/insecure.png differ diff --git a/client/src/docs/Troubleshooting/using-https.mdx b/client/src/docs/Troubleshooting/using-https.mdx new file mode 100644 index 00000000..d0ee400f --- /dev/null +++ b/client/src/docs/Troubleshooting/using-https.mdx @@ -0,0 +1,58 @@ +--- +title: Using HTTPS +order: 1 +--- + +# Using HTTPS + +Thorium Nova has clients connect using HTTP by default. However, browsers like +Chrome only allow certain features to be used when the connection is using +HTTPS. These include things like WebUSB, WebMIDI, WebRTC, and video and audio +capture. + +None of these features are currently being used, but eventually we hope to +incorporate them into the controls. These might be used for things like: + +- Connecting DMX lights and controlling them from any connected client. +- Using a MIDI control board as an interface for the controls. +- Peer-to-peer voice chat for conversations between remote crew members and + between the crew and flight director. + +## Activating HTTPS + +Since Thorium Nova is hosted within the networks of players and not on the open +internet, it isn't possible to avoid the security warnings of browsers. However, +it is possible to get around them. + +You can activate HTTPS by clicking the "Use HTTPS" button on the Thorium Nova +main screen. This will redirect your browser to the same page, but using the +HTTPS protocol and with the port number incremented by one, which by default +is 4445. For example, the new URL will be `https://:4445`. + +The first time you do this, you will likely see a warning from the browser about +the security of the connection. Different browsers provide different ways to +ignore this warning. + +![Google Chrome showing a security warning](./insecure.png) + +In Google Chrome, you need to click on the page and type `thisisunsafe` into the +window. There isn't a text box to type it into, you just type it on the window. +This will cause the page to actually load. + +## A Note About Security + +If a web browser is giving you a security warning, that means the website does +have some kind of security vulnerability. Using HTTPS with Thorium Nova is +**not** an exception. + +Of course, Thorium Nova isn't designed to pose a risk to you or your computer. +While it is possible Thorium Nova could use it's HTTPS connection to do +nefarious things, if you trust the developers of Thorium Nova and the code, you +can be reasonably confident that using HTTPS with Thorium Nova is safe. Since +it's open-source, you are always welcome to review the code yourself. + +If you can't bring yourself to trust Thorium Nova, than it's probably best to +not use it with the built-in HTTPS. There are ways you can set up Thorium Nova +to work with HTTPS in a secure way. You could create your own security +certificate to use with Thorium Nova or you could connect to the Thorium Nova +HTTP server using a proxy. diff --git a/desktop/main/electron.ts b/desktop/main/electron.ts index 25fa0a72..a92e904f 100644 --- a/desktop/main/electron.ts +++ b/desktop/main/electron.ts @@ -31,7 +31,7 @@ const cert = fs.readFileSync( ), "utf8" ); -const port = process.env.PORT || 4444; +const port = Number(process.env.PORT) || 4444; async function createWindow() { await startThoriumServer(); @@ -72,7 +72,9 @@ async function createWindow() { // e.preventDefault(); }); - win.loadURL(`https://localhost:${port}`); + // We add 1 to the port, since we want to connect to the HTTPS server + // which is 1 more than the default port + win.loadURL(`https://localhost:${port + 1}`); win.on("closed", () => { win = null; }); diff --git a/package-lock.json b/package-lock.json index 96eb66a0..671ceb8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "thorium-nova", - "version": "0.0.0-development", + "version": "1.0.0-alpha.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "thorium-nova", - "version": "0.0.0-development", + "version": "1.0.0-alpha.1", "hasInstallScript": true, "license": "Apache 2.0", "workspaces": [ @@ -11568,6 +11568,35 @@ "resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.1.tgz", "integrity": "sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ==" }, + "node_modules/fastify-http-proxy": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/fastify-http-proxy/-/fastify-http-proxy-6.2.1.tgz", + "integrity": "sha512-1ApZwz5v7QOWrJkHbKXf66RNJdSbxiQAEY+E9e2W/93cZRDVr+MZxG3u/P/HR06O5maVnxiERcRXRU3RquVQEg==", + "dependencies": { + "fastify-reply-from": "^6.0.0", + "ws": "^8.0.0" + } + }, + "node_modules/fastify-http-proxy/node_modules/ws": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz", + "integrity": "sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/fastify-multipart": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/fastify-multipart/-/fastify-multipart-5.2.1.tgz", @@ -11588,6 +11617,81 @@ "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz", "integrity": "sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w==" }, + "node_modules/fastify-reply-from": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fastify-reply-from/-/fastify-reply-from-6.4.2.tgz", + "integrity": "sha512-3HCP6BCWBSwtGdqQqS8A3joBuNHcp4dhQiVtkZ3zMPsfDaahBGT4ceFW4XMHL97r+oF+Rv2+l/uw4mGeQ6P/9g==", + "dependencies": { + "end-of-stream": "^1.4.4", + "fastify-plugin": "^3.0.0", + "http-errors": "^2.0.0", + "pump": "^3.0.0", + "semver": "^7.3.5", + "tiny-lru": "^7.0.6", + "undici": "^4.0.0" + }, + "engines": { + "node": ">=12.18" + } + }, + "node_modules/fastify-reply-from/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fastify-reply-from/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fastify-reply-from/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fastify-reply-from/node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/fastify-reply-from/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fastify-reply-from/node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/fastify-static": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/fastify-static/-/fastify-static-4.5.0.tgz", @@ -23530,7 +23634,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -27666,6 +27769,14 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/undici": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-4.13.0.tgz", + "integrity": "sha512-8lk8S/f2V0VUNGf2scU2b+KI2JSzEQLdCyRNRF3XmHu+5jectlSDaPSBCXAHFaUlt1rzngzOBVDgJS9/Gue/KA==", + "engines": { + "node": ">=12.18" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -28997,6 +29108,7 @@ "fastify": "^3.25.0", "fastify-cookie": "^5.4.0", "fastify-cors": "^6.0.2", + "fastify-http-proxy": "^6.2.1", "fastify-multipart": "^5.2.1", "fastify-static": "^4.5.0", "fastify-websocket": "^4.0.0", @@ -37698,6 +37810,23 @@ "resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.1.tgz", "integrity": "sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ==" }, + "fastify-http-proxy": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/fastify-http-proxy/-/fastify-http-proxy-6.2.1.tgz", + "integrity": "sha512-1ApZwz5v7QOWrJkHbKXf66RNJdSbxiQAEY+E9e2W/93cZRDVr+MZxG3u/P/HR06O5maVnxiERcRXRU3RquVQEg==", + "requires": { + "fastify-reply-from": "^6.0.0", + "ws": "^8.0.0" + }, + "dependencies": { + "ws": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz", + "integrity": "sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==", + "requires": {} + } + } + }, "fastify-multipart": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/fastify-multipart/-/fastify-multipart-5.2.1.tgz", @@ -37718,6 +37847,62 @@ "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz", "integrity": "sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w==" }, + "fastify-reply-from": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fastify-reply-from/-/fastify-reply-from-6.4.2.tgz", + "integrity": "sha512-3HCP6BCWBSwtGdqQqS8A3joBuNHcp4dhQiVtkZ3zMPsfDaahBGT4ceFW4XMHL97r+oF+Rv2+l/uw4mGeQ6P/9g==", + "requires": { + "end-of-stream": "^1.4.4", + "fastify-plugin": "^3.0.0", + "http-errors": "^2.0.0", + "pump": "^3.0.0", + "semver": "^7.3.5", + "tiny-lru": "^7.0.6", + "undici": "^4.0.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + } + } + }, "fastify-static": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/fastify-static/-/fastify-static-4.5.0.tgz", @@ -46544,7 +46729,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -48213,12 +48397,13 @@ "@types/three": "^0.135.0", "@types/ws": "^8.2.2", "@types/yauzl": "^2.9.2", - "@types/yazl": "*", + "@types/yazl": "^2.4.2", "esbuild": "^0.14.5", "fast-glob": "^3.2.7", "fastify": "^3.25.0", "fastify-cookie": "^5.4.0", "fastify-cors": "^6.0.2", + "fastify-http-proxy": "*", "fastify-multipart": "^5.2.1", "fastify-static": "^4.5.0", "fastify-websocket": "^4.0.0", @@ -49709,6 +49894,11 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "undici": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-4.13.0.tgz", + "integrity": "sha512-8lk8S/f2V0VUNGf2scU2b+KI2JSzEQLdCyRNRF3XmHu+5jectlSDaPSBCXAHFaUlt1rzngzOBVDgJS9/Gue/KA==" + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", diff --git a/server/API.md b/server/API.md index 3d06f8ed..5f72d941 100644 --- a/server/API.md +++ b/server/API.md @@ -3,7 +3,7 @@ ## Environment Variables - `PORT` - Set the port for the HTTP server. Useful for headless setups. - Defaults to 4444. + Defaults to 4444. The HTTPS server with use the port + 1, defaulting to 4445. - `COOKIE_SECRET` - A secret key used to encrypt secure cookies. This is currently unused. - `THORIUM_PATH` - The directory that will contain the data and assets for diff --git a/server/package.json b/server/package.json index 2910821d..626d5109 100644 --- a/server/package.json +++ b/server/package.json @@ -33,6 +33,7 @@ "fastify": "^3.25.0", "fastify-cookie": "^5.4.0", "fastify-cors": "^6.0.2", + "fastify-http-proxy": "^6.2.1", "fastify-multipart": "^5.2.1", "fastify-static": "^4.5.0", "fastify-websocket": "^4.0.0", diff --git a/server/src/index.ts b/server/src/index.ts index d3bde36a..47cf53e2 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -10,6 +10,7 @@ import chalk from "chalk"; import {FlightDataModel} from "./classes/FlightDataModel"; import {promises as fs, existsSync} from "fs"; import {unzip} from "./utils/unzipFolder"; +import {buildHttpsProxy} from "./init/httpsProxy"; setBasePath(thoriumPath); const isHeadless = !process.env.FORK; @@ -60,14 +61,22 @@ export async function startServer() { await applyDataChannel(app, database); setUpAPI(app, database); const PORT = - process.env.PORT || (process.env.NODE_ENV === "production" ? 4444 : 3001); + Number(process.env.PORT) || + (process.env.NODE_ENV === "production" ? 4444 : 3001); + const HTTPSPort = PORT + 1; + const proxy = buildHttpsProxy(PORT); + try { await app.listen(PORT, "0.0.0.0"); + if (process.env.NODE_ENV === "production") { + await proxy.listen(HTTPSPort, "0.0.0.0"); + } console.info(chalk.greenBright(`Access app at http://localhost:${PORT}`)); console.info( chalk.cyan(`Doing port forwarding? Open this port in your router:`) ); console.info(chalk.cyan(` - TCP ${PORT} for web app access`)); + console.info(chalk.cyan(` - TCP ${HTTPSPort} for HTTPS access`)); process.send?.("ready"); } catch (err) { process.send?.("error"); diff --git a/server/src/init/httpServer.ts b/server/src/init/httpServer.ts index 9c746063..9b342927 100644 --- a/server/src/init/httpServer.ts +++ b/server/src/init/httpServer.ts @@ -4,7 +4,7 @@ import staticServe from "fastify-static"; import cors from "fastify-cors"; import path from "path"; import {thoriumPath, rootPath} from "../utils/appPaths"; -import {promises as fs, createWriteStream, readFileSync} from "fs"; +import {promises as fs, createWriteStream} from "fs"; import {pipeline} from "stream/promises"; import uniqid from "@thorium/uniqid"; import os from "os"; @@ -17,16 +17,7 @@ export default function buildHTTPServer({ staticRoot?: string; port?: number; } = {}) { - const certDir = isHeadless ? "./resources" : "../../app"; - const app = fastify({ - https: - process.env.NODE_ENV === "production" - ? { - key: readFileSync(path.join(rootPath, certDir, "server.key")), - cert: readFileSync(path.join(rootPath, certDir, "server.cert")), - } - : (undefined as any), - }); + const app = fastify(); async function onFile(part: MultipartFile) { const tmpdir = os.tmpdir(); diff --git a/server/src/init/httpsProxy.ts b/server/src/init/httpsProxy.ts new file mode 100644 index 00000000..9eadacd3 --- /dev/null +++ b/server/src/init/httpsProxy.ts @@ -0,0 +1,29 @@ +import fastify from "fastify"; +import fastifyHttpProxy from "fastify-http-proxy"; +import {readFileSync} from "fs"; +import path from "path"; +import {rootPath} from "../utils/appPaths"; + +const isHeadless = !process.env.FORK; +export function buildHttpsProxy(port: number) { + // Create a proxy server to handle the HTTPS requests + // All requests are proxied to the HTTP server + const certDir = + process.env.NODE_ENV !== "production" + ? "../../../desktop/resources" + : isHeadless + ? "./resources" + : "../../app"; + + const proxy = fastify({ + https: { + key: readFileSync(path.join(rootPath, certDir, "server.key")), + cert: readFileSync(path.join(rootPath, certDir, "server.cert")), + }, + }); + proxy.register(fastifyHttpProxy, { + upstream: `http://localhost:${port}`, + }); + + return proxy; +}