fix: context isolation don't allow Observable passing

fixes https://github.com/electron/electron/issues/28176
This commit is contained in:
tiddlygit-test 2021-03-16 23:40:39 +08:00
parent e6adb1f0e5
commit 3ca09cf366
7 changed files with 95 additions and 14 deletions

View file

@ -1,8 +1,8 @@
import { Subscribable, Observer, TeardownLogic, Observable } from 'rxjs';
import { Subscribable, Observer, TeardownLogic, Observable, isObservable } from 'rxjs';
import { IpcRenderer, ipcRenderer, Event } from 'electron';
import { v4 as uuid } from 'uuid';
import Errio from 'errio';
import { IpcProxyError } from './utils';
import { getSubscriptionKey, IpcProxyError } from './utils';
import { Request, RequestType, Response, ResponseType, ProxyDescriptor, ProxyPropertyType } from './common';
export type ObservableConstructor = new (subscribe: (obs: Observer<any>) => TeardownLogic) => Subscribable<any>;
@ -20,10 +20,23 @@ export function createProxy<T>(descriptor: ProxyDescriptor, ObservableCtor: Obse
);
}
Object.defineProperty(result, propertyKey, {
enumerable: true,
get: memoize(() => getProperty(propertyType, propertyKey, descriptor.channel, ObservableCtor, transport)),
});
// fix https://github.com/electron/electron/issues/28176
if (propertyType === ProxyPropertyType.Value$) {
Object.defineProperty(result, getSubscriptionKey(propertyKey), {
enumerable: true,
get: memoize(() => (next: (value?: any) => void) => {
const originalObservable = getProperty(propertyType, propertyKey, descriptor.channel, ObservableCtor, transport);
if (isObservable(originalObservable)) {
originalObservable.subscribe((value: any) => next(value));
}
}),
});
} else {
Object.defineProperty(result, propertyKey, {
enumerable: true,
get: memoize(() => getProperty(propertyType, propertyKey, descriptor.channel, ObservableCtor, transport)),
});
}
});
return result as T;

View file

@ -0,0 +1,27 @@
/**
* fix https://github.com/electron/electron/issues/28176
* We cannot pass Observable across contextBridge, so we have to add a hidden patch to the object on preload script, and use that patch to regenerate Observable on renderer side
*/
import { Observable } from 'rxjs';
import { ProxyDescriptor, ProxyPropertyType } from './common';
import { getSubscriptionKey } from './utils';
export function ipcProxyFixContextIsolation<T extends Record<string, any>>(service: T, descriptor: ProxyDescriptor): void {
for (const key in descriptor.properties) {
if (descriptor.properties[key] === ProxyPropertyType.Value$ && !(key in service) && getSubscriptionKey(key) in service) {
// object is not extensible as contextBridge uses https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions
// but we can still assign property to __proto__
service.__proto__[key as keyof T] = new Observable((observer) => {
service[getSubscriptionKey(key)]((value: any) => observer.next(value));
}) as T[keyof T];
}
}
}
export function fixContextIsolation(): void {
const { descriptors, ...services } = window.service;
for (const key in services) {
const serviceName = key as Exclude<keyof typeof window.service, 'descriptors'>;
ipcProxyFixContextIsolation(services[serviceName], descriptors[serviceName]);
}
}

View file

