MeshChatX/electron/shellOrigin.js

201 lines
5.3 KiB
JavaScript

"use strict";
const LOCAL_BACKEND_HOSTS = new Set(["127.0.0.1", "localhost"]);
const LOCAL_BACKEND_PORT = "9337";
/**
* Parse a URL string. Returns null when the input is not a valid absolute URL.
* @param {unknown} url
* @returns {URL | null}
*/
function parseAbsoluteUrl(url) {
if (!url || typeof url !== "string") {
return null;
}
try {
return new URL(url);
} catch {
return null;
}
}
/**
* Inner http(s) URL of a blob URL, or null.
* blob:https://host/uuid has an origin of https://host, not the blob scheme itself.
* @param {unknown} url
* @returns {string | null}
*/
function blobInnerHttpUrl(url) {
if (!url || typeof url !== "string" || !url.startsWith("blob:")) {
return null;
}
const inner = url.slice("blob:".length);
const parsed = parseAbsoluteUrl(inner);
if (!parsed) {
return null;
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return null;
}
return inner;
}
/**
* Whether the URL is the MeshChatX local backend origin (loading / API checks).
* Parses so userinfo like http://127.0.0.1:9337@example.com is not local.
* @param {unknown} url
* @returns {boolean}
*/
function isLocalBackendUrl(url) {
const parsed = parseAbsoluteUrl(url);
if (!parsed) {
return false;
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return false;
}
if (parsed.username !== "" || parsed.password !== "") {
return false;
}
const host = String(parsed.hostname || "").toLowerCase();
if (!LOCAL_BACKEND_HOSTS.has(host)) {
return false;
}
return parsed.port === LOCAL_BACKEND_PORT;
}
/**
* blob: URLs whose inner origin is the local backend (print preview).
* @param {unknown} url
* @returns {boolean}
*/
function isTrustedBlobUrl(url) {
const inner = blobInnerHttpUrl(url);
return inner != null && isLocalBackendUrl(inner);
}
/**
* file: loading.html and crash.html in the Electron shell. Not arbitrary files.
* @param {unknown} url
* @returns {boolean}
*/
function isTrustedShellFileUrl(url) {
const parsed = parseAbsoluteUrl(url);
if (!parsed || parsed.protocol !== "file:") {
return false;
}
let pathname = parsed.pathname || "";
try {
pathname = decodeURIComponent(pathname);
} catch {
return false;
}
const normalized = pathname.replace(/\\/g, "/").toLowerCase();
return normalized.endsWith("/loading.html") || normalized.endsWith("/crash.html");
}
/**
* Origins allowed to call preload window.electron IPC.
* file: loading/crash pages, the local backend, and trusted print blobs.
* @param {unknown} url
* @returns {boolean}
*/
function isTrustedShellOrigin(url) {
if (isTrustedShellFileUrl(url)) {
return true;
}
if (isTrustedBlobUrl(url)) {
return true;
}
return isLocalBackendUrl(url);
}
/**
* Whether window.open should create a child Electron window instead of the OS browser.
* Local backend popouts and call.html must stay in Electron so they keep the app session.
* @param {unknown} url
* @returns {boolean}
*/
function shouldOpenInElectronWindow(url) {
if (!url || typeof url !== "string") {
return false;
}
if (url.startsWith("blob:")) {
return isTrustedBlobUrl(url);
}
if (!isLocalBackendUrl(url)) {
return false;
}
const parsed = parseAbsoluteUrl(url);
if (!parsed) {
return false;
}
const pathname = parsed.pathname || "";
if (pathname === "/call.html" || pathname.endsWith("/call.html")) {
return true;
}
return parsed.hash.startsWith("#/popout/");
}
/**
* Whether the main frame may navigate to this URL inside Electron (local app shell).
* External http(s) links must open in the system browser instead.
* data: and file: are denied. blob: is allowed only when the inner origin is local.
* @param {unknown} url
* @returns {boolean}
*/
function shouldAllowInWindowNavigation(url) {
if (!url || typeof url !== "string") {
return false;
}
if (url.startsWith("blob:")) {
return isTrustedBlobUrl(url);
}
return isLocalBackendUrl(url);
}
/**
* URL of the renderer frame that invoked an ipcMain handler.
* Prefers senderFrame.url, then sender.getURL().
* @param {unknown} event
* @returns {string}
*/
function senderUrlFromIpcEvent(event) {
if (!event || typeof event !== "object") {
return "";
}
const frame = event.senderFrame;
if (frame && typeof frame.url === "string" && frame.url) {
return frame.url;
}
const sender = event.sender;
if (sender && typeof sender.getURL === "function") {
try {
const url = sender.getURL();
return typeof url === "string" ? url : "";
} catch {
return "";
}
}
return "";
}
/**
* Whether ipcMain may run for this invoke. Same allowlist as preload.
* @param {unknown} event
* @returns {boolean}
*/
function isTrustedIpcEvent(event) {
return isTrustedShellOrigin(senderUrlFromIpcEvent(event));
}
module.exports = {
isLocalBackendUrl,
isTrustedBlobUrl,
isTrustedShellFileUrl,
isTrustedShellOrigin,
isTrustedIpcEvent,
senderUrlFromIpcEvent,
shouldOpenInElectronWindow,
shouldAllowInWindowNavigation,
};