this repo has no description
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}