diff --git a/injector/index.js b/injector/index.js index 40103e1..78f004f 100644 --- a/injector/index.js +++ b/injector/index.js @@ -49,7 +49,10 @@ const getNeptuneBundle = () => ? fetchPromise : Promise.resolve( fs.readFileSync(path.join(localBundle, "neptune.js"), "utf8") + - `\n//# sourceMappingURL=file:////${path.join(localBundle, "neptune.js.map")}`, + `\n//# sourceMappingURL=file:////${path.join( + localBundle, + "neptune.js.map" + )}` ); // #endregion @@ -64,7 +67,7 @@ electron.ipcMain.handle("NEPTUNE_BUNDLE_FETCH", getNeptuneBundle); // #region Redux Devtools electron.app.whenReady().then(() => { electron.session.defaultSession.loadExtension( - path.join(process.resourcesPath, "app", "redux-devtools"), + path.join(process.resourcesPath, "app", "redux-devtools") ); }); // #endregion @@ -81,13 +84,13 @@ async function attachDebugger(dbg, domain = "desktop.tidal.com") { { requestId: params.requestId, }, - sessionId, + sessionId ); let body = res.base64Encoded ? atob(res.body) : res.body; body = body.replace( //, - "", + "" ); // Add header to identify patched request in cache @@ -104,7 +107,7 @@ async function attachDebugger(dbg, domain = "desktop.tidal.com") { responseHeaders: params.responseHeaders, body: btoa(body), }, - sessionId, + sessionId ); } else if (method === "Target.attachedToTarget") { const { sessionId } = params; @@ -123,7 +126,7 @@ async function attachDebugger(dbg, domain = "desktop.tidal.com") { }, ], }, - sessionId, + sessionId ); } }); @@ -152,10 +155,13 @@ async function attachDebugger(dbg, domain = "desktop.tidal.com") { if (caches.length !== 1) return; const { cacheId } = caches[0]; - const { cacheDataEntries, returnCount } = await dbg.sendCommand("CacheStorage.requestEntries", { - cacheId, - pathFilter: "/index.html", - }); + const { cacheDataEntries, returnCount } = await dbg.sendCommand( + "CacheStorage.requestEntries", + { + cacheId, + pathFilter: "/index.html", + } + ); if (returnCount !== 1) return; const entry = cacheDataEntries[0]; @@ -169,6 +175,63 @@ async function attachDebugger(dbg, domain = "desktop.tidal.com") { } // #endregion +// #region IPC Bullshit +let evalHandleCount = 0; +let evalHandles = {}; +electron.ipcMain.on("NEPTUNE_CREATE_EVAL_SCOPE", (ev, code) => { + const scopeEval = eval(`(function () { + try { + ${code} + + return (code) => eval(code) + } catch {} + + return eval; + })()`); + + const id = evalHandleCount++; + evalHandles[id] = scopeEval; + + ev.returnValue = id; +}); + +electron.ipcMain.on("NEPTUNE_RUN_IN_EVAL_SCOPE", (ev, scopeId, code) => { + try { + const retVal = evalHandles[scopeId](code); + + if (retVal?.then && retVal?.catch) { + const promiseId = "NEPTUNE_PROMISE_" + Math.random().toString().slice(2); + ev.returnValue = { type: "promise", value: promiseId }; + + try { + const getAllWindows = () => electron.BrowserWindow.getAllWindows(); + + retVal.then((v) => + getAllWindows().forEach((w) => + w.webContents.send(promiseId, { type: "resolve", value: v }) + ) + ); + + retVal.catch((v) => + getAllWindows().forEach((w) => + w.webContents.send(promiseId, { type: "reject", value: v }) + ) + ); + } catch {} + } + + ev.returnValue = { type: "success", value: retVal }; + } catch (err) { + ev.returnValue = { type: "error", value: err }; + } +}); + +electron.ipcMain.on("NEPTUNE_DELETE_EVAL_SCOPE", (ev, arg) => { + delete evalHandles[arg]; + ev.returnValue = true; +}); +// #endregion + // #region BrowserWindow const ProxiedBrowserWindow = new Proxy(electron.BrowserWindow, { construct(target, args) { @@ -176,7 +239,8 @@ const ProxiedBrowserWindow = new Proxy(electron.BrowserWindow, { let originalPreload; // tidal-hifi does not set the title, rely on dev tools instead. - const isTidalWindow = options.title == "TIDAL" || options.webPreferences?.devTools; + const isTidalWindow = + options.title == "TIDAL" || options.webPreferences?.devTools; if (isTidalWindow) { originalPreload = options.webPreferences?.preload; @@ -185,8 +249,9 @@ const ProxiedBrowserWindow = new Proxy(electron.BrowserWindow, { options.webPreferences.preload = path.join(__dirname, "preload.js"); // Shhh. I can feel your judgement from here. It's okay. Let it out. Everything will be alright in the end. - options.webPreferences.contextIsolation = false; - options.webPreferences.nodeIntegration = true; + // options.webPreferences.contextIsolation = false; + // options.webPreferences.nodeIntegration = true; + options.webPreferences.sandbox = false; // Allows local plugin loading options.webPreferences.allowDisplayingInsecureContent = true; @@ -198,9 +263,16 @@ const ProxiedBrowserWindow = new Proxy(electron.BrowserWindow, { window.webContents.originalPreload = originalPreload; + window.webContents.on("did-navigate", () => { + // Clean up eval handles + evalHandles = {} + }) + attachDebugger( window.webContents.debugger, - options.webPreferences?.devTools ? "listen.tidal.com" : "desktop.tidal.com", // tidal-hifi uses listen.tidal.com + options.webPreferences?.devTools + ? "listen.tidal.com" + : "desktop.tidal.com" // tidal-hifi uses listen.tidal.com ); return window; }, @@ -226,12 +298,16 @@ electron.Menu.buildFromTemplate = (template) => { }; // #endregion -logger.log("Starting original..."); // #region Start original +logger.log("Starting original..."); + let originalPath = path.join(process.resourcesPath, "app.asar"); -if (!fs.existsSync(originalPath)) originalPath = path.join(process.resourcesPath, "original.asar"); +if (!fs.existsSync(originalPath)) + originalPath = path.join(process.resourcesPath, "original.asar"); -const originalPackage = require(path.resolve(path.join(originalPath, "package.json"))); +const originalPackage = require(path.resolve( + path.join(originalPath, "package.json") +)); const startPath = path.join(originalPath, originalPackage.main); require.main.filename = startPath; diff --git a/injector/preload.js b/injector/preload.js index 4f1a95e..c12b3db 100644 --- a/injector/preload.js +++ b/injector/preload.js @@ -1,21 +1,84 @@ const electron = require("electron"); -const electronPath = require.resolve("electron"); -delete require.cache[electronPath].exports; - -// God, have mercy on my soul. I'm so sorry. -require.cache[electronPath].exports = { - ...electron, - contextBridge: { - exposeInMainWorld(name, properties) { - window[name] = properties; - }, - }, -}; - electron.ipcRenderer.invoke("NEPTUNE_BUNDLE_FETCH").then((bundle) => { electron.webFrame.executeJavaScript(bundle); }); -const originalPreload = electron.ipcRenderer.sendSync("NEPTUNE_ORIGINAL_PRELOAD"); -if (originalPreload) require(originalPreload); \ No newline at end of file +function createEvalScope(code) { + return electron.ipcRenderer.sendSync("NEPTUNE_CREATE_EVAL_SCOPE", code); +} + +function getNativeValue(id, name) { + if ( + electron.ipcRenderer.sendSync( + "NEPTUNE_RUN_IN_EVAL_SCOPE", + id, + `typeof neptuneExports.${name}` + ).value == "function" + ) + return (...args) => { + funcReturn = electron.ipcRenderer.sendSync( + "NEPTUNE_RUN_IN_EVAL_SCOPE", + id, + `neptuneExports.${name}(${args + .map((arg) => + typeof arg != "function" ? JSON.stringify(arg) : arg.toString() + ) + .join(",")})` + ); + + if (funcReturn.type == "promise") { + return new Promise((res, rej) => { + electron.ipcRenderer.once(funcReturn.value, (ev, { type, value }) => { + type == "resolve" ? res(value) : rej(value); + }); + }); + } + + if (funcReturn.type == "error") { + throw new Error(funcReturn.value); + } + + return funcReturn.value; + }; + + return electron.ipcRenderer.sendSync( + "NEPTUNE_RUN_IN_EVAL_SCOPE", + id, + `neptuneExports.${name}` + ); +} + +function deleteEvalScope(id) { + return electron.ipcRenderer.sendSync("NEPTUNE_DELETE_EVAL_SCOPE", id); +} + +electron.contextBridge.exposeInMainWorld("NeptuneNative", { + createEvalScope, + getNativeValue, + deleteEvalScope, +}); + +electron.contextBridge.exposeInMainWorld("electron", { + ipcRenderer: Object.fromEntries( + [ + "on", + "off", + "once", + "addListener", + "removeListener", + "removeAllListeners", + "send", + "invoke", + "sendSync", + "postMessage", + "sendToHost", + ].map((n) => [n, electron.ipcRenderer[n]]) + ), +}); + +const originalPreload = electron.ipcRenderer.sendSync( + "NEPTUNE_ORIGINAL_PRELOAD" +); + +if (originalPreload) require(originalPreload); diff --git a/package.json b/package.json index c0fca36..5b140a8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "rollup --config rollup.config.js", "format": "prettier -wc ./src", "watch": "rollup --config rollup.config.js -w", - "run": "npm run build && set NEPTUNE_DIST_PATH = \"./dist\" && %localappdata%/TIDAL/TIDAL.exe" + "run": "npm run build && set NEPTUNE_DIST_PATH=%cd%\\dist&& %LOCALAPPDATA%\\TIDAL\\TIDAL.exe" }, "keywords": [], "author": "", diff --git a/src/api/plugins.js b/src/api/plugins.js index 61eba15..8c6d3c7 100644 --- a/src/api/plugins.js +++ b/src/api/plugins.js @@ -14,7 +14,6 @@ export const getPluginById = (id) => pluginStore.find((p) => p.id == id); export async function disablePlugin(id) { getPluginById(id).enabled = false; const onUnload = enabled?.[id]?.onUnload; - const unloadables = enabled?.[id]?.unloadables; delete enabled[id]; @@ -22,8 +21,6 @@ export async function disablePlugin(id) { await onUnload?.(); } catch (e) { console.error("Failed to completely clean up neptune plugin!\n", e); - } finally { - await unloadables.forEach(u => u()) } } @@ -52,8 +49,8 @@ async function runPlugin(plugin) { manifest: plugin.manifest, storage: persistentStorage, addUnloadable(callback) { - unloadables.push(callback) - } + unloadables.push(callback); + }, }; const { onUnload, Settings } = await quartz(plugin.code, { @@ -86,7 +83,14 @@ async function runPlugin(plugin) { ], }); - enabled[plugin.id] = { onUnload: onUnload ?? (() => {}), unloadables }; + enabled[plugin.id] = { + onUnload: () => { + onUnload?.(); + for (const ul of unloadables) { + ul(); + } + }, + }; if (Settings) enabled[plugin.id].Settings = Settings; } catch (e) { await disablePlugin(plugin.id);