diff --git a/apps/desktop/src/main/native-messaging.main.spec.ts b/apps/desktop/src/main/native-messaging.main.spec.ts new file mode 100644 index 000000000000..16bc47a704d9 --- /dev/null +++ b/apps/desktop/src/main/native-messaging.main.spec.ts @@ -0,0 +1,124 @@ +import { existsSync, promises as fs } from "fs"; +import { homedir, tmpdir } from "os"; +import * as path from "path"; + +import { mock } from "jest-mock-extended"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +import { NativeMessagingMain } from "./native-messaging.main"; +import { WindowMain } from "./window.main"; + +// native-messaging.main.ts registers ipcMain handlers at construction, and its +// WindowMain import registers a privileged scheme at module load time. Mock the +// surface both touch so the module can be loaded in Jest. +jest.mock("electron", () => ({ + app: {}, + BrowserWindow: jest.fn(), + ipcMain: { on: jest.fn(), handle: jest.fn() }, + nativeTheme: {}, + screen: {}, + session: {}, + protocol: { registerSchemesAsPrivileged: jest.fn() }, + net: {}, +})); + +// Both modules load native .node modules at import time. +jest.mock("@bitwarden/desktop-napi", () => ({ + ipc: { NativeIpcServer: { listen: jest.fn() } }, + windows_registry: { createKey: jest.fn(), deleteKey: jest.fn() }, + processisolations: { + isolateProcess: jest.fn(), + isCoreDumpingDisabled: jest.fn(), + disableCoredumps: jest.fn(), + }, +})); + +jest.mock("os", () => ({ + ...jest.requireActual("os"), + homedir: jest.fn(), +})); + +describe("NativeMessagingMain", () => { + const originalPlatform = process.platform; + + let home: string; + let logService: LogService; + let nativeMessagingMain: NativeMessagingMain; + + const firefoxManifest = () => + path.join(home, ".mozilla", "native-messaging-hosts", "com.8bit.bitwarden.json"); + const chromeManifest = () => + path.join(home, ".config", "google-chrome", "NativeMessagingHosts", "com.8bit.bitwarden.json"); + const edgeManifest = () => + path.join(home, ".config", "microsoft-edge", "NativeMessagingHosts", "com.8bit.bitwarden.json"); + + beforeEach(async () => { + Object.defineProperty(process, "platform", { value: "linux", configurable: true }); + globalThis.BIT_ENVIRONMENT = "production"; + + home = await fs.mkdtemp(path.join(tmpdir(), "bw-nmh-")); + jest.mocked(homedir).mockReturnValue(home); + + // The proxy binary must exist for generateManifests to proceed. + const appDir = path.join(home, "app"); + await fs.mkdir(appDir, { recursive: true }); + await fs.writeFile(path.join(appDir, "desktop_proxy"), ""); + + logService = mock(); + nativeMessagingMain = new NativeMessagingMain( + logService, + mock(), + path.join(home, "userData"), + path.join(appDir, "bitwarden"), + appDir, + ); + }); + + afterEach(async () => { + Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true }); + await fs.rm(home, { recursive: true, force: true }); + jest.clearAllMocks(); + }); + + describe("generateManifests on linux", () => { + it("creates the native messaging hosts directory for chromium browsers", async () => { + // Browser config directory present, but no NativeMessagingHosts subdirectory in it. + await fs.mkdir(path.join(home, ".config", "google-chrome"), { recursive: true }); + + await nativeMessagingMain.generateManifests(); + + expect(existsSync(chromeManifest())).toBe(true); + expect( + existsSync( + path.join( + home, + ".config", + "google-chrome", + "NativeMessagingHosts", + ".bitwarden_desktop_proxy", + ), + ), + ).toBe(true); + }); + + it("continues with the remaining browsers when one fails", async () => { + await fs.mkdir(path.join(home, ".mozilla"), { recursive: true }); + await fs.mkdir(path.join(home, ".config", "chromium"), { recursive: true }); + await fs.mkdir(path.join(home, ".config", "microsoft-edge"), { recursive: true }); + + // A file where the directory belongs makes the Chromium step fail. Chromium sits + // between Firefox and Microsoft Edge in the iteration order. + await fs.writeFile(path.join(home, ".config", "chromium", "NativeMessagingHosts"), ""); + + await expect(nativeMessagingMain.generateManifests()).resolves.not.toThrow(); + + expect(existsSync(firefoxManifest())).toBe(true); + expect(existsSync(edgeManifest())).toBe(true); + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to set up Chromium"), + expect.anything(), + ); + }); + }); +}); diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index 0fb1e75b8b7f..17f11b1a8e1c 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -191,20 +191,24 @@ export class NativeMessagingMain { case "darwin": { const nmhs = this.getDarwinNMHS(); for (const [key, browserDirectory] of Object.entries(nmhs)) { - if (existsSync(browserDirectory)) { - const nmhsPath = path.join(browserDirectory, "NativeMessagingHosts"); - const manifestPath = path.join(nmhsPath, "com.8bit.bitwarden.json"); - - let manifest: any = await this.generateChromeJson(binaryPath); - if (key === "Firefox" || key === "Zen") { - // Only generate the NMHS dir if the browser directory exists - await fs.mkdir(nmhsPath, { recursive: true }); - manifest = await this.generateFirefoxJson(binaryPath); - } - - await this.writeManifest(manifestPath, manifest); - } else { + if (!existsSync(browserDirectory)) { this.logService.warning(`${key} not found, skipping.`); + continue; + } + + const nmhsPath = path.join(browserDirectory, "NativeMessagingHosts"); + const manifestPath = path.join(nmhsPath, "com.8bit.bitwarden.json"); + + try { + await fs.mkdir(nmhsPath, { recursive: true }); + await this.writeManifest( + manifestPath, + key === "Firefox" || key === "Zen" + ? await this.generateFirefoxJson(binaryPath) + : await this.generateChromeJson(binaryPath), + ); + } catch (e) { + this.logService.error(`[Native messaging] Failed to set up ${key}, skipping:`, e); } } break; @@ -217,46 +221,40 @@ export class NativeMessagingMain { // Unsandboxed browser for (const [key, browserDirectory] of Object.entries(this.getLinuxNMHS())) { - if (existsSync(browserDirectory)) { - let nhmsPath = path.join(browserDirectory, "NativeMessagingHosts"); - if (key === "Firefox") { - nhmsPath = path.join(browserDirectory, "native-messaging-hosts"); - } - const browserBinaryPath = path.join(nhmsPath, ".bitwarden_desktop_proxy"); + if (!existsSync(browserDirectory)) { + this.logService.warning(`${key} not found, skipping.`); + continue; + } - if (key === "Firefox") { - // Only generate the NMHS dir if the browser directory exists - await fs.mkdir(nhmsPath, { recursive: true }); - } + const nmhsPath = path.join( + browserDirectory, + key === "Firefox" ? "native-messaging-hosts" : "NativeMessagingHosts", + ); + const browserBinaryPath = path.join(nmhsPath, ".bitwarden_desktop_proxy"); + try { + await fs.mkdir(nmhsPath, { recursive: true }); await this.linkOrCopy(binaryPath, browserBinaryPath); - this.logService.info( - `[Native messaging] Hard-linked ${binaryPath} to ${browserBinaryPath}`, + await this.writeManifest( + path.join(nmhsPath, "com.8bit.bitwarden.json"), + key === "Firefox" + ? await this.generateFirefoxJson(browserBinaryPath) + : await this.generateChromeJson(browserBinaryPath), ); - - if (key === "Firefox") { - await this.writeManifest( - path.join(nhmsPath, "com.8bit.bitwarden.json"), - await this.generateFirefoxJson(browserBinaryPath), - ); - } else { - await this.writeManifest( - path.join(nhmsPath, "com.8bit.bitwarden.json"), - await this.generateChromeJson(browserBinaryPath), - ); - } - } else { - this.logService.warning(`${key} not found, skipping.`); + } catch (e) { + this.logService.error(`[Native messaging] Failed to set up ${key}, skipping:`, e); } } for (const [key, value] of Object.entries(this.getFlatpakNMHS())) { - if (existsSync(value)) { + if (!existsSync(value)) { + this.logService.warning(`${key} not found, skipping.`); + continue; + } + + try { const sandboxedProxyBinaryPath = path.join(value, ".bitwarden_desktop_proxy"); await this.linkOrCopy(binaryPath, sandboxedProxyBinaryPath); - this.logService.info( - `[Native messaging] Hard-linked ${binaryPath} to ${sandboxedProxyBinaryPath}`, - ); if (key === "Firefox") { await this.writeManifest( @@ -271,8 +269,11 @@ export class NativeMessagingMain { } else { this.logService.warning(`Flatpak ${key} not supported, skipping.`); } - } else { - this.logService.warning(`${key} not found, skipping.`); + } catch (e) { + this.logService.error( + `[Native messaging] Failed to set up Flatpak ${key}, skipping:`, + e, + ); } }