diff --git a/src/main/about.ts b/src/main/about.ts index 3e3e5f0..e0f7e2d 100644 --- a/src/main/about.ts +++ b/src/main/about.ts @@ -5,10 +5,9 @@ */ import { app, BrowserWindow } from "electron"; -import { join } from "path"; -import { VIEW_DIR } from "shared/paths"; import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; +import { loadView } from "./vesktopStatic"; export async function createAboutWindow() { const height = 750; @@ -27,9 +26,7 @@ export async function createAboutWindow() { APP_VERSION: app.getVersion() }); - about.loadFile(join(VIEW_DIR, "about.html"), { - search: data.toString() - }); + loadView(about, "about.html", data); return about; } diff --git a/src/main/firstLaunch.ts b/src/main/firstLaunch.ts index 4ba335a..d926f81 100644 --- a/src/main/firstLaunch.ts +++ b/src/main/firstLaunch.ts @@ -9,13 +9,13 @@ import { BrowserWindow } from "electron/main"; import { copyFileSync, mkdirSync, readdirSync } from "fs"; import { join } from "path"; import { SplashProps } from "shared/browserWinProperties"; -import { VIEW_DIR } from "shared/paths"; import { autoStart } from "./autoStart"; import { DATA_DIR } from "./constants"; import { createWindows } from "./mainWindow"; import { Settings, State } from "./settings"; import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; +import { loadView } from "./vesktopStatic"; interface Data { discordBranch: "stable" | "canary" | "ptb"; @@ -37,7 +37,7 @@ export function createFirstLaunchTour() { makeLinksOpenExternally(win); - win.loadFile(join(VIEW_DIR, "first-launch.html")); + loadView(win, "first-launch.html"); win.webContents.addListener("console-message", (_e, _l, msg) => { if (msg === "cancel") return app.exit(); diff --git a/src/main/index.ts b/src/main/index.ts index dbc8fdd..48c0e9b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -7,6 +7,7 @@ import "./updater"; import "./ipc"; import "./userAssets"; +import "./vesktopProtocol"; import { app, BrowserWindow, nativeTheme } from "electron"; diff --git a/src/main/splash.ts b/src/main/splash.ts index 1b9b972..6ea8dea 100644 --- a/src/main/splash.ts +++ b/src/main/splash.ts @@ -7,9 +7,9 @@ import { BrowserWindow } from "electron"; import { join } from "path"; import { SplashProps } from "shared/browserWinProperties"; -import { VIEW_DIR } from "shared/paths"; import { Settings } from "./settings"; +import { loadView } from "./vesktopStatic"; let splash: BrowserWindow | undefined; @@ -22,7 +22,7 @@ export function createSplashWindow(startMinimized = false) { } }); - splash.loadFile(join(VIEW_DIR, "splash.html")); + loadView(splash, "splash.html"); const { splashBackground, splashColor, splashTheming } = Settings.store; diff --git a/src/main/updater.ts b/src/main/updater.ts index e8bec2b..ae8bd65 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -8,12 +8,12 @@ import { app, BrowserWindow, ipcMain } from "electron"; import { autoUpdater, UpdateInfo } from "electron-updater"; import { join } from "path"; import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents"; -import { VIEW_DIR } from "shared/paths"; import { Millis } from "shared/utils/millis"; import { State } from "./settings"; import { handle } from "./utils/ipcWrappers"; import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; +import { loadView } from "./vesktopStatic"; let updaterWindow: BrowserWindow | null = null; @@ -77,5 +77,5 @@ function openUpdater(update: UpdateInfo) { updaterWindow = null; }); - updaterWindow.loadFile(join(VIEW_DIR, "updater.html")); + loadView(updaterWindow, "updater.html"); } diff --git a/src/main/userAssets.ts b/src/main/userAssets.ts index 51219ce..6a39338 100644 --- a/src/main/userAssets.ts +++ b/src/main/userAssets.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { app, dialog, net, protocol } from "electron"; +import { app, dialog, net } from "electron"; import { copyFile, mkdir, rm } from "fs/promises"; import { join } from "path"; import { IpcEvents } from "shared/IpcEvents"; @@ -41,30 +41,21 @@ export async function resolveAssetPath(asset: UserAssetType) { return join(STATIC_DIR, DEFAULT_ASSETS[asset]); } -app.whenReady().then(() => { - protocol.handle("vesktop", async req => { - if (!req.url.startsWith("vesktop://assets/")) { - return new Response(null, { status: 404 }); - } +export async function handleVesktopAssetsProtocol(path: string, req: Request) { + const asset = path.replace(/\?v=\d+$/, "").replace(/\/+$/, ""); - const asset = decodeURI(req.url) - .slice("vesktop://assets/".length) - .replace(/\?v=\d+$/, "") - .replace(/\/+$/, ""); + // @ts-expect-error dumb types + if (!CUSTOMIZABLE_ASSETS.includes(asset)) { + return new Response(null, { status: 404 }); + } - // @ts-expect-error dumb types - if (!CUSTOMIZABLE_ASSETS.includes(asset)) { - return new Response(null, { status: 404 }); - } + try { + const res = await net.fetch(pathToFileURL(join(UserAssetFolder, asset)).href); + if (res.ok) return res; + } catch {} - try { - const res = await net.fetch(pathToFileURL(join(UserAssetFolder, asset)).href); - if (res.ok) return res; - } catch {} - - return net.fetch(pathToFileURL(join(STATIC_DIR, DEFAULT_ASSETS[asset])).href); - }); -}); + return net.fetch(pathToFileURL(join(STATIC_DIR, DEFAULT_ASSETS[asset])).href); +} handle(IpcEvents.CHOOSE_USER_ASSET, async (_event, asset: UserAssetType, value?: null) => { if (!CUSTOMIZABLE_ASSETS.includes(asset)) { diff --git a/src/main/utils/ipcWrappers.ts b/src/main/utils/ipcWrappers.ts index a8d0f34..bd59538 100644 --- a/src/main/utils/ipcWrappers.ts +++ b/src/main/utils/ipcWrappers.ts @@ -18,7 +18,7 @@ export function validateSender(frame: WebFrameMain | null, event: string) { throw new Error(`ipc[${event}]: Invalid URL ${frame.url}`); } - if (protocol === "file:") return; + if (protocol === "file:" || protocol === "vesktop:") return; if (!DISCORD_HOSTNAMES.includes(hostname)) { throw new Error(`ipc[${event}]: Disallowed hostname ${hostname}`); diff --git a/src/main/utils/isPathInDirectory.ts b/src/main/utils/isPathInDirectory.ts new file mode 100644 index 0000000..b153b57 --- /dev/null +++ b/src/main/utils/isPathInDirectory.ts @@ -0,0 +1,16 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2025 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { resolve, sep } from "path"; + +export function isPathInDirectory(filePath: string, directory: string) { + const resolvedPath = resolve(filePath); + const resolvedDirectory = resolve(directory); + + const normalizedDirectory = resolvedDirectory.endsWith(sep) ? resolvedDirectory : resolvedDirectory + sep; + + return resolvedPath.startsWith(normalizedDirectory) || resolvedPath === resolvedDirectory; +} diff --git a/src/main/vesktopProtocol.ts b/src/main/vesktopProtocol.ts new file mode 100644 index 0000000..553692c --- /dev/null +++ b/src/main/vesktopProtocol.ts @@ -0,0 +1,27 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2025 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { app, protocol } from "electron"; + +import { handleVesktopAssetsProtocol } from "./userAssets"; +import { handleVesktopStaticProtocol } from "./vesktopStatic"; + +app.whenReady().then(() => { + protocol.handle("vesktop", async req => { + const url = decodeURI(req.url).slice("vesktop://".length); + const [channel, ...pathParts] = url.split("/"); + const path = pathParts.join("/"); + + if (channel === "assets") { + return handleVesktopAssetsProtocol(path, req); + } + if (channel === "static") { + return handleVesktopStaticProtocol(path, req); + } + + return new Response(null, { status: 404 }); + }); +}); diff --git a/src/main/vesktopStatic.ts b/src/main/vesktopStatic.ts new file mode 100644 index 0000000..c2658f3 --- /dev/null +++ b/src/main/vesktopStatic.ts @@ -0,0 +1,33 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2025 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { BrowserWindow, net } from "electron"; +import { join } from "path"; +import { pathToFileURL } from "url"; + +import { isPathInDirectory } from "./utils/isPathInDirectory"; + +const STATIC_DIR = join(__dirname, "..", "..", "static"); + +export async function handleVesktopStaticProtocol(path: string, req: Request) { + const staticPath = new URL(path, "vesktop://").pathname; + + const fullPath = join(STATIC_DIR, staticPath); + if (!isPathInDirectory(fullPath, STATIC_DIR)) { + return new Response(null, { status: 404 }); + } + + return net.fetch(pathToFileURL(fullPath).href); +} + +export function loadView(browserWindow: BrowserWindow, view: string, params?: URLSearchParams) { + const url = new URL(`vesktop://static/views/${view}`); + if (params) { + url.search = params.toString(); + } + + return browserWindow.loadURL(url.toString()); +} diff --git a/src/shared/paths.ts b/src/shared/paths.ts index 30e0132..db5033a 100644 --- a/src/shared/paths.ts +++ b/src/shared/paths.ts @@ -7,5 +7,4 @@ import { join } from "path"; export const STATIC_DIR = /* @__PURE__ */ join(__dirname, "..", "..", "static"); -export const VIEW_DIR = /* @__PURE__ */ join(STATIC_DIR, "views"); export const BADGE_DIR = /* @__PURE__ */ join(STATIC_DIR, "badges");