this repo has no description
at v1.0.5 5.4 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 require(asarPath); 161} 162 163function patchElectron() { 164 const electronClone = {}; 165 166 for (const property of Object.getOwnPropertyNames(electron)) { 167 if (property === "BrowserWindow") { 168 Object.defineProperty(electronClone, property, { 169 get: () => BrowserWindow, 170 enumerable: true, 171 configurable: false 172 }); 173 } else { 174 Object.defineProperty( 175 electronClone, 176 property, 177 Object.getOwnPropertyDescriptor(electron, property)! 178 ); 179 } 180 } 181 182 // exports is a getter only on Windows, let's do some cursed shit instead 183 const electronPath = require.resolve("electron"); 184 const cachedElectron = require.cache[electronPath]!; 185 require.cache[electronPath] = new Module(cachedElectron.id, require.main); 186 require.cache[electronPath]!.exports = electronClone; 187}