Skip to content

Commit

Permalink
Merge pull request #3 from twelho/release-0.2
Browse files Browse the repository at this point in the history
Iteration 0.2: socket activation, setup binary, instrumentation and better errors
  • Loading branch information
twelho authored Oct 25, 2023
2 parents b2d8e8c + 488a8d6 commit e6cd733
Show file tree
Hide file tree
Showing 17 changed files with 1,023 additions and 384 deletions.
231 changes: 181 additions & 50 deletions Cargo.lock

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nm-proxy"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
authors = ["Dennis Marttinen <[email protected]>"]
description = "Native messaging proxy for Flatpak'ed browsers"
Expand All @@ -19,6 +19,10 @@ path = "src/client/main.rs"
name = "daemon"
path = "src/daemon/main.rs"

[[bin]]
name = "setup"
path = "src/setup/main.rs"

[profile.release]
lto = true # Enable link-time optimizations
strip = true # Strip symbols from the binary
Expand All @@ -28,11 +32,14 @@ anyhow = "1.0.75"
byteorder = "1.5.0"
expanduser = "1.2.2"
libc = "0.2.149"
log = "0.4.20"
nix = { version = "0.27.1", features = ["signal"] }
rust-ini = "0.19.0"
sd-listen-fds = "0.2.0"
serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.107"
tokio = { version = "1.33.0", features = ["full"] }
tokio-fd = "0.3.0"
tokio-util = "0.7.9"
toml = "0.8.2"
tracing-subscriber = "0.3.17"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
61 changes: 57 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,68 @@

## Architecture

`nm-proxy` consists of a client and daemon binary. The client binary is executed by the Flatpak'ed browser, and forwards stdio through an exposed socket to the daemon on the host, which runs the native binary and forwards the socket traffic to its stdio.
`nm-proxy` consists of a client, daemon, and setup binary. The client binary is executed by the Flatpak'ed browser, and forwards stdio through an exposed socket to the daemon on the host, which runs the native binary and forwards the socket traffic to its stdio. Sockets are handled by systemd to enable transparent daemon restarts without losing the inodes forwarded into the Flatpak namespaces.

The daemon is intended to be run as a systemd user service, and will read its configuration as well as the [native manifests](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests) from `~/.config/nm-proxy` (configuration guidelines will be printed if missing). On launch, the daemon takes care of installing the client binary and manifest as well configuring the Flatpak environment for each specified browser.
The daemon is intended to be run as a systemd user service, and will read its configuration as well as the [native manifests](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests) from `~/.config/nm-proxy` (guidelines will be printed if configuration is missing). The setup binary helps the daemon take care of installing the client binary and manifest as well configuring the Flatpak environment for each specified browser.

The manifests themselves (`.json` files) must be supplied by the user. Here is the upstream manifest of the Plasma Integration extension, with which `nm-proxy` was tested during development:

```json
{
"name": "org.kde.plasma.browser_integration",
"description": "Native connector for KDE Plasma",
"path": "/usr/bin/plasma-browser-integration-host",
"type": "stdio",
"allowed_extensions": ["[email protected]"]
}
```

## Configuration

```toml
# nm-proxy 0.2.0 configuration file
#
# [daemon]
# proxy_client = "/path/to/client" # Path to nm-proxy client binary
#
# [browsers.<name>] # Define configuration for browser <name>
# app_id = "app.example.com" # Flatpak 3-part app ID
# nmh_dir = ".<name>/native-messaging-hosts" # Native messaging host application directory
#
# Example configuration:

[daemon]
proxy_client = "~/path/to/client"

[browsers.firefox]
app_id = "org.mozilla.firefox"
nmh_dir = ".mozilla/native-messaging-hosts"

[browsers.librewolf]
app_id = "io.gitlab.librewolf-community"
nmh_dir = ".librewolf/native-messaging-hosts"

[browsers.chromium]
app_id = "org.chromium.Chromium"
nmh_dir = ".config/chromium/NativeMessagingHosts"
```

## Installation

```shell
$ ./install.sh
Usage: ./install.sh <browser>...
<browser> refers to the name of a browser entry in the configuration file of
the nm-proxy daemon. Examples include "firefox", "librewolf", and "chromium".
```

## Building

The following builds all three binaries:

```shell
cargo build --bin client --release
cargo build --bin daemon --release
rustup target add x86_64-unknown-linux-musl
cargo build --release
```

## Acknowledgements
Expand Down
69 changes: 69 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/bin/sh -e

if [ "$#" -eq 0 ]; then
cat <<-EOF
Usage: $0 <browser>...
<browser> refers to the name of a browser entry in the configuration file of
the nm-proxy daemon. Examples include "firefox", "librewolf", and "chromium".
EOF

exit 1
fi

rustup target add x86_64-unknown-linux-musl
cargo build --release

