use custom protocol instead of file:// for better security
This commit is contained in:
@@ -5,10 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, BrowserWindow } from "electron";
|
import { app, BrowserWindow } from "electron";
|
||||||
import { join } from "path";
|
|
||||||
import { VIEW_DIR } from "shared/paths";
|
|
||||||
|
|
||||||
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
|
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
|
||||||
|
import { loadView } from "./vesktopStatic";
|
||||||
|
|
||||||
export async function createAboutWindow() {
|
export async function createAboutWindow() {
|
||||||
const height = 750;
|
const height = 750;
|
||||||
@@ -27,9 +26,7 @@ export async function createAboutWindow() {
|
|||||||
APP_VERSION: app.getVersion()
|
APP_VERSION: app.getVersion()
|
||||||
});
|
});
|
||||||
|
|
||||||
about.loadFile(join(VIEW_DIR, "about.html"), {
|
loadView(about, "about.html", data);
|
||||||
search: data.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
return about;
|
return about;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ import { BrowserWindow } from "electron/main";
|
|||||||
import { copyFileSync, mkdirSync, readdirSync } from "fs";
|
import { copyFileSync, mkdirSync, readdirSync } from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { SplashProps } from "shared/browserWinProperties";
|
import { SplashProps } from "shared/browserWinProperties";
|
||||||
import { VIEW_DIR } from "shared/paths";
|
|
||||||
|
|
||||||
import { autoStart } from "./autoStart";
|
import { autoStart } from "./autoStart";
|
||||||
import { DATA_DIR } from "./constants";
|
import { DATA_DIR } from "./constants";
|
||||||
import { createWindows } from "./mainWindow";
|
import { createWindows } from "./mainWindow";
|
||||||
import { Settings, State } from "./settings";
|
import { Settings, State } from "./settings";
|
||||||
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
|
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
|
||||||
|
import { loadView } from "./vesktopStatic";
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
discordBranch: "stable" | "canary" | "ptb";
|
discordBranch: "stable" | "canary" | "ptb";
|
||||||
@@ -37,7 +37,7 @@ export function createFirstLaunchTour() {
|
|||||||
|
|
||||||
makeLinksOpenExternally(win);
|
makeLinksOpenExternally(win);
|
||||||
|
|
||||||
win.loadFile(join(VIEW_DIR, "first-launch.html"));
|
loadView(win, "first-launch.html");
|
||||||
win.webContents.addListener("console-message", (_e, _l, msg) => {
|
win.webContents.addListener("console-message", (_e, _l, msg) => {
|
||||||
if (msg === "cancel") return app.exit();
|
if (msg === "cancel") return app.exit();
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import "./updater";
|
import "./updater";
|
||||||
import "./ipc";
|
import "./ipc";
|
||||||
import "./userAssets";
|
import "./userAssets";
|
||||||
|
import "./vesktopProtocol";
|
||||||
|
|
||||||
import { app, BrowserWindow, nativeTheme } from "electron";
|
import { app, BrowserWindow, nativeTheme } from "electron";
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
import { BrowserWindow } from "electron";
|
import { BrowserWindow } from "electron";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { SplashProps } from "shared/browserWinProperties";
|
import { SplashProps } from "shared/browserWinProperties";
|
||||||
import { VIEW_DIR } from "shared/paths";
|
|
||||||
|
|
||||||
import { Settings } from "./settings";
|
import { Settings } from "./settings";
|
||||||
|
import { loadView } from "./vesktopStatic";
|
||||||
|
|
||||||
let splash: BrowserWindow | undefined;
|
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;
|
const { splashBackground, splashColor, splashTheming } = Settings.store;
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import { app, BrowserWindow, ipcMain } from "electron";
|
|||||||
import { autoUpdater, UpdateInfo } from "electron-updater";
|
import { autoUpdater, UpdateInfo } from "electron-updater";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
|
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
|
||||||
import { VIEW_DIR } from "shared/paths";
|
|
||||||
import { Millis } from "shared/utils/millis";
|
import { Millis } from "shared/utils/millis";
|
||||||
|
|
||||||
import { State } from "./settings";
|
import { State } from "./settings";
|
||||||
import { handle } from "./utils/ipcWrappers";
|
import { handle } from "./utils/ipcWrappers";
|
||||||
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
|
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
|
||||||
|
import { loadView } from "./vesktopStatic";
|
||||||
|
|
||||||
let updaterWindow: BrowserWindow | null = null;
|
let updaterWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
@@ -77,5 +77,5 @@ function openUpdater(update: UpdateInfo) {
|
|||||||
updaterWindow = null;
|
updaterWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
updaterWindow.loadFile(join(VIEW_DIR, "updater.html"));
|
loadView(updaterWindow, "updater.html");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* 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 { copyFile, mkdir, rm } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { IpcEvents } from "shared/IpcEvents";
|
import { IpcEvents } from "shared/IpcEvents";
|
||||||
@@ -41,30 +41,21 @@ export async function resolveAssetPath(asset: UserAssetType) {
|
|||||||
return join(STATIC_DIR, DEFAULT_ASSETS[asset]);
|
return join(STATIC_DIR, DEFAULT_ASSETS[asset]);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
export async function handleVesktopAssetsProtocol(path: string, req: Request) {
|
||||||
protocol.handle("vesktop", async req => {
|
const asset = path.replace(/\?v=\d+$/, "").replace(/\/+$/, "");
|
||||||
if (!req.url.startsWith("vesktop://assets/")) {
|
|
||||||
return new Response(null, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const asset = decodeURI(req.url)
|
// @ts-expect-error dumb types
|
||||||
.slice("vesktop://assets/".length)
|
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
|
||||||
.replace(/\?v=\d+$/, "")
|
return new Response(null, { status: 404 });
|
||||||
.replace(/\/+$/, "");
|
}
|
||||||
|
|
||||||
// @ts-expect-error dumb types
|
try {
|
||||||
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
|
const res = await net.fetch(pathToFileURL(join(UserAssetFolder, asset)).href);
|
||||||
return new Response(null, { status: 404 });
|
if (res.ok) return res;
|
||||||
}
|
} catch {}
|
||||||
|
|
||||||
try {
|
return net.fetch(pathToFileURL(join(STATIC_DIR, DEFAULT_ASSETS[asset])).href);
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
handle(IpcEvents.CHOOSE_USER_ASSET, async (_event, asset: UserAssetType, value?: null) => {
|
handle(IpcEvents.CHOOSE_USER_ASSET, async (_event, asset: UserAssetType, value?: null) => {
|
||||||
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
|
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function validateSender(frame: WebFrameMain | null, event: string) {
|
|||||||
throw new Error(`ipc[${event}]: Invalid URL ${frame.url}`);
|
throw new Error(`ipc[${event}]: Invalid URL ${frame.url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (protocol === "file:") return;
|
if (protocol === "file:" || protocol === "vesktop:") return;
|
||||||
|
|
||||||
if (!DISCORD_HOSTNAMES.includes(hostname)) {
|
if (!DISCORD_HOSTNAMES.includes(hostname)) {
|
||||||
throw new Error(`ipc[${event}]: Disallowed hostname ${hostname}`);
|
throw new Error(`ipc[${event}]: Disallowed hostname ${hostname}`);
|
||||||
|
|||||||
16
src/main/utils/isPathInDirectory.ts
Normal file
16
src/main/utils/isPathInDirectory.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
27
src/main/vesktopProtocol.ts
Normal file
27
src/main/vesktopProtocol.ts
Normal file
@@ -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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
33
src/main/vesktopStatic.ts
Normal file
33
src/main/vesktopStatic.ts
Normal file
@@ -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());
|
||||||
|
}
|
||||||
@@ -7,5 +7,4 @@
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
export const STATIC_DIR = /* @__PURE__ */ join(__dirname, "..", "..", "static");
|
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");
|
export const BADGE_DIR = /* @__PURE__ */ join(STATIC_DIR, "badges");
|
||||||
|
|||||||
Reference in New Issue
Block a user