From cd4d42b46999c3bb9cedcf0d336fb5992ed7392d Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 19 May 2025 02:12:51 +0200 Subject: [PATCH] Implement flatpak browser integration --- apps/desktop/desktop_native/Cargo.lock | 16 ++-- .../desktop_native/core/src/ipc/mod.rs | 24 ++++- .../desktop_native/core/src/ipc/server.rs | 51 +++++----- apps/desktop/desktop_native/napi/index.d.ts | 2 +- apps/desktop/desktop_native/napi/src/lib.rs | 19 ++-- apps/desktop/desktop_native/proxy/src/main.rs | 8 +- .../com.bitwarden.desktop.devel.yaml | 7 ++ .../desktop/src/main/native-messaging.main.ts | 92 ++++++++++++++----- 8 files changed, 157 insertions(+), 62 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index a08764fc9d8..a943eb6eae8 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -889,7 +889,7 @@ dependencies = [ "ssh-encoding", "ssh-key", "sysinfo", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", "tokio-stream", "tokio-util", @@ -931,7 +931,7 @@ dependencies = [ "cc", "core-foundation", "glob", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", ] @@ -2480,7 +2480,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2971,11 +2971,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -2991,9 +2991,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", diff --git a/apps/desktop/desktop_native/core/src/ipc/mod.rs b/apps/desktop/desktop_native/core/src/ipc/mod.rs index 531aeaa0a0b..b4c59245e6b 100644 --- a/apps/desktop/desktop_native/core/src/ipc/mod.rs +++ b/apps/desktop/desktop_native/core/src/ipc/mod.rs @@ -19,6 +19,10 @@ pub const NATIVE_MESSAGING_BUFFER_SIZE: usize = 1024 * 1024; /// but ideally the messages should be processed as quickly as possible. pub const MESSAGE_CHANNEL_BUFFER: usize = 32; +pub const FLATPAK_PATHS: [&str; 1] = [ + "org.mozilla.firefox/.mozilla/native-messaging-hosts", +]; + /// This is the codec used for communication through the UNIX socket / Windows named pipe. /// It's an internal implementation detail, but we want to make sure that both the client /// and the server use the same one. @@ -29,7 +33,7 @@ fn internal_ipc_codec(inner: T) -> Framed std::path::PathBuf { #[cfg(target_os = "windows")] { @@ -82,3 +86,21 @@ pub fn path(name: &str) -> std::path::PathBuf { path_dir.join(format!("app.{name}")) } } + +/// Paths to the ipc sockets including alternative paths. +/// For flatpak, a path per sandbox is created. +pub fn all_paths(name: &str) -> Vec { + let mut paths = vec![path(name)]; + #[cfg(target_os = "linux")] + { + // On Linux, in flatpak, we mount sockets in each app's sandboxed directory. + let user_home = dirs::home_dir().unwrap(); + let flatpak_path = user_home.join(".var/app/"); + let flatpak_paths = FLATPAK_PATHS + .iter() + .map(|path| flatpak_path.join(path).join(format!(".app.{name}.socket"))) + .collect::>(); + paths.extend(flatpak_paths); + } + paths +} diff --git a/apps/desktop/desktop_native/core/src/ipc/server.rs b/apps/desktop/desktop_native/core/src/ipc/server.rs index a1c77e7ab16..a9cae09833c 100644 --- a/apps/desktop/desktop_native/core/src/ipc/server.rs +++ b/apps/desktop/desktop_native/core/src/ipc/server.rs @@ -1,6 +1,6 @@ use std::{ error::Error, - path::{Path, PathBuf}, + path::PathBuf, }; use futures::{SinkExt, StreamExt, TryFutureExt}; @@ -32,7 +32,7 @@ pub enum MessageType { } pub struct Server { - pub path: PathBuf, + pub paths: Vec, cancel_token: CancellationToken, server_to_clients_send: broadcast::Sender, } @@ -45,19 +45,9 @@ impl Server { /// - `name`: The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. /// - `client_to_server_send`: This [`mpsc::Sender`] will receive all the [`Message`]'s that the clients send to this server. pub fn start( - path: &Path, + paths: Vec, client_to_server_send: mpsc::Sender, ) -> Result> { - // If the unix socket file already exists, we get an error when trying to bind to it. So we remove it first. - // Any processes that were using the old socket should remain connected to it but any new connections will use the new socket. - if !cfg!(windows) { - let _ = std::fs::remove_file(path); - } - - let name = path.as_os_str().to_fs_name::()?; - let opts = ListenerOptions::new().name(name); - let listener = opts.create_tokio()?; - // This broadcast channel is used for sending messages to all connected clients, and so the sender // will be stored in the server while the receiver will be cloned and passed to each client handler. let (server_to_clients_send, server_to_clients_recv) = @@ -67,20 +57,37 @@ impl Server { // tasks without having to wait on all the pending tasks finalizing first let cancel_token = CancellationToken::new(); + for path in paths.iter() { + // If the unix socket file already exists, we get an error when trying to bind to it. So we remove it first. + // Any processes that were using the old socket should remain connected to it but any new connections will use the new socket. + if !cfg!(windows) { + if path.exists() { + std::fs::remove_file(path)?; + } + } + + let name = path.as_os_str().to_fs_name::()?; + let opts = ListenerOptions::new().name(name); + let listener = opts.create_tokio()?; + + let client_to_server_send = client_to_server_send.clone(); + let server_to_clients_recv = server_to_clients_recv.resubscribe(); + let cancel_token = cancel_token.clone(); + tokio::spawn(listen_incoming( + listener, + client_to_server_send, + server_to_clients_recv, + cancel_token, + )); + } + // Create the server and start listening for incoming connections // in a separate task to avoid blocking the current task let server = Server { - path: path.to_owned(), + paths, cancel_token: cancel_token.clone(), - server_to_clients_send, + server_to_clients_send }; - tokio::spawn(listen_incoming( - listener, - client_to_server_send, - server_to_clients_recv, - cancel_token, - )); - Ok(server) } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 952f2571c5d..839094916e2 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -99,7 +99,7 @@ export declare namespace ipc { */ static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise /** Return the path to the IPC server. */ - getPath(): string + getPaths(): Array /** Stop the IPC server. */ stop(): void /** diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 37796ef6f59..5cc277d4fb1 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -436,9 +436,9 @@ pub mod ipc { } }); - let path = desktop_core::ipc::path(&name); + let path = desktop_core::ipc::all_paths(&name); - let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { + let server = desktop_core::ipc::server::Server::start(path.clone(), send).map_err(|e| { napi::Error::from_reason(format!( "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" )) @@ -449,8 +449,11 @@ pub mod ipc { /// Return the path to the IPC server. #[napi] - pub fn get_path(&self) -> String { - self.server.path.to_string_lossy().to_string() + pub fn get_paths(&self) -> Vec { + self.server + .paths + .iter().map(|p| p.to_string_lossy().to_string()) + .collect() } /// Stop the IPC server. @@ -702,7 +705,7 @@ pub mod autofill { let path = desktop_core::ipc::path(&name); - let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { + let server = desktop_core::ipc::server::Server::start(vec![path.clone()], send).map_err(|e| { napi::Error::from_reason(format!( "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" )) @@ -714,7 +717,11 @@ pub mod autofill { /// Return the path to the IPC server. #[napi] pub fn get_path(&self) -> String { - self.server.path.to_string_lossy().to_string() + self.server + .paths + .get(0) + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_default() } /// Stop the IPC server. diff --git a/apps/desktop/desktop_native/proxy/src/main.rs b/apps/desktop/desktop_native/proxy/src/main.rs index ba29e00cf13..6925eda1522 100644 --- a/apps/desktop/desktop_native/proxy/src/main.rs +++ b/apps/desktop/desktop_native/proxy/src/main.rs @@ -55,7 +55,11 @@ async fn main() { #[cfg(target_os = "windows")] let should_foreground = windows::allow_foreground(); - let sock_path = desktop_core::ipc::path("bitwarden"); + let sock_paths = desktop_core::ipc::all_paths("bitwarden"); + let sock_path = *sock_paths.iter().filter(|p| p.exists()).collect::>().first().unwrap_or_else(|| { + error!("No valid socket path found."); + std::process::exit(1); + }); let log_path = { let mut path = sock_path.clone(); @@ -93,7 +97,7 @@ async fn main() { let (out_send, mut out_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER); let mut handle = tokio::spawn( - desktop_core::ipc::client::connect(sock_path, out_send, in_recv) + desktop_core::ipc::client::connect(sock_path.to_path_buf(), out_send, in_recv) .map(|r| r.map_err(|e| e.to_string())), ); diff --git a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml index 3aeebfd809d..2df7b6ca9ec 100644 --- a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml +++ b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml @@ -23,6 +23,13 @@ finish-args: - --system-talk-name=org.freedesktop.login1 - --filesystem=xdg-download - --device=all + + # Browser integration + # The config directory is needed to write manifests for non-flatpak + # Sockets are mounted in each app's directory + - --filesystem=xdg-config + - --filesystem=home/.mozilla + - --filesystem=~/.var/app/org.mozilla.firefox/ modules: - name: bitwarden-desktop buildsystem: simple diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index 93525164ff5..27b9378895e 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -110,7 +110,9 @@ export class NativeMessagingMain { } }); - this.logService.info("Native messaging server started at:", this.ipcServer.getPath()); + for (const path in this.ipcServer.getPaths()) { + this.logService.info("Native messaging server started at:", path); + } ipcMain.on("nativeMessagingReply", (event, msg) => { if (msg != null) { @@ -128,32 +130,43 @@ export class NativeMessagingMain { this.ipcServer?.send(JSON.stringify(message)); } - async generateManifests() { - const baseJson = { + private async generateChromeJson(binaryPath: string) { + return { name: "com.8bit.bitwarden", description: "Bitwarden desktop <-> browser bridge", - path: this.binaryPath(), + path: binaryPath, type: "stdio", - }; - - if (!existsSync(baseJson.path)) { - throw new Error(`Unable to find binary: ${baseJson.path}`); - } - - const firefoxJson = { - ...baseJson, - ...{ allowed_extensions: ["{446900e4-71c2-419f-a6a7-df9c091e268b}"] }, - }; - const chromeJson = { - ...baseJson, allowed_origins: await this.loadChromeIds(), }; + } + + private async generateFirefoxJson(binaryPath: string) { + return { + name: "com.8bit.bitwarden", + description: "Bitwarden desktop <-> browser bridge", + path: binaryPath, + type: "stdio", + allowed_extensions: ["{446900e4-71c2-419f-a6a7-df9c091e268b}"], + }; + } + + async generateManifests() { + const binaryPath = this.binaryPath(); + if (!existsSync(binaryPath)) { + throw new Error(`Unable to find proxy binary: ${binaryPath}`); + } switch (process.platform) { case "win32": { const destination = path.join(this.userPath, "browsers"); - await this.writeManifest(path.join(destination, "firefox.json"), firefoxJson); - await this.writeManifest(path.join(destination, "chrome.json"), chromeJson); + await this.writeManifest( + path.join(destination, "firefox.json"), + await this.generateFirefoxJson(binaryPath), + ); + await this.writeManifest( + path.join(destination, "chrome.json"), + await this.generateChromeJson(binaryPath), + ); const nmhs = this.getWindowsNMHS(); for (const [name, [key, subkey]] of Object.entries(nmhs)) { @@ -171,9 +184,9 @@ export class NativeMessagingMain { if (existsSync(value)) { const p = path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"); - let manifest: any = chromeJson; + let manifest: any = await this.generateChromeJson(binaryPath); if (key === "Firefox" || key === "Zen") { - manifest = firefoxJson; + manifest = await this.generateFirefoxJson(binaryPath); } await this.writeManifest(p, manifest); @@ -189,18 +202,42 @@ export class NativeMessagingMain { if (key === "Firefox") { await this.writeManifest( path.join(value, "native-messaging-hosts", "com.8bit.bitwarden.json"), - firefoxJson, + await this.generateFirefoxJson(binaryPath), ); } else { await this.writeManifest( path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"), - chromeJson, + await this.generateChromeJson(binaryPath), ); } } else { this.logService.warning(`${key} not found, skipping.`); } } + + for (const [key, value] of Object.entries(this.getFlatpakNMHS())) { + this.logService.info(`Flatpak ${key} found at ${value}`); + if (existsSync(value)) { + this.logService.info(`Flatpak ${key} found at ${value}`); + const sandboxedProxyBinaryPath = path.join(value, "bitwarden_desktop_proxy"); + await fs.copyFile(binaryPath, path.join(value, "bitwarden_desktop_proxy")); + this.logService.info( + `Copied ${sandboxedProxyBinaryPath} to ${path.join(value, "bitwarden_desktop_proxy")}`, + ); + + if (key === "Firefox") { + await this.writeManifest( + path.join(value, "com.8bit.bitwarden.json"), + await this.generateFirefoxJson(sandboxedProxyBinaryPath), + ); + } else { + this.logService.warning(`Flatpak ${key} not supported, skipping.`); + } + } else { + this.logService.warning(`${key} not found, skipping.`); + } + } + break; } default: @@ -266,6 +303,11 @@ export class NativeMessagingMain { } } + for (const [, value] of Object.entries(this.getFlatpakNMHS())) { + await this.removeIfExists(path.join(value, "com.8bit.bitwarden.json")); + await this.removeIfExists(path.join(value, "bitwarden_desktop_proxy")); + } + break; } default: @@ -327,6 +369,12 @@ export class NativeMessagingMain { }; } + private getFlatpakNMHS() { + return { + Firefox: `${this.homedir()}/.var/app/org.mozilla.firefox/.mozilla/native-messaging-hosts/`, + }; + } + private async writeManifest(destination: string, manifest: object) { this.logService.debug(`Writing manifest: ${destination}`);