DAEMON_PATH=$(readlink -f target/x86_64-unknown-linux-musl/release/daemon)
SETUP_PATH=$(readlink -f target/x86_64-unknown-linux-musl/release/setup)

cat >~/.config/systemd/user/nm-proxy-setup.service <<EOF
[Unit]
Description=nm-proxy setup helper
[Service]
#Environment=RUST_LOG=trace
ExecStart="$SETUP_PATH"
[Install]
WantedBy=default.target
EOF

cat >~/.config/systemd/user/nm-proxy.service <<EOF
[Unit]
Description=nm-proxy daemon
[Service]
#Environment=RUST_LOG=trace
ExecStart="$DAEMON_PATH"
KillSignal=SIGINT
NonBlocking=true
EOF

# The ListenStreams can't be touched after the socket services have been started without losing the FDs
cat >~/.config/systemd/user/[email protected] <<EOF
[Unit]
Description=nm-proxy daemon sockets
[Socket]
Service=nm-proxy.service
ListenStream=%t/nm-proxy-%I.socket
FileDescriptorName=%I
[Install]
WantedBy=sockets.target
EOF

for browser in "$@"; do
systemctl --user stop "nm-proxy@$browser.socket"
done

systemctl --user stop nm-proxy.service
systemctl --user daemon-reload

systemctl --user enable nm-proxy-setup.service
systemctl --user start nm-proxy-setup.service

for browser in "$@"; do
systemctl --user enable "nm-proxy@$browser.socket"
systemctl --user start "nm-proxy@$browser.socket"
done
50 changes: 20 additions & 30 deletions src/client/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
// SPDX-License-Identifier: GPL-3.0-or-later

use std::env;
use std::os::unix::prelude::*;
use std::path::PathBuf;

use anyhow::{anyhow, bail, Context, Result};
use tokio::fs::read_dir;
use anyhow::{anyhow, Context, Result};
use tokio::io::copy;
use tokio::net::UnixStream;
use tokio::signal;
use tokio::task::JoinSet;
use tokio::{fs, signal};
use tokio_fd::AsyncFd;

use nm_proxy::common;
use nm_proxy::common::constants::*;
use nm_proxy::common::traits::*;

async fn parse_args() -> Result<(String, Vec<String>)> {
let mut args = env::args().collect::<Vec<_>>().into_iter();
let mut args = env::args();
let invocation_path = args
.next()
.ok_or(anyhow!("Unable to acquire invocation path"))?;
Expand All @@ -26,34 +26,26 @@ async fn parse_args() -> Result<(String, Vec<String>)> {
let manifest_name = file_name
.to_os_string()
.into_string()
.map_err(|s| anyhow!("Failed to parse file name {:?}", s))?;
.map_err(|s| anyhow!("{:?}", s).context("Failed to parse file name"))?;
return Ok((manifest_name, vec![manifest_path, app_id]));
}
}

bail!(
Err(anyhow!(
"Usage: {} <app-manifest-path> <extension-id>\n\
This binary should be invoked by a browser through native messaging.",
invocation_path
)
))
}

