aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/main/src/index.ts84
-rw-r--r--packages/service-inject/src/index.ts8
-rw-r--r--packages/service-inject/src/shims/userAgentData.ts103
-rw-r--r--packages/service-inject/src/utils.ts104
-rw-r--r--packages/service-preload/src/index.ts21
-rw-r--r--packages/service-shared/src/index.ts2
-rw-r--r--packages/service-shared/src/schemas.ts7
7 files changed, 59 insertions, 270 deletions
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 {
22 app, 22 app,
23 BrowserView, 23 BrowserView,
24 BrowserWindow, 24 BrowserWindow,
25 IpcMainEvent, 25 ipcMain,
26} from 'electron'; 26} from 'electron';
27import { readFile, readFileSync } from 'fs'; 27import { readFile, readFileSync } from 'fs';
28import { autorun } from 'mobx'; 28import { autorun } from 'mobx';
@@ -31,6 +31,7 @@ import { join } from 'path';
31import { 31import {
32 ServiceToMainIpcMessage, 32 ServiceToMainIpcMessage,
33 unreadCount, 33 unreadCount,
34 WebSource,
34} from '@sophie/service-shared'; 35} from '@sophie/service-shared';
35import { 36import {
36 browserViewBounds, 37 browserViewBounds,
@@ -73,30 +74,26 @@ app.commandLine.appendSwitch(
73// Remove sophie and electron from the user-agent string to avoid detection. 74// Remove sophie and electron from the user-agent string to avoid detection.
74const originalUserAgent = app.userAgentFallback; 75const originalUserAgent = app.userAgentFallback;
75const userAgent = originalUserAgent.replaceAll(/\s(sophie|Electron)\/\S+/g, ''); 76const userAgent = originalUserAgent.replaceAll(/\s(sophie|Electron)\/\S+/g, '');
76const platformInUa = userAgent.match(/\((Win|Mac|X11; L)/); 77const chromelessUserAgent = userAgent.replace(/ Chrome\/\S+/, '');
77let platform = 'Unknown';
78if (platformInUa !== null) {
79 switch (platformInUa[1]) {
80 case 'Win':
81 platform = 'Windows';
82 break;
83 case 'Mac':
84 platform = 'macOS';
85 break;
86 case 'X11; L':
87 platform = 'Linux';
88 break;
89 }
90}
91const chromiumVersion = process.versions.chrome.split('.')[0];
92// Removing the electron version breaks redux devtools, so we only do this in production. 78// Removing the electron version breaks redux devtools, so we only do this in production.
93if (!isDevelopment) { 79if (!isDevelopment) {
94 app.userAgentFallback = userAgent; 80 app.userAgentFallback = userAgent;
95} 81}
96 82
83function getResourcePath(relativePath: string): string {
84 return join(__dirname, relativePath);
85}
86
87function getResourceUrl(relativePath: string): string {
88 return new URL(relativePath, `file://${__dirname}`).toString();
89}
90
97let serviceInjectRelativePath = '../../service-inject/dist/index.cjs'; 91let serviceInjectRelativePath = '../../service-inject/dist/index.cjs';
98let serviceInjectPath = join(__dirname, serviceInjectRelativePath); 92let serviceInjectPath = getResourcePath(serviceInjectRelativePath);
99let serviceInject: string = readFileSync(serviceInjectPath, 'utf8'); 93let serviceInject: WebSource = {
94 code: readFileSync(serviceInjectPath, 'utf8'),
95 url: getResourceUrl(serviceInjectRelativePath),
96};
100 97
101if (isDevelopment) { 98if (isDevelopment) {
102 installDevToolsExtensions(app); 99 installDevToolsExtensions(app);
@@ -111,12 +108,12 @@ function createWindow(): Promise<unknown> {
111 show: false, 108 show: false,
112 webPreferences: { 109 webPreferences: {
113 sandbox: true, 110 sandbox: true,
114 preload: join(__dirname, '../../preload/dist/index.cjs'), 111 preload: getResourcePath('../../preload/dist/index.cjs'),
115 }, 112 },
116 }); 113 });
117 114
118 if (isDevelopment) { 115 if (isDevelopment) {
119 // openDevToolsWhenReady(mainWindow); 116 openDevToolsWhenReady(mainWindow);
120 } 117 }
121 118
122 mainWindow.on('ready-to-show', () => { 119 mainWindow.on('ready-to-show', () => {
@@ -131,7 +128,7 @@ function createWindow(): Promise<unknown> {
131 webPreferences: { 128 webPreferences: {
132 sandbox: true, 129 sandbox: true,
133 nodeIntegrationInSubFrames: true, 130 nodeIntegrationInSubFrames: true,
134 preload: join(__dirname, '../../service-preload/dist/index.cjs'), 131 preload: getResourcePath('../../service-preload/dist/index.cjs'),
135 partition: 'persist:service', 132 partition: 'persist:service',
136 }, 133 },
137 }); 134 });
@@ -142,17 +139,6 @@ function createWindow(): Promise<unknown> {
142 }); 139 });
143 mainWindow.setBrowserView(browserView); 140 mainWindow.setBrowserView(browserView);
144 141
145 browserView.webContents.on(
146 'did-frame-navigate',
147 (_event, _url, _statusCode, _statusText, isMainFrame, _processId, routingId) => {
148 const { webContents: { mainFrame } } = browserView;
149 const frame = isMainFrame
150 ? mainFrame
151 : mainFrame.framesInSubtree.find((f) => f.routingId === routingId);
152 frame?.executeJavaScript(serviceInject).catch((err) => console.log(err));
153 }
154 );
155
156 webContents.on('ipc-message', (_event, channel, ...args) => { 142 webContents.on('ipc-message', (_event, channel, ...args) => {
157 try { 143 try {
158 switch (channel) { 144 switch (channel) {
@@ -168,7 +154,7 @@ function createWindow(): Promise<unknown> {
168 case RendererToMainIpcMessage.ReloadAllServices: 154 case RendererToMainIpcMessage.ReloadAllServices:
169 readFile(serviceInjectPath, 'utf8', (err, data) => { 155 readFile(serviceInjectPath, 'utf8', (err, data) => {
170 if (err === null) { 156 if (err === null) {
171 serviceInject = data; 157 serviceInject.code = data;
172 } else { 158 } else {
173 console.error('Error while reloading', serviceInjectPath, err); 159 console.error('Error while reloading', serviceInjectPath, err);
174 } 160 }
@@ -188,10 +174,17 @@ function createWindow(): Promise<unknown> {
188 webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch); 174 webContents.send(MainToRendererIpcMessage.SharedStorePatch, patch);
189 }); 175 });
190 176
177 ipcMain.on(ServiceToMainIpcMessage.ApiExposedInMainWorld, (event) => {
178 event.returnValue = event.sender.id == browserView.webContents.id
179 ? serviceInject
180 : null;
181 });
182
191 browserView.webContents.on('ipc-message', (_event, channel, ...args) => { 183 browserView.webContents.on('ipc-message', (_event, channel, ...args) => {
192 try { 184 try {
193 switch (channel) { 185 switch (channel) {
194 case ServiceToMainIpcMessage.ApiExposedInMainWorld: 186 case ServiceToMainIpcMessage.ApiExposedInMainWorld:
187 // Synchronous message must be handled with `ipcMain.on`
195 break; 188 break;
196 case ServiceToMainIpcMessage.SetUnreadCount: 189 case ServiceToMainIpcMessage.SetUnreadCount:
197 console.log('Unread count:', unreadCount.parse(args[0])); 190 console.log('Unread count:', unreadCount.parse(args[0]));
@@ -205,37 +198,22 @@ function createWindow(): Promise<unknown> {
205 } 198 }
206 }); 199 });
207 200
208 // Inject CSS to simulate `browserView.setBackgroundColor`.
209 // This is injected before the page loads, so the styles from the website will overwrite it.
210 browserView.webContents.on('did-navigate', () => {
211 browserView.webContents.insertCSS(
212 'html { background-color: #fff; }',
213 {
214 cssOrigin: 'author',
215 },
216 );
217 });
218
219 browserView.webContents.session.webRequest.onBeforeSendHeaders(({ url, requestHeaders }, callback) => { 201 browserView.webContents.session.webRequest.onBeforeSendHeaders(({ url, requestHeaders }, callback) => {
220 if (url.match(/accounts\.google/)) { 202 if (url.match(/^[^:]+:\/\/accounts\.google\.[^.\/]+\//)) {
221 requestHeaders['User-Agent'] = userAgent.replace(/ Chrome\/\S+/, ''); 203 requestHeaders['User-Agent'] = chromelessUserAgent;
222 } else { 204 } else {
223 requestHeaders['User-Agent'] = userAgent; 205 requestHeaders['User-Agent'] = userAgent;
224 } 206 }
225 requestHeaders['User-Agent'] = userAgent;
226 requestHeaders['Sec-CH-UA'] = `" Not A;Brand";v="99", "Chromium";v="${chromiumVersion}"`;
227 requestHeaders['Sec-CH-UA-Mobile'] = '?0';
228 requestHeaders['Sec-CH-UA-Platform'] = platform;
229 callback({ requestHeaders }); 207 callback({ requestHeaders });
230 }); 208 });
231 209
232 const pageUrl = (isDevelopment && import.meta.env.VITE_DEV_SERVER_URL !== undefined) 210 const pageUrl = (isDevelopment && import.meta.env.VITE_DEV_SERVER_URL !== undefined)
233 ? import.meta.env.VITE_DEV_SERVER_URL 211 ? import.meta.env.VITE_DEV_SERVER_URL
234 : new URL('../renderer/dist/index.html', `file://${__dirname}`).toString(); 212 : getResourceUrl('../renderer/dist/index.html');
235 213
236 return Promise.all([ 214 return Promise.all([
237 mainWindow.loadURL(pageUrl), 215 mainWindow.loadURL(pageUrl),
238 browserView.webContents.loadURL('https://gmail.com').then(() => browserView.webContents.openDevTools()), 216 browserView.webContents.loadURL('https://git.marussy.com/sophie/about'),
239 ]); 217 ]);
240} 218}
241 219
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 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { shimUserAgentData } from './shims/userAgentData'; 21export {}
22
23try {
24 shimUserAgentData('96', 'Linux');
25} catch (err) {
26 console.log('Failed to execute injected script:', err);
27}
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 @@
1/*
2 * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import {
22 defineProtoProperty,
23 deleteProtoProperty,
24 simulateNativeClass,
25 simulateNativeFunction,
26} from '../utils';
27
28export function shimUserAgentData(chromeVersion: string | null, platform: string): void {
29 const brands = [
30 {
31 brand: ' Not A; Brand',
32 version: '99',
33 },
34 ];
35 if (chromeVersion !== null) {
36 brands.push({
37 brand: 'Chromium',
38 version: '96',
39 });
40 }
41 const mobile = false;
42
43 const simulatedNavigatorUa = simulateNativeClass('NavigatorUAData', function NavigatorUAData() {
44 // Nothing to initiailize.
45 }, {
46 brands: {
47 configurable: true,
48 enumerable: true,
49 get: simulateNativeFunction('brands', () => brands),
50 },
51 mobile: {
52 configurable: true,
53 enumerable: true,
54 get: simulateNativeFunction('mobile', () => mobile),
55 },
56 platform: {
57 configurable: true,
58 enumerable: true,
59 get: simulateNativeFunction('platform', () => platform),
60 },
61 getHighEntropyValues: {
62 configurable: true,
63 enumerable: false,
64 value: simulateNativeFunction('getHighEntropyValues', (...args: unknown[]) => {
65 if (args.length == 0) {
66 throw new TypeError("Failed to execute 'getHighEntropyValues' on 'NavigatorUAData': 1 argument required, but only 0 present.");
67 }
68 const hints = Array.from(args[0] as Iterable<string>);
69 if (hints.length === 0) {
70 return {};
71 }
72 const data: Record<string, unknown> = {
73 brands,
74 mobile,
75 }
76 if (hints.includes('platform')) {
77 data['platform'] = platform;
78 }
79 return Promise.resolve(data);
80 })
81 },
82 toJSON: {
83 configurable: true,
84 enumerable: false,
85 value: simulateNativeFunction('toJSON', () => ({
86 brands,
87 mobile,
88 })),
89 writable: false,
90 },
91 });
92
93 const simulatedUserAgentData = Reflect.construct(simulatedNavigatorUa, []);
94 defineProtoProperty(globalThis.navigator, 'userAgentData', {
95 configurable: true,
96 enumerable: true,
97 get: simulateNativeFunction('userAgentData', () => simulatedUserAgentData),
98 });
99}
100
101export function deleteUserAgentData(): void {
102 deleteProtoProperty(globalThis.navigator, 'userAgentData');
103}
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 @@
1/*
2 * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21/**
22 * Simulates a function defined in native code, i.e., one with
23 * `[native code]` in its `toString`.
24 *
25 * @param name The name of the function.
26 * @param f The function to transform.
27 * @return The transformed function.
28 */
29export function simulateNativeFunction<T, P extends unknown[]>(
30 name: string,
31 f: (this: null, ...args: P) => T,
32): (...args: P) => T {
33 // Bound functions say `[native code]`, but unfortunately they omit the function name:
34 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/toString#description
35 // The type of `f` contains type variables, so we need some magic type casting.
36 const boundFunc = f.bind(null as ThisParameterType<typeof f>);
37 Object.defineProperty(boundFunc, 'name', {
38 configurable: true,
39 enumerable: false,
40 value: name,
41 writable: false,
42 });
43 return boundFunc;
44}
45
46/**
47 * Simulates a native class available on `globalThis`.
48 *
49 * @param name The name of the class.
50 * @param constructor The constructor function. Must already be a constructor (a named `function`).
51 * @param properties The properties to define on the prototype.
52 */
53export function simulateNativeClass(
54 name: string,
55 constructor: () => void,
56 properties: PropertyDescriptorMap,
57) {
58 Object.defineProperties(constructor.prototype, {
59 [Symbol.toStringTag]: {
60 configurable: true,
61 enumerable: false,
62 value: name,
63 writable: false,
64 },
65 ...properties,
66 });
67 const simulatedConstructor = simulateNativeFunction(name, constructor);
68 Object.defineProperty(globalThis, name, {
69 configurable: true,
70 enumerable: true,
71 value: simulatedConstructor,
72 writable: true,
73 });
74 return simulatedConstructor;
75}
76
77/**
78 * Defines a property on the prototype of an object.
79 *
80 * Only use this with singleton objects, e.g., `window.navigator`.
81 *
82 * @param o The object to modify. Must be a singleton.
83 * @param property The key of the property being defined or modified.
84 * @param attributes The descriptor of the property being defined or modified.
85 */
86export function defineProtoProperty(
87 o: object,
88 property: PropertyKey,
89 attributes: PropertyDescriptor,
90): void {
91 Object.defineProperty(Object.getPrototypeOf(o), property, attributes);
92}
93
94/**
95 * Deletes a property from the prototype of an object.
96 *
97 * Only use this with singleton objects, e.g., `window.navigator`.
98 *
99 * @param o The object to modify. Must be a singleton.
100 * @param property The key of the property being deleted.
101 */
102export function deleteProtoProperty(o: object, property: PropertyKey): void {
103 Reflect.deleteProperty(Object.getPrototypeOf(o), property);
104}
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 @@
18 * SPDX-License-Identifier: AGPL-3.0-only 18 * SPDX-License-Identifier: AGPL-3.0-only
19 */ 19 */
20 20
21import { ipcRenderer } from 'electron'; 21import { ipcRenderer, webFrame } from 'electron';
22import { ServiceToMainIpcMessage } from '@sophie/service-shared'; 22import { ServiceToMainIpcMessage, webSource } from '@sophie/service-shared';
23 23
24ipcRenderer.send(ServiceToMainIpcMessage.ApiExposedInMainWorld); 24if (webFrame.parent === null) {
25 // Inject CSS to simulate `browserView.setBackgroundColor`.
26 // This is injected before the page loads, so the styles from the website will overwrite it.
27 webFrame.insertCSS('html { background-color: #fff; }');
28}
29
30const injectSource = webSource.safeParse(ipcRenderer.sendSync(ServiceToMainIpcMessage.ApiExposedInMainWorld));
31if (injectSource.success) {
32 webFrame.executeJavaScriptInIsolatedWorld(0, [
33 injectSource.data,
34 ]).catch((err) => {
35 console.log('Failed to inject source:', err);
36 });
37} else {
38 console.log('Invalid source to inject:', injectSource.error);
39}
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';
22 22
23export type { 23export type {
24 UnreadCount, 24 UnreadCount,
25 WebSource,
25} from './schemas'; 26} from './schemas';
26export { 27export {
27 unreadCount, 28 unreadCount,
29 webSource,
28} from './schemas'; 30} 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({
26}); 26});
27 27
28export type UnreadCount = z.infer<typeof unreadCount>; 28export type UnreadCount = z.infer<typeof unreadCount>;
29
30export const webSource = z.object({
31 code: z.string(),
32 url: z.string().nonempty(),
33});
34
35export type WebSource = z.infer<typeof webSource>;