From 772b42f5b53f483a956b3e4fec31b83e54742418 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 8 Apr 2025 15:06:39 +0200 Subject: [PATCH] [PM-18039] Add initial verison of IpcServices to client (#13373) * feat: add foreground ipc service * refactor: create abstract ipc service in libs * wip: remove IPC service complexity The code was making some wrong assumptions about how IPC is going to work. I'm removing everything and starting the content-script instead * feat: working message sending from page to background * refactor: move into common * feat: somewhat complete web <-> browser link * wip: ping command from web * fix: import path * fix: wip urls * wip: add console log * feat: successfull message sending (not receiving) * feat: implement IPC using new refactored framework * wip: add some console logs * wip: almost working ping/pong * feat: working ping/pong * chore: clean-up ping/pong and some console logs * chore: remove unused file * fix: override lint rule * chore: remove unused ping message * feat: add tests for message queue * fix: adapt to name changes and modifications to SDK branch * fix: missing import * fix: remove content script from manifest The feature is not ready for prodution code yet. We will add dynamic injection with feature-flag support in a follow-up PR * fix: remove fileless lp * fix: make same changes to manifest v2 * fix: initialization functions Add missing error handling, wait for the SDK to load and properly depend on the log service * feat: use named id field * chore: update sdk version to include IPC changes * fix: remove messages$ buffer * fix: forgot to commit package-lock * feat: add additional destination check * feat: only import type in ipc-message * fix: typing issues * feat: check message origin --- .../browser/src/background/main.background.ts | 7 +++ .../ipc/background-communication-backend.ts | 41 +++++++++++++++ .../ipc/content/ipc-content-script.ts | 51 +++++++++++++++++++ .../platform/ipc/ipc-background.service.ts | 26 ++++++++++ apps/browser/webpack.config.js | 1 + apps/web/src/app/core/core.module.ts | 9 +++- apps/web/src/app/core/init.service.ts | 3 ++ .../ipc/web-communication-provider.ts | 41 +++++++++++++++ .../src/app/platform/ipc/web-ipc.service.ts | 25 +++++++++ libs/common/src/platform/ipc/index.ts | 2 + libs/common/src/platform/ipc/ipc-message.ts | 10 ++++ libs/common/src/platform/ipc/ipc.service.ts | 51 +++++++++++++++++++ .../src/platform/ipc/message-queue.spec.ts | 48 +++++++++++++++++ libs/common/src/platform/ipc/message-queue.ts | 20 ++++++++ package-lock.json | 9 ++-- package.json | 2 +- 16 files changed, 340 insertions(+), 6 deletions(-) create mode 100644 apps/browser/src/platform/ipc/background-communication-backend.ts create mode 100644 apps/browser/src/platform/ipc/content/ipc-content-script.ts create mode 100644 apps/browser/src/platform/ipc/ipc-background.service.ts create mode 100644 apps/web/src/app/platform/ipc/web-communication-provider.ts create mode 100644 apps/web/src/app/platform/ipc/web-ipc.service.ts create mode 100644 libs/common/src/platform/ipc/index.ts create mode 100644 libs/common/src/platform/ipc/ipc-message.ts create mode 100644 libs/common/src/platform/ipc/ipc.service.ts create mode 100644 libs/common/src/platform/ipc/message-queue.spec.ts create mode 100644 libs/common/src/platform/ipc/message-queue.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 663fc863f5e..aeee58c6fdf 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -111,6 +111,7 @@ import { } from "@bitwarden/common/platform/abstractions/storage.service"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { IpcService } from "@bitwarden/common/platform/ipc"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency creation import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; @@ -259,6 +260,7 @@ import { BackgroundBrowserBiometricsService } from "../key-management/biometrics import VaultTimeoutService from "../key-management/vault-timeout/vault-timeout.service"; import { BrowserApi } from "../platform/browser/browser-api"; import { flagEnabled } from "../platform/flags"; +import { IpcBackgroundService } from "../platform/ipc/ipc-background.service"; import { UpdateBadge } from "../platform/listeners/update-badge"; /* eslint-disable no-restricted-imports */ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender"; @@ -403,6 +405,8 @@ export default class MainBackground { inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; taskService: TaskService; + ipcService: IpcService; + onUpdatedRan: boolean; onReplacedRan: boolean; loginToAutoFill: CipherView = null; @@ -1309,6 +1313,8 @@ export default class MainBackground { ); this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + + this.ipcService = new IpcBackgroundService(this.logService); } async bootstrap() { @@ -1382,6 +1388,7 @@ export default class MainBackground { } await this.initOverlayAndTabsBackground(); + await this.ipcService.init(); return new Promise((resolve) => { setTimeout(async () => { diff --git a/apps/browser/src/platform/ipc/background-communication-backend.ts b/apps/browser/src/platform/ipc/background-communication-backend.ts new file mode 100644 index 00000000000..6c5b374dd56 --- /dev/null +++ b/apps/browser/src/platform/ipc/background-communication-backend.ts @@ -0,0 +1,41 @@ +import { IpcMessage, isIpcMessage } from "@bitwarden/common/platform/ipc"; +import { MessageQueue } from "@bitwarden/common/platform/ipc/message-queue"; +import { CommunicationBackend, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-internal"; + +import { BrowserApi } from "../browser/browser-api"; + +export class BackgroundCommunicationBackend implements CommunicationBackend { + private queue = new MessageQueue(); + + constructor() { + BrowserApi.messageListener("platform.ipc", (message, sender) => { + if (!isIpcMessage(message)) { + return; + } + + if (sender.tab?.id === undefined || sender.tab.id === chrome.tabs.TAB_ID_NONE) { + // Ignore messages from non-tab sources + return; + } + + void this.queue.enqueue({ ...message.message, source: { Web: { id: sender.tab.id } } }); + }); + } + + async send(message: OutgoingMessage): Promise { + if (typeof message.destination === "object" && "Web" in message.destination) { + await BrowserApi.tabSendMessage( + { id: message.destination.Web.id } as chrome.tabs.Tab, + { type: "bitwarden-ipc-message", message } satisfies IpcMessage, + { frameId: 0 }, + ); + return; + } + + throw new Error("Destination not supported."); + } + + async receive(): Promise { + return this.queue.dequeue(); + } +} diff --git a/apps/browser/src/platform/ipc/content/ipc-content-script.ts b/apps/browser/src/platform/ipc/content/ipc-content-script.ts new file mode 100644 index 00000000000..a21ad07be4b --- /dev/null +++ b/apps/browser/src/platform/ipc/content/ipc-content-script.ts @@ -0,0 +1,51 @@ +// TODO: This content script should be dynamically reloaded when the extension is updated, +// to avoid "Extension context invalidated." errors. + +import { isIpcMessage } from "@bitwarden/common/platform/ipc/ipc-message"; + +// Web -> Background +export function sendExtensionMessage(message: unknown) { + if ( + typeof browser !== "undefined" && + typeof browser.runtime !== "undefined" && + typeof browser.runtime.sendMessage !== "undefined" + ) { + void browser.runtime.sendMessage(message); + return; + } + + void chrome.runtime.sendMessage(message); +} + +window.addEventListener("message", (event) => { + if (event.origin !== window.origin) { + return; + } + + if (isIpcMessage(event.data)) { + sendExtensionMessage(event.data); + } +}); + +// Background -> Web +function setupMessageListener() { + function listener(message: unknown) { + if (isIpcMessage(message)) { + void window.postMessage(message); + } + } + + if ( + typeof browser !== "undefined" && + typeof browser.runtime !== "undefined" && + typeof browser.runtime.onMessage !== "undefined" + ) { + browser.runtime.onMessage.addListener(listener); + return; + } + + // eslint-disable-next-line no-restricted-syntax -- This doesn't run in the popup but in the content script + chrome.runtime.onMessage.addListener(listener); +} + +setupMessageListener(); diff --git a/apps/browser/src/platform/ipc/ipc-background.service.ts b/apps/browser/src/platform/ipc/ipc-background.service.ts new file mode 100644 index 00000000000..a87f6bb4acb --- /dev/null +++ b/apps/browser/src/platform/ipc/ipc-background.service.ts @@ -0,0 +1,26 @@ +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import { IpcService } from "@bitwarden/common/platform/ipc"; +import { IpcClient } from "@bitwarden/sdk-internal"; + +import { BackgroundCommunicationBackend } from "./background-communication-backend"; + +export class IpcBackgroundService extends IpcService { + private communicationProvider?: BackgroundCommunicationBackend; + + constructor(private logService: LogService) { + super(); + } + + override async init() { + try { + // This function uses classes and functions defined in the SDK, so we need to wait for the SDK to load. + await SdkLoadService.Ready; + this.communicationProvider = new BackgroundCommunicationBackend(); + + await super.initWithClient(new IpcClient(this.communicationProvider)); + } catch (e) { + this.logService.error("[IPC] Initialization failed", e); + } + } +} diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index f386a3167ec..09d1133a4df 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -205,6 +205,7 @@ const mainConfig = { "content/content-message-handler": "./src/autofill/content/content-message-handler.ts", "content/fido2-content-script": "./src/autofill/fido2/content/fido2-content-script.ts", "content/fido2-page-script": "./src/autofill/fido2/content/fido2-page-script.ts", + "content/ipc-content-script": "./src/platform/ipc/content/ipc-content-script.ts", "notification/bar": "./src/autofill/notification/bar.ts", "overlay/menu-button": "./src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts", diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 9e6f88d18d6..1fa4fd80e52 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -75,6 +75,7 @@ import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sd import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { ThemeTypes } from "@bitwarden/common/platform/enums"; +import { IpcService } from "@bitwarden/common/platform/ipc"; // eslint-disable-next-line no-restricted-imports -- Needed for DI import { UnsupportedWebPushConnectionService, @@ -122,9 +123,11 @@ import { WebSsoComponentService } from "../auth/core/services/login/web-sso-comp import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service"; import { HtmlStorageService } from "../core/html-storage.service"; import { I18nService } from "../core/i18n.service"; +import { WebFileDownloadService } from "../core/web-file-download.service"; import { WebLockComponentService } from "../key-management/lock/services/web-lock-component.service"; import { WebProcessReloadService } from "../key-management/services/web-process-reload.service"; import { WebBiometricsService } from "../key-management/web-biometric.service"; +import { WebIpcService } from "../platform/ipc/web-ipc.service"; import { WebEnvironmentService } from "../platform/web-environment.service"; import { WebMigrationRunner } from "../platform/web-migration-runner"; import { WebSdkLoadService } from "../platform/web-sdk-load.service"; @@ -135,7 +138,6 @@ import { InitService } from "./init.service"; import { ENV_URLS } from "./injection-tokens"; import { ModalService } from "./modal.service"; import { RouterService } from "./router.service"; -import { WebFileDownloadService } from "./web-file-download.service"; import { WebPlatformUtilsService } from "./web-platform-utils.service"; /** @@ -368,6 +370,11 @@ const safeProviders: SafeProvider[] = [ useClass: WebLoginDecryptionOptionsService, deps: [MessagingService, RouterService, AcceptOrganizationInviteService], }), + safeProvider({ + provide: IpcService, + useClass: WebIpcService, + deps: [], + }), safeProvider({ provide: SshImportPromptService, useClass: DefaultSshImportPromptService, diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index 24b5631abcc..43547ff5d57 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -14,6 +14,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; +import { IpcService } from "@bitwarden/common/platform/ipc"; import { NotificationsService } from "@bitwarden/common/platform/notifications"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; @@ -38,6 +39,7 @@ export class InitService { private userAutoUnlockKeyService: UserAutoUnlockKeyService, private accountService: AccountService, private versionService: VersionService, + private ipcService: IpcService, private sdkLoadService: SdkLoadService, private configService: ConfigService, private bulkEncryptService: BulkEncryptService, @@ -72,6 +74,7 @@ export class InitService { htmlEl.classList.add("locale_" + this.i18nService.translationLocale); this.themingService.applyThemeChangesTo(this.document); this.versionService.applyVersionToWindow(); + void this.ipcService.init(); const containerService = new ContainerService(this.keyService, this.encryptService); containerService.attachToGlobal(this.win); diff --git a/apps/web/src/app/platform/ipc/web-communication-provider.ts b/apps/web/src/app/platform/ipc/web-communication-provider.ts new file mode 100644 index 00000000000..85353ab77af --- /dev/null +++ b/apps/web/src/app/platform/ipc/web-communication-provider.ts @@ -0,0 +1,41 @@ +import { Injectable } from "@angular/core"; + +import { IpcMessage, isIpcMessage } from "@bitwarden/common/platform/ipc"; +import { MessageQueue } from "@bitwarden/common/platform/ipc/message-queue"; +import { CommunicationBackend, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-internal"; + +@Injectable({ providedIn: "root" }) +export class WebCommunicationProvider implements CommunicationBackend { + private queue = new MessageQueue(); + + constructor() { + window.addEventListener("message", async (event: MessageEvent) => { + if (event.origin !== window.origin) { + return; + } + + const message = event.data; + if (!isIpcMessage(message)) { + return; + } + + await this.queue.enqueue({ ...message.message, source: "BrowserBackground" }); + }); + } + + async send(message: OutgoingMessage): Promise { + if (message.destination === "BrowserBackground") { + window.postMessage( + { type: "bitwarden-ipc-message", message } satisfies IpcMessage, + window.location.origin, + ); + return; + } + + throw new Error(`Destination not supported: ${message.destination}`); + } + + receive(): Promise { + return this.queue.dequeue(); + } +} diff --git a/apps/web/src/app/platform/ipc/web-ipc.service.ts b/apps/web/src/app/platform/ipc/web-ipc.service.ts new file mode 100644 index 00000000000..59a715f31f5 --- /dev/null +++ b/apps/web/src/app/platform/ipc/web-ipc.service.ts @@ -0,0 +1,25 @@ +import { inject } from "@angular/core"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; +import { IpcService } from "@bitwarden/common/platform/ipc"; +import { IpcClient } from "@bitwarden/sdk-internal"; + +import { WebCommunicationProvider } from "./web-communication-provider"; + +export class WebIpcService extends IpcService { + private logService = inject(LogService); + private communicationProvider?: WebCommunicationProvider; + + override async init() { + try { + // This function uses classes and functions defined in the SDK, so we need to wait for the SDK to load. + await SdkLoadService.Ready; + this.communicationProvider = new WebCommunicationProvider(); + + await super.initWithClient(new IpcClient(this.communicationProvider)); + } catch (e) { + this.logService.error("[IPC] Initialization failed", e); + } + } +} diff --git a/libs/common/src/platform/ipc/index.ts b/libs/common/src/platform/ipc/index.ts new file mode 100644 index 00000000000..f1acccdddbf --- /dev/null +++ b/libs/common/src/platform/ipc/index.ts @@ -0,0 +1,2 @@ +export * from "./ipc-message"; +export * from "./ipc.service"; diff --git a/libs/common/src/platform/ipc/ipc-message.ts b/libs/common/src/platform/ipc/ipc-message.ts new file mode 100644 index 00000000000..78cce011f49 --- /dev/null +++ b/libs/common/src/platform/ipc/ipc-message.ts @@ -0,0 +1,10 @@ +import type { OutgoingMessage } from "@bitwarden/sdk-internal"; + +export interface IpcMessage { + type: "bitwarden-ipc-message"; + message: OutgoingMessage; +} + +export function isIpcMessage(message: any): message is IpcMessage { + return message.type === "bitwarden-ipc-message"; +} diff --git a/libs/common/src/platform/ipc/ipc.service.ts b/libs/common/src/platform/ipc/ipc.service.ts new file mode 100644 index 00000000000..7d2c7479be7 --- /dev/null +++ b/libs/common/src/platform/ipc/ipc.service.ts @@ -0,0 +1,51 @@ +import { Observable, shareReplay } from "rxjs"; + +import { IpcClient, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-internal"; + +export abstract class IpcService { + private _client?: IpcClient; + protected get client(): IpcClient { + if (!this._client) { + throw new Error("IpcService not initialized"); + } + return this._client; + } + + private _messages$?: Observable; + protected get messages$(): Observable { + if (!this._messages$) { + throw new Error("IpcService not initialized"); + } + return this._messages$; + } + + abstract init(): Promise; + + protected async initWithClient(client: IpcClient): Promise { + this._client = client; + this._messages$ = new Observable((subscriber) => { + let isSubscribed = true; + + const receiveLoop = async () => { + while (isSubscribed) { + try { + const message = await this.client.receive(); + subscriber.next(message); + } catch (error) { + subscriber.error(error); + break; + } + } + }; + void receiveLoop(); + + return () => { + isSubscribed = false; + }; + }).pipe(shareReplay({ bufferSize: 0, refCount: true })); + } + + async send(message: OutgoingMessage) { + await this.client.send(message); + } +} diff --git a/libs/common/src/platform/ipc/message-queue.spec.ts b/libs/common/src/platform/ipc/message-queue.spec.ts new file mode 100644 index 00000000000..9a9ebec1227 --- /dev/null +++ b/libs/common/src/platform/ipc/message-queue.spec.ts @@ -0,0 +1,48 @@ +import { MessageQueue } from "./message-queue"; + +type Message = symbol; + +describe("MessageQueue", () => { + let messageQueue!: MessageQueue; + + beforeEach(() => { + messageQueue = new MessageQueue(); + }); + + it("waits for a new message when queue is empty", async () => { + const message = createMessage(); + + // Start a promise to dequeue a message + let dequeuedValue: Message | undefined; + void messageQueue.dequeue().then((value) => { + dequeuedValue = value; + }); + + // No message is enqueued yet + expect(dequeuedValue).toBeUndefined(); + + // Enqueue a message + await messageQueue.enqueue(message); + + // Expect the message to be dequeued + await new Promise(process.nextTick); + expect(dequeuedValue).toBe(message); + }); + + it("returns existing message when queue is not empty", async () => { + const message = createMessage(); + + // Enqueue a message + await messageQueue.enqueue(message); + + // Dequeue the message + const dequeuedValue = await messageQueue.dequeue(); + + // Expect the message to be dequeued + expect(dequeuedValue).toBe(message); + }); +}); + +function createMessage(name?: string): symbol { + return Symbol(name); +} diff --git a/libs/common/src/platform/ipc/message-queue.ts b/libs/common/src/platform/ipc/message-queue.ts new file mode 100644 index 00000000000..8ab484141b0 --- /dev/null +++ b/libs/common/src/platform/ipc/message-queue.ts @@ -0,0 +1,20 @@ +import { firstValueFrom, Subject } from "rxjs"; + +export class MessageQueue { + private queue: T[] = []; + private messageAvailable$ = new Subject(); + + async enqueue(message: T): Promise { + this.queue.push(message); + this.messageAvailable$.next(); + } + + async dequeue(): Promise { + if (this.queue.length > 0) { + return this.queue.shift() as T; + } + + await firstValueFrom(this.messageAvailable$); + return this.queue.shift() as T; + } +} diff --git a/package-lock.json b/package-lock.json index 7d5d56b46bd..c880e5f60d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.124", + "@bitwarden/sdk-internal": "0.2.0-main.133", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", @@ -4699,9 +4699,10 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.124", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.124.tgz", - "integrity": "sha512-7F+DlPFng/thT4EVIQk2tRC7kff6G2B7alHAIxBdioJc9vE64Z5R5pviUyMZzqLnA5e9y8EnQdtWsQzUkHxisQ==" + "version": "0.2.0-main.133", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.133.tgz", + "integrity": "sha512-KzKJGf9cKlcQzfRmqkAwVGBN1kDpcRFkTMm7nrphZSrjfaWJWI1lBEJ0DhnkbMMHJXhQavGyoVk5TIn/Y8ylmw==", + "license": "GPL-3.0" }, "node_modules/@bitwarden/send-ui": { "resolved": "libs/tools/send/send-ui", diff --git a/package.json b/package.json index 051ff4f7251..f24588c4c82 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.124", + "@bitwarden/sdk-internal": "0.2.0-main.133", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2",