diff --git a/packages/connect/setupJest.ts b/packages/connect/setupJest.ts index 4556f914448..f0f1c3525a7 100644 --- a/packages/connect/setupJest.ts +++ b/packages/connect/setupJest.ts @@ -1,27 +1,10 @@ /* WARNING! This file should be imported ONLY in tests! */ -import { AbstractApiTransport, UsbApi, SessionsBackground } from '@trezor/transport'; +import { AbstractApiTransport, UsbApi } from '@trezor/transport'; import { DeviceModelInternal, type Features, type FirmwareRelease } from './src/types'; class TestTransport extends AbstractApiTransport { name = 'TestTransport' as any; - - init() { - return this.scheduleAction(() => { - const sessionsBackground = new SessionsBackground(); - this.sessionsClient.init({ - requestFn: params => sessionsBackground.handleMessage(params), - registerBackgroundCallbacks: onDescriptorsCallback => { - sessionsBackground.on('descriptors', descriptors => { - onDescriptorsCallback(descriptors); - }); - }, - }); - this.stopped = false; - - return this.sessionsClient.handshake(); - }); - } } // mock of navigator.usb @@ -52,14 +35,11 @@ const createTransportApi = (override = {}) => ...override, }) as unknown as UsbApi; -export const createTestTransport = (apiMethods = {}) => { - const transport = new TestTransport({ +export const createTestTransport = (apiMethods = {}) => + new TestTransport({ api: createTransportApi(apiMethods), }); - return transport; -}; - export const getDeviceFeatures = (feat?: Partial): Features => ({ vendor: 'trezor.io', major_version: 2, diff --git a/packages/transport-bridge/src/core.ts b/packages/transport-bridge/src/core.ts index 3f527fbc352..4942b7817ca 100644 --- a/packages/transport-bridge/src/core.ts +++ b/packages/transport-bridge/src/core.ts @@ -18,15 +18,7 @@ export const createCore = (apiArg: 'usb' | 'udp' | AbstractApi, logger?: Log) => let api: AbstractApi; const sessionsBackground = new SessionsBackground(); - - const sessionsClient = new SessionsClient(); - sessionsClient.init({ - requestFn: args => sessionsBackground.handleMessage(args), - registerBackgroundCallbacks: () => {}, - }); - sessionsBackground.on('descriptors', descriptors => { - sessionsClient.emit('descriptors', descriptors); - }); + const sessionsClient = new SessionsClient(sessionsBackground); if (typeof apiArg === 'string') { api = diff --git a/packages/transport/src/sessions/background-browser.ts b/packages/transport/src/sessions/background-browser.ts index b62e0dfc4c2..06d4954518c 100644 --- a/packages/transport/src/sessions/background-browser.ts +++ b/packages/transport/src/sessions/background-browser.ts @@ -1,102 +1,69 @@ import type { Descriptor } from '../types'; import { SessionsBackground } from './background'; -import { RegisterBackgroundCallbacks } from './types'; - -const defaultSessionsBackgroundUrl = - window.location.origin + - `${process.env.ASSET_PREFIX || ''}/workers/sessions-background-sharedworker.js` - // just in case so that whoever defines ASSET_PREFIX does not need to worry about trailing slashes - .replace(/\/+/g, '/'); +import { HandleMessageParams, HandleMessageResponse, SessionsBackgroundInterface } from './types'; /** - * calling initBackgroundInBrowser initiates sessions-background for browser based environments and returns: - * - `requestFn` which is used to send messages to sessions background - * - `registerBackgroundCallbacks` which is used to accept information about descriptors change from + * creating BrowserSessionsBackground initiates sessions-background for browser based environments and provides: + * - `handleMessage` which is used to send messages to sessions background + * - `on` which is used to accept information about descriptors change from * another tab and notify local transport * if possible sessions background utilizes native Sharedworker. If for whatever reason - * Sharedworker is not available, it fallbacks to local module behavior + * Sharedworker is not available, the constructor throws an error. */ -export const initBackgroundInBrowser = async ( - sessionsBackgroundUrl = defaultSessionsBackgroundUrl, -): Promise<{ - background: SessionsBackground | SharedWorker; - requestFn: SessionsBackground['handleMessage']; - registerBackgroundCallbacks: RegisterBackgroundCallbacks; -}> => { - try { - // fetch to validate - failed fetch via SharedWorker constructor does not throw. It even hangs resulting in all kinds of weird behaviors - await fetch(sessionsBackgroundUrl, { method: 'HEAD' }).then(response => { - if (!response.ok) { - throw new Error( - `Failed to fetch sessions-background SharedWorker from url: ${sessionsBackgroundUrl}`, - ); - } - }); - const background = new SharedWorker(sessionsBackgroundUrl, { +export class BrowserSessionsBackground implements SessionsBackgroundInterface { + private readonly background; + + constructor(sessionsBackgroundUrl: string) { + this.background = new SharedWorker(sessionsBackgroundUrl, { name: '@trezor/connect-web transport sessions worker', }); + } - const requestFn: SessionsBackground['handleMessage'] = ( - params: Parameters[0], - ) => - new Promise(resolve => { - const onmessage = (message: MessageEvent) => { - if (params.id === message.data.id) { - resolve(message.data); - background.port.removeEventListener('message', onmessage); - } - }; + handleMessage(params: M): Promise> { + const { background } = this; - background.port.addEventListener('message', onmessage); + return new Promise(resolve => { + const onmessage = (message: MessageEvent) => { + if (params.id === message.data.id) { + resolve(message.data); + background.port.removeEventListener('message', onmessage); + } + }; - background.port.onmessageerror = message => { - // not sure under what circumstances this error occurs. let's observe it during testing - console.error('background-browser onmessageerror,', message); + background.port.addEventListener('message', onmessage); - background.port.removeEventListener('message', onmessage); - }; - background.port.postMessage(params); - }); + background.port.onmessageerror = message => { + // not sure under what circumstances this error occurs. let's observe it during testing + console.error('background-browser onmessageerror,', message); - const registerBackgroundCallbacks: RegisterBackgroundCallbacks = onDescriptorsCallback => { - background.port.addEventListener( - 'message', - ( - e: MessageEvent< - // either standard response from sessions background (we ignore this one) - | Awaited> - // or artificially broadcasted message to all clients (see background-sharedworker) - | { type: 'descriptors'; payload: Descriptor[] } - >, - ) => { - if ('type' in e?.data) { - if (e.data.type === 'descriptors') { - onDescriptorsCallback(e.data.payload); - } - } - }, - ); - }; + background.port.removeEventListener('message', onmessage); + }; + background.port.postMessage(params); + }); + } - return { background, requestFn, registerBackgroundCallbacks }; - } catch (err) { - console.warn( - 'Unable to load background-sharedworker. Falling back to use local module. Say bye bye to tabs synchronization. Error details: ', - err.message, + on(_event: 'descriptors', listener: (descriptors: Descriptor[]) => void): void { + this.background.port.addEventListener( + 'message', + ( + e: MessageEvent< + // either standard response from sessions background (we ignore this one) + | Awaited> + // or artificially broadcasted message to all clients (see background-sharedworker) + | { type: 'descriptors'; payload: Descriptor[] } + >, + ) => { + if ('type' in e?.data) { + if (e.data.type === 'descriptors') { + listener(e.data.payload); + } + } + }, ); + } - const background = new SessionsBackground(); - const registerBackgroundCallbacks: RegisterBackgroundCallbacks = onDescriptorsCallback => { - background.on('descriptors', descriptors => { - onDescriptorsCallback(descriptors); - }); - }; - - return { - background, - requestFn: background.handleMessage.bind(background), - registerBackgroundCallbacks, - }; + dispose() { + /* is it needed? */ } -}; +} diff --git a/packages/transport/src/sessions/background.ts b/packages/transport/src/sessions/background.ts index 5d7f2ed9678..9d748a84615 100644 --- a/packages/transport/src/sessions/background.ts +++ b/packages/transport/src/sessions/background.ts @@ -21,6 +21,7 @@ import type { GetPathBySessionRequest, HandleMessageParams, HandleMessageResponse, + SessionsBackgroundInterface, } from './types'; import type { Descriptor, PathInternal, Success } from '../types'; import { PathPublic, Session } from '../types'; @@ -35,13 +36,16 @@ type DescriptorsDict = Record; // in nodeusb, enumeration operation takes ~3 seconds const lockDuration = 1000 * 4; -export class SessionsBackground extends TypedEmitter<{ - /** - * updated descriptors (session has changed) - * note: we can't send diff from here (see abstract transport) although it would make sense, because we need to support also bridge which does not use this sessions background. - */ - descriptors: Descriptor[]; -}> { +export class SessionsBackground + extends TypedEmitter<{ + /** + * updated descriptors (session has changed) + * note: we can't send diff from here (see abstract transport) although it would make sense, because we need to support also bridge which does not use this sessions background. + */ + descriptors: Descriptor[]; + }> + implements SessionsBackgroundInterface +{ /** * Dictionary where key is path and value is Descriptor */ @@ -323,5 +327,6 @@ export class SessionsBackground extends TypedEmitter<{ this.locksTimeoutQueue.forEach(timeout => clearTimeout(timeout)); this.descriptors = {}; this.lastSessionId = 0; + this.removeAllListeners(); } } diff --git a/packages/transport/src/sessions/client.ts b/packages/transport/src/sessions/client.ts index 3a065efcb1e..626eb113fa7 100644 --- a/packages/transport/src/sessions/client.ts +++ b/packages/transport/src/sessions/client.ts @@ -9,9 +9,9 @@ import { ReleaseDoneRequest, GetPathBySessionRequest, AcquireDoneRequest, - RegisterBackgroundCallbacks, + SessionsBackgroundInterface, + HandleMessageParams, } from './types'; -import { SessionsBackground } from './background'; /** * SessionsClient gives you API for communication with SessionsBackground. @@ -20,39 +20,28 @@ import { SessionsBackground } from './background'; export class SessionsClient extends TypedEmitter<{ descriptors: Descriptor[]; }> { - // request method responsible for communication with sessions background. - private request: SessionsBackground['handleMessage'] = () => { - throw new Error('SessionsClient: request method not provided'); - }; - // used only for debugging - discriminating sessions clients in sessions background log private caller = getWeakRandomId(3); - private id: number = 0; + private id; + private background; - public init({ - requestFn, - registerBackgroundCallbacks, - }: { - requestFn: SessionsBackground['handleMessage']; - registerBackgroundCallbacks?: RegisterBackgroundCallbacks; - }) { + constructor(background: SessionsBackgroundInterface) { + super(); this.id = 0; - this.request = params => { - if (!requestFn) { - throw new Error('SessionsClient: requestFn not provided'); - } - params.caller = this.caller; - params.id = this.id; - this.id++; + this.background = background; + background.on('descriptors', descriptors => this.emit('descriptors', descriptors)); + } + + public setBackground(background: SessionsBackgroundInterface) { + this.background.dispose(); - return requestFn(params); - }; + this.id = 0; + this.background = background; + background.on('descriptors', descriptors => this.emit('descriptors', descriptors)); + } - if (registerBackgroundCallbacks) { - registerBackgroundCallbacks(descriptors => { - this.emit('descriptors', descriptors); - }); - } + private request(params: M) { + return this.background.handleMessage({ ...params, caller: this.caller, id: this.id++ }); } public handshake() { diff --git a/packages/transport/src/sessions/types.ts b/packages/transport/src/sessions/types.ts index f459d880bc2..2f1504ad3df 100644 --- a/packages/transport/src/sessions/types.ts +++ b/packages/transport/src/sessions/types.ts @@ -111,6 +111,8 @@ export type HandleMessageResponse

