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 "core/src/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
98export async function inject(asarPath: string) {
99 try {
100 const config = readConfig();
101 const extensions = getExtensions();
102
103 // Duplicated in node-preload... oops
104 // eslint-disable-next-line no-inner-declarations
105 function getConfig(ext: string) {
106 const val = config.extensions[ext];
107 if (val == null || typeof val === "boolean") return undefined;
108 return val.config;
109 }
110
111 global.moonlightHost = {
112 asarPath,
113 config,
114 events: new EventEmitter(),
115 extensions,
116 processedExtensions: {
117 extensions: [],
118 dependencyGraph: new Map()
119 },
120
121 getConfig,
122 getConfigOption: <T>(ext: string, name: string) => {
123 const config = getConfig(ext);
124 if (config == null) return undefined;
125 const option = config[name];
126 if (option == null) return undefined;
127 return option as T;
128 },
129 getLogger: (id: string) => {
130 return new Logger(id);
131 }
132 };
133
134 patchElectron();
135
136 global.moonlightHost.processedExtensions = await loadExtensions(extensions);
137 await loadProcessedExtensions(global.moonlightHost.processedExtensions);
138 } catch (e) {
139 logger.error("Failed to inject", e);
140 }
141
142 require(asarPath);
143}
144
145function patchElectron() {
146 const electronClone = {};
147
148 for (const property of Object.getOwnPropertyNames(electron)) {
149 if (property === "BrowserWindow") {
150 Object.defineProperty(electronClone, property, {
151 get: () => BrowserWindow,
152 enumerable: true,
153 configurable: false
154 });
155 } else {
156 Object.defineProperty(
157 electronClone,
158 property,
159 Object.getOwnPropertyDescriptor(electron, property)!
160 );
161 }
162 }
163
164 // exports is a getter only on Windows, let's do some cursed shit instead
165 const electronPath = require.resolve("electron");
166 const cachedElectron = require.cache[electronPath]!;
167 require.cache[electronPath] = new Module(cachedElectron.id, require.main);
168 require.cache[electronPath]!.exports = electronClone;
169}