From dbdd48d62d4f8865186a566842c55af9bfe3a16c Mon Sep 17 00:00:00 2001 From: NotNite Date: Mon, 14 Oct 2024 16:21:11 -0400 Subject: [PATCH] Introduce moonlightNodeSandboxed --- packages/browser/src/index.ts | 123 +++++++++--------- .../core-extensions/src/moonbase/native.ts | 34 ++--- packages/core/src/config.ts | 6 +- packages/core/src/cors.ts | 17 +++ packages/core/src/extension.ts | 58 +++++---- packages/core/src/util/data.ts | 20 +-- packages/injector/src/index.ts | 7 +- packages/node-preload/src/index.ts | 36 ++++- packages/types/src/globals.ts | 7 + packages/types/src/index.ts | 5 +- 10 files changed, 190 insertions(+), 123 deletions(-) create mode 100644 packages/core/src/cors.ts diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 1d48a61..5275020 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -26,68 +26,73 @@ window._moonlightBrowserInit = async () => { } }); - window.moonlightFS = { - async readFile(path) { - return new Uint8Array(await fs.readFile(path)); - }, - async readFileString(path) { - const file = await this.readFile(path); - return new TextDecoder().decode(file); - }, - async writeFile(path, data) { - await fs.writeFile(path, data); - }, - async writeFileString(path, data) { - const file = new TextEncoder().encode(data); - await this.writeFile(path, file); - }, - async unlink(path) { - await fs.unlink(path); - }, - - async readdir(path) { - return await fs.readdir(path); - }, - async mkdir(path) { - const parts = getParts(path); - for (let i = 0; i < parts.length; i++) { - const path = this.join(...parts.slice(0, i + 1)); - if (!(await this.exists(path))) await fs.mkdir(path); - } - }, - - async rmdir(path) { - const entries = await this.readdir(path); - - for (const entry of entries) { - const fullPath = this.join(path, entry); - const isFile = await this.isFile(fullPath); - if (isFile) { - await this.unlink(fullPath); - } else { - await this.rmdir(fullPath); + window.moonlightNodeSandboxed = { + fs: { + async readFile(path) { + return new Uint8Array(await fs.readFile(path)); + }, + async readFileString(path) { + const file = await this.readFile(path); + return new TextDecoder().decode(file); + }, + async writeFile(path, data) { + await fs.writeFile(path, data); + }, + async writeFileString(path, data) { + const file = new TextEncoder().encode(data); + await this.writeFile(path, file); + }, + async unlink(path) { + await fs.unlink(path); + }, + + async readdir(path) { + return await fs.readdir(path); + }, + async mkdir(path) { + const parts = getParts(path); + for (let i = 0; i < parts.length; i++) { + const path = this.join(...parts.slice(0, i + 1)); + if (!(await this.exists(path))) await fs.mkdir(path); + } + }, + + async rmdir(path) { + const entries = await this.readdir(path); + + for (const entry of entries) { + const fullPath = this.join(path, entry); + const isFile = await this.isFile(fullPath); + if (isFile) { + await this.unlink(fullPath); + } else { + await this.rmdir(fullPath); + } } - } - - await fs.rmdir(path); - }, - - async exists(path) { - return await fs.exists(path); - }, - async isFile(path) { - return (await fs.stat(path)).isFile(); - }, - join(...parts) { - let str = parts.join("/"); - if (!str.startsWith("/")) str = "/" + str; - return str; + await fs.rmdir(path); + }, + + async exists(path) { + return await fs.exists(path); + }, + async isFile(path) { + return (await fs.stat(path)).isFile(); + }, + + join(...parts) { + let str = parts.join("/"); + if (!str.startsWith("/")) str = "/" + str; + return str; + }, + dirname(path) { + const parts = getParts(path); + return "/" + parts.slice(0, parts.length - 1).join("/"); + } }, - dirname(path) { - const parts = getParts(path); - return "/" + parts.slice(0, parts.length - 1).join("/"); - } + // TODO + addCors(url) {}, + addBlocked(url) {} }; // Actual loading begins here diff --git a/packages/core-extensions/src/moonbase/native.ts b/packages/core-extensions/src/moonbase/native.ts index c54a614..0695858 100644 --- a/packages/core-extensions/src/moonbase/native.ts +++ b/packages/core-extensions/src/moonbase/native.ts @@ -104,9 +104,9 @@ export default function getNatives(): MoonbaseNatives { if (!tar || !ref) return; - const dist = moonlightFS.join(moonlightNode.getMoonlightDir(), distDir); - if (await moonlightFS.exists(dist)) await moonlightFS.rmdir(dist); - await moonlightFS.mkdir(dist); + const dist = moonlightNodeSandboxed.fs.join(moonlightNode.getMoonlightDir(), distDir); + if (await moonlightNodeSandboxed.fs.exists(dist)) await moonlightNodeSandboxed.fs.rmdir(dist); + await moonlightNodeSandboxed.fs.mkdir(dist); logger.debug("Extracting update"); const files = await parseTarGzip(tar); @@ -115,15 +115,15 @@ export default function getNatives(): MoonbaseNatives { // @ts-expect-error What do you mean their own types are wrong if (file.type !== "file") continue; - const fullFile = moonlightFS.join(dist, file.name); - const fullDir = moonlightFS.dirname(fullFile); - if (!(await moonlightFS.exists(fullDir))) await moonlightFS.mkdir(fullDir); - await moonlightFS.writeFile(fullFile, file.data); + const fullFile = moonlightNodeSandboxed.fs.join(dist, file.name); + const fullDir = moonlightNodeSandboxed.fs.dirname(fullFile); + if (!(await moonlightNodeSandboxed.fs.exists(fullDir))) await moonlightNodeSandboxed.fs.mkdir(fullDir); + await moonlightNodeSandboxed.fs.writeFile(fullFile, file.data); } logger.debug("Writing version file:", ref); - const versionFile = moonlightFS.join(moonlightNode.getMoonlightDir(), installedVersionFile); - await moonlightFS.writeFileString(versionFile, ref.trim()); + const versionFile = moonlightNodeSandboxed.fs.join(moonlightNode.getMoonlightDir(), installedVersionFile); + await moonlightNodeSandboxed.fs.writeFileString(versionFile, ref.trim()); logger.debug("Update extracted"); }, @@ -159,25 +159,25 @@ export default function getNatives(): MoonbaseNatives { const dir = moonlightNode.getExtensionDir(manifest.id); // remake it in case of updates - if (await moonlightFS.exists(dir)) await moonlightFS.rmdir(dir); - await moonlightFS.mkdir(dir); + if (await moonlightNodeSandboxed.fs.exists(dir)) await moonlightNodeSandboxed.fs.rmdir(dir); + await moonlightNodeSandboxed.fs.mkdir(dir); const buffer = await req.arrayBuffer(); const files = extractAsar(buffer); for (const [file, buf] of Object.entries(files)) { - const fullFile = moonlightFS.join(dir, file); - const fullDir = moonlightFS.dirname(fullFile); + const fullFile = moonlightNodeSandboxed.fs.join(dir, file); + const fullDir = moonlightNodeSandboxed.fs.dirname(fullFile); - if (!(await moonlightFS.exists(fullDir))) await moonlightFS.mkdir(fullDir); - await moonlightFS.writeFile(moonlightFS.join(dir, file), buf); + if (!(await moonlightNodeSandboxed.fs.exists(fullDir))) await moonlightNodeSandboxed.fs.mkdir(fullDir); + await moonlightNodeSandboxed.fs.writeFile(moonlightNodeSandboxed.fs.join(dir, file), buf); } - await moonlightFS.writeFileString(moonlightFS.join(dir, repoUrlFile), repo); + await moonlightNodeSandboxed.fs.writeFileString(moonlightNodeSandboxed.fs.join(dir, repoUrlFile), repo); }, async deleteExtension(id) { const dir = moonlightNode.getExtensionDir(id); - await moonlightFS.rmdir(dir); + await moonlightNodeSandboxed.fs.rmdir(dir); }, getExtensionConfig(id, key) { diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index a878e16..b9ac1ff 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -18,7 +18,7 @@ const defaultConfig: Config = { export async function writeConfig(config: Config) { try { const configPath = await getConfigPath(); - await moonlightFS.writeFileString(configPath, JSON.stringify(config, null, 2)); + await moonlightNodeSandboxed.fs.writeFileString(configPath, JSON.stringify(config, null, 2)); } catch (e) { logger.error("Failed to write config", e); } @@ -30,12 +30,12 @@ export async function readConfig(): Promise { } const configPath = await getConfigPath(); - if (!(await moonlightFS.exists(configPath))) { + if (!(await moonlightNodeSandboxed.fs.exists(configPath))) { await writeConfig(defaultConfig); return defaultConfig; } else { try { - let config: Config = JSON.parse(await moonlightFS.readFileString(configPath)); + let config: Config = JSON.parse(await moonlightNodeSandboxed.fs.readFileString(configPath)); // Assign the default values if they don't exist (newly added) config = { ...defaultConfig, ...config }; await writeConfig(config); diff --git a/packages/core/src/cors.ts b/packages/core/src/cors.ts new file mode 100644 index 0000000..cadcd5b --- /dev/null +++ b/packages/core/src/cors.ts @@ -0,0 +1,17 @@ +const cors: string[] = []; +const blocked: string[] = []; + +export function registerCors(url: string) { + cors.push(url); +} + +export function registerBlocked(url: string) { + blocked.push(url); +} + +export function getDynamicCors() { + return { + cors, + blocked + }; +} diff --git a/packages/core/src/extension.ts b/packages/core/src/extension.ts index 3f3ceda..7e876af 100644 --- a/packages/core/src/extension.ts +++ b/packages/core/src/extension.ts @@ -8,14 +8,14 @@ const logger = new Logger("core/extension"); async function findManifests(dir: string): Promise { const ret = []; - if (await moonlightFS.exists(dir)) { - for (const file of await moonlightFS.readdir(dir)) { - const path = moonlightFS.join(dir, file); + if (await moonlightNodeSandboxed.fs.exists(dir)) { + for (const file of await moonlightNodeSandboxed.fs.readdir(dir)) { + const path = moonlightNodeSandboxed.fs.join(dir, file); if (file === "manifest.json") { ret.push(path); } - if (!(await moonlightFS.isFile(path))) { + if (!(await moonlightNodeSandboxed.fs.isFile(path))) { ret.push(...(await findManifests(path))); } } @@ -34,48 +34,54 @@ async function loadDetectedExtensions( const manifests = await findManifests(dir); for (const manifestPath of manifests) { try { - if (!(await moonlightFS.exists(manifestPath))) continue; - const dir = moonlightFS.dirname(manifestPath); + if (!(await moonlightNodeSandboxed.fs.exists(manifestPath))) continue; + const dir = moonlightNodeSandboxed.fs.dirname(manifestPath); - const manifest: ExtensionManifest = JSON.parse(await moonlightFS.readFileString(manifestPath)); + const manifest: ExtensionManifest = JSON.parse(await moonlightNodeSandboxed.fs.readFileString(manifestPath)); if (seen.has(manifest.id)) { logger.warn(`Duplicate extension found, skipping: ${manifest.id}`); continue; } seen.add(manifest.id); - const webPath = moonlightFS.join(dir, "index.js"); - const nodePath = moonlightFS.join(dir, "node.js"); - const hostPath = moonlightFS.join(dir, "host.js"); + const webPath = moonlightNodeSandboxed.fs.join(dir, "index.js"); + const nodePath = moonlightNodeSandboxed.fs.join(dir, "node.js"); + const hostPath = moonlightNodeSandboxed.fs.join(dir, "host.js"); // if none exist (empty manifest) don't give a shit - if (!moonlightFS.exists(webPath) && !moonlightFS.exists(nodePath) && !moonlightFS.exists(hostPath)) { + if ( + !moonlightNodeSandboxed.fs.exists(webPath) && + !moonlightNodeSandboxed.fs.exists(nodePath) && + !moonlightNodeSandboxed.fs.exists(hostPath) + ) { continue; } - const web = (await moonlightFS.exists(webPath)) ? await moonlightFS.readFileString(webPath) : undefined; + const web = (await moonlightNodeSandboxed.fs.exists(webPath)) + ? await moonlightNodeSandboxed.fs.readFileString(webPath) + : undefined; let url: string | undefined = undefined; - const urlPath = moonlightFS.join(dir, constants.repoUrlFile); - if (type === ExtensionLoadSource.Normal && (await moonlightFS.exists(urlPath))) { - url = await moonlightFS.readFileString(urlPath); + const urlPath = moonlightNodeSandboxed.fs.join(dir, constants.repoUrlFile); + if (type === ExtensionLoadSource.Normal && (await moonlightNodeSandboxed.fs.exists(urlPath))) { + url = await moonlightNodeSandboxed.fs.readFileString(urlPath); } const wpModules: Record = {}; - const wpModulesPath = moonlightFS.join(dir, "webpackModules"); - if (await moonlightFS.exists(wpModulesPath)) { - const wpModulesFile = await moonlightFS.readdir(wpModulesPath); + const wpModulesPath = moonlightNodeSandboxed.fs.join(dir, "webpackModules"); + if (await moonlightNodeSandboxed.fs.exists(wpModulesPath)) { + const wpModulesFile = await moonlightNodeSandboxed.fs.readdir(wpModulesPath); for (const wpModuleFile of wpModulesFile) { if (wpModuleFile.endsWith(".js")) { - wpModules[wpModuleFile.replace(".js", "")] = await moonlightFS.readFileString( - moonlightFS.join(wpModulesPath, wpModuleFile) + wpModules[wpModuleFile.replace(".js", "")] = await moonlightNodeSandboxed.fs.readFileString( + moonlightNodeSandboxed.fs.join(wpModulesPath, wpModuleFile) ); } } } - const stylePath = moonlightFS.join(dir, "style.css"); + const stylePath = moonlightNodeSandboxed.fs.join(dir, "style.css"); ret.push({ id: manifest.id, @@ -88,9 +94,11 @@ async function loadDetectedExtensions( web, webPath: web != null ? webPath : undefined, webpackModules: wpModules, - nodePath: (await moonlightFS.exists(nodePath)) ? nodePath : undefined, - hostPath: (await moonlightFS.exists(hostPath)) ? hostPath : undefined, - style: (await moonlightFS.exists(stylePath)) ? await moonlightFS.readFileString(stylePath) : undefined + nodePath: (await moonlightNodeSandboxed.fs.exists(nodePath)) ? nodePath : undefined, + hostPath: (await moonlightNodeSandboxed.fs.exists(hostPath)) ? hostPath : undefined, + style: (await moonlightNodeSandboxed.fs.exists(stylePath)) + ? await moonlightNodeSandboxed.fs.readFileString(stylePath) + : undefined } }); } catch (e) { @@ -155,7 +163,7 @@ async function getExtensionsBrowser(): Promise { seen.add(manifest.id); } - if (await moonlightFS.exists("/extensions")) { + if (await moonlightNodeSandboxed.fs.exists("/extensions")) { ret.push(...(await loadDetectedExtensions("/extensions", ExtensionLoadSource.Normal, seen))); } diff --git a/packages/core/src/util/data.ts b/packages/core/src/util/data.ts index f88647a..11e07b5 100644 --- a/packages/core/src/util/data.ts +++ b/packages/core/src/util/data.ts @@ -16,8 +16,8 @@ export async function getMoonlightDir() { appData = electron.ipcRenderer.sendSync(constants.ipcGetAppData); } - const dir = moonlightFS.join(appData, "moonlight-mod"); - if (!(await moonlightFS.exists(dir))) await moonlightFS.mkdir(dir); + const dir = moonlightNodeSandboxed.fs.join(appData, "moonlight-mod"); + if (!(await moonlightNodeSandboxed.fs.exists(dir))) await moonlightNodeSandboxed.fs.mkdir(dir); return dir; } @@ -36,12 +36,12 @@ export async function getConfigPath() { let configPath = ""; - const buildInfoPath = moonlightFS.join(process.resourcesPath, "build_info.json"); - if (!(await moonlightFS.exists(buildInfoPath))) { - configPath = moonlightFS.join(dir, "desktop.json"); + const buildInfoPath = moonlightNodeSandboxed.fs.join(process.resourcesPath, "build_info.json"); + if (!(await moonlightNodeSandboxed.fs.exists(buildInfoPath))) { + configPath = moonlightNodeSandboxed.fs.join(dir, "desktop.json"); } else { - const buildInfo: BuildInfo = JSON.parse(await moonlightFS.readFileString(buildInfoPath)); - configPath = moonlightFS.join(dir, buildInfo.releaseChannel + ".json"); + const buildInfo: BuildInfo = JSON.parse(await moonlightNodeSandboxed.fs.readFileString(buildInfoPath)); + configPath = moonlightNodeSandboxed.fs.join(dir, buildInfo.releaseChannel + ".json"); } return configPath; @@ -50,8 +50,8 @@ export async function getConfigPath() { async function getPathFromMoonlight(...names: string[]) { const dir = await getMoonlightDir(); - const target = moonlightFS.join(dir, ...names); - if (!(await moonlightFS.exists(target))) await moonlightFS.mkdir(target); + const target = moonlightNodeSandboxed.fs.join(dir, ...names); + if (!(await moonlightNodeSandboxed.fs.exists(target))) await moonlightNodeSandboxed.fs.mkdir(target); return target; } @@ -61,5 +61,5 @@ export async function getExtensionsPath() { } export function getCoreExtensionsPath(): string { - return moonlightFS.join(__dirname, constants.coreExtensionsDir); + return moonlightNodeSandboxed.fs.join(__dirname, constants.coreExtensionsDir); } diff --git a/packages/injector/src/index.ts b/packages/injector/src/index.ts index 728590c..4c0ab98 100644 --- a/packages/injector/src/index.ts +++ b/packages/injector/src/index.ts @@ -188,7 +188,12 @@ Object.defineProperty(BrowserWindow, "name", { export async function inject(asarPath: string) { isMoonlightDesktop = asarPath === "moonlightDesktop"; - global.moonlightFS = createFS(); + global.moonlightNodeSandboxed = { + fs: createFS(), + // These aren't supposed to be used from host + addCors(url) {}, + addBlocked(url) {} + }; try { const config = await readConfig(); diff --git a/packages/node-preload/src/index.ts b/packages/node-preload/src/index.ts index a3a833d..7e38f71 100644 --- a/packages/node-preload/src/index.ts +++ b/packages/node-preload/src/index.ts @@ -9,9 +9,28 @@ import { getExtensionsPath, getMoonlightDir } from "@moonlight-mod/core/util/dat import Logger, { initLogger } from "@moonlight-mod/core/util/logger"; import { loadExtensions, loadProcessedExtensions } from "@moonlight-mod/core/extension/loader"; import createFS from "@moonlight-mod/core/fs"; +import { registerCors, registerBlocked, getDynamicCors } from "@moonlight-mod/core/cors"; + +let initialized = false; + +function setCors() { + const data = getDynamicCors(); + ipcRenderer.invoke(constants.ipcSetCorsList, data.cors); + ipcRenderer.invoke(constants.ipcSetBlockedList, data.blocked); +} async function injectGlobals() { - global.moonlightFS = createFS(); + global.moonlightNodeSandboxed = { + fs: createFS(), + addCors(url) { + registerCors(url); + if (initialized) setCors(); + }, + addBlocked(url) { + registerBlocked(url); + if (initialized) setCors(); + } + }; const config = await readConfig(); initLogger(config); @@ -62,17 +81,24 @@ async function injectGlobals() { contextBridge.exposeInMainWorld("moonlightNode", moonlightNode); const extCors = moonlightNode.processedExtensions.extensions.flatMap((x) => x.manifest.cors ?? []); + for (const cors of extCors) { + registerCors(cors); + } for (const repo of moonlightNode.config.repositories) { const url = new URL(repo); url.pathname = "/"; - extCors.push(url.toString()); + registerCors(url.toString()); } - ipcRenderer.invoke(constants.ipcSetCorsList, extCors); - const extBlocked = moonlightNode.processedExtensions.extensions.flatMap((e) => e.manifest.blocked ?? []); - ipcRenderer.invoke(constants.ipcSetBlockedList, extBlocked); + for (const blocked of extBlocked) { + registerBlocked(blocked); + } + + setCors(); + + initialized = true; } async function loadPreload() { diff --git a/packages/types/src/globals.ts b/packages/types/src/globals.ts index 13912bd..28f030c 100644 --- a/packages/types/src/globals.ts +++ b/packages/types/src/globals.ts @@ -5,6 +5,7 @@ import type EventEmitter from "events"; import type LunAST from "@moonlight-mod/lunast"; import type Moonmap from "@moonlight-mod/moonmap"; import type { EventPayloads, EventType, MoonlightEventEmitter } from "./core/event"; +import { MoonlightFS } from "./fs"; export type MoonlightHost = { asarPath: string; @@ -41,6 +42,12 @@ export type MoonlightNode = { writeConfig: (config: Config) => Promise; }; +export type MoonlightNodeSandboxed = { + fs: MoonlightFS; + addCors: (url: string) => void; + addBlocked: (url: string) => void; +}; + export type MoonlightWeb = { unpatched: Set; pendingModules: Set; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 1f70dba..0582c4d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -4,8 +4,7 @@ /// /* eslint-disable no-var */ -import { MoonlightFS } from "./fs"; -import { MoonlightEnv, MoonlightHost, MoonlightNode, MoonlightWeb } from "./globals"; +import { MoonlightEnv, MoonlightHost, MoonlightNode, MoonlightNodeSandboxed, MoonlightWeb } from "./globals"; export * from "./discord"; export * from "./config"; @@ -31,8 +30,8 @@ declare global { var moonlightHost: MoonlightHost; var moonlightNode: MoonlightNode; + var moonlightNodeSandboxed: MoonlightNodeSandboxed; var moonlight: MoonlightWeb; - var moonlightFS: MoonlightFS; var _moonlightBrowserInit: () => Promise; var _moonlightBrowserLoad: () => Promise;