From e321534fbea9f09b139d440584f6b84ad0afb80f Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sat, 25 Dec 2021 00:01:18 +0100 Subject: refactor: Simplify script injection Inject CSS and main world scripts synchronously to avoid race conditions with page loading. Don't try to miming userAgentData for now, since it won't bypass google's checks. However, simply omitting chrome from the user agent does bypass them, at least for now. --- packages/main/src/index.ts | 84 ++++++----------- packages/service-inject/src/index.ts | 8 +- packages/service-inject/src/shims/userAgentData.ts | 103 -------------------- packages/service-inject/src/utils.ts | 104 --------------------- packages/service-preload/src/index.ts | 21 ++++- packages/service-shared/src/index.ts | 2 + packages/service-shared/src/schemas.ts | 7 ++ 7 files changed, 59 insertions(+), 270 deletions(-) delete mode 100644 packages/service-inject/src/shims/userAgentData.ts delete mode 100644 packages/service-inject/src/utils.ts (limited to 'packages') diff --git a/packages/main/src/index.ts b/packages/main/src/index.ts index 02d6c97..0c0a585 100644 --- a/packages/main/src/index.ts +++ b/packages/main/src/index.ts @@ -22,7 +22,7 @@ import { app, BrowserView, BrowserWindow, - IpcMainEvent, + ipcMain, } from 'electron'; import { readFile, readFileSync } from 'fs'; import { autorun } from 'mobx'; @@ -31,6 +31,7 @@ import { join } from 'path'; import { ServiceToMainIpcMessage, unreadCount, + WebSource, } from '@sophie/service-shared'; import { browserViewBounds, @@ -73,30 +74,26 @@ app.commandLine.appendSwitch( // Remove sophie and electron from the user-agent string to avoid detection. const originalUserAgent = app.userAgentFallback; const userAgent = originalUserAgent.replaceAll(/\s(sophie|Electron)\/\S+/g, ''); -const platformInUa = userAgent.match(/\((Win|Mac|X11; L)/); -let platform = 'Unknown'; -if (platformInUa !== null) { - switch (platformInUa[1]) { - case 'Win': - platform = 'Windows'; - break; - case 'Mac': - platform = 'macOS'; - break; - case 'X11; L': - platform = 'Linux'; - break; - } -} -const chromiumVersion = process.versions.chrome.split('.')[0]; +const chromelessUserAgent = userAgent.replace(/ Chrome\/\S+/, ''); // Removing the electron version breaks redux devtools, so we only do this in production. if (!isDevelopment) { app.userAgentFallback = userAgent; } +function getResourcePath(relativePath: string): string { + return join(__dirname, relativePath); +} + +function getResourceUrl(relativePath: string): string { + return new URL(relativePath, `file://${__dirname}`).toString(); +} + let serviceInjectRelativePath = '../../service-inject/dist/index.cjs'; -let serviceInjectPath = join(__dirname, serviceInjectRelativePath); -let serviceInject: string = readFileSync(serviceInjectPath, 'utf8'); +let serviceInjectPath = getResourcePath(serviceInjectRelativePath); +let serviceInject: WebSource = { + code: readFileSync(serviceInjectPath, 'utf8'), + url: getResourceUrl(serviceInjectRelativePath), +}; if (isDevelopment) { installDevToolsExtensions(app); @@ -111,12 +108,12 @@ function createWindow(): Promise { show: false, webPreferences: { sandbox: true, - preload: join(__dirname, '../../preload/dist/index.cjs'), + preload: getResourcePath('../../preload/dist/index.cjs'), }, }); if (isDevelopment) { - // openDevToolsWhenReady(mainWindow); + openDevToolsWhenReady(mainWindow); } mainWindow.on('ready-to-show', () => { @@ -131,7 +128,7 @@ function createWindow(): Promise { webPreferences: { sandbox: true, nodeIntegrationInSubFrames: true, - preload: join(__dirname, '../../service-preload/dist/index.cjs'), + preload: getResourcePath('../../service-preload/dist/index.cjs'), partition: 'persist:service', }, }); @@ -142,17 +139,6 @@ function createWindow(): Promise { }); mainWindow.setBrowserView(browserView); - browserView.webContents.on( - 'did-frame-navigate', - (_event, _url, _statusCode, _statusText, isMainFrame, _processId, routingId) => { - const { webContents: { mainFrame } } = browserView; - const frame = isMainFrame - ? mainFrame - : mainFrame.framesInSubtree.find((f) => f.routingId === routingId); - frame?.executeJavaScript(serviceInject).catch((err) => console.log(err)); - } - ); - webContents.on('ipc-message', (_event, channel, ...args) => { try { switch (channel) { @@ -168,7 +154,7 @@ function createWindow(): Promise { case RendererToMainIpcMessage.ReloadAllServices: readFile(serviceInjectPath, 'utf8', (err, data) => { if (err === null) { - serviceInject = data; + serviceInject.code = data; } else { console.error('Error while reloading', serviceInjectPath, err); } @@ -188,10 +174,17 @@ function createWindow(): Promise { webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch); }); + ipcMain.on(ServiceToMainIpcMessage.ApiExposedInMainWorld, (event) => { + event.returnValue = event.sender.id == browserView.webContents.id + ? serviceInject + : null; + }); + browserView.webContents.on('ipc-message', (_event, channel, ...args) => { try { switch (channel) { case ServiceToMainIpcMessage.ApiExposedInMainWorld: + // Synchronous message must be handled with `ipcMain.on` break; case ServiceToMainIpcMessage.SetUnreadCount: console.log('Unread count:', unreadCount.parse(args[0])); @@ -205,37 +198,22 @@ function createWindow(): Promise { } }); - // Inject CSS to simulate `browserView.setBackgroundColor`. - // This is injected before the page loads, so the styles from the website will overwrite it. - browserView.webContents.on('did-navigate', () => { - browserView.webContents.insertCSS( - 'html { background-color: #fff; }', - { - cssOrigin: 'author', - }, - ); - }); - browserView.webContents.session.webRequest.onBeforeSendHeaders(({ url, requestHeaders }, callback) => { - if (url.match(/accounts\.google/)) { - requestHeaders['User-Agent'] = userAgent.replace(/ Chrome\/\S+/, ''); + if (url.match(/^[^:]+:\/\/accounts\.google\.[^.\/]+\//)) { + requestHeaders['User-Agent'] = chromelessUserAgent; } else { requestHeaders['User-Agent'] = userAgent; } - requestHeaders['User-Agent'] = userAgent; - requestHeaders['Sec-CH-UA'] = `" Not A;Brand";v="99", "Chromium";v="${chromiumVersion}"`; - requestHeaders['Sec-CH-UA-Mobile'] = '?0'; - requestHeaders['Sec-CH-UA-Platform'] = platform; callback({ requestHeaders }); }); const pageUrl = (isDevelopment && import.meta.env.VITE_DEV_SERVER_URL !== undefined) ? import.meta.env.VITE_DEV_SERVER_URL - : new URL('../renderer/dist/index.html', `file://${__dirname}`).toString(); + : getResourceUrl('../renderer/dist/index.html'); return Promise.all([ mainWindow.loadURL(pageUrl), - browserView.webContents.loadURL('https://gmail.com').then(() => browserView.webContents.openDevTools()), + browserView.webContents.loadURL('https://git.marussy.com/sophie/about'), ]); } diff --git a/packages/service-inject/src/index.ts b/packages/service-inject/src/index.ts index f699f11..a7ada84 100644 --- a/packages/service-inject/src/index.ts +++ b/packages/service-inject/src/index.ts @@ -18,10 +18,4 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { shimUserAgentData } from './shims/userAgentData'; - -try { - shimUserAgentData('96', 'Linux'); -} catch (err) { - console.log('Failed to execute injected script:', err); -} +export {} diff --git a/packages/service-inject/src/shims/userAgentData.ts b/packages/service-inject/src/shims/userAgentData.ts deleted file mode 100644 index 7e2c825..0000000 --- a/packages/service-inject/src/shims/userAgentData.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { - defineProtoProperty, - deleteProtoProperty, - simulateNativeClass, - simulateNativeFunction, -} from '../utils'; - -export function shimUserAgentData(chromeVersion: string | null, platform: string): void { - const brands = [ - { - brand: ' Not A; Brand', - version: '99', - }, - ]; - if (chromeVersion !== null) { - brands.push({ - brand: 'Chromium', - version: '96', - }); - } - const mobile = false; - - const simulatedNavigatorUa = simulateNativeClass('NavigatorUAData', function NavigatorUAData() { - // Nothing to initiailize. - }, { - brands: { - configurable: true, - enumerable: true, - get: simulateNativeFunction('brands', () => brands), - }, - mobile: { - configurable: true, - enumerable: true, - get: simulateNativeFunction('mobile', () => mobile), - }, - platform: { - configurable: true, - enumerable: true, - get: simulateNativeFunction('platform', () => platform), - }, - getHighEntropyValues: { - configurable: true, - enumerable: false, - value: simulateNativeFunction('getHighEntropyValues', (...args: unknown[]) => { - if (args.length == 0) { - throw new TypeError("Failed to execute 'getHighEntropyValues' on 'NavigatorUAData': 1 argument required, but only 0 present."); - } - const hints = Array.from(args[0] as Iterable); - if (hints.length === 0) { - return {}; - } - const data: Record = { - brands, - mobile, - } - if (hints.includes('platform')) { - data['platform'] = platform; - } - return Promise.resolve(data); - }) - }, - toJSON: { - configurable: true, - enumerable: false, - value: simulateNativeFunction('toJSON', () => ({ - brands, - mobile, - })), - writable: false, - }, - }); - - const simulatedUserAgentData = Reflect.construct(simulatedNavigatorUa, []); - defineProtoProperty(globalThis.navigator, 'userAgentData', { - configurable: true, - enumerable: true, - get: simulateNativeFunction('userAgentData', () => simulatedUserAgentData), - }); -} - -export function deleteUserAgentData(): void { - deleteProtoProperty(globalThis.navigator, 'userAgentData'); -} diff --git a/packages/service-inject/src/utils.ts b/packages/service-inject/src/utils.ts deleted file mode 100644 index 4bb3fba..0000000 --- a/packages/service-inject/src/utils.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2021-2022 Kristóf Marussy - * - * This file is part of Sophie. - * - * Sophie is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/** - * Simulates a function defined in native code, i.e., one with - * `[native code]` in its `toString`. - * - * @param name The name of the function. - * @param f The function to transform. - * @return The transformed function. - */ -export function simulateNativeFunction( - name: string, - f: (this: null, ...args: P) => T, -): (...args: P) => T { - // Bound functions say `[native code]`, but unfortunately they omit the function name: - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/toString#description - // The type of `f` contains type variables, so we need some magic type casting. - const boundFunc = f.bind(null as ThisParameterType); - Object.defineProperty(boundFunc, 'name', { - configurable: true, - enumerable: false, - value: name, - writable: false, - }); - return boundFunc; -} - -/** - * Simulates a native class available on `globalThis`. - * - * @param name The name of the class. - * @param constructor The constructor function. Must already be a constructor (a named `function`). - * @param properties The properties to define on the prototype. - */ -export function simulateNativeClass( - name: string, - constructor: () => void, - properties: PropertyDescriptorMap, -) { - Object.defineProperties(constructor.prototype, { - [Symbol.toStringTag]: { - configurable: true, - enumerable: false, - value: name, - writable: false, - }, - ...properties, - }); - const simulatedConstructor = simulateNativeFunction(name, constructor); - Object.defineProperty(globalThis, name, { - configurable: true, - enumerable: true, - value: simulatedConstructor, - writable: true, - }); - return simulatedConstructor; -} - -/** - * Defines a property on the prototype of an object. - * - * Only use this with singleton objects, e.g., `window.navigator`. - * - * @param o The object to modify. Must be a singleton. - * @param property The key of the property being defined or modified. - * @param attributes The descriptor of the property being defined or modified. - */ -export function defineProtoProperty( - o: object, - property: PropertyKey, - attributes: PropertyDescriptor, -): void { - Object.defineProperty(Object.getPrototypeOf(o), property, attributes); -} - -/** - * Deletes a property from the prototype of an object. - * - * Only use this with singleton objects, e.g., `window.navigator`. - * - * @param o The object to modify. Must be a singleton. - * @param property The key of the property being deleted. - */ -export function deleteProtoProperty(o: object, property: PropertyKey): void { - Reflect.deleteProperty(Object.getPrototypeOf(o), property); -} diff --git a/packages/service-preload/src/index.ts b/packages/service-preload/src/index.ts index 3f54c0b..e42c406 100644 --- a/packages/service-preload/src/index.ts +++ b/packages/service-preload/src/index.ts @@ -18,7 +18,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ipcRenderer } from 'electron'; -import { ServiceToMainIpcMessage } from '@sophie/service-shared'; +import { ipcRenderer, webFrame } from 'electron'; +import { ServiceToMainIpcMessage, webSource } from '@sophie/service-shared'; -ipcRenderer.send(ServiceToMainIpcMessage.ApiExposedInMainWorld); +if (webFrame.parent === null) { + // Inject CSS to simulate `browserView.setBackgroundColor`. + // This is injected before the page loads, so the styles from the website will overwrite it. + webFrame.insertCSS('html { background-color: #fff; }'); +} + +const injectSource = webSource.safeParse(ipcRenderer.sendSync(ServiceToMainIpcMessage.ApiExposedInMainWorld)); +if (injectSource.success) { + webFrame.executeJavaScriptInIsolatedWorld(0, [ + injectSource.data, + ]).catch((err) => { + console.log('Failed to inject source:', err); + }); +} else { + console.log('Invalid source to inject:', injectSource.error); +} diff --git a/packages/service-shared/src/index.ts b/packages/service-shared/src/index.ts index c517959..564ebe8 100644 --- a/packages/service-shared/src/index.ts +++ b/packages/service-shared/src/index.ts @@ -22,7 +22,9 @@ export { ServiceToMainIpcMessage } from './ipc'; export type { UnreadCount, + WebSource, } from './schemas'; export { unreadCount, + webSource, } from './schemas'; diff --git a/packages/service-shared/src/schemas.ts b/packages/service-shared/src/schemas.ts index 1513e43..586750c 100644 --- a/packages/service-shared/src/schemas.ts +++ b/packages/service-shared/src/schemas.ts @@ -26,3 +26,10 @@ export const unreadCount = z.object({ }); export type UnreadCount = z.infer; + +export const webSource = z.object({ + code: z.string(), + url: z.string().nonempty(), +}); + +export type WebSource = z.infer; -- cgit v1.2.3-54-g00ecf