diff --git a/gcs/electron/main.ts b/gcs/electron/main.ts index 57b37d90b..c554dbb54 100644 --- a/gcs/electron/main.ts +++ b/gcs/electron/main.ts @@ -129,54 +129,55 @@ function getUserConfiguration() { return userSettings } -ipcMain.handle("getSettings", () => { +ipcMain.handle("settings:fetch-settings", () => { return getUserConfiguration() }) -ipcMain.handle("setSettings", (_, settings) => { +ipcMain.handle("settings:save-settings", (_, settings) => { saveUserConfiguration(settings) }) -ipcMain.handle("isMac", () => { +ipcMain.handle("app:is-mac", () => { return process.platform == "darwin" }) -ipcMain.on("close", () => { +ipcMain.on("window:close", () => { closeWithBackend() }) -ipcMain.on("minimise", () => { +ipcMain.on("window:minimise", () => { getWindow()?.minimize() }) -ipcMain.on("maximise", () => { +ipcMain.on("window:maximise", () => { getWindow()?.isMaximized() ? getWindow()?.unmaximize() : getWindow()?.maximize() }) -ipcMain.on("reload", () => { +ipcMain.on("window:reload", () => { getWindow()?.reload() }) -ipcMain.on("force_reload", () => { +ipcMain.on("window:force-reload", () => { getWindow()?.webContents.reloadIgnoringCache() }) -ipcMain.on("toggle_developer_tools", () => { +ipcMain.on("window:toggle-developer-tools", () => { getWindow()?.webContents.toggleDevTools() }) -ipcMain.on("actual_size", () => { +ipcMain.on("window:actual-size", () => { getWindow()?.webContents.setZoomFactor(1) }) -ipcMain.on("toggle_fullscreen", () => { +ipcMain.on("window:toggle-fullscreen", () => { getWindow()?.isFullScreen() ? getWindow()?.setFullScreen(false) : getWindow()?.setFullScreen(true) }) -ipcMain.on("zoom_in", () => { +ipcMain.on("window:zoom-in", () => { const window = getWindow()?.webContents window?.setZoomFactor(window?.getZoomFactor() + 0.1) }) -ipcMain.on("zoom_out", () => { +ipcMain.on("window:zoom-out", () => { const window = getWindow()?.webContents window?.setZoomFactor(window?.getZoomFactor() - 0.1) }) -ipcMain.on("openFileInExplorer", (_event, filePath) => { + +ipcMain.on("window:open-file-in-explorer", (_event, filePath) => { shell.showItemInFolder(filePath) }) diff --git a/gcs/electron/modules/aboutWindow.ts b/gcs/electron/modules/aboutWindow.ts index 431e92348..17990bc8a 100644 --- a/gcs/electron/modules/aboutWindow.ts +++ b/gcs/electron/modules/aboutWindow.ts @@ -37,10 +37,6 @@ export function openAboutPopout() { return { action: "deny" } }) - // Windows doesn't consider maximising to be fullscreening so we must prevent default - aboutPopoutWin.on("maximize", (e: Event) => { - e.preventDefault() - }) aboutPopoutWin.on("close", () => { aboutPopoutWin = null }) @@ -58,11 +54,11 @@ export function destroyAboutWindow() { } export default function registerAboutIPC() { - ipcMain.removeHandler("openAboutWindow") - ipcMain.removeHandler("closeAboutWindow") + ipcMain.removeHandler("app:open-about-window") + ipcMain.removeHandler("app:close-about-window") - ipcMain.handle("openAboutWindow", () => { + ipcMain.handle("app:open-about-window", () => { openAboutPopout() }) - ipcMain.handle("closeAboutWindow", () => closeAboutPopout()) + ipcMain.handle("app:close-about-window", () => closeAboutPopout()) } diff --git a/gcs/electron/modules/linkStatsWindow.ts b/gcs/electron/modules/linkStatsWindow.ts index 6f7d91014..d04250572 100644 --- a/gcs/electron/modules/linkStatsWindow.ts +++ b/gcs/electron/modules/linkStatsWindow.ts @@ -29,10 +29,6 @@ export function openLinkStatsWindow() { linkStatsWin?.loadFile(path.join(process.env.DIST, "linkStats.html")) } - // Windows doesn't consider maximising to be fullscreening so we must prevent default - linkStatsWin.on("maximize", (e: Event) => { - e.preventDefault() - }) linkStatsWin.on("close", () => { linkStatsWin = null }) @@ -50,16 +46,16 @@ export function destroyLinkStatsWindow() { } export default function registerLinkStatsIPC() { - ipcMain.removeHandler("openLinkStatsWindow") - ipcMain.removeHandler("closeLinkStatsWindow") - ipcMain.removeHandler("update-link-stats") + ipcMain.removeHandler("app:open-link-stats-window") + ipcMain.removeHandler("app:close-link-stats-window") + ipcMain.removeHandler("app:update-link-stats") - ipcMain.handle("openLinkStatsWindow", () => { + ipcMain.handle("app:open-link-stats-window", () => { openLinkStatsWindow() }) - ipcMain.handle("closeLinkStatsWindow", () => closeLinkStatsWindow()) - ipcMain.handle("update-link-stats", (_, linkStats) => { - linkStatsWin?.webContents.send("send-link-stats", linkStats) + ipcMain.handle("app:close-link-stats-window", () => closeLinkStatsWindow()) + ipcMain.handle("app:update-link-stats", (_, linkStats) => { + linkStatsWin?.webContents.send("app:send-link-stats", linkStats) const uptimeFormatted = new Date(Math.round(linkStats.uptime) * 1000) .toISOString() .substring(11, 19) diff --git a/gcs/electron/modules/webcam.ts b/gcs/electron/modules/webcam.ts index 738b90573..aa8b40ef0 100644 --- a/gcs/electron/modules/webcam.ts +++ b/gcs/electron/modules/webcam.ts @@ -71,11 +71,6 @@ export function openWebcamPopout( }) }) - // Windows doesn't consider maximising to be fullscreening so we must prevent default - webcamPopoutWin.on("maximize", (e: Event) => { - e.preventDefault() - }) - // Ensure initial size fits the aspect ratio () webcamPopoutWin.setSize( webcamPopoutWin.getBounds().width, @@ -92,7 +87,7 @@ export function openWebcamPopout( export function closeWebcamPopout(mainWindow: BrowserWindow | null) { console.log("Destroying webcam window") destroyWebcamWindow() - mainWindow?.webContents.send("webcam-closed") + mainWindow?.webContents.send("app:webcam-closed") } export function destroyWebcamWindow() { @@ -101,11 +96,11 @@ export function destroyWebcamWindow() { } export default function registerWebcamIPC(mainWindow: BrowserWindow) { - ipcMain.removeHandler("openWebcamWindow") - ipcMain.removeHandler("closeWebcamWindow") + ipcMain.removeHandler("app:open-webcam-window") + ipcMain.removeHandler("app:close-webcam-window") - ipcMain.handle("openWebcamWindow", (_, videoStreamId, name, aspect) => { + ipcMain.handle("app:open-webcam-window", (_, videoStreamId, name, aspect) => { openWebcamPopout(videoStreamId, name, aspect) }) - ipcMain.handle("closeWebcamWindow", () => closeWebcamPopout(mainWindow)) + ipcMain.handle("app:close-webcam-window", () => closeWebcamPopout(mainWindow)) } diff --git a/gcs/electron/preload.js b/gcs/electron/preload.js index bbb174752..c5a229acf 100644 --- a/gcs/electron/preload.js +++ b/gcs/electron/preload.js @@ -1,50 +1,82 @@ import { contextBridge, ipcRenderer } from "electron" // --------- Expose some API to the Renderer process --------- +// Whitelist of allowed IPC channels for security +const ALLOWED_INVOKE_CHANNELS = [ + "fla:open-file", + "fla:get-recent-logs", + "fla:clear-recent-logs", + "missions:get-save-mission-file-path", + "app:get-node-env", + "app:get-version", + "app:is-mac", + "settings:fetch-settings", + "settings:save-settings", + "app:open-webcam-window", + "app:close-webcam-window", + "app:open-about-window", + "app:close-about-window", + "app:open-link-stats-window", + "app:close-link-stats-window", + "app:update-link-stats", +] + +const ALLOWED_SEND_CHANNELS = [ + "window:close", + "window:minimise", + "window:maximise", + "window:reload", + "window:force-reload", + "window:toggle-developer-tools", + "window:actual-size", + "window:toggle-fullscreen", + "window:zoom-in", + "window:zoom-out", + "window:open-file-in-explorer", +] + +const ALLOWED_ON_CHANNELS = [ + "main-process-message", + "app:webcam-closed", + "app:send-link-stats", + "fla:log-parse-progress", +] + contextBridge.exposeInMainWorld("ipcRenderer", { - ...withPrototype(ipcRenderer), - loadFile: (data) => ipcRenderer.invoke("fla:open-file", data), - getRecentLogs: () => ipcRenderer.invoke("fla:get-recent-logs"), - clearRecentLogs: () => ipcRenderer.invoke("fla:clear-recent-logs"), - getSaveMissionFilePath: (options) => - ipcRenderer.invoke("missions:get-save-mission-file-path", options), - getNodeEnv: () => ipcRenderer.invoke("app:get-node-env"), - getVersion: () => ipcRenderer.invoke("app:get-version"), - getSettings: () => ipcRenderer.invoke("getSettings"), - saveSettings: (settings) => ipcRenderer.invoke("setSettings", settings), - openWebcamWindow: (id, name, aspect) => - ipcRenderer.invoke("openWebcamWindow", id, name, aspect), - closeWebcamWindow: () => ipcRenderer.invoke("closeWebcamWindow"), - onCameraWindowClose: (callback) => - ipcRenderer.on("webcam-closed", () => callback()), - openAboutWindow: () => ipcRenderer.invoke("openAboutWindow"), - closeAboutWindow: () => ipcRenderer.invoke("closeAboutWindow"), - openLinkStatsWindow: () => ipcRenderer.invoke("openLinkStatsWindow"), - closeLinkStatsWindow: () => ipcRenderer.invoke("closeLinkStatsWindow"), - updateLinkStats: (linkStats) => - ipcRenderer.invoke("update-link-stats", linkStats), - onGetLinkStats: (callback) => - ipcRenderer.on("send-link-stats", (_, stats) => callback(stats)), -}) + // Secure invoke method - only allows whitelisted channels + invoke: (channel, ...args) => { + if (ALLOWED_INVOKE_CHANNELS.includes(channel)) { + return ipcRenderer.invoke(channel, ...args) + } + throw new Error(`IPC invoke channel '${channel}' is not allowed`) + }, -// `exposeInMainWorld` can't detect attributes and methods of `prototype`, manually patching it. -function withPrototype(obj) { - const protos = Object.getPrototypeOf(obj) + // Secure send method - only allows whitelisted channels + send: (channel, ...args) => { + if (ALLOWED_SEND_CHANNELS.includes(channel)) { + return ipcRenderer.send(channel, ...args) + } + throw new Error(`IPC send channel '${channel}' is not allowed`) + }, - for (const [key, value] of Object.entries(protos)) { - if (Object.prototype.hasOwnProperty.call(obj, key)) continue + // Secure on method - only allows whitelisted channels + on: (channel, callback) => { + if (ALLOWED_ON_CHANNELS.includes(channel)) { + return ipcRenderer.on(channel, callback) + } + throw new Error(`IPC on channel '${channel}' is not allowed`) + }, - if (typeof value === "function") { - // Some native APIs, like `NodeJS.EventEmitter['on']`, don't work in the Renderer process. Wrapping them into a function. - obj[key] = function (...args) { - return value.call(obj, ...args) - } - } else { - obj[key] = value + // Secure removeAllListeners - only for whitelisted channels + removeAllListeners: (channel) => { + if (ALLOWED_ON_CHANNELS.includes(channel)) { + return ipcRenderer.removeAllListeners(channel) } - } - return obj -} + throw new Error( + `IPC removeAllListeners channel '${channel}' is not allowed`, + ) + }, +}) // --------- Preload scripts loading --------- function domReady(condition = ["complete", "interactive"]) { diff --git a/gcs/package.json b/gcs/package.json index 7c66c9e50..bc3e774bc 100644 --- a/gcs/package.json +++ b/gcs/package.json @@ -87,7 +87,7 @@ "@vitejs/plugin-react": "^4.0.4", "@vitest/browser": "^2.0.5", "autoprefixer": "^10.4.16", - "electron": "^26.1.0", + "electron": "^38.0.0", "electron-builder": "^24.6.4", "eslint": "^8.48.0", "eslint-plugin-react-hooks": "^4.6.0", @@ -96,7 +96,7 @@ "postcss-preset-mantine": "^1.12.0", "postcss-simple-vars": "^7.0.1", "typescript": "^5.2.2", - "vite": "^4.5.3", + "vite": "^7.1.5", "vite-plugin-electron": "^0.14.0", "vite-plugin-electron-renderer": "^0.14.5" }, diff --git a/gcs/src/components/SingleRunWrapper.jsx b/gcs/src/components/SingleRunWrapper.jsx index 236fbe94a..d54e23d5d 100644 --- a/gcs/src/components/SingleRunWrapper.jsx +++ b/gcs/src/components/SingleRunWrapper.jsx @@ -48,10 +48,10 @@ export default function SingleRunWrapper({ children }) { // this is done by getting the latest release from the GitHub API and comparing // it to the current version. - const nodeEnv = await window.ipcRenderer.getNodeEnv() + const nodeEnv = await window.ipcRenderer.invoke("app:get-node-env") if (nodeEnv !== "production") return - const currentVersion = await window.ipcRenderer.getVersion() + const currentVersion = await window.ipcRenderer.invoke("app:get-version") // https://docs.github.com/en/rest/releases/releases#get-the-latest-release const latestGithubRelease = await octokit.request( diff --git a/gcs/src/components/dashboard/tabsSectionTabs/cameraTabsSection.jsx b/gcs/src/components/dashboard/tabsSectionTabs/cameraTabsSection.jsx index 3cd449540..c05634223 100644 --- a/gcs/src/components/dashboard/tabsSectionTabs/cameraTabsSection.jsx +++ b/gcs/src/components/dashboard/tabsSectionTabs/cameraTabsSection.jsx @@ -4,15 +4,15 @@ */ // Native -import { useCallback, useState, useEffect, useRef } from "react" +import { useCallback, useEffect, useRef, useState } from "react" // Mantine +import { Select, Tabs } from "@mantine/core" import { useSessionStorage } from "@mantine/hooks" -import { Tabs, Select } from "@mantine/core" // Helper -import Webcam from "react-webcam" import { IconExternalLink, IconVideoOff } from "@tabler/icons-react" +import Webcam from "react-webcam" export default function CameraTabsSection({ tabPadding }) { // Camera devices @@ -21,7 +21,7 @@ export default function CameraTabsSection({ tabPadding }) { defaultValue: null, }) - window.ipcRenderer.onCameraWindowClose(() => setPictureInPicture(false)) + window.ipcRenderer.on("app:webcam-closed", () => setPictureInPicture(false)) // Ref used to get video capture stream to send to new electron window const videoRef = useRef(null) @@ -47,8 +47,9 @@ export default function CameraTabsSection({ tabPadding }) { streamTrack.getSettings().width / streamTrack.getSettings().height pictureInPicture - ? window.ipcRenderer.closeWebcamWindow() - : window.ipcRenderer.openWebcamWindow( + ? window.ipcRenderer.invoke("app:close-webcam-window") + : window.ipcRenderer.invoke( + "app:open-webcam-window", deviceId, streamTrack.label, streamAspect, diff --git a/gcs/src/components/dashboard/webcam/webcam.jsx b/gcs/src/components/dashboard/webcam/webcam.jsx index 04b8377a0..3f0de511f 100644 --- a/gcs/src/components/dashboard/webcam/webcam.jsx +++ b/gcs/src/components/dashboard/webcam/webcam.jsx @@ -1,8 +1,8 @@ "use client" -import Webcam from "react-webcam" import { IconX } from "@tabler/icons-react" import { useRef } from "react" +import Webcam from "react-webcam" export default function CameraWindow() { const searchParams = new URLSearchParams(window.location.search) @@ -22,7 +22,7 @@ export default function CameraWindow() {