this repo has no description
at v1.0.6 5.5 kB view raw
1import electron, { 2 BrowserWindowConstructorOptions, 3 BrowserWindow as ElectronBrowserWindow, 4 ipcMain, 5 app 6} from "electron"; 7import Module from "module"; 8import { constants } from "@moonlight-mod/types"; 9import { readConfig } from "@moonlight-mod/core/config"; 10import { getExtensions } from "@moonlight-mod/core/extension"; 11import Logger from "@moonlight-mod/core/util/logger"; 12import { 13 loadExtensions, 14 loadProcessedExtensions 15} from "@moonlight-mod/core/extension/loader"; 16import EventEmitter from "events"; 17 18const logger = new Logger("injector"); 19 20let oldPreloadPath = ""; 21let corsAllow: string[] = []; 22 23ipcMain.on(constants.ipcGetOldPreloadPath, (e) => { 24 e.returnValue = oldPreloadPath; 25}); 26ipcMain.on(constants.ipcGetAppData, (e) => { 27 e.returnValue = app.getPath("appData"); 28}); 29ipcMain.handle(constants.ipcMessageBox, (_, opts) => { 30 electron.dialog.showMessageBoxSync(opts); 31}); 32ipcMain.handle(constants.ipcSetCorsList, (_, list) => { 33 corsAllow = list; 34}); 35 36function patchCsp(headers: Record<string, string[]>) { 37 const directives = [ 38 "style-src", 39 "connect-src", 40 "img-src", 41 "font-src", 42 "media-src", 43 "worker-src", 44 "prefetch-src" 45 ]; 46 const values = ["*", "blob:", "data:", "'unsafe-inline'", "disclip:"]; 47 48 const csp = "content-security-policy"; 49 if (headers[csp] == null) return; 50 51 // This parsing is jank af lol 52 const entries = headers[csp][0] 53 .trim() 54 .split(";") 55 .map((x) => x.trim()) 56 .filter((x) => x.length > 0) 57 .map((x) => x.split(" ")) 58 .map((x) => [x[0], x.slice(1)]); 59 const parts = Object.fromEntries(entries); 60 61 for (const directive of directives) { 62 parts[directive] = values; 63 } 64 65 const stringified = Object.entries<string[]>(parts) 66 .map(([key, value]) => { 67 return `${key} ${value.join(" ")}`; 68 }) 69 .join("; "); 70 headers[csp] = [stringified]; 71} 72 73class BrowserWindow extends ElectronBrowserWindow { 74 constructor(opts: BrowserWindowConstructorOptions) { 75 oldPreloadPath = opts.webPreferences!.preload!; 76 opts.webPreferences!.preload = require.resolve("./node-preload.js"); 77 78 moonlightHost.events.emit("window-options", opts); 79 super(opts); 80 moonlightHost.events.emit("window-created", this); 81 82 this.webContents.session.webRequest.onHeadersReceived((details, cb) => { 83 if (details.responseHeaders != null) { 84 if (details.resourceType === "mainFrame") { 85 patchCsp(details.responseHeaders); 86 } 87 88 if (corsAllow.some((x) => details.url.startsWith(x))) { 89 details.responseHeaders["access-control-allow-origin"] = ["*"]; 90 } 91 92 cb({ cancel: false, responseHeaders: details.responseHeaders }); 93 } 94 }); 95 } 96} 97 98/* 99 Fun fact: esbuild transforms that BrowserWindow class statement into this: 100 101 var variableName = class extends electronImport.BrowserWindow { 102 ... 103 } 104 105 This means that in production builds, variableName is minified, and for some 106 ungodly reason this breaks electron (because it needs to be named BrowserWindow). 107 Without it, random things fail and crash (like opening DevTools). There is no 108 esbuild option to preserve only a single name, so you get the next best thing: 109*/ 110Object.defineProperty(BrowserWindow, "name", { 111 value: "BrowserWindow", 112 writable: false 113}); 114// "aight i'm writing exclusively C# from now on and never touching JavaScript again" 115 116export async function inject(asarPath: string) { 117 try { 118 const config = readConfig(); 119 const extensions = getExtensions(); 120 121 // Duplicated in node-preload... oops 122 // eslint-disable-next-line no-inner-declarations 123 function getConfig(ext: string) { 124 const val = config.extensions[ext]; 125 if (val == null || typeof val === "boolean") return undefined; 126 return val.config; 127 } 128 129 global.moonlightHost = { 130 asarPath, 131 config, 132 events: new EventEmitter(), 133 extensions, 134 processedExtensions: { 135 extensions: [], 136 dependencyGraph: new Map() 137 }, 138 139 getConfig, 140 getConfigOption: <T>(ext: string, name: string) => { 141 const config = getConfig(ext); 142 if (config == null) return undefined; 143 const option = config[name]; 144 if (option == null) return undefined; 145 return option as T; 146 }, 147 getLogger: (id: string) => { 148 return new Logger(id); 149 } 150 }; 151 152 patchElectron(); 153 154 global.moonlightHost.processedExtensions = await loadExtensions(extensions); 155 await loadProcessedExtensions(global.moonlightHost.processedExtensions); 156 } catch (e) { 157 logger.error("Failed to inject", e); 158 } 159 160 // Need to do this instead of require() or it breaks require.main 161 // @ts-expect-error why are you not documented 162 Module._load(asarPath, Module, true); 163} 164 165function patchElectron() { 166 const electronClone = {}; 167 168 for (const property of Object.getOwnPropertyNames(electron)) { 169 if (property === "BrowserWindow") { 170 Object.defineProperty(electronClone, property, { 171 get: () => BrowserWindow, 172 enumerable: true, 173 configurable: false 174 }); 175 } else { 176 Object.defineProperty( 177 electronClone, 178 property, 179 Object.getOwnPropertyDescriptor(electron, property)! 180 ); 181 } 182 } 183 184 // exports is a getter only on Windows, let's do some cursed shit instead 185 const electronPath = require.resolve("electron"); 186 const cachedElectron = require.cache[electronPath]!; 187 require.cache[electronPath] = new Module(cachedElectron.id, require.main); 188 require.cache[electronPath]!.exports = electronClone; 189}