Skip to content

Commit 39c099b

Browse files
NotNiteCynosphere
andauthored
initial stophack implementation (#317)
Co-authored-by: Cynthia Foxwell <gamers@riseup.net>
2 parents 17f14ee + c50a6a3 commit 39c099b

9 files changed

Lines changed: 111 additions & 204 deletions

File tree

packages/browser/manifest.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@
2828
}
2929
]
3030
},
31-
"background": {
32-
"service_worker": "background.js",
33-
"type": "module"
34-
},
3531
"web_accessible_resources": [
3632
{
3733
"resources": ["index.js"],

packages/browser/moonlight-filter.json

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,5 @@
1515
"resourceTypes": ["main_frame"],
1616
"requestDomains": ["discord.com"]
1717
}
18-
},
19-
{
20-
"id": 2,
21-
"priority": 1,
22-
"action": {
23-
"type": "block"
24-
},
25-
"condition": {
26-
"requestDomains": ["discord.com", "discordapp.com"],
27-
"regexFilter": "\/assets\/.+\\..+\\.js$",
28-
"resourceTypes": ["script"]
29-
}
3018
}
3119
]

packages/browser/src/background-mv2.js

Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,5 @@
11
/** biome-ignore-all lint/suspicious/noConsole: background script */
22

3-
chrome.webRequest.onBeforeRequest.addListener(
4-
async (details) => {
5-
if (details.tabId === -1) return;
6-
7-
const origin = new URL(details.originUrl);
8-
if (
9-
(origin.host.endsWith("discord.com") || origin.host.endsWith("discordapp.com")) &&
10-
origin.pathname.match(/^\/developers\//)
11-
)
12-
return;
13-
14-
const url = new URL(details.url);
15-
const hasUrl =
16-
url.pathname.match(/\/assets\/[a-zA-Z\-]+\./) &&
17-
!url.searchParams.has("inj") &&
18-
(url.host.endsWith("discord.com") || url.host.endsWith("discordapp.com"));
19-
20-
const initScripts = ["web."];
21-
const allowScripts = ["popout."];
22-
const testScripts = (scripts) => scripts.some((script) => url.pathname.startsWith(`/assets/${script}`));
23-
const shouldInit = hasUrl && testScripts(initScripts);
24-
const shouldBlock = hasUrl && !testScripts(allowScripts);
25-
26-
if (shouldInit) {
27-
setTimeout(async () => {
28-
console.log("Starting moonlight", details.url);
29-
await chrome.scripting.executeScript({
30-
target: { tabId: details.tabId },
31-
world: "MAIN",
32-
args: [[details.url]],
33-
func: async (blockedScripts) => {
34-
console.log("Initializing moonlight");
35-
try {
36-
await window._moonlightBrowserInit();
37-
} catch (e) {
38-
console.error(e);
39-
}
40-
41-
console.log("Readding scripts");
42-
try {
43-
const scripts = [...document.querySelectorAll("script")].filter(
44-
(script) => script.src && blockedScripts.some((url) => url.includes(script.src))
45-
);
46-
47-
blockedScripts.reverse();
48-
for (const url of blockedScripts) {
49-
if (!url.includes("/web.")) continue;
50-
51-
const script = scripts.find((script) => url.includes(script.src));
52-
const newScript = document.createElement("script");
53-
for (const attr of script.attributes) {
54-
if (attr.name === "src") attr.value += "?inj";
55-
newScript.setAttribute(attr.name, attr.value);
56-
}
57-
script.remove();
58-
document.documentElement.appendChild(newScript);
59-
}
60-
} catch (e) {
61-
console.error(e);
62-
}
63-
}
64-
});
65-
}, 0);
66-
}
67-
68-
if (shouldBlock) return { cancel: true };
69-
},
70-
{
71-
urls: ["https://*.discord.com/assets/*.js", "https://*.discordapp.com/assets/*.js"]
72-
},
73-
["blocking"]
74-
);
75-
763
chrome.webRequest.onHeadersReceived.addListener(
774
(details) => {
785
return {

packages/browser/src/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,27 @@ window._moonlightBrowserInit = async () => {
164164
// This is set by web-preload for us
165165
await window._moonlightWebLoad!();
166166
};
167+
168+
// violentmonkey's document-start injector logic
169+
const getOwnProp = (obj: any, key: any, defVal?: any) => {
170+
try {
171+
if (obj && Object.hasOwn(obj, key)) {
172+
defVal = obj[key];
173+
}
174+
} catch {
175+
// noop
176+
}
177+
return defVal;
178+
};
179+
const elemByTag = (tag: string, i?: number) => getOwnProp(document.getElementsByTagName(tag), i || 0);
180+
const observer = new MutationObserver(() => {
181+
if (elemByTag("*")) {
182+
observer.disconnect();
183+
184+
setTimeout(() => {
185+
window.stop();
186+
window._moonlightBrowserInit!();
187+
}, 0);
188+
}
189+
});
190+
observer.observe(document, { childList: true, subtree: true });

packages/core/src/patch.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,17 @@ function patchModule(id: string, patchId: string, replaced: string, entry: Webpa
121121
}
122122
}
123123

124-
function patchModules(entry: WebpackJsonpEntry[1]) {
124+
const previouslyPatched = new Map<string, IdentifiedPatch>();
125+
126+
function patchModules(entry: WebpackJsonpEntry[1], chunkId?: WebpackJsonpEntry[0]) {
125127
// Populate the module cache
126128
for (const [id, func] of Object.entries(entry)) {
127-
if (!Object.hasOwn(moduleCache, id) && func.__moonlight !== true) {
129+
if (func.__moonlight !== true) {
130+
if (Object.hasOwn(moduleCache, id)) {
131+
logger.debug(`Chunk "${chunkId?.join(", ") ?? "<unknown>"}" replacing module "${id}"`);
132+
if (previouslyPatched.has(id)) moonlight.unpatched.add(previouslyPatched.get(id)!);
133+
}
134+
128135
moduleCache[id] = func.toString().replace(/\n/g, "");
129136
moonlight.moonmap.parseScript(id, moduleCache[id]);
130137
}
@@ -217,7 +224,10 @@ function patchModules(entry: WebpackJsonpEntry[1]) {
217224
exts.add(patch.ext);
218225
}
219226

220-
if (isPatched) moonlight.unpatched.delete(patch);
227+
if (isPatched) {
228+
previouslyPatched.set(id, patch);
229+
moonlight.unpatched.delete(patch);
230+
}
221231
if (shouldRemove) patches.splice(i--, 1);
222232
}
223233
}
@@ -501,7 +511,7 @@ export async function installWebpackPatcher() {
501511
});
502512

503513
try {
504-
patchModules(items[1]);
514+
patchModules(items[1], items[0]);
505515
} catch (err) {
506516
logger.warn("Failed to patch Webpack modules:", err);
507517
}

packages/injector/src/index.ts

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,14 @@ let oldPreloadPath: string | undefined;
2424
let corsAllow: string[] = [];
2525
let blockedUrls: RegExp[] = [];
2626
let injectorConfig: InjectorConfig | undefined;
27-
let initCalled = false;
2827

2928
ipcMain.on(constants.ipcGetOldPreloadPath, (e) => {
30-
initCalled = true;
3129
e.returnValue = oldPreloadPath;
3230
});
3331

3432
ipcMain.on(constants.ipcGetAppData, (e) => {
3533
e.returnValue = app.getPath("appData");
3634
});
37-
ipcMain.on(constants.ipcGetInjectorConfig, (e) => {
38-
e.returnValue = injectorConfig;
39-
});
40-
ipcMain.on(constants.ipcNodePreloadKickoff, (e) => {
41-
e.returnValue = true;
42-
});
4335
ipcMain.handle(constants.ipcMessageBox, (_, opts) => {
4436
electron.dialog.showMessageBoxSync(opts);
4537
});
@@ -142,10 +134,6 @@ class BrowserWindow extends ElectronBrowserWindow {
142134
}
143135
}
144136

145-
this.webContents.on("did-navigate", () => {
146-
initCalled = false;
147-
});
148-
149137
this.webContents.session.webRequest.onHeadersReceived((details, cb) => {
150138
if (details.responseHeaders != null) {
151139
// Patch CSP so things can use externally hosted assets
@@ -171,41 +159,6 @@ class BrowserWindow extends ElectronBrowserWindow {
171159
});
172160

173161
this.webContents.session.webRequest.onBeforeRequest((details, cb) => {
174-
/*
175-
In order to get moonlight loading to be truly async, we prevent Discord
176-
from loading their scripts immediately. We block the requests, keep note
177-
of their URLs, and then send them off to node-preload when we get all of
178-
them. node-preload then loads node side, web side, and then recreates
179-
the script elements to cause them to re-fetch.
180-
181-
The browser extension also does this, but in a background script (see
182-
packages/browser/src/background.js - we should probably get this working
183-
with esbuild someday).
184-
*/
185-
if (details.resourceType === "script" && isMainWindow) {
186-
const url = new URL(details.url);
187-
const hasUrl =
188-
url.pathname.match(/\/assets\/(\d{3,5}|[a-zA-Z\-]+)\./) &&
189-
!url.searchParams.has("inj") &&
190-
(url.host.endsWith("discord.com") || url.host.endsWith("discordapp.com"));
191-
192-
const initScripts = ["web."];
193-
const allowScripts = ["popout."];
194-
const testScripts = (scripts: string[]) =>
195-
scripts.some((script) => url.pathname.startsWith(`/assets/${script}`));
196-
const shouldInit = hasUrl && testScripts(initScripts);
197-
const shouldBlock = hasUrl && !testScripts(allowScripts);
198-
199-
if (shouldInit) {
200-
setTimeout(() => {
201-
logger.debug("Kicking off node-preload", details.url);
202-
this.webContents.send(constants.ipcNodePreloadKickoff, [details.url]);
203-
}, 0);
204-
}
205-
206-
if (shouldBlock && !initCalled) return cb({ cancel: true });
207-
}
208-
209162
// Allow plugins to block some URLs,
210163
// this is needed because multiple webRequest handlers cannot be registered at once
211164
cb({ cancel: blockedUrls.some((u) => u.test(details.url)) });

packages/node-preload/src/index.ts

Lines changed: 38 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -147,69 +147,44 @@ if (isOverlay) {
147147
// something from the original process
148148
require(oldPreloadPath);
149149
} else {
150-
ipcRenderer.on(constants.ipcNodePreloadKickoff, (_, blockedScripts: string[]) => {
151-
(async () => {
152-
try {
153-
await init();
154-
logger.debug("Blocked scripts:", blockedScripts);
155-
156-
const oldPreloadPath: string = ipcRenderer.sendSync(constants.ipcGetOldPreloadPath);
157-
logger.debug("Old preload path:", oldPreloadPath);
158-
if (oldPreloadPath) require(oldPreloadPath);
159-
160-
// Do this to get global.DiscordNative assigned
161-
// @ts-expect-error Lying to discord_desktop_core
162-
process.emit("loaded");
163-
164-
function replayScripts() {
165-
const ignoreScripts = [
166-
// We never blocked this in the first place
167-
"popout.",
168-
// We don't want this to load at all
169-
"sentry."
170-
];
171-
172-
const scripts = [...document.querySelectorAll("script")].filter((script) => {
173-
if (!script.src) return false;
174-
175-
try {
176-
const url = new URL(script.src);
177-
const hasUrl =
178-
url.pathname.match(/\/assets\/(\d{3,5}|[a-zA-Z-]+)\./) &&
179-
!url.searchParams.has("inj") &&
180-
(url.host.endsWith("discord.com") || url.host.endsWith("discordapp.com"));
181-
const shouldIgnore = ignoreScripts.some((other) => url.pathname.startsWith(`/assets/${other}`));
182-
return hasUrl && !shouldIgnore;
183-
} catch {
184-
return false;
185-
}
186-
});
187-
188-
// bruh.
189-
if (moonlightNode.config.patchAll) scripts.sort((a, b) => a.src.localeCompare(b.src));
190-
191-
for (const script of scripts) {
192-
const newScript = document.createElement("script");
193-
for (const attr of script.attributes) {
194-
if (attr.name === "src") attr.value += "?inj";
195-
newScript.setAttribute(attr.name, attr.value);
196-
}
197-
198-
if (script.src.includes("/assets/web.")) ipcRenderer.sendSync(constants.ipcNodePreloadKickoff);
199-
200-
script.remove();
201-
document.documentElement.appendChild(newScript);
202-
}
203-
}
204-
205-
if (document.readyState === "complete") {
206-
replayScripts();
207-
} else {
208-
window.addEventListener("load", replayScripts);
209-
}
210-
} catch (e) {
211-
logger.error("Error restoring original scripts:", e);
150+
const doInit = async () => {
151+
try {
152+
await init();
153+
154+
const oldPreloadPath: string = ipcRenderer.sendSync(constants.ipcGetOldPreloadPath);
155+
logger!.debug("Old preload path:", oldPreloadPath);
156+
if (oldPreloadPath) require(oldPreloadPath);
157+
158+
// Do this to get global.DiscordNative assigned
159+
// @ts-expect-error Lying to discord_desktop_core
160+
process.emit("loaded");
161+
} catch (e) {
162+
// biome-ignore lint/suspicious/noConsole: logger unlikely to be initialized
163+
console.error("[moonlight:node-preload] Error initializing:", e);
164+
}
165+
};
166+
167+
// violentmonkey's document-start injector logic
168+
const getOwnProp = (obj: any, key: any, defVal?: any) => {
169+
try {
170+
if (obj && Object.hasOwn(obj, key)) {
171+
defVal = obj[key];
212172
}
213-
})();
173+
} catch {
174+
// noop
175+
}
176+
return defVal;
177+
};
178+
const elemByTag = (tag: string, i?: number) => getOwnProp(document.getElementsByTagName(tag), i || 0);
179+
const observer = new MutationObserver(() => {
180+
if (elemByTag("*")) {
181+
observer.disconnect();
182+
183+
setTimeout(() => {
184+
window.stop();
185+
doInit();
186+
}, 0);
187+
}
214188
});
189+
observer.observe(document, { childList: true, subtree: true });
215190
}

packages/types/src/constants.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ export const coreExtensionsDir = "core-extensions";
33
export const repoUrlFile = ".moonlight-repo-url";
44
export const installedVersionFile = ".moonlight-installed-version";
55

6-
export const ipcNodePreloadKickoff = "_moonlight_nodePreloadKickoff";
76
export const ipcGetOldPreloadPath = "_moonlight_getOldPreloadPath";
87

98
export const ipcGetAppData = "_moonlight_getAppData";

0 commit comments

Comments
 (0)