[PM-7489] Introduce `MessageSender` & `MessageListener` (#8709)

* Introduce MessageSender

* Update `messageSenderFactory`

* Remove Comment

* Use BrowserApi

* Update Comment

* Rename to CommandDefinition

* Add More Documentation to MessageSender

* Add `EMPTY` helpers and remove NoopMessageSender

* Calm Down Logging

* Limit Logging On Known Errors

* Use `messageStream` Parameter

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Add eslint rules

* Update Error Handling

Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com>

* Delete Lazy Classes In Favor of Observable Factories

* Remove Fido Messages

---------

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
Co-authored-by: Cesar Gonzalez <cesar.a.gonzalezcs@gmail.com>
This commit is contained in:
Justin Baur 2024-04-19 14:02:40 -05:00 committed by GitHub
parent 9a4279c8bb
commit 395ed3f5d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 855 additions and 361 deletions

View File

@ -246,6 +246,22 @@
}
]
}
},
{
"files": ["**/*.ts"],
"excludedFiles": ["**/platform/**/*.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
"**/platform/**/internal", // General internal pattern
// All features that have been converted to barrel files
"**/platform/messaging/**"
]
}
]
}
}
]
}

View File

@ -1,4 +1,4 @@
import { firstValueFrom } from "rxjs";
import { Subject, firstValueFrom, merge } from "rxjs";
import {
PinCryptoServiceAbstraction,
@ -82,7 +82,6 @@ import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/co
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
AbstractMemoryStorageService,
@ -95,6 +94,9 @@ import {
DefaultBiometricStateService,
} from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
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";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
@ -208,13 +210,14 @@ import { Account } from "../models/account";
import { BrowserApi } from "../platform/browser/browser-api";
import { flagEnabled } from "../platform/flags";
import { UpdateBadge } from "../platform/listeners/update-badge";
/* eslint-disable no-restricted-imports */
import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender";
/* eslint-enable no-restricted-imports */
import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service";
import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service";
import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service";
import BrowserMessagingService from "../platform/services/browser-messaging.service";
import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service";
import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service";
import I18nService from "../platform/services/i18n.service";
@ -223,6 +226,7 @@ import { BackgroundPlatformUtilsService } from "../platform/services/platform-ut
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider";
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging";
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
import FilelessImporterBackground from "../tools/background/fileless-importer.background";
import { Fido2Background as Fido2BackgroundAbstraction } from "../vault/fido2/background/abstractions/fido2.background";
@ -236,7 +240,7 @@ import { NativeMessagingBackground } from "./nativeMessaging.background";
import RuntimeBackground from "./runtime.background";
export default class MainBackground {
messagingService: MessagingServiceAbstraction;
messagingService: MessageSender;
storageService: BrowserLocalStorageService;
secureStorageService: AbstractStorageService;
memoryStorageService: AbstractMemoryStorageService;
@ -326,6 +330,8 @@ export default class MainBackground {
stateEventRunnerService: StateEventRunnerService;
ssoLoginService: SsoLoginServiceAbstraction;
billingAccountProfileStateService: BillingAccountProfileStateService;
// eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module
intraprocessMessagingSubject: Subject<Message<object>>;
userKeyInitService: UserKeyInitService;
scriptInjectorService: BrowserScriptInjectorService;
@ -369,15 +375,25 @@ export default class MainBackground {
const logoutCallback = async (expired: boolean, userId?: UserId) =>
await this.logout(expired, userId);
this.messagingService =
this.isPrivateMode && BrowserApi.isManifestVersion(2)
? new BrowserMessagingPrivateModeBackgroundService()
: new BrowserMessagingService();
this.logService = new ConsoleLogService(false);
this.cryptoFunctionService = new WebCryptoFunctionService(self);
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);
this.storageService = new BrowserLocalStorageService();
this.intraprocessMessagingSubject = new Subject<Message<object>>();
this.messagingService = MessageSender.combine(
new SubjectMessageSender(this.intraprocessMessagingSubject),
new ChromeMessageSender(this.logService),
);
const messageListener = new MessageListener(
merge(
this.intraprocessMessagingSubject.asObservable(), // For messages from the same context
fromChromeRuntimeMessaging(), // For messages from other contexts
),
);
const mv3MemoryStorageCreator = (partitionName: string) => {
// TODO: Consider using multithreaded encrypt service in popup only context
return new LocalBackedSessionStorageService(
@ -560,21 +576,6 @@ export default class MainBackground {
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
// eslint-disable-next-line
const that = this;
const backgroundMessagingService = new (class extends MessagingServiceAbstraction {
// AuthService should send the messages to the background not popup.
send = (subscriber: string, arg: any = {}) => {
if (BrowserApi.isManifestVersion(3)) {
that.messagingService.send(subscriber, arg);
return;
}
const message = Object.assign({}, { command: subscriber }, arg);
void that.runtimeBackground.processMessage(message, that as any);
};
})();
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
@ -605,7 +606,7 @@ export default class MainBackground {
this.authService = new AuthService(
this.accountService,
backgroundMessagingService,
this.messagingService,
this.cryptoService,
this.apiService,
this.stateService,
@ -626,7 +627,7 @@ export default class MainBackground {
this.tokenService,
this.appIdService,
this.platformUtilsService,
backgroundMessagingService,
this.messagingService,
this.logService,
this.keyConnectorService,
this.environmentService,
@ -914,6 +915,7 @@ export default class MainBackground {
this.logService,
this.configService,
this.fido2Background,
messageListener,
);
this.nativeMessagingBackground = new NativeMessagingBackground(
this.accountService,

View File

@ -399,7 +399,7 @@ export class NativeMessagingBackground {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.runtimeBackground.processMessage({ command: "unlocked" }, null);
this.runtimeBackground.processMessage({ command: "unlocked" });
}
break;
}

View File

@ -1,4 +1,4 @@
import { firstValueFrom } from "rxjs";
import { firstValueFrom, mergeMap } from "rxjs";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
@ -10,6 +10,7 @@ import { SystemService } from "@bitwarden/common/platform/abstractions/system.se
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherType } from "@bitwarden/common/vault/enums";
import { MessageListener } from "../../../../libs/common/src/platform/messaging";
import {
closeUnlockPopout,
openSsoAuthResultPopout,
@ -44,6 +45,7 @@ export default class RuntimeBackground {
private logService: LogService,
private configService: ConfigService,
private fido2Background: Fido2Background,
private messageListener: MessageListener,
) {
// onInstalled listener must be wired up before anything else, so we do it in the ctor
chrome.runtime.onInstalled.addListener((details: any) => {
@ -60,92 +62,47 @@ export default class RuntimeBackground {
const backgroundMessageListener = (
msg: any,
sender: chrome.runtime.MessageSender,
sendResponse: any,
sendResponse: (response: any) => void,
) => {
const messagesWithResponse = ["biometricUnlock"];
if (messagesWithResponse.includes(msg.command)) {
this.processMessage(msg, sender).then(
this.processMessageWithSender(msg, sender).then(
(value) => sendResponse({ result: value }),
(error) => sendResponse({ error: { ...error, message: error.message } }),
);
return true;
}
this.processMessage(msg, sender).catch((e) => this.logService.error(e));
void this.processMessageWithSender(msg, sender).catch((err) =>
this.logService.error(
`Error while processing message in RuntimeBackground '${msg?.command}'. Error: ${err?.message ?? "Unknown Error"}`,
),
);
return false;
};
this.messageListener.allMessages$
.pipe(
mergeMap(async (message: any) => {
await this.processMessage(message);
}),
)
.subscribe();
// For messages that require the full on message interface
BrowserApi.messageListener("runtime.background", backgroundMessageListener);
if (this.main.popupOnlyContext) {
(self as any).bitwardenBackgroundMessageListener = backgroundMessageListener;
}
}
async processMessage(msg: any, sender: chrome.runtime.MessageSender) {
// Messages that need the chrome sender and send back a response need to be registered in this method.
async processMessageWithSender(msg: any, sender: chrome.runtime.MessageSender) {
switch (msg.command) {
case "loggedIn":
case "unlocked": {
let item: LockedVaultPendingNotificationsData;
if (msg.command === "loggedIn") {
await this.sendBwInstalledMessageToVault();
}
if (this.lockedVaultPendingNotifications?.length > 0) {
item = this.lockedVaultPendingNotifications.pop();
await closeUnlockPopout();
}
await this.notificationsService.updateConnection(msg.command === "loggedIn");
await this.main.refreshBadge();
await this.main.refreshMenu(false);
this.systemService.cancelProcessReload();
if (item) {
await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId);
await BrowserApi.focusTab(item.commandToRetry.sender.tab.id);
await BrowserApi.tabSendMessageData(
item.commandToRetry.sender.tab,
"unlockCompleted",
item,
);
}
break;
}
case "addToLockedVaultPendingNotifications":
this.lockedVaultPendingNotifications.push(msg.data);
break;
case "logout":
await this.main.logout(msg.expired, msg.userId);
break;
case "syncCompleted":
if (msg.successfully) {
setTimeout(async () => {
await this.main.refreshBadge();
await this.main.refreshMenu();
}, 2000);
await this.configService.ensureConfigFetched();
}
break;
case "openPopup":
await this.main.openPopup();
break;
case "triggerAutofillScriptInjection":
await this.autofillService.injectAutofillScripts(sender.tab, sender.frameId);
break;
case "bgCollectPageDetails":
await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId);
break;
case "bgUpdateContextMenu":
case "editedCipher":
case "addedCipher":
case "deletedCipher":
await this.main.refreshBadge();
await this.main.refreshMenu();
break;
case "bgReseedStorage":
await this.main.reseedStorage();
break;
case "collectPageDetailsResponse":
switch (msg.sender) {
case "autofiller":
@ -209,6 +166,72 @@ export default class RuntimeBackground {
break;
}
break;
case "biometricUnlock": {
const result = await this.main.biometricUnlock();
return result;
}
}
}
async processMessage(msg: any) {
switch (msg.command) {
case "loggedIn":
case "unlocked": {
let item: LockedVaultPendingNotificationsData;
if (msg.command === "loggedIn") {
await this.sendBwInstalledMessageToVault();
}
if (this.lockedVaultPendingNotifications?.length > 0) {
item = this.lockedVaultPendingNotifications.pop();
await closeUnlockPopout();
}
await this.notificationsService.updateConnection(msg.command === "loggedIn");
await this.main.refreshBadge();
await this.main.refreshMenu(false);
this.systemService.cancelProcessReload();
if (item) {
await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId);
await BrowserApi.focusTab(item.commandToRetry.sender.tab.id);
await BrowserApi.tabSendMessageData(
item.commandToRetry.sender.tab,
"unlockCompleted",
item,
);
}
break;
}
case "addToLockedVaultPendingNotifications":
this.lockedVaultPendingNotifications.push(msg.data);
break;
case "logout":
await this.main.logout(msg.expired, msg.userId);
break;
case "syncCompleted":
if (msg.successfully) {
setTimeout(async () => {
await this.main.refreshBadge();
await this.main.refreshMenu();
}, 2000);
await this.configService.ensureConfigFetched();
}
break;
case "openPopup":
await this.main.openPopup();
break;
case "bgUpdateContextMenu":
case "editedCipher":
case "addedCipher":
case "deletedCipher":
await this.main.refreshBadge();
await this.main.refreshMenu();
break;
case "bgReseedStorage":
await this.main.reseedStorage();
break;
case "authResult": {
const env = await firstValueFrom(this.environmentService.environment$);
const vaultUrl = env.getWebVaultUrl();
@ -265,9 +288,6 @@ export default class RuntimeBackground {
await this.main.clearClipboard(msg.clipboardValue, msg.timeoutMs);
break;
}
case "biometricUnlock": {
return await this.main.biometricUnlock();
}
}
}

View File

@ -0,0 +1,17 @@
import { MessageSender } from "@bitwarden/common/platform/messaging";
import { CachedServices, factory, FactoryOptions } from "./factory-options";
type MessagingServiceFactoryOptions = FactoryOptions;
export type MessageSenderInitOptions = MessagingServiceFactoryOptions;
export function messageSenderFactory(
cache: { messagingService?: MessageSender } & CachedServices,
opts: MessageSenderInitOptions,
): Promise<MessageSender> {
// NOTE: Name needs to match that of MainBackground property until we delete these.
return factory(cache, "messagingService", opts, () => {
throw new Error("Not implemented, not expected to be used.");
});
}

View File

@ -1,19 +1,5 @@
import { MessagingService as AbstractMessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../background/service-factories/factory-options";
import BrowserMessagingService from "../../services/browser-messaging.service";
type MessagingServiceFactoryOptions = FactoryOptions;
export type MessagingServiceInitOptions = MessagingServiceFactoryOptions;
export function messagingServiceFactory(
cache: { messagingService?: AbstractMessagingService } & CachedServices,
opts: MessagingServiceInitOptions,
): Promise<AbstractMessagingService> {
return factory(cache, "messagingService", opts, () => new BrowserMessagingService());
}
// Export old messaging service stuff to minimize changes
export {
messageSenderFactory as messagingServiceFactory,
MessageSenderInitOptions as MessagingServiceInitOptions,
} from "./message-sender.factory";

View File

@ -0,0 +1,37 @@
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CommandDefinition, MessageSender } from "@bitwarden/common/platform/messaging";
import { getCommand } from "@bitwarden/common/platform/messaging/internal";
type ErrorHandler = (logger: LogService, command: string) => void;
const HANDLED_ERRORS: Record<string, ErrorHandler> = {
"Could not establish connection. Receiving end does not exist.": (logger, command) =>
logger.debug(`Receiving end didn't exist for command '${command}'`),
"The message port closed before a response was received.": (logger, command) =>
logger.debug(`Port was closed for command '${command}'`),
};
export class ChromeMessageSender implements MessageSender {
constructor(private readonly logService: LogService) {}
send<T extends object>(
commandDefinition: string | CommandDefinition<T>,
payload: object | T = {},
): void {
const command = getCommand(commandDefinition);
chrome.runtime.sendMessage(Object.assign(payload, { command: command }), () => {
if (chrome.runtime.lastError) {
const errorHandler = HANDLED_ERRORS[chrome.runtime.lastError.message];
if (errorHandler != null) {
errorHandler(this.logService, command);
return;
}
this.logService.warning(
`Unhandled error while sending message with command '${command}': ${chrome.runtime.lastError.message}`,
);
}
});
}
}

View File

@ -1,8 +0,0 @@
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
export default class BrowserMessagingPrivateModeBackgroundService implements MessagingService {
send(subscriber: string, arg: any = {}) {
const message = Object.assign({}, { command: subscriber }, arg);
(self as any).bitwardenPopupMainMessageListener(message);
}
}

View File

@ -1,8 +0,0 @@
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
export default class BrowserMessagingPrivateModePopupService implements MessagingService {
send(subscriber: string, arg: any = {}) {
const message = Object.assign({}, { command: subscriber }, arg);
(self as any).bitwardenBackgroundMessageListener(message);
}
}

View File

@ -1,9 +0,0 @@
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { BrowserApi } from "../browser/browser-api";
export default class BrowserMessagingService implements MessagingService {
send(subscriber: string, arg: any = {}) {
return BrowserApi.sendMessage(subscriber, arg);
}
}

View File

@ -0,0 +1,26 @@
import { map, share } from "rxjs";
import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal";
import { fromChromeEvent } from "../browser/from-chrome-event";
/**
* Creates an observable that listens to messages through `chrome.runtime.onMessage`.
* @returns An observable stream of messages.
*/
export const fromChromeRuntimeMessaging = () => {
return fromChromeEvent(chrome.runtime.onMessage).pipe(
map(([message, sender]) => {
message ??= {};
// Force the sender onto the message as long as we won't overwrite anything
if (!("webExtSender" in message)) {
message.webExtSender = sender;
}
return message;
}),
tagAsExternal,
share(),
);
};

View File

@ -1,17 +1,16 @@
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
import { filter, concatMap, Subject, takeUntil, firstValueFrom, map } from "rxjs";
import { filter, concatMap, Subject, takeUntil, firstValueFrom, tap, map } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { MessageListener } from "@bitwarden/common/platform/messaging";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
import { BrowserApi } from "../platform/browser/browser-api";
import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service";
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service";
import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service";
@ -34,7 +33,6 @@ export class AppComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
constructor(
private broadcasterService: BroadcasterService,
private authService: AuthService,
private i18nService: I18nService,
private router: Router,
@ -46,7 +44,7 @@ export class AppComponent implements OnInit, OnDestroy {
private ngZone: NgZone,
private platformUtilsService: PlatformUtilsService,
private dialogService: DialogService,
private browserMessagingApi: ZonedMessageListenerService,
private messageListener: MessageListener,
private toastService: ToastService,
) {}
@ -78,77 +76,76 @@ export class AppComponent implements OnInit, OnDestroy {
window.onkeypress = () => this.recordActivity();
});
const bitwardenPopupMainMessageListener = (msg: any, sender: any) => {
if (msg.command === "doneLoggingOut") {
this.authService.logOut(async () => {
if (msg.expired) {
this.toastService.showToast({
variant: "warning",
title: this.i18nService.t("loggedOut"),
message: this.i18nService.t("loginExpired"),
this.messageListener.allMessages$
.pipe(
tap((msg: any) => {
if (msg.command === "doneLoggingOut") {
this.authService.logOut(async () => {
if (msg.expired) {
this.toastService.showToast({
variant: "warning",
title: this.i18nService.t("loggedOut"),
message: this.i18nService.t("loginExpired"),
});
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["home"]);
});
this.changeDetectorRef.detectChanges();
} else if (msg.command === "authBlocked") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["home"]);
} else if (
msg.command === "locked" &&
(msg.userId == null || msg.userId == this.activeUserId)
) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["lock"]);
} else if (msg.command === "showDialog") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.showDialog(msg);
} else if (msg.command === "showNativeMessagingFinterprintDialog") {
// TODO: Should be refactored to live in another service.
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.showNativeMessagingFingerprintDialog(msg);
} else if (msg.command === "showToast") {
this.toastService._showToast(msg);
} else if (msg.command === "reloadProcess") {
const forceWindowReload =
this.platformUtilsService.isSafari() ||
this.platformUtilsService.isFirefox() ||
this.platformUtilsService.isOpera();
// Wait to make sure background has reloaded first.
window.setTimeout(
() => BrowserApi.reloadExtension(forceWindowReload ? window : null),
2000,
);
} else if (msg.command === "reloadPopup") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/"]);
} else if (msg.command === "convertAccountToKeyConnector") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/remove-password"]);
} else if (msg.command === "switchAccountFinish") {
// TODO: unset loading?
// this.loading = false;
} else if (msg.command == "update-temp-password") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/update-temp-password"]);
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["home"]);
});
this.changeDetectorRef.detectChanges();
} else if (msg.command === "authBlocked") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["home"]);
} else if (
msg.command === "locked" &&
(msg.userId == null || msg.userId == this.activeUserId)
) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["lock"]);
} else if (msg.command === "showDialog") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.showDialog(msg);
} else if (msg.command === "showNativeMessagingFinterprintDialog") {
// TODO: Should be refactored to live in another service.
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.showNativeMessagingFingerprintDialog(msg);
} else if (msg.command === "showToast") {
this.toastService._showToast(msg);
} else if (msg.command === "reloadProcess") {
const forceWindowReload =
this.platformUtilsService.isSafari() ||
this.platformUtilsService.isFirefox() ||
this.platformUtilsService.isOpera();
// Wait to make sure background has reloaded first.
window.setTimeout(
() => BrowserApi.reloadExtension(forceWindowReload ? window : null),
2000,
);
} else if (msg.command === "reloadPopup") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/"]);
} else if (msg.command === "convertAccountToKeyConnector") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/remove-password"]);
} else if (msg.command === "switchAccountFinish") {
// TODO: unset loading?
// this.loading = false;
} else if (msg.command == "update-temp-password") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/update-temp-password"]);
} else {
msg.webExtSender = sender;
this.broadcasterService.send(msg);
}
};
(self as any).bitwardenPopupMainMessageListener = bitwardenPopupMainMessageListener;
this.browserMessagingApi.messageListener("app.component", bitwardenPopupMainMessageListener);
}),
takeUntil(this.destroy$),
)
.subscribe();
// eslint-disable-next-line rxjs/no-async-subscribe
this.router.events.pipe(takeUntil(this.destroy$)).subscribe(async (event) => {

View File

@ -1,5 +1,6 @@
import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
import { Router } from "@angular/router";
import { Subject, merge } from "rxjs";
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards";
import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service";
@ -11,6 +12,7 @@ import {
OBSERVABLE_MEMORY_STORAGE,
SYSTEM_THEME_OBSERVABLE,
SafeInjectionToken,
INTRAPROCESS_MESSAGING_SUBJECT,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import {
@ -54,7 +56,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
import {
@ -63,6 +64,9 @@ import {
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
@ -89,20 +93,23 @@ import AutofillService from "../../autofill/services/autofill.service";
import MainBackground from "../../background/main.background";
import { Account } from "../../models/account";
import { BrowserApi } from "../../platform/browser/browser-api";
import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator";
/* eslint-disable no-restricted-imports */
import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sender";
/* eslint-enable no-restricted-imports */
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service";
import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service";
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service";
import BrowserMessagingService from "../../platform/services/browser-messaging.service";
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service";
import I18nService from "../../platform/services/i18n.service";
import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service";
import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider";
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service";
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service";
@ -155,15 +162,6 @@ const safeProviders: SafeProvider[] = [
useClass: UnauthGuardService,
deps: [AuthServiceAbstraction, Router],
}),
safeProvider({
provide: MessagingService,
useFactory: () => {
return needsBackgroundInit && BrowserApi.isManifestVersion(2)
? new BrowserMessagingPrivateModePopupService()
: new BrowserMessagingService();
},
deps: [],
}),
safeProvider({
provide: TwoFactorService,
useFactory: getBgService<TwoFactorService>("twoFactorService"),
@ -484,6 +482,65 @@ const safeProviders: SafeProvider[] = [
useClass: BrowserSendStateService,
deps: [StateProvider],
}),
safeProvider({
provide: MessageListener,
useFactory: (subject: Subject<Message<object>>, ngZone: NgZone) =>
new MessageListener(
merge(
subject.asObservable(), // For messages in the same context
fromChromeRuntimeMessaging().pipe(runInsideAngular(ngZone)), // For messages in the same context
),
),
deps: [INTRAPROCESS_MESSAGING_SUBJECT, NgZone],
}),
safeProvider({
provide: MessageSender,
useFactory: (subject: Subject<Message<object>>, logService: LogService) =>
MessageSender.combine(
new SubjectMessageSender(subject), // For sending messages in the same context
new ChromeMessageSender(logService), // For sending messages to different contexts
),
deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService],
}),
safeProvider({
provide: INTRAPROCESS_MESSAGING_SUBJECT,
useFactory: () => {
if (BrowserPopupUtils.backgroundInitializationRequired()) {
// There is no persistent main background which means we have one in memory,
// we need the same instance that our in memory background is utilizing.
return getBgService("intraprocessMessagingSubject")();
} else {
return new Subject<Message<object>>();
}
},
deps: [],
}),
safeProvider({
provide: MessageSender,
useFactory: (subject: Subject<Message<object>>, logService: LogService) =>
MessageSender.combine(
new SubjectMessageSender(subject), // For sending messages in the same context
new ChromeMessageSender(logService), // For sending messages to different contexts
),
deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService],
}),
safeProvider({
provide: INTRAPROCESS_MESSAGING_SUBJECT,
useFactory: () => {
if (needsBackgroundInit) {
// We will have created a popup within this context, in that case
// we want to make sure we have the same subject as that context so we
// can message with it.
return getBgService("intraprocessMessagingSubject")();
} else {
// There isn't a locally created background so we will communicate with
// the true background through chrome apis, in that case, we can just create
// one for ourself.
return new Subject<Message<object>>();
}
},
deps: [],
}),
];
@NgModule({

View File

@ -60,10 +60,10 @@ import {
} from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { MessageSender } from "@bitwarden/common/platform/messaging";
import { Account } from "@bitwarden/common/platform/models/domain/account";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service";
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
@ -75,7 +75,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
import { StateService } from "@bitwarden/common/platform/services/state.service";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import {
@ -155,7 +154,7 @@ global.DOMParser = new jsdom.JSDOM().window.DOMParser;
const packageJson = require("../package.json");
export class Main {
messagingService: NoopMessagingService;
messagingService: MessageSender;
storageService: LowdbStorageService;
secureStorageService: NodeEnvSecureStorageService;
memoryStorageService: MemoryStorageService;
@ -212,7 +211,6 @@ export class Main {
organizationService: OrganizationService;
providerService: ProviderService;
twoFactorService: TwoFactorService;
broadcasterService: BroadcasterService;
folderApiService: FolderApiService;
userVerificationApiService: UserVerificationApiService;
organizationApiService: OrganizationApiServiceAbstraction;
@ -298,7 +296,7 @@ export class Main {
stateEventRegistrarService,
);
this.messagingService = new NoopMessagingService();
this.messagingService = MessageSender.EMPTY;
this.accountService = new AccountServiceImplementation(
this.messagingService,
@ -422,8 +420,6 @@ export class Main {
this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider);
this.broadcasterService = new BroadcasterService();
this.collectionService = new CollectionService(
this.cryptoService,
this.i18nService,

View File

@ -1,4 +1,5 @@
import { APP_INITIALIZER, NgModule } from "@angular/core";
import { Subject, merge } from "rxjs";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import {
@ -14,6 +15,7 @@ import {
SYSTEM_THEME_OBSERVABLE,
SafeInjectionToken,
STATE_FACTORY,
INTRAPROCESS_MESSAGING_SUBJECT,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
@ -23,7 +25,6 @@ import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/ab
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@ -42,6 +43,9 @@ import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
@ -63,11 +67,12 @@ import {
ELECTRON_SUPPORTS_SECURE_STORAGE,
ElectronPlatformUtilsService,
} from "../../platform/services/electron-platform-utils.service";
import { ElectronRendererMessagingService } from "../../platform/services/electron-renderer-messaging.service";
import { ElectronRendererMessageSender } from "../../platform/services/electron-renderer-message.sender";
import { ElectronRendererSecureStorageService } from "../../platform/services/electron-renderer-secure-storage.service";
import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service";
import { ElectronStateService } from "../../platform/services/electron-state.service";
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging";
import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme";
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
import { NativeMessageHandlerService } from "../../services/native-message-handler.service";
@ -138,9 +143,24 @@ const safeProviders: SafeProvider[] = [
deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider],
}),
safeProvider({
provide: MessagingServiceAbstraction,
useClass: ElectronRendererMessagingService,
deps: [BroadcasterServiceAbstraction],
provide: MessageSender,
useFactory: (subject: Subject<Message<object>>) =>
MessageSender.combine(
new ElectronRendererMessageSender(), // Communication with main process
new SubjectMessageSender(subject), // Communication with ourself
),
deps: [INTRAPROCESS_MESSAGING_SUBJECT],
}),
safeProvider({
provide: MessageListener,
useFactory: (subject: Subject<Message<object>>) =>
new MessageListener(
merge(
subject.asObservable(), // For messages from the same context
fromIpcMessaging(), // For messages from the main process
),
),
deps: [INTRAPROCESS_MESSAGING_SUBJECT],
}),
safeProvider({
provide: AbstractStorageService,

View File

@ -1,7 +1,7 @@
import * as path from "path";
import { app } from "electron";
import { firstValueFrom } from "rxjs";
import { Subject, firstValueFrom } from "rxjs";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
@ -11,6 +11,9 @@ import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwar
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DefaultBiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { Message, MessageSender } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- For dependency creation
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
@ -18,7 +21,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service";
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
@ -59,7 +61,7 @@ export class Main {
storageService: ElectronStorageService;
memoryStorageService: MemoryStorageService;
memoryStorageForStateProviders: MemoryStorageServiceForStateProviders;
messagingService: ElectronMainMessagingService;
messagingService: MessageSender;
stateService: StateService;
environmentService: DefaultEnvironmentService;
mainCryptoFunctionService: MainCryptoFunctionService;
@ -131,7 +133,7 @@ export class Main {
this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider);
const accountService = new AccountServiceImplementation(
new NoopMessagingService(),
MessageSender.EMPTY,
this.logService,
globalStateProvider,
);
@ -223,7 +225,13 @@ export class Main {
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain);
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService);
this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => {
const messageSubject = new Subject<Message<object>>();
this.messagingService = MessageSender.combine(
new SubjectMessageSender(messageSubject), // For local messages
new ElectronMainMessagingService(this.windowMain),
);
messageSubject.asObservable().subscribe((message) => {
this.messagingMain.onMessage(message);
});

View File

@ -1,6 +1,7 @@
import { powerMonitor } from "electron";
import { ElectronMainMessagingService } from "../services/electron-main-messaging.service";
import { MessageSender } from "@bitwarden/common/platform/messaging";
import { isSnapStore } from "../utils";
// tslint:disable-next-line
@ -10,7 +11,7 @@ const IdleCheckInterval = 30 * 1000; // 30 seconds
export class PowerMonitorMain {
private idle = false;
constructor(private messagingService: ElectronMainMessagingService) {}
constructor(private messagingService: MessageSender) {}
init() {
// ref: https://github.com/electron/electron/issues/13767

View File

@ -124,12 +124,21 @@ export default {
sendMessage: (message: { command: string } & any) =>
ipcRenderer.send("messagingService", message),
onMessage: (callback: (message: { command: string } & any) => void) => {
ipcRenderer.on("messagingService", (_event, message: any) => {
if (message.command) {
callback(message);
}
});
onMessage: {
addListener: (callback: (message: { command: string } & any) => void) => {
ipcRenderer.addListener("messagingService", (_event, message: any) => {
if (message.command) {
callback(message);
}
});
},
removeListener: (callback: (message: { command: string } & any) => void) => {
ipcRenderer.removeListener("messagingService", (_event, message: any) => {
if (message.command) {
callback(message);
}
});
},
},
launchUri: (uri: string) => ipcRenderer.invoke("launchUri", uri),

View File

@ -0,0 +1,12 @@
import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/messaging";
import { getCommand } from "@bitwarden/common/platform/messaging/internal";
export class ElectronRendererMessageSender implements MessageSender {
send<T extends object>(
commandDefinition: CommandDefinition<T> | string,
payload: object | T = {},
): void {
const command = getCommand(commandDefinition);
ipc.platform.sendMessage(Object.assign({}, { command: command }, payload));
}
}

View File

@ -1,20 +0,0 @@
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
export class ElectronRendererMessagingService implements MessagingService {
constructor(private broadcasterService: BroadcasterService) {
ipc.platform.onMessage((message) => this.sendMessage(message.command, message, false));
}
send(subscriber: string, arg: any = {}) {
this.sendMessage(subscriber, arg, true);
}
private sendMessage(subscriber: string, arg: any = {}, toMain: boolean) {
const message = Object.assign({}, { command: subscriber }, arg);
this.broadcasterService.send(message);
if (toMain) {
ipc.platform.sendMessage(message);
}
}
}

View File

@ -0,0 +1,15 @@
import { fromEventPattern, share } from "rxjs";
import { Message } from "@bitwarden/common/platform/messaging";
import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal";
/**
* Creates an observable that when subscribed to will listen to messaging events through IPC.
* @returns An observable stream of messages.
*/
export const fromIpcMessaging = () => {
return fromEventPattern<Message<object>>(
(handler) => ipc.platform.onMessage.addListener(handler),
(handler) => ipc.platform.onMessage.removeListener(handler),
).pipe(tagAsExternal, share());
};

View File

@ -2,18 +2,17 @@ import * as path from "path";
import { app, dialog, ipcMain, Menu, MenuItem, nativeTheme, Notification, shell } from "electron";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- Using implementation helper in implementation
import { getCommand } from "@bitwarden/common/platform/messaging/internal";
import { SafeUrls } from "@bitwarden/common/platform/misc/safe-urls";
import { WindowMain } from "../main/window.main";
import { RendererMenuItem } from "../utils";
export class ElectronMainMessagingService implements MessagingService {
constructor(
private windowMain: WindowMain,
private onMessage: (message: any) => void,
) {
export class ElectronMainMessagingService implements MessageSender {
constructor(private windowMain: WindowMain) {
ipcMain.handle("appVersion", () => {
return app.getVersion();
});
@ -88,9 +87,9 @@ export class ElectronMainMessagingService implements MessagingService {
});
}
send(subscriber: string, arg: any = {}) {
const message = Object.assign({}, { command: subscriber }, arg);
this.onMessage(message);
send<T extends object>(commandDefinition: CommandDefinition<T> | string, arg: T | object = {}) {
const command = getCommand(commandDefinition);
const message = Object.assign({}, { command: command }, arg);
if (this.windowMain.win != null) {
this.windowMain.win.webContents.send("messagingService", message);
}

View File

@ -103,7 +103,8 @@ export class TrialBillingStepComponent implements OnInit {
planDescription,
});
this.messagingService.send("organizationCreated", organizationId);
// TODO: No one actually listening to this?
this.messagingService.send("organizationCreated", { organizationId });
}
protected changedCountry() {

View File

@ -587,7 +587,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.formPromise = doSubmit();
const organizationId = await this.formPromise;
this.onSuccess.emit({ organizationId: organizationId });
this.messagingService.send("organizationCreated", organizationId);
// TODO: No one actually listening to this message?
this.messagingService.send("organizationCreated", { organizationId });
} catch (e) {
this.logService.error(e);
}

View File

@ -1,14 +0,0 @@
import { Injectable } from "@angular/core";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@Injectable()
export class BroadcasterMessagingService implements MessagingService {
constructor(private broadcasterService: BroadcasterService) {}
send(subscriber: string, arg: any = {}) {
const message = Object.assign({}, { command: subscriber }, arg);
this.broadcasterService.send(message);
}
}

View File

@ -22,7 +22,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
@ -51,7 +50,6 @@ import { WebStorageServiceProvider } from "../platform/web-storage-service.provi
import { WindowStorageService } from "../platform/window-storage.service";
import { CollectionAdminService } from "../vault/core/collection-admin.service";
import { BroadcasterMessagingService } from "./broadcaster-messaging.service";
import { EventService } from "./event.service";
import { InitService } from "./init.service";
import { ModalService } from "./modal.service";
@ -117,11 +115,6 @@ const safeProviders: SafeProvider[] = [
useClass: WebPlatformUtilsService,
useAngularDecorators: true,
}),
safeProvider({
provide: MessagingServiceAbstraction,
useClass: BroadcasterMessagingService,
useAngularDecorators: true,
}),
safeProvider({
provide: ModalServiceAbstraction,
useClass: ModalService,

View File

@ -4,15 +4,15 @@ import { of } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { MessageSender } from "@bitwarden/common/platform/messaging";
import { BadgeModule, I18nMockService } from "@bitwarden/components";
import { PremiumBadgeComponent } from "./premium-badge.component";
class MockMessagingService implements MessagingService {
send(subscriber: string, arg?: any) {
class MockMessagingService implements MessageSender {
send = () => {
alert("Clicked on badge");
}
};
}
export default {
@ -31,7 +31,7 @@ export default {
},
},
{
provide: MessagingService,
provide: MessageSender,
useFactory: () => {
return new MockMessagingService();
},

View File

@ -1,6 +0,0 @@
import { Injectable } from "@angular/core";
import { BroadcasterService as BaseBroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service";
@Injectable()
export class BroadcasterService extends BaseBroadcasterService {}

View File

@ -1,5 +1,5 @@
import { InjectionToken } from "@angular/core";
import { Observable } from "rxjs";
import { Observable, Subject } from "rxjs";
import {
AbstractMemoryStorageService,
@ -8,6 +8,7 @@ import {
} from "@bitwarden/common/platform/abstractions/storage.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { Message } from "@bitwarden/common/platform/messaging";
declare const tag: unique symbol;
/**
@ -49,3 +50,6 @@ export const LOG_MAC_FAILURES = new SafeInjectionToken<boolean>("LOG_MAC_FAILURE
export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken<Observable<ThemeType>>(
"SYSTEM_THEME_OBSERVABLE",
);
export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken<Subject<Message<object>>>(
"INTRAPROCESS_MESSAGING_SUBJECT",
);

View File

@ -1,4 +1,5 @@
import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core";
import { Subject } from "rxjs";
import {
AuthRequestServiceAbstraction,
@ -116,7 +117,7 @@ import { BillingApiService } from "@bitwarden/common/billing/services/billing-ap
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services/payment-method-warnings.service";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -137,6 +138,9 @@ import {
DefaultBiometricStateService,
} from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
import { devFlagEnabled, flagEnabled } from "@bitwarden/common/platform/misc/flags";
import { Account } from "@bitwarden/common/platform/models/domain/account";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
@ -147,6 +151,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service";
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
@ -247,7 +252,6 @@ import {
import { AuthGuard } from "../auth/guards/auth.guard";
import { UnauthGuard } from "../auth/guards/unauth.guard";
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
import { BroadcasterService } from "../platform/services/broadcaster.service";
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
import { LoggingErrorHandler } from "../platform/services/logging-error-handler";
import { AngularThemingService } from "../platform/services/theming/angular-theming.service";
@ -270,6 +274,7 @@ import {
SYSTEM_LANGUAGE,
SYSTEM_THEME_OBSERVABLE,
WINDOW,
INTRAPROCESS_MESSAGING_SUBJECT,
} from "./injection-tokens";
import { ModalService } from "./modal.service";
@ -625,7 +630,11 @@ const safeProviders: SafeProvider[] = [
BillingAccountProfileStateService,
],
}),
safeProvider({ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService, deps: [] }),
safeProvider({
provide: BroadcasterService,
useClass: DefaultBroadcasterService,
deps: [MessageSender, MessageListener],
}),
safeProvider({
provide: VaultTimeoutSettingsServiceAbstraction,
useClass: VaultTimeoutSettingsService,
@ -1127,6 +1136,21 @@ const safeProviders: SafeProvider[] = [
useClass: LoggingErrorHandler,
deps: [],
}),
safeProvider({
provide: INTRAPROCESS_MESSAGING_SUBJECT,
useFactory: () => new Subject<Message<object>>(),
deps: [],
}),
safeProvider({
provide: MessageListener,
useFactory: (subject: Subject<Message<object>>) => new MessageListener(subject.asObservable()),
deps: [INTRAPROCESS_MESSAGING_SUBJECT],
}),
safeProvider({
provide: MessageSender,
useFactory: (subject: Subject<Message<object>>) => new SubjectMessageSender(subject),
deps: [INTRAPROCESS_MESSAGING_SUBJECT],
}),
safeProvider({
provide: ProviderApiServiceAbstraction,
useClass: ProviderApiService,

View File

@ -1,3 +1,3 @@
export abstract class MessagingService {
abstract send(subscriber: string, arg?: any): void;
}
// Export the new message sender as the legacy MessagingService to minimize changes in the initial PR,
// team specific PR's will come after.
export { MessageSender as MessagingService } from "../messaging/message.sender";

View File

@ -0,0 +1,46 @@
import { Subject, firstValueFrom } from "rxjs";
import { getCommand, isExternalMessage, tagAsExternal } from "./helpers";
import { Message, CommandDefinition } from "./types";
describe("helpers", () => {
describe("getCommand", () => {
it("can get the command from just a string", () => {
const command = getCommand("myCommand");
expect(command).toEqual("myCommand");
});
it("can get the command from a message definition", () => {
const commandDefinition = new CommandDefinition<object>("myCommand");
const command = getCommand(commandDefinition);
expect(command).toEqual("myCommand");
});
});
describe("tag integration", () => {
it("can tag and identify as tagged", async () => {
const messagesSubject = new Subject<Message<object>>();
const taggedMessages = messagesSubject.asObservable().pipe(tagAsExternal);
const firstValuePromise = firstValueFrom(taggedMessages);
messagesSubject.next({ command: "test" });
const result = await firstValuePromise;
expect(isExternalMessage(result)).toEqual(true);
});
});
describe("isExternalMessage", () => {
it.each([null, { command: "myCommand", test: "object" }, undefined] as Message<
Record<string, unknown>
>[])("returns false when value is %s", (value: Message<object>) => {
expect(isExternalMessage(value)).toBe(false);
});
});
});

View File

@ -0,0 +1,23 @@
import { MonoTypeOperatorFunction, map } from "rxjs";
import { Message, CommandDefinition } from "./types";
export const getCommand = (commandDefinition: CommandDefinition<object> | string) => {
if (typeof commandDefinition === "string") {
return commandDefinition;
} else {
return commandDefinition.command;
}
};
export const EXTERNAL_SOURCE_TAG = Symbol("externalSource");
export const isExternalMessage = (message: Message<object>) => {
return (message as Record<PropertyKey, unknown>)?.[EXTERNAL_SOURCE_TAG] === true;
};
export const tagAsExternal: MonoTypeOperatorFunction<Message<object>> = map(
(message: Message<object>) => {
return Object.assign(message, { [EXTERNAL_SOURCE_TAG]: true });
},
);

View File

@ -0,0 +1,4 @@
export { MessageListener } from "./message.listener";
export { MessageSender } from "./message.sender";
export { Message, CommandDefinition } from "./types";
export { isExternalMessage } from "./helpers";

View File

@ -0,0 +1,5 @@
// Built in implementations
export { SubjectMessageSender } from "./subject-message.sender";
// Helpers meant to be used only by other implementations
export { tagAsExternal, getCommand } from "./helpers";

View File

@ -0,0 +1,47 @@
import { Subject } from "rxjs";
import { subscribeTo } from "../../../spec/observable-tracker";
import { MessageListener } from "./message.listener";
import { Message, CommandDefinition } from "./types";
describe("MessageListener", () => {
const subject = new Subject<Message<{ test: number }>>();
const sut = new MessageListener(subject.asObservable());
const testCommandDefinition = new CommandDefinition<{ test: number }>("myCommand");
describe("allMessages$", () => {
it("runs on all nexts", async () => {
const tracker = subscribeTo(sut.allMessages$);
const pausePromise = tracker.pauseUntilReceived(2);
subject.next({ command: "command1", test: 1 });
subject.next({ command: "command2", test: 2 });
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "command1", test: 1 });
expect(tracker.emissions[1]).toEqual({ command: "command2", test: 2 });
});
});
describe("messages$", () => {
it("runs on only my commands", async () => {
const tracker = subscribeTo(sut.messages$(testCommandDefinition));
const pausePromise = tracker.pauseUntilReceived(2);
subject.next({ command: "notMyCommand", test: 1 });
subject.next({ command: "myCommand", test: 2 });
subject.next({ command: "myCommand", test: 3 });
subject.next({ command: "notMyCommand", test: 4 });
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 2 });
expect(tracker.emissions[1]).toEqual({ command: "myCommand", test: 3 });
});
});
});

View File

@ -0,0 +1,41 @@
import { EMPTY, Observable, filter } from "rxjs";
import { Message, CommandDefinition } from "./types";
/**
* A class that allows for listening to messages coming through the application,
* allows for listening of all messages or just the messages you care about.
*
* @note Consider NOT using messaging at all if you can. State Providers offer an observable stream of
* data that is persisted. This can serve messages that might have been used to notify of settings changes
* or vault data changes and those observables should be preferred over messaging.
*/
export class MessageListener {
constructor(private readonly messageStream: Observable<Message<object>>) {}
/**
* A stream of all messages sent through the application. It does not contain type information for the
* other properties on the messages. You are encouraged to instead subscribe to an individual message
* through {@link messages$}.
*/
allMessages$ = this.messageStream;
/**
* Creates an observable stream filtered to just the command given via the {@link CommandDefinition} and typed
* to the generic contained in the CommandDefinition. Be careful using this method unless all your messages are being
* sent through `MessageSender.send`, if that isn't the case you should have lower confidence in the message
* payload being the expected type.
*
* @param commandDefinition The CommandDefinition containing the information about the message type you care about.
*/
messages$<T extends object>(commandDefinition: CommandDefinition<T>): Observable<T> {
return this.allMessages$.pipe(
filter((msg) => msg?.command === commandDefinition.command),
) as Observable<T>;
}
/**
* A helper property for returning a MessageListener that will never emit any messages and will immediately complete.
*/
static readonly EMPTY = new MessageListener(EMPTY);
}

View File

@ -0,0 +1,62 @@
import { CommandDefinition } from "./types";
class MultiMessageSender implements MessageSender {
constructor(private readonly innerMessageSenders: MessageSender[]) {}
send<T extends object>(
commandDefinition: string | CommandDefinition<T>,
payload: object | T = {},
): void {
for (const messageSender of this.innerMessageSenders) {
messageSender.send(commandDefinition, payload);
}
}
}
export abstract class MessageSender {
/**
* A method for sending messages in a type safe manner. The passed in command definition
* will require you to provide a compatible type in the payload parameter.
*
* @example
* const MY_COMMAND = new CommandDefinition<{ test: number }>("myCommand");
*
* this.messageSender.send(MY_COMMAND, { test: 14 });
*
* @param commandDefinition
* @param payload
*/
abstract send<T extends object>(commandDefinition: CommandDefinition<T>, payload: T): void;
/**
* A legacy method for sending messages in a non-type safe way.
*
* @remarks Consider defining a {@link CommandDefinition} and passing that in for the first parameter to
* get compilation errors when defining an incompatible payload.
*
* @param command The string based command of your message.
* @param payload Extra contextual information regarding the message. Be aware that this payload may
* be serialized and lose all prototype information.
*/
abstract send(command: string, payload?: object): void;
/** Implementation of the other two overloads, read their docs instead. */
abstract send<T extends object>(
commandDefinition: CommandDefinition<T> | string,
payload: T | object,
): void;
/**
* A helper method for combine multiple {@link MessageSender}'s.
* @param messageSenders The message senders that should be combined.
* @returns A message sender that will relay all messages to the given message senders.
*/
static combine(...messageSenders: MessageSender[]) {
return new MultiMessageSender(messageSenders);
}
/**
* A helper property for creating a {@link MessageSender} that sends to nowhere.
*/
static readonly EMPTY: MessageSender = new MultiMessageSender([]);
}

View File

@ -0,0 +1,65 @@
import { Subject } from "rxjs";
import { subscribeTo } from "../../../spec/observable-tracker";
import { SubjectMessageSender } from "./internal";
import { MessageSender } from "./message.sender";
import { Message, CommandDefinition } from "./types";
describe("SubjectMessageSender", () => {
const subject = new Subject<Message<{ test: number }>>();
const subjectObservable = subject.asObservable();
const sut: MessageSender = new SubjectMessageSender(subject);
describe("send", () => {
it("will send message with command from message definition", async () => {
const commandDefinition = new CommandDefinition<{ test: number }>("myCommand");
const tracker = subscribeTo(subjectObservable);
const pausePromise = tracker.pauseUntilReceived(1);
sut.send(commandDefinition, { test: 1 });
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 });
});
it("will send message with command from normal string", async () => {
const tracker = subscribeTo(subjectObservable);
const pausePromise = tracker.pauseUntilReceived(1);
sut.send("myCommand", { test: 1 });
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 });
});
it("will send message with object even if payload not given", async () => {
const tracker = subscribeTo(subjectObservable);
const pausePromise = tracker.pauseUntilReceived(1);
sut.send("myCommand");
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand" });
});
it.each([null, undefined])(
"will send message with object even if payload is null-ish (%s)",
async (payloadValue) => {
const tracker = subscribeTo(subjectObservable);
const pausePromise = tracker.pauseUntilReceived(1);
sut.send("myCommand", payloadValue);
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand" });
},
);
});
});

View File

@ -0,0 +1,17 @@
import { Subject } from "rxjs";
import { getCommand } from "./internal";
import { MessageSender } from "./message.sender";
import { Message, CommandDefinition } from "./types";
export class SubjectMessageSender implements MessageSender {
constructor(private readonly messagesSubject: Subject<Message<object>>) {}
send<T extends object>(
commandDefinition: string | CommandDefinition<T>,
payload: object | T = {},
): void {
const command = getCommand(commandDefinition);
this.messagesSubject.next(Object.assign(payload ?? {}, { command: command }));
}
}

View File

@ -0,0 +1,13 @@
declare const tag: unique symbol;
/**
* A class for defining information about a message, this is helpful
* alonside `MessageSender` and `MessageListener` for providing a type
* safe(-ish) way of sending and receiving messages.
*/
export class CommandDefinition<T extends object> {
[tag]: T;
constructor(readonly command: string) {}
}
export type Message<T extends object> = { command: string } & T;

View File

@ -1,34 +0,0 @@
import {
BroadcasterService as BroadcasterServiceAbstraction,
MessageBase,
} from "../abstractions/broadcaster.service";
export class BroadcasterService implements BroadcasterServiceAbstraction {
subscribers: Map<string, (message: MessageBase) => void> = new Map<
string,
(message: MessageBase) => void
>();
send(message: MessageBase, id?: string) {
if (id != null) {
if (this.subscribers.has(id)) {
this.subscribers.get(id)(message);
}
return;
}
this.subscribers.forEach((value) => {
value(message);
});
}
subscribe(id: string, messageCallback: (message: MessageBase) => void) {
this.subscribers.set(id, messageCallback);
}
unsubscribe(id: string) {
if (this.subscribers.has(id)) {
this.subscribers.delete(id);
}
}
}

View File

@ -0,0 +1,36 @@
import { Subscription } from "rxjs";
import { BroadcasterService, MessageBase } from "../abstractions/broadcaster.service";
import { MessageListener, MessageSender } from "../messaging";
/**
* Temporary implementation that just delegates to the message sender and message listener
* and manages their subscriptions.
*/
export class DefaultBroadcasterService implements BroadcasterService {
subscriptions = new Map<string, Subscription>();
constructor(
private readonly messageSender: MessageSender,
private readonly messageListener: MessageListener,
) {}
send(message: MessageBase, id?: string) {
this.messageSender.send(message?.command, message);
}
subscribe(id: string, messageCallback: (message: MessageBase) => void) {
this.subscriptions.set(
id,
this.messageListener.allMessages$.subscribe((message) => {
messageCallback(message);
}),
);
}
unsubscribe(id: string) {
const subscription = this.subscriptions.get(id);
subscription?.unsubscribe();
this.subscriptions.delete(id);
}
}

View File

@ -1,7 +0,0 @@
import { MessagingService } from "../abstractions/messaging.service";
export class NoopMessagingService implements MessagingService {
send(subscriber: string, arg: any = {}) {
// Do nothing...
}
}