@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
import { Observable, Subscription } from 'rxjs';
import { Observable, Subscription, isObservable } from 'rxjs';
import { ipcMain, IpcMain, WebContents, IpcMainEvent } from 'electron';
import Errio from 'errio';
import { IpcProxyError, isFunction, isObservable } from './utils';
import { IpcProxyError, isFunction } from './utils';
import {
Request,
RequestType,
@ -116,6 +116,9 @@ class ProxyServerHandler {
if (!isObservable(obs)) {
throw new IpcProxyError(`Remote property [${propKey}] is not an observable`);
}
if (typeof subscriptionId !== 'string') {
throw new IpcProxyError(`subscriptionId [${subscriptionId}] is not a string`);
}
this.doSubscribe(obs, subscriptionId, sender);
}
@ -133,6 +136,9 @@ class ProxyServerHandler {
if (!isObservable(obs)) {
throw new IpcProxyError(`Remote function [${propKey}] did not return an observable`);
}
if (typeof subscriptionId !== 'string') {
throw new IpcProxyError(`subscriptionId [${subscriptionId}] is not a string`);
}
this.doSubscribe(obs, subscriptionId, sender);
}
@ -148,8 +154,17 @@ class ProxyServerHandler {
() => sender.send(subscriptionId, { type: ResponseType.Complete }),
);
/* If the sender does not clean up after itself then we need to do it */
sender.once('destroyed', () => this.doUnsubscribe(subscriptionId));
/*
* If the sender does not clean up after itself then we need to do it
* This won't be called when webContent refresh by CMD+R, so beware this kind of memory leak.
* But we will try to detect devtools-reload-page
*/
sender.once('destroyed', () => {
this.doUnsubscribe(subscriptionId);
});
sender.once('devtools-reload-page', () => {
this.doUnsubscribe(subscriptionId);
});
}
private handleUnsubscribe(request: UnsubscribeRequest): void {

View file

@ -1,4 +1,3 @@
import { Observable } from 'rxjs';
import Errio from 'errio';
/* Custom Error */
@ -15,6 +14,11 @@ export function isFunction(value: any): value is Function {
return value && typeof value === 'function';
}
export function isObservable<T>(value: any): value is Observable<T> {
return value && typeof value.subscribe === 'function';
/**
* Fix ContextIsolation
* @param key original key
* @returns
*/
export function getSubscriptionKey(key: string): string {
return `${key}Subscribe`;
}

View file

@ -60,3 +60,22 @@ export const wikiGitWorkspace = createProxy<AsyncifyProxy<IWikiGitWorkspaceServi
export const window = createProxy<AsyncifyProxy<IWindowService>>(WindowServiceIPCDescriptor);
export const workspace = createProxy<AsyncifyProxy<IWorkspaceService>>(WorkspaceServiceIPCDescriptor);
export const workspaceView = createProxy<AsyncifyProxy<IWorkspaceViewService>>(WorkspaceViewServiceIPCDescriptor);
export const descriptors = {
auth: AuthenticationServiceIPCDescriptor,
context: ContextServiceIPCDescriptor,
git: GitServiceIPCDescriptor,
menu: MenuServiceIPCDescriptor,
native: NativeServiceIPCDescriptor,
notification: NotificationServiceIPCDescriptor,
preference: PreferenceServiceIPCDescriptor,
systemPreference: SystemPreferenceServiceIPCDescriptor,
theme: ThemeServiceIPCDescriptor,
updater: UpdaterServiceIPCDescriptor,
view: ViewServiceIPCDescriptor,
wiki: WikiServiceIPCDescriptor,
wikiGitWorkspace: WikiGitWorkspaceServiceIPCDescriptor,
window: WindowServiceIPCDescriptor,
workspace: WorkspaceServiceIPCDescriptor,
workspaceView: WorkspaceViewServiceIPCDescriptor,
};

View file

@ -5,7 +5,7 @@ import path from 'path';
import './common/i18n';
import './common/authing-postmessage';
import * as service from './common/services';
import { MetaDataChannel, ViewChannel, ContextChannel, WindowChannel } from '@/constants/channels';
import { MetaDataChannel, ViewChannel, WindowChannel } from '@/constants/channels';
import { WindowNames, WindowMeta, IPossibleWindowMeta } from '@services/windows/WindowProperties';
const extraMetaJSONString = process.argv.pop() as string;

View file

@ -15,6 +15,9 @@ import { WindowNames, WindowMeta, IPreferenceWindowMeta } from '@services/window
import 'typeface-roboto/index.css';
import { initI18N } from './i18n';
import { fixContextIsolation } from './helpers/electron-ipc-proxy/fixContextIsolation';
fixContextIsolation();
const Main = React.lazy(async () => await import('./pages/Main'));
const AboutPage = React.lazy(async () => await import('./pages/About'));