async fn find_socket() -> Result<String> {
let runtime_dir = common::parse_env("XDG_RUNTIME_DIR", None)?;

let mut stream = read_dir(&runtime_dir)
let mut stream = fs::read_dir(&runtime_dir)
.await
.with_context(|| format!("Failed to read {}", runtime_dir))?;
.path_context(&runtime_dir)?;

while let Some(entry) = stream
.next_entry()
.await
.with_context(|| format!("Failed to access entry in {}", runtime_dir))?
{
let name = entry
.file_name()
.into_string()
.map_err(|s| anyhow!("Failed to parse file name {:?}", s))?;
while let Some(entry) = stream.next_entry().await.path_context(&runtime_dir)? {
let name = entry.file_name().into_string_result()?;

//eprintln!("Parsing file: {}, type: {:?}", name, entry.file_type().await?);
// TODO: is_socket() does not work in Flatpak, bug in Rust? Debug information:
Expand All @@ -65,19 +57,16 @@ async fn find_socket() -> Result<String> {
if entry
.file_type()
.await
.with_context(|| format!("Failed to read type of {}", name))?
.path_context(entry.path())?
.is_file()
&& name.starts_with(common::SOCKET_PREFIX)
&& name.starts_with(SOCKET_PREFIX)
&& name.ends_with(SOCKET_SUFFIX)
{
return Ok(entry
.path()
.into_os_string()
.into_string()
.map_err(|s| anyhow!("Failed to parse path {:?}", s))?);
return Ok(entry.path().into_string_result()?);
}
}

bail!("No valid socket found in {}", runtime_dir)
Err(anyhow!("No valid socket found in {}", runtime_dir))
}

#[tokio::main]
Expand All @@ -88,7 +77,8 @@ async fn main() -> Result<()> {
// Connect to the socket
let stream = UnixStream::connect(&socket_path)
.await
.with_context(|| format!("Failed to connect to socket {}", socket_path))?;
.context(socket_path)
.context("Failed to connect to socket")?;

// Split the socket stream into RX/TX
let (mut socket_rx, mut socket_tx) = stream.into_split();
Expand Down Expand Up @@ -138,6 +128,6 @@ async fn main() -> Result<()> {
if graceful {
Ok(())
} else {
bail!("Unclean shutdown, did the socket close?")
Err(anyhow!("Unclean shutdown, did the socket close?"))
}
}
17 changes: 8 additions & 9 deletions src/daemon/config.rs → src/common/config.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// (c) Dennis Marttinen 2023
// SPDX-License-Identifier: GPL-3.0-or-later

use crate::common;
use crate::common::constants::*;
use anyhow::{Context, Error, Result};
use expanduser::expanduser;
use nm_proxy::common;
use serde::de::Error as DeError;
use serde::{Deserialize, Deserializer};
use std::collections::HashMap;
Expand All @@ -30,7 +31,7 @@ Ensure that it is present, and contains the following:
# Example configuration:
[daemon]
proxy_client = "~/bin/nm-proxy-client"
proxy_client = "~/path/to/client"
[browsers.firefox]
app_id = "org.mozilla.firefox"
Expand Down Expand Up @@ -67,7 +68,7 @@ pub struct Config {
}

impl Config {
fn browsers(&self) -> impl Iterator<Item = &String> {
pub fn browsers(&self) -> impl Iterator<Item = &String> {
self.browsers.keys()
}

Expand Down Expand Up @@ -105,11 +106,10 @@ fn path_parser<'de, D: Deserializer<'de>>(deserializer: D) -> Result<PathBuf, D:
expanduser(s).map_err(|e| D::Error::custom(e))
}

// TODO: Cache the results of this
pub async fn form_config_path() -> Result<PathBuf> {
let mut path = expanduser(common::parse_env("XDG_CONFIG_HOME", Some("~/.config"))?)
.context("Path expansion failed")?;
path.push(common::CONFIG_DIR);
.context("Configuration file path expansion failed")?;
path.push(CONFIG_DIR);
path.canonicalize()
.context("Configuration file path canonicalization failed")
}
Expand All @@ -121,9 +121,8 @@ async fn read_config(config_path: &Path) -> Result<Config> {
toml::from_str(&contents).map_err(|e| Error::from(e))
}

pub async fn load_config() -> Result<Config> {
let mut path = form_config_path().await?;
path.push(common::CONFIG_FILE);
pub async fn load_config(path: impl AsRef<Path>) -> Result<Config> {
let path = path.as_ref().join(CONFIG_FILE);
read_config(&path)
.await
.with_context(|| format!("{}", path.display()))
Expand Down
10 changes: 10 additions & 0 deletions src/common/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// (c) Dennis Marttinen 2023
// SPDX-License-Identifier: GPL-3.0-or-later

pub const SOCKET_PREFIX: &str = "nm-proxy-";
pub const SOCKET_SUFFIX: &str = ".socket";
pub const CONFIG_DIR: &str = "nm-proxy";
pub const CONFIG_FILE: &str = "config.toml";
pub const APP_MANIFEST_DIR: &str = "manifest";
pub const PROXY_CLIENT_BIN: &str = "nm-proxy-client";
pub const SETTINGS_FILE_NAME: &str = "nm-proxy-settings.toml";
10 changes: 3 additions & 7 deletions src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,9 @@ use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};

pub const SOCKET_PREFIX: &str = "nm-proxy-";
pub const EXTENSION_KEY: &str = "extension";
pub const CONFIG_DIR: &str = "nm-proxy";
pub const CONFIG_FILE: &str = "config.toml";
pub const APP_MANIFEST_DIR: &str = "manifest";
pub const PROXY_CLIENT_BIN: &str = "nm-proxy-client";

pub mod config;
pub mod constants;
pub mod runtime;
pub mod traits;

#[derive(Serialize, Deserialize)]
Expand Down
21 changes: 21 additions & 0 deletions src/common/runtime/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// (c) Dennis Marttinen 2023
// SPDX-License-Identifier: GPL-3.0-or-later

use anyhow::{anyhow, bail, Result};
use std::env;

pub mod settings;
pub use settings::*;

pub async fn parse_runtime_dir(context: &str) -> Result<String> {
let mut args = env::args();
let invocation_path = args
.next()
.ok_or(anyhow!("Unable to acquire invocation path"))?;

if let (Some(dir), None) = (args.next(), args.next()) {
return Ok(dir);
}

bail!("Usage: {} <runtime-dir>\n{}", invocation_path, context);
}
Loading

0 comments on commit e6cd733

Please sign in to comment.