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:
16
apps/desktop/desktop_native/Cargo.lock
generated
16
apps/desktop/desktop_native/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
2
apps/desktop/desktop_native/napi/index.d.ts
vendored
2
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -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
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())),
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user