= P extends { type: infer T } : never : never; -export type RegisterBackgroundCallbacks = ( - onDescriptorsCallback: (args: Descriptor[]) => void, -) => void; +export interface SessionsBackgroundInterface { + on(event: 'descriptors', listener: (descriptors: Descriptor[]) => void): void; + handleMessage(message: M): Promise>; + dispose(): void; +} diff --git a/packages/transport/src/transports/abstractApi.ts b/packages/transport/src/transports/abstractApi.ts index c194101ca53..bfa22db2abf 100644 --- a/packages/transport/src/transports/abstractApi.ts +++ b/packages/transport/src/transports/abstractApi.ts @@ -13,6 +13,7 @@ import { receiveAndParse } from '../utils/receive'; import { SessionsClient } from '../sessions/client'; import * as ERRORS from '../errors'; import { Session } from '../types'; +import { SessionsBackgroundInterface } from '../sessions/types'; interface ConstructorParams extends AbstractTransportParams { api: AbstractApi; @@ -24,30 +25,21 @@ interface ConstructorParams extends AbstractTransportParams { export abstract class AbstractApiTransport extends AbstractTransport { // sessions client is a standardized interface for communicating with sessions backend // which can live in couple of context (shared worker, local module, websocket server etc) - protected sessionsClient = new SessionsClient(); - private sessionsBackground = new SessionsBackground(); + protected sessionsClient: SessionsClient; + protected sessionsBackground: SessionsBackgroundInterface; protected api: AbstractApi; constructor({ messages, api, logger }: ConstructorParams) { super({ messages, logger }); this.api = api; + this.sessionsBackground = new SessionsBackground(); + this.sessionsClient = new SessionsClient(this.sessionsBackground); } public init({ signal }: AbstractTransportMethodParams<'init'> = {}) { return this.scheduleAction( async () => { - // in nodeusb there is no synchronization yet. this is a followup and needs to be decided - // so far, sessionsClient has direct access to sessionBackground - this.sessionsClient.init({ - requestFn: args => this.sessionsBackground.handleMessage(args), - registerBackgroundCallbacks: () => {}, - }); - - this.sessionsBackground.on('descriptors', descriptors => { - this.sessionsClient.emit('descriptors', descriptors); - }); - const handshakeRes = await this.sessionsClient.handshake(); this.stopped = !handshakeRes.success; diff --git a/packages/transport/src/transports/webusb.browser.ts b/packages/transport/src/transports/webusb.browser.ts index 05283b2f56e..285997517e9 100644 --- a/packages/transport/src/transports/webusb.browser.ts +++ b/packages/transport/src/transports/webusb.browser.ts @@ -2,7 +2,13 @@ import { AbstractTransportMethodParams, AbstractTransportParams } from './abstra import { AbstractApiTransport } from './abstractApi'; import { UsbApi } from '../api/usb'; -import { initBackgroundInBrowser } from '../sessions/background-browser'; +import { BrowserSessionsBackground } from '../sessions/background-browser'; + +const defaultSessionsBackgroundUrl = + window.location.origin + + `${process.env.ASSET_PREFIX || ''}/workers/sessions-background-sharedworker.js` + // just in case so that whoever defines ASSET_PREFIX does not need to worry about trailing slashes + .replace(/\/+/g, '/'); type WebUsbTransportParams = AbstractTransportParams & { sessionsBackgroundUrl?: string }; @@ -19,34 +25,36 @@ export class WebUsbTransport extends AbstractApiTransport { constructor({ messages, logger, sessionsBackgroundUrl }: WebUsbTransportParams) { super({ messages, - api: new UsbApi({ - usbInterface: navigator.usb, - logger, - }), + api: new UsbApi({ usbInterface: navigator.usb, logger }), logger, }); - this.sessionsBackgroundUrl = sessionsBackgroundUrl; + this.sessionsBackgroundUrl = sessionsBackgroundUrl ?? defaultSessionsBackgroundUrl; } - public init({ signal }: AbstractTransportMethodParams<'init'> = {}) { - return this.scheduleAction( - async () => { - const { sessionsBackgroundUrl } = this; - const { requestFn, registerBackgroundCallbacks } = - await initBackgroundInBrowser(sessionsBackgroundUrl); + private async trySetSessionsBackground() { + try { + const response = await fetch(this.sessionsBackgroundUrl, { method: 'HEAD' }); + if (!response.ok) { + console.warn( + `Failed to fetch sessions-background SharedWorker from url: ${this.sessionsBackgroundUrl}`, + ); + } else { + this.sessionsBackground = new BrowserSessionsBackground(this.sessionsBackgroundUrl); // sessions client initiated with a request fn facilitating communication with a session backend (shared worker in case of webusb) - this.sessionsClient.init({ - requestFn, - registerBackgroundCallbacks, - }); - - const handshakeRes = await this.sessionsClient.handshake(); - this.stopped = !handshakeRes.success; - - return handshakeRes; - }, - { signal }, - ); + this.sessionsClient.setBackground(this.sessionsBackground); + } + } catch (err) { + console.warn( + 'Unable to load background-sharedworker. Falling back to use local module. Say bye bye to tabs synchronization. Error details: ', + err.message, + ); + } + } + + public async init({ signal }: AbstractTransportMethodParams<'init'> = {}) { + await this.trySetSessionsBackground(); + + return super.init({ signal }); } public listen() { diff --git a/packages/transport/tests/abstractUsb.test.ts b/packages/transport/tests/abstractUsb.test.ts index d4ebc79a393..5a47754e95a 100644 --- a/packages/transport/tests/abstractUsb.test.ts +++ b/packages/transport/tests/abstractUsb.test.ts @@ -2,8 +2,6 @@ import { v1 as v1Protocol } from '@trezor/protocol'; import { AbstractTransport } from '../src/transports/abstract'; import { AbstractApiTransport } from '../src/transports/abstractApi'; import { UsbApi } from '../src/api/usb'; -import { SessionsClient } from '../src/sessions/client'; -import { SessionsBackground } from '../src/sessions/background'; import * as messages from '@trezor/protobuf/messages.json'; import { PathPublic, Session } from '../src/types'; @@ -43,37 +41,8 @@ const createUsbMock = (optional = {}) => class TestUsbTransport extends AbstractApiTransport { public name = 'WebUsbTransport' as const; - public sessionsClient = new SessionsClient({}); - - constructor({ messages, api }: ConstructorParameters[0]) { - super({ - messages, - api, - }); - } - - init() { - return this.scheduleAction(async () => { - const sessionsBackground = new SessionsBackground(); - - // in nodeusb there is no synchronization yet. this is a followup and needs to be decided - // so far, sessionsClient has direct access to sessionBackground - this.sessionsClient.init({ - requestFn: args => sessionsBackground.handleMessage(args), - registerBackgroundCallbacks: () => {}, - }); - - sessionsBackground.on('descriptors', descriptors => { - this.sessionsClient.emit('descriptors', descriptors); - }); - - const handshakeRes = await this.sessionsClient.handshake(); - this.stopped = !handshakeRes.success; - - return handshakeRes; - }); - } } + // we cant directly use abstract class (UsbTransport) const initTest = async () => { let transport: AbstractTransport; diff --git a/packages/transport/tests/sessions.test.ts b/packages/transport/tests/sessions.test.ts index c4d4ce3a67a..75db6b763f7 100644 --- a/packages/transport/tests/sessions.test.ts +++ b/packages/transport/tests/sessions.test.ts @@ -3,16 +3,14 @@ import { SessionsBackground } from '../src/sessions/background'; import { PathInternal, PathPublic, Session } from '../src/types'; describe('sessions', () => { - let requestFn: SessionsClient['request']; + let background: SessionsBackground; beforeEach(() => { - const background = new SessionsBackground(); - requestFn = params => background.handleMessage(params); + background = new SessionsBackground(); }); test('acquire without previous enumerate', async () => { - const client1 = new SessionsClient(); - client1.init({ requestFn }); + const client1 = new SessionsClient(background); await client1.handshake(); const acquireIntent = await client1.acquireIntent({ @@ -28,8 +26,7 @@ describe('sessions', () => { }); test('acquire', async () => { - const client1 = new SessionsClient(); - client1.init({ requestFn }); + const client1 = new SessionsClient(background); await client1.handshake(); await client1.enumerateDone({ descriptors: [{ path: PathInternal('abc'), type: 1 }] }); @@ -73,8 +70,7 @@ describe('sessions', () => { test('acquire', async () => { expect.assertions(3); - const client1 = new SessionsClient(); - client1.init({ requestFn }); + const client1 = new SessionsClient(background); await client1.handshake(); await client1.enumerateDone({ descriptors: [{ path: PathInternal('1'), type: 1 }] }); @@ -131,8 +127,7 @@ describe('sessions', () => { }); test('acquire - release - acquire', async () => { - const client1 = new SessionsClient(); - client1.init({ requestFn }); + const client1 = new SessionsClient(background); await client1.handshake(); await client1.enumerateDone({ descriptors: [{ path: PathInternal('1'), type: 1 }] });