1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 12:13:45 +00:00

Implement flatpak browser integration

This commit is contained in:
Bernd Schoolmann
2025-05-19 02:12:51 +02:00
parent e35882afc8
commit cd4d42b469
8 changed files with 157 additions and 62 deletions

View File

@@ -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",

View File

@@ -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<T: AsyncRead + AsyncWrite>(inner: T) -> Framed<T, LengthDe
.new_framed(inner)
}
/// Resolve the path to the IPC socket.
/// The main path to the IPC socket.
pub fn path(name: &str) -> 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<std::path::PathBuf> {
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::<Vec<_>>();
paths.extend(flatpak_paths);
}
paths
}

View File

@@ -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<PathBuf>,
cancel_token: CancellationToken,
server_to_clients_send: broadcast::Sender<String>,
}
@@ -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<Message>`] will receive all the [`Message`]'s that the clients send to this server.
pub fn start(
path: &Path,
paths: Vec<PathBuf>,
client_to_server_send: mpsc::Sender<Message>,
) -> Result<Self, Box<dyn Error>> {
// 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::<GenericFilePath>()?;
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::<GenericFilePath>()?;
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)
}

View File

@@ -99,7 +99,7 @@ export declare namespace ipc {
*/
static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise<IpcServer>
/** Return the path to the IPC server. */
getPath(): string
getPaths(): Array<string>
/** Stop the IPC server. */
stop(): void
/**

View File

@@ -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<String> {
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.

View File

@@ -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::<Vec<_>>().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())),
);

View File

@@ -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

View File

@@ -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}`);