├── .githooks ├── pre-commit ├── pre-push └── util.sh ├── .github ├── FUNDING.yml ├── renovate.json └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .rustfmt.toml ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── LICENSE-MIT ├── README.md ├── data └── logo.svg ├── src ├── bin │ └── busd.rs ├── bus │ └── mod.rs ├── config │ ├── mod.rs │ ├── policy.rs │ ├── rule.rs │ └── xml.rs ├── fdo │ ├── dbus.rs │ ├── mod.rs │ └── monitoring.rs ├── lib.rs ├── match_rules.rs ├── name_registry.rs ├── peer │ ├── mod.rs │ ├── monitor.rs │ └── stream.rs ├── peers.rs └── tracing_subscriber.rs └── tests ├── config.rs ├── data ├── example-session-disable-stats.conf ├── example-system-enable-stats.conf ├── includedir │ ├── a.conf │ └── not_included.xml ├── missing_include.conf ├── session.conf ├── system.conf ├── transitive_missing_include.conf ├── valid.conf └── valid_included.conf ├── fdo.rs ├── greet.rs ├── monitor.rs └── multiple_conns.rs /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | GITHOOKS_DIR=$( cd -- "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd ) 3 | source $GITHOOKS_DIR/util.sh 4 | 5 | ensure_rustup_installed 6 | ensure_rustfmt_installed 7 | 8 | check_formatting 9 | -------------------------------------------------------------------------------- /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | GITHOOKS_DIR=$( cd -- "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd ) 3 | source $GITHOOKS_DIR/util.sh 4 | 5 | ensure_rustup_installed 6 | ensure_clippy_installed 7 | 8 | check_clippy 9 | -------------------------------------------------------------------------------- /.githooks/util.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Utility functions for git hook scripts. 4 | 5 | if test -t 1 && test -n "$(tput colors)" && test "$(tput colors)" -ge 8; then 6 | bold="$(tput bold)" 7 | normal="$(tput sgr0)" 8 | green="$(tput setaf 2)" 9 | red="$(tput setaf 1)" 10 | blue="$(tput setaf 4)" 11 | 12 | function hook_failure { 13 | echo "${red}${bold}FAILED:${normal} ${1}${normal}" 14 | exit 1 15 | } 16 | 17 | function hook_info { 18 | echo "${blue}${1}${normal}" 19 | } 20 | 21 | function hook_success { 22 | echo "${green}${bold}SUCCESS:${normal} ${1}${normal}" 23 | echo 24 | echo 25 | } 26 | 27 | else 28 | function hook_failure { 29 | echo "FAILED: ${1}" 30 | exit 1 31 | } 32 | 33 | function hook_info { 34 | echo "{$1}" 35 | } 36 | 37 | function hook_success { 38 | echo "SUCCESS: ${1}" 39 | echo 40 | echo 41 | } 42 | fi 43 | 44 | function ensure_rustup_installed() { 45 | hook_info "📦️ Ensuring that rustup is installed" 46 | if ! which rustup &> /dev/null; then 47 | curl https://sh.rustup.rs -sSf | sh -s -- -y 48 | export PATH=$PATH:$HOME/.cargo/bin 49 | if ! which rustup &> /dev/null; then 50 | hook_failure "Failed to install rustup" 51 | else 52 | hook_success "rustup installed." 53 | fi 54 | else 55 | hook_success "rustup is already installed." 56 | fi 57 | } 58 | 59 | function ensure_rustfmt_installed() { 60 | hook_info "📦️ Ensuring that nightly rustfmt is installed" 61 | if ! rustup component list --toolchain nightly|grep 'rustfmt-preview.*(installed)' &> /dev/null; then 62 | rustup component add rustfmt-preview --toolchain nightly 63 | hook_success "rustfmt installed." 64 | else 65 | hook_success "rustfmt is already installed." 66 | fi 67 | } 68 | 69 | function ensure_clippy_installed() { 70 | hook_info "📦️ Ensuring that clippy is installed" 71 | if ! rustup component list --toolchain stable|grep 'clippy.*(installed)' &> /dev/null; then 72 | rustup component add clippy 73 | hook_success "clippy installed." 74 | else 75 | hook_success "clippy is already installed." 76 | fi 77 | } 78 | 79 | function check_formatting() { 80 | hook_info "🎨 Running 'cargo +nightly fmt -- --check'" 81 | cargo +nightly fmt -- --check \ 82 | && hook_success "Project is formatted" \ 83 | || hook_failure "Cargo format detected errors." 84 | } 85 | 86 | function check_clippy() { 87 | hook_info "🔍 Running 'cargo clippy -- -D warnings'" 88 | cargo clippy -- -D warnings \ 89 | && hook_success "Clippy detected no issues" \ 90 | || hook_failure "Cargo clippy detected errors." 91 | } 92 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: zeenix -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageRules": [ 3 | { 4 | "matchManagers": ["github-actions"], 5 | "commitMessagePrefix": "⬆️ " 6 | }, 7 | { 8 | "matchManagers": ["cargo"], 9 | "commitMessagePrefix": "⬆️ ", 10 | "commitMessageTopic": "{{depName}}", 11 | "lockFileMaintenance": { "enabled": true } 12 | }, 13 | { 14 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 15 | "automerge": true, 16 | "rebaseWhen": "conflicted" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Run `cargo test` on windwos, linux and mac 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | test: 12 | env: 13 | RUST_LOG: "trace" 14 | RUST_BACKTRACE: "full" 15 | RUSTFLAGS: -D warnings 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, macos-latest] 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: dtolnay/rust-toolchain@master 23 | with: 24 | toolchain: stable 25 | - uses: Swatinem/rust-cache@v2 26 | - name: Run tests on stable Rust 27 | run: cargo --locked test 28 | 29 | test_nightly: 30 | env: 31 | RUST_LOG: "trace" 32 | RUST_BACKTRACE: "full" 33 | runs-on: ${{ matrix.os }} 34 | strategy: 35 | matrix: 36 | os: [ubuntu-latest, macos-latest] 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: dtolnay/rust-toolchain@master 40 | with: 41 | toolchain: nightly 42 | - uses: Swatinem/rust-cache@v2 43 | - name: Run tests on nightly Rust 44 | run: cargo --locked test 45 | 46 | fmt: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: dtolnay/rust-toolchain@master 51 | with: 52 | toolchain: nightly 53 | components: rustfmt 54 | - uses: Swatinem/rust-cache@v2 55 | - name: Check formatting 56 | run: cargo --locked fmt --all -- --check 57 | 58 | clippy: 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v4 62 | - uses: dtolnay/rust-toolchain@master 63 | with: 64 | toolchain: stable 65 | components: clippy 66 | - uses: Swatinem/rust-cache@v2 67 | - name: Check common mistakes 68 | run: cargo --locked clippy --all -- -D warnings 69 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*.*.*" 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | ref: ${{ github.ref }} 17 | - uses: spenserblack/actions-tag-to-release@main 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | zbus/target 3 | zvariant/target 4 | **/*.rs.bk 5 | *.swp 6 | *.orig 7 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | comment_width = 100 3 | wrap_comments = true 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to busd 2 | 3 | We welcome contributions from everyone in the form of suggestions, bug reports, pull requests, and 4 | feedback. This document gives some guidance if you are thinking of helping us. 5 | 6 | Please reach out here in a Github issue, or in the 7 | [#zbus:matrix.org](https://matrix.to/#/#zbus:matrix.org) Matrix room if we can do anything to help 8 | you contribute. 9 | 10 | ## Submitting bug reports and feature requests 11 | 12 | You can create issues [here](https://github.com/dbus2/busd/issues/new). When 13 | reporting a bug or asking for help, please include enough details so that the people helping you 14 | can reproduce the behavior you are seeing. For some tips on how to approach this, read about how to 15 | produce a [Minimal, Complete, and Verifiable Example](https://stackoverflow.com/help/mcve). 16 | 17 | When making a feature request, please make it clear what problem you intend to solve with the 18 | feature, any ideas for how the crate in question could support solving that problem, any possible 19 | alternatives, and any disadvantages. 20 | 21 | ## Submitting Pull Requests 22 | 23 | Same rules apply here as for bug reports and feature requests. Plus: 24 | 25 | * We prefer atomic commits. Please read 26 | [this excellent blog post](https://www.aleksandrhovhannisyan.com/blog/atomic-git-commits/) for 27 | more information, including the rationale. 28 | * Please try your best to follow [these guidelines](https://wiki.gnome.org/Git/CommitMessages) for 29 | commit messages. 30 | * We also prefer adding [emoji prefixes to commit messages](https://gitmoji.carloscuesta.me/). Since 31 | the `gitmoji` CLI tool can be very [slow](https://github.com/zeenix/gimoji#rationale), we 32 | recommend using [`gimoji`](https://github.com/zeenix/gimoji) instead. You can also pick an emoji 33 | direcitly from [here](https://gitmoji.dev/). 34 | * Add details to each commit about the changes it contains. PR description is for summarizing the 35 | overall changes in the PR, while commit logs are for describing the specific changes of the 36 | commit in question. 37 | * When addressesing review comments, fix the existing commits in the PR (rather than adding 38 | additional commits) and force push (as in `git push -f`) to your branch. You may find 39 | [`git-absorb`](https://github.com/tummychow/git-absorb) and 40 | [`git-revise`](https://github.com/mystor/git-revise) extremely useful, especially if you're not 41 | very familiar with interactive rebasing and modifying commits in git. 42 | 43 | ### Legal Notice 44 | 45 | When contributing to this project, you **implicitly** declare that: 46 | 47 | * you have authored 100% of the content, 48 | * you have the necessary rights to the content, and 49 | * you agree to providing the content under the [project's license](LICENSE). 50 | 51 | ## Running the test suite 52 | 53 | We encourage you to check that the test suite passes locally before submitting a pull request with 54 | your changes. If anything does not pass, typically it will be easier to iterate and fix it locally 55 | than waiting for the CI servers to run tests for you. 56 | 57 | ```sh 58 | # Run the full test suite, including doc test and compile-tests 59 | cargo test --all-features 60 | ``` 61 | 62 | Also please ensure that code is formatted correctly by running: 63 | 64 | ```sh 65 | cargo +nightly fmt --all 66 | ``` 67 | 68 | and clippy doesn't see anything wrong with the code: 69 | 70 | ```sh 71 | cargo clippy -- -D warnings 72 | ``` 73 | 74 | Please note that there are times when clippy is wrong and you know what you are doing. In such 75 | cases, it's acceptable to tell clippy to 76 | [ignore the specific error or warning in the code](https://github.com/rust-lang/rust-clippy#allowingdenying-lints). 77 | 78 | If you intend to contribute often or think that's very likely, we recommend you setup the git hook 79 | scripts contained within this repository. You can enable them with: 80 | 81 | ```sh 82 | cp .githooks/* .git/hooks/ 83 | ``` 84 | 85 | 86 | ## Conduct 87 | 88 | In all busd-related forums, we follow the 89 | [Rust Code of Conduct](https://www.rust-lang.org/conduct.html). For escalation or moderation issues 90 | please contact Zeeshan (zeeshanak@gnome.org) instead of the Rust moderation team. 91 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "busd" 3 | version = "0.4.0" 4 | authors = ["Zeeshan Ali Khan "] 5 | description = "A D-Bus bus (broker) implementation" 6 | edition = "2021" 7 | license = "MIT" 8 | readme = "README.md" 9 | rust-version = "1.74" 10 | repository = "https://github.com/dbus2/busd" 11 | keywords = ["D-Bus", "DBus", "IPC"] 12 | categories = ["network-programming"] 13 | exclude = ["LICENSE"] 14 | 15 | [lib] 16 | name = "busd" 17 | path = "src/lib.rs" 18 | 19 | [[bin]] 20 | name = "busd" 21 | path = "src/bin/busd.rs" 22 | 23 | [dependencies] 24 | #zbus = { version = "5.0", features = [ 25 | zbus = { git = "https://github.com/dbus2/zbus/", features = [ 26 | "tokio", 27 | "bus-impl", 28 | ], default-features = false } 29 | tokio = { version = "1.37.0", features = [ 30 | "macros", 31 | "rt-multi-thread", 32 | "signal", 33 | "tracing", 34 | ] } 35 | clap = { version = "4.5.4", features = [ 36 | "derive", 37 | "std", 38 | "help", 39 | ], default-features = false } 40 | tracing = "0.1.40" 41 | tracing-subscriber = { version = "0.3.18", features = [ 42 | "env-filter", 43 | "fmt", 44 | "ansi", 45 | ], default-features = false, optional = true } 46 | anyhow = "1.0.82" 47 | # Explicitly depend on serde to enable `rc` feature. 48 | serde = { version = "1.0.200", features = ["rc"] } 49 | futures-util = { version = "0.3.30", default-features = false } 50 | enumflags2 = "0.7.9" 51 | console-subscriber = { version = "0.4.0", optional = true } 52 | xdg-home = "1.1.0" 53 | event-listener = "5.3.0" 54 | fastrand = "2.2.0" 55 | quick-xml = { version = "0.37.0", features = ["serialize"] } 56 | 57 | nix = { version = "0.30.0", features = ["user"] } 58 | 59 | [features] 60 | default = ["tracing-subscriber"] 61 | 62 | [dev-dependencies] 63 | ntest = "0.9.2" 64 | rand = "0.9.0" 65 | futures-util = { version = "0.3.30", default-features = true } 66 | 67 | [profile.release] 68 | lto = "fat" 69 | codegen-units = 1 70 | opt-level = "s" 71 | panic = "abort" 72 | # generates a separate *.dwp/*.dSYM so the binary can get stripped 73 | split-debuginfo = "packed" 74 | strip = "symbols" 75 | # No one needs an undebuggable release binary 76 | debug = "full" 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE-MIT -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Project logo: a bus 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/dbus2/busd/ci.yml?branch=main)](https://github.com/dbus2/busd/actions?query=branch%3Amain) 4 | [![crates.io](https://img.shields.io/crates/v/busd.svg)](https://crates.io/crates/busd) 5 | 6 | # busd 7 | 8 | A D-Bus bus (broker) implementation in Rust. Since it's pure Rust, it's much easier to cross-build 9 | than other D-Bus brokers. 10 | 11 | ## Status 12 | 13 | Alpha. It's not ready for production use yet. Only the essentials are in place. 14 | 15 | ## Installation & Use 16 | 17 | Currently, we can only offer installation from source: 18 | 19 | ```bash 20 | cargo install -f busd 21 | ``` 22 | 23 | Running a session instance is super easy: 24 | 25 | ```bash 26 | busd --print-address 27 | ``` 28 | 29 | `--print-address` will print the address of the bus to stdout. You can then use that address to 30 | connect to the bus: 31 | 32 | ```bash 33 | export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus,guid=d0af79a44c000ce7985797ba649dbc05" 34 | busctl --user introspect org.freedesktop.DBus /org/freedesktop/DBus 35 | busctl --user list 36 | ``` 37 | 38 | Since auto-starting of services is not yet implemented, you'll have to start services manually: 39 | 40 | ```bash 41 | # Probably not the best example since the service just exits after a call to it. 42 | /usr/libexec/dleyna-renderer-service & 43 | busctl call --user com.intel.dleyna-renderer /com/intel/dLeynaRenderer com.intel.dLeynaRenderer.Manager GetRenderers 44 | ``` 45 | 46 | ## The plan 47 | 48 | ### Full compatibility with the D-Bus specification 49 | 50 | Implement all features that manadated and recommended by the [specification]. 51 | 52 | ### Additional Header Fields 53 | 54 | While the D-Bus spec does not allow custom header fields in messages, `busd` will still support a 55 | few additional on-demand (only) fields, that are useful for certain applications. One example is 56 | addition of [peer credentials] on every message, which can avoid round-trips on the bus. 57 | 58 | ## License 59 | 60 | [MIT](LICENSE-MIT) 61 | 62 | [specification]: https://dbus.freedesktop.org/doc/dbus-specification.html 63 | [peer credentials]: https://github.com/dbus2/busd/issues/29 64 | -------------------------------------------------------------------------------- /data/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 42 | 49 | 53 | 56 | 60 | 61 | 65 | 69 | 73 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/bin/busd.rs: -------------------------------------------------------------------------------- 1 | extern crate busd; 2 | 3 | use std::{fs::File, io::Write, os::fd::FromRawFd, path::PathBuf}; 4 | 5 | use busd::{bus, config::Config}; 6 | 7 | use anyhow::Result; 8 | use clap::Parser; 9 | use tokio::{select, signal::unix::SignalKind}; 10 | use tracing::{error, info, warn}; 11 | 12 | /// A simple D-Bus broker. 13 | #[derive(Parser, Debug)] 14 | #[clap(author, version, about, long_about = None)] 15 | struct Args { 16 | /// The address to listen on. 17 | /// Takes precedence over any `` element in the configuration file. 18 | #[clap(short = 'a', long, value_parser)] 19 | address: Option, 20 | 21 | /// Use the given configuration file. 22 | #[clap(long)] 23 | config: Option, 24 | 25 | /// Print the address of the message bus to standard output. 26 | #[clap(long)] 27 | print_address: bool, 28 | 29 | /// File descriptor to which readiness notifications are sent. 30 | /// 31 | /// Once the server is listening to connections on the specified socket, it will print 32 | /// `READY=1\n` into this file descriptor and close it. 33 | /// 34 | /// This readiness notification mechanism which works on both systemd and s6. 35 | /// 36 | /// This feature is only available on unix-like platforms. 37 | #[clap(long)] 38 | ready_fd: Option, 39 | 40 | /// Equivalent to `--config /usr/share/dbus-1/session.conf`. 41 | /// This is the default if `--config` and `--system` are unspecified. 42 | #[clap(long)] 43 | session: bool, 44 | 45 | /// Equivalent to `--config /usr/share/dbus-1/system.conf`. 46 | #[clap(long)] 47 | system: bool, 48 | } 49 | 50 | #[tokio::main] 51 | async fn main() -> Result<()> { 52 | busd::tracing_subscriber::init(); 53 | 54 | let args = Args::parse(); 55 | 56 | let config_path = if args.system { 57 | PathBuf::from("/usr/share/dbus-1/system.conf") 58 | } else if let Some(config_path) = args.config { 59 | config_path 60 | } else { 61 | PathBuf::from("/usr/share/dbus-1/session.conf") 62 | }; 63 | info!("reading configuration file {} ...", config_path.display()); 64 | let config = Config::read_file(&config_path)?; 65 | 66 | let address = if let Some(address) = args.address { 67 | Some(address) 68 | } else { 69 | config.listen.map(|address| format!("{address}")) 70 | }; 71 | 72 | let mut bus = bus::Bus::for_address(address.as_deref()).await?; 73 | 74 | if let Some(fd) = args.ready_fd { 75 | // SAFETY: We don't have any way to know if the fd is valid or not. The parent process is 76 | // responsible for passing a valid fd. 77 | let mut ready_file = unsafe { File::from_raw_fd(fd) }; 78 | ready_file.write_all(b"READY=1\n")?; 79 | } 80 | 81 | if args.print_address { 82 | println!("{}", bus.address()); 83 | } 84 | 85 | let mut sig_int = tokio::signal::unix::signal(SignalKind::interrupt())?; 86 | 87 | select! { 88 | _ = sig_int.recv() => { 89 | info!("Received SIGINT, shutting down.."); 90 | } 91 | res = bus.run() => match res { 92 | Ok(()) => warn!("Bus stopped, shutting down.."), 93 | Err(e) => error!("Bus stopped with an error: {}", e), 94 | } 95 | } 96 | 97 | if let Err(e) = bus.cleanup().await { 98 | error!("Failed to clean up: {}", e); 99 | } 100 | 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /src/bus/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Ok, Result}; 2 | use std::{env, path::Path, str::FromStr, sync::Arc}; 3 | use tokio::{fs::remove_file, spawn}; 4 | use tracing::{debug, info, trace, warn}; 5 | use zbus::{ 6 | address::{ 7 | transport::{Tcp, Unix, UnixSocket}, 8 | Transport, 9 | }, 10 | connection::{self, socket::BoxedSplit}, 11 | Address, AuthMechanism, Connection, Guid, OwnedGuid, 12 | }; 13 | 14 | use crate::{ 15 | fdo::{self, DBus, Monitoring}, 16 | peers::Peers, 17 | }; 18 | 19 | /// The bus. 20 | #[derive(Debug)] 21 | pub struct Bus { 22 | inner: Inner, 23 | listener: Listener, 24 | } 25 | 26 | // All (cheaply) cloneable fields of `Bus` go here. 27 | #[derive(Clone, Debug)] 28 | pub struct Inner { 29 | address: Address, 30 | peers: Arc, 31 | guid: OwnedGuid, 32 | next_id: usize, 33 | auth_mechanism: AuthMechanism, 34 | _self_conn: Connection, 35 | } 36 | 37 | #[derive(Debug)] 38 | enum Listener { 39 | Unix(tokio::net::UnixListener), 40 | Tcp(tokio::net::TcpListener), 41 | } 42 | 43 | impl Bus { 44 | pub async fn for_address(address: Option<&str>) -> Result { 45 | let mut address = match address { 46 | Some(address) => Address::from_str(address)?, 47 | None => Address::from_str(&default_address())?, 48 | }; 49 | let guid: OwnedGuid = match address.guid() { 50 | Some(guid) => guid.to_owned().into(), 51 | None => { 52 | let guid = Guid::generate(); 53 | address = address.set_guid(guid.clone())?; 54 | 55 | guid.into() 56 | } 57 | }; 58 | let (listener, auth_mechanism) = match address.transport() { 59 | Transport::Unix(unix) => { 60 | // Resolve address specification into address that clients can use. 61 | let addr = Self::unix_addr(unix)?; 62 | address = Address::new(Transport::Unix(Unix::new(UnixSocket::File( 63 | addr.as_pathname() 64 | .expect("Address created for UNIX socket should always have a path.") 65 | .to_path_buf(), 66 | )))) 67 | .set_guid(guid.clone())?; 68 | 69 | ( 70 | Self::unix_stream(addr.clone()).await?, 71 | AuthMechanism::External, 72 | ) 73 | } 74 | Transport::Tcp(tcp) => (Self::tcp_stream(tcp).await?, AuthMechanism::Anonymous), 75 | _ => bail!("Unsupported address `{}`.", address), 76 | }; 77 | 78 | let peers = Peers::new(); 79 | 80 | let dbus = DBus::new(peers.clone(), guid.clone()); 81 | let monitoring = Monitoring::new(peers.clone()); 82 | 83 | // Create a peer for ourselves. 84 | trace!("Creating self-dial connection."); 85 | let (client_socket, peer_socket) = zbus::connection::socket::Channel::pair(); 86 | let service_conn = connection::Builder::authenticated_socket(client_socket, guid.clone())? 87 | .p2p() 88 | .unique_name(fdo::BUS_NAME)? 89 | .name(fdo::BUS_NAME)? 90 | .serve_at(fdo::DBus::PATH, dbus)? 91 | .serve_at(fdo::Monitoring::PATH, monitoring)? 92 | .build() 93 | .await?; 94 | let peer_conn = connection::Builder::authenticated_socket(peer_socket, guid.clone())? 95 | .p2p() 96 | .build() 97 | .await?; 98 | 99 | peers.add_us(peer_conn).await; 100 | trace!("Self-dial connection created."); 101 | 102 | Ok(Self { 103 | listener, 104 | inner: Inner { 105 | address, 106 | peers, 107 | guid, 108 | next_id: 0, 109 | auth_mechanism, 110 | _self_conn: service_conn, 111 | }, 112 | }) 113 | } 114 | 115 | pub fn address(&self) -> &Address { 116 | &self.inner.address 117 | } 118 | 119 | pub async fn run(&mut self) -> Result<()> { 120 | loop { 121 | self.accept_next().await?; 122 | } 123 | } 124 | 125 | // AsyncDrop would have been nice! 126 | pub async fn cleanup(self) -> Result<()> { 127 | match self.inner.address.transport() { 128 | Transport::Unix(unix) => match unix.path() { 129 | UnixSocket::File(path) => remove_file(path).await.map_err(Into::into), 130 | _ => Ok(()), 131 | }, 132 | _ => Ok(()), 133 | } 134 | } 135 | 136 | fn unix_addr(unix: &Unix) -> Result { 137 | use std::os::unix::net::SocketAddr; 138 | 139 | Ok(match unix.path() { 140 | #[cfg(target_os = "linux")] 141 | UnixSocket::Abstract(name) => { 142 | use std::os::linux::net::SocketAddrExt; 143 | 144 | let addr = SocketAddr::from_abstract_name(name.as_encoded_bytes())?; 145 | info!( 146 | "Listening on abstract UNIX socket `{}`.", 147 | name.to_string_lossy() 148 | ); 149 | 150 | addr 151 | } 152 | UnixSocket::File(path) => { 153 | let addr = SocketAddr::from_pathname(path)?; 154 | info!( 155 | "Listening on UNIX socket file `{}`.", 156 | path.to_string_lossy() 157 | ); 158 | 159 | addr 160 | } 161 | UnixSocket::Dir(dir) | UnixSocket::TmpDir(dir) => { 162 | let path = dir.join(format!("dbus-{}", fastrand::u32(1_000_000..u32::MAX))); 163 | let addr = SocketAddr::from_pathname(&path)?; 164 | info!( 165 | "Listening on UNIX socket file `{}`.", 166 | path.to_string_lossy() 167 | ); 168 | 169 | addr 170 | } 171 | _ => bail!("Unsupported address."), 172 | }) 173 | } 174 | 175 | async fn unix_stream(addr: std::os::unix::net::SocketAddr) -> Result { 176 | // TODO: Use tokio::net::UnixListener directly once it supports abstract sockets: 177 | // 178 | // https://github.com/tokio-rs/tokio/issues/4610 179 | 180 | let std_listener = 181 | tokio::task::spawn_blocking(move || std::os::unix::net::UnixListener::bind_addr(&addr)) 182 | .await??; 183 | std_listener.set_nonblocking(true)?; 184 | tokio::net::UnixListener::from_std(std_listener) 185 | .map(Listener::Unix) 186 | .map_err(Into::into) 187 | } 188 | 189 | async fn tcp_stream(tcp: &Tcp) -> Result { 190 | if tcp.nonce_file().is_some() { 191 | bail!("`nonce-tcp` transport is not supported (yet)."); 192 | } 193 | info!("Listening on `{}:{}`.", tcp.host(), tcp.port()); 194 | let address = (tcp.host(), tcp.port()); 195 | 196 | tokio::net::TcpListener::bind(address) 197 | .await 198 | .map(Listener::Tcp) 199 | .map_err(Into::into) 200 | } 201 | 202 | async fn accept_next(&mut self) -> Result<()> { 203 | let socket = self.accept().await?; 204 | 205 | let id = self.next_id(); 206 | let inner = self.inner.clone(); 207 | spawn(async move { 208 | if let Err(e) = inner 209 | .peers 210 | .clone() 211 | .add(&inner.guid, id, socket, inner.auth_mechanism) 212 | .await 213 | { 214 | warn!("Failed to establish connection: {}", e); 215 | } 216 | }); 217 | 218 | Ok(()) 219 | } 220 | 221 | async fn accept(&mut self) -> Result { 222 | let stream = match &mut self.listener { 223 | Listener::Unix(listener) => listener.accept().await.map(|(stream, _)| stream.into())?, 224 | Listener::Tcp(listener) => listener.accept().await.map(|(stream, _)| stream.into())?, 225 | }; 226 | debug!("Accepted connection on address `{}`", self.inner.address); 227 | 228 | Ok(stream) 229 | } 230 | 231 | pub fn peers(&self) -> &Arc { 232 | &self.inner.peers 233 | } 234 | 235 | pub fn guid(&self) -> &OwnedGuid { 236 | &self.inner.guid 237 | } 238 | 239 | pub fn auth_mechanism(&self) -> AuthMechanism { 240 | self.inner.auth_mechanism 241 | } 242 | 243 | fn next_id(&mut self) -> usize { 244 | self.inner.next_id += 1; 245 | 246 | self.inner.next_id 247 | } 248 | } 249 | 250 | fn default_address() -> String { 251 | let runtime_dir = env::var("XDG_RUNTIME_DIR") 252 | .as_ref() 253 | .map(|s| Path::new(s).to_path_buf()) 254 | .ok() 255 | .unwrap_or_else(|| { 256 | Path::new("/run") 257 | .join("user") 258 | .join(format!("{}", nix::unistd::Uid::current())) 259 | }); 260 | 261 | format!("unix:dir={}", runtime_dir.display()) 262 | } 263 | 264 | #[cfg(not(unix))] 265 | fn default_address() -> String { 266 | "tcp:host=127.0.0.1,port=4242".to_string() 267 | } 268 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::var, 3 | path::{Path, PathBuf}, 4 | str::FromStr, 5 | }; 6 | 7 | use anyhow::{Error, Result}; 8 | use policy::OptionalPolicy; 9 | use serde::Deserialize; 10 | use zbus::{Address, AuthMechanism}; 11 | 12 | pub mod policy; 13 | pub mod rule; 14 | mod xml; 15 | 16 | pub use policy::Policy; 17 | pub use rule::{ 18 | Access, ConnectOperation, NameOwnership, Operation, ReceiveOperation, SendOperation, 19 | }; 20 | use xml::{Document, Element, TypeElement}; 21 | 22 | /// The bus configuration. 23 | /// 24 | /// This is currently only loaded from the [XML configuration files] defined by the specification. 25 | /// We plan to add support for other formats (e.g JSON) in the future. 26 | /// 27 | /// [XML configuration files]: https://dbus.freedesktop.org/doc/dbus-daemon.1.html#configuration_file 28 | #[derive(Clone, Debug, Default, Deserialize, PartialEq)] 29 | pub struct Config { 30 | /// If `true`, connections that authenticated using the ANONYMOUS mechanism will be authorized 31 | /// to connect. This option has no practical effect unless the ANONYMOUS mechanism has also 32 | /// been enabled using the `auth` option. 33 | pub allow_anonymous: bool, 34 | 35 | /// Lists permitted authorization mechanisms. 36 | /// If this element doesn't exist, then all known mechanisms are allowed. 37 | // TODO: warn when multiple `` elements are defined, as we only support one 38 | // TODO: consider implementing `Deserialize` over in zbus crate, then removing this "skip..." 39 | #[serde(default, skip_deserializing)] 40 | pub auth: Option, 41 | 42 | /// If `true`, the bus daemon becomes a real daemon (forks into the background, etc.). 43 | pub fork: bool, 44 | 45 | /// If `true`, the bus daemon keeps its original umask when forking. 46 | /// This may be useful to avoid affecting the behavior of child processes. 47 | pub keep_umask: bool, 48 | 49 | /// Address that the bus should listen on. 50 | /// The address is in the standard D-Bus format that contains a transport name plus possible 51 | /// parameters/options. 52 | // TODO: warn when multiple `` elements are defined, as we only support one 53 | // TODO: consider implementing `Deserialize` over in zbus crate, then removing this "skip..." 54 | #[serde(default, skip_deserializing)] 55 | pub listen: Option
, 56 | 57 | /// The bus daemon will write its pid to the specified file. 58 | pub pidfile: Option, 59 | 60 | pub policies: Vec, 61 | 62 | /// Adds a directory to search for .service files, 63 | /// which tell the dbus-daemon how to start a program to provide a particular well-known bus 64 | /// name. 65 | #[serde(default)] 66 | pub servicedirs: Vec, 67 | 68 | /// Specifies the setuid helper that is used to launch system daemons with an alternate user. 69 | pub servicehelper: Option, 70 | 71 | /// If `true`, the bus daemon will log to syslog. 72 | pub syslog: bool, 73 | 74 | /// This element only controls which message bus specific environment variables are set in 75 | /// activated clients. 76 | pub r#type: Option, 77 | 78 | /// The user account the daemon should run as, as either a username or a UID. 79 | /// If the daemon cannot change to this UID on startup, it will exit. 80 | /// If this element is not present, the daemon will not change or care about its UID. 81 | pub user: Option, 82 | } 83 | 84 | impl TryFrom for Config { 85 | type Error = Error; 86 | 87 | fn try_from(value: Document) -> std::result::Result { 88 | let mut config = Config::default(); 89 | 90 | for element in value.busconfig { 91 | match element { 92 | Element::AllowAnonymous => config.allow_anonymous = true, 93 | Element::Auth(auth) => { 94 | config.auth = Some(AuthMechanism::from_str(&auth)?); 95 | } 96 | Element::Fork => config.fork = true, 97 | Element::Include(_) => { 98 | // NO-OP: removed during `Document::resolve_includes` 99 | } 100 | Element::Includedir(_) => { 101 | // NO-OP: removed during `Document::resolve_includedirs` 102 | } 103 | Element::KeepUmask => config.keep_umask = true, 104 | Element::Limit => { 105 | // NO-OP: deprecated and ignored 106 | } 107 | Element::Listen(listen) => { 108 | config.listen = Some(Address::from_str(&listen)?); 109 | } 110 | Element::Pidfile(p) => config.pidfile = Some(p), 111 | Element::Policy(pe) => { 112 | if let Some(p) = OptionalPolicy::try_from(pe)? { 113 | config.policies.push(p); 114 | } 115 | } 116 | Element::Servicedir(p) => { 117 | config.servicedirs.push(p); 118 | } 119 | Element::Servicehelper(p) => { 120 | // NOTE: we're assuming this has the same "last one wins" behaviour as `` 121 | 122 | // TODO: warn and then ignore if we aren't reading: 123 | // /usr/share/dbus-1/system.conf 124 | config.servicehelper = Some(p); 125 | } 126 | Element::StandardSessionServicedirs => { 127 | // TODO: warn and then ignore if we aren't reading: /etc/dbus-1/session.conf 128 | if let Ok(runtime_dir) = var("XDG_RUNTIME_DIR") { 129 | config 130 | .servicedirs 131 | .push(PathBuf::from(runtime_dir).join("dbus-1/services")); 132 | } 133 | if let Ok(data_dir) = var("XDG_DATA_HOME") { 134 | config 135 | .servicedirs 136 | .push(PathBuf::from(data_dir).join("dbus-1/services")); 137 | } 138 | let mut servicedirs_in_data_dirs = xdg_data_dirs() 139 | .iter() 140 | .map(|p| p.join("dbus-1/services")) 141 | .collect(); 142 | config.servicedirs.append(&mut servicedirs_in_data_dirs); 143 | config 144 | .servicedirs 145 | .push(PathBuf::from("/usr/share/dbus-1/services")); 146 | } 147 | Element::StandardSystemServicedirs => { 148 | // TODO: warn and then ignore if we aren't reading: 149 | // /usr/share/dbus-1/system.conf 150 | config 151 | .servicedirs 152 | .extend(STANDARD_SYSTEM_SERVICEDIRS.iter().map(PathBuf::from)); 153 | } 154 | Element::Syslog => config.syslog = true, 155 | Element::Type(TypeElement { r#type: value }) => config.r#type = Some(value), 156 | Element::User(s) => config.user = Some(s), 157 | } 158 | } 159 | 160 | Ok(config) 161 | } 162 | } 163 | 164 | impl Config { 165 | pub fn parse(s: &str) -> Result { 166 | // TODO: validate that our DOCTYPE and root element are correct 167 | quick_xml::de::from_str::(s)?.try_into() 168 | } 169 | 170 | pub fn read_file(file_path: impl AsRef) -> Result { 171 | // TODO: error message should contain file path to missing `` 172 | Document::read_file(&file_path)?.try_into() 173 | } 174 | } 175 | 176 | #[derive(Clone, Debug, Deserialize, PartialEq)] 177 | #[serde(rename_all = "lowercase")] 178 | pub enum BusType { 179 | Session, 180 | System, 181 | } 182 | 183 | #[derive(Clone, Debug, Default, Deserialize, PartialEq)] 184 | #[serde(rename_all = "snake_case")] 185 | pub enum MessageType { 186 | #[default] 187 | #[serde(rename = "*")] 188 | Any, 189 | MethodCall, 190 | MethodReturn, 191 | Signal, 192 | Error, 193 | } 194 | 195 | #[derive(Clone, Debug, Deserialize, PartialEq)] 196 | #[serde(rename_all = "lowercase")] 197 | pub enum Name { 198 | #[serde(rename = "*")] 199 | Any, 200 | Exact(String), 201 | Prefix(String), 202 | } 203 | 204 | const DEFAULT_DATA_DIRS: &[&str] = &["/usr/local/share", "/usr/share"]; 205 | 206 | const STANDARD_SYSTEM_SERVICEDIRS: &[&str] = &[ 207 | "/usr/local/share/dbus-1/system-services", 208 | "/usr/share/dbus-1/system-services", 209 | "/lib/dbus-1/system-services", 210 | ]; 211 | 212 | fn xdg_data_dirs() -> Vec { 213 | if let Ok(ok) = var("XDG_DATA_DIRS") { 214 | return ok.split(":").map(PathBuf::from).collect(); 215 | } 216 | DEFAULT_DATA_DIRS.iter().map(PathBuf::from).collect() 217 | } 218 | 219 | #[cfg(test)] 220 | mod tests { 221 | use rule::{ 222 | Access, ConnectOperation, NameOwnership, Operation, ReceiveOperation, SendOperation, 223 | }; 224 | 225 | use super::*; 226 | 227 | #[test] 228 | fn config_parse_with_dtd_and_root_element_ok() { 229 | let input = r#" 231 | 232 | "#; 233 | Config::parse(input).expect("should parse XML input"); 234 | } 235 | 236 | #[test] 237 | #[should_panic] 238 | fn config_parse_with_type_error() { 239 | let input = r#" 241 | 242 | not-a-valid-message-bus-type 243 | 244 | "#; 245 | Config::parse(input).expect("should parse XML input"); 246 | } 247 | 248 | #[test] 249 | fn config_parse_with_allow_anonymous_and_fork_and_keep_umask_and_syslog_ok() { 250 | let input = r#" 252 | 253 | 254 | 255 | 256 | 257 | 258 | "#; 259 | 260 | let config = Config::parse(input).expect("should parse XML input"); 261 | 262 | assert_eq!( 263 | config, 264 | Config { 265 | allow_anonymous: true, 266 | fork: true, 267 | keep_umask: true, 268 | syslog: true, 269 | ..Default::default() 270 | } 271 | ); 272 | } 273 | 274 | #[test] 275 | fn config_parse_with_auth_ok() { 276 | let input = r#" 278 | 279 | ANONYMOUS 280 | EXTERNAL 281 | 282 | "#; 283 | 284 | let config = Config::parse(input).expect("should parse XML input"); 285 | 286 | assert_eq!( 287 | config, 288 | Config { 289 | auth: Some(AuthMechanism::External), 290 | ..Default::default() 291 | } 292 | ); 293 | } 294 | 295 | #[test] 296 | fn config_parse_with_limit_ok() { 297 | let input = r#" 299 | 300 | 1000000000 301 | 302 | "#; 303 | 304 | Config::parse(input).expect("should parse XML input"); 305 | } 306 | 307 | #[test] 308 | fn config_parse_with_listen_ok() { 309 | let input = r#" 311 | 312 | unix:path=/tmp/foo 313 | tcp:host=localhost,port=1234 314 | tcp:host=localhost,port=0,family=ipv4 315 | 316 | "#; 317 | 318 | let config = Config::parse(input).expect("should parse XML input"); 319 | 320 | assert_eq!( 321 | config, 322 | Config { 323 | listen: Some( 324 | Address::from_str("tcp:host=localhost,port=0,family=ipv4") 325 | .expect("should parse address") 326 | ), 327 | ..Default::default() 328 | } 329 | ); 330 | } 331 | 332 | #[test] 333 | fn config_parse_with_overlapped_lists_ok() { 334 | // confirm this works with/without quick-xml's [`overlapped-lists`] feature 335 | // [`overlapped-lists`]: https://docs.rs/quick-xml/latest/quick_xml/#overlapped-lists 336 | let input = r#" 338 | 339 | ANONYMOUS 340 | unix:path=/tmp/foo 341 | 342 | 343 | 344 | 345 | 346 | EXTERNAL 347 | tcp:host=localhost,port=1234 348 | 349 | 350 | 351 | 352 | 353 | 354 | "#; 355 | 356 | let config = Config::parse(input).expect("should parse XML input"); 357 | 358 | assert_eq!( 359 | config, 360 | Config { 361 | auth: Some(AuthMechanism::External), 362 | listen: Some( 363 | Address::from_str("tcp:host=localhost,port=1234") 364 | .expect("should parse address") 365 | ), 366 | policies: vec![ 367 | Policy::DefaultContext(vec![ 368 | ( 369 | Access::Allow, 370 | Operation::Own(NameOwnership { 371 | own: Some(Name::Any) 372 | }) 373 | ), 374 | ( 375 | Access::Deny, 376 | Operation::Own(NameOwnership { 377 | own: Some(Name::Any) 378 | }) 379 | ), 380 | ( 381 | Access::Allow, 382 | Operation::Own(NameOwnership { 383 | own: Some(Name::Any) 384 | }) 385 | ), 386 | ]), 387 | Policy::DefaultContext(vec![ 388 | ( 389 | Access::Deny, 390 | Operation::Own(NameOwnership { 391 | own: Some(Name::Any) 392 | }) 393 | ), 394 | ( 395 | Access::Allow, 396 | Operation::Own(NameOwnership { 397 | own: Some(Name::Any) 398 | }) 399 | ), 400 | ( 401 | Access::Deny, 402 | Operation::Own(NameOwnership { 403 | own: Some(Name::Any) 404 | }) 405 | ), 406 | ]), 407 | ], 408 | ..Default::default() 409 | } 410 | ); 411 | } 412 | 413 | #[test] 414 | fn config_parse_with_pidfile_ok() { 415 | let input = r#" 417 | 418 | /var/run/busd.pid 419 | 420 | "#; 421 | 422 | let config = Config::parse(input).expect("should parse XML input"); 423 | 424 | assert_eq!( 425 | config, 426 | Config { 427 | pidfile: Some(PathBuf::from("/var/run/busd.pid")), 428 | ..Default::default() 429 | } 430 | ); 431 | } 432 | 433 | #[test] 434 | fn config_parse_with_policies_ok() { 435 | let input = r#" 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 456 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | "#; 476 | 477 | let config = Config::parse(input).expect("should parse XML input"); 478 | 479 | assert_eq!( 480 | config, 481 | Config { 482 | policies: vec![ 483 | Policy::DefaultContext(vec![ 484 | ( 485 | Access::Allow, 486 | Operation::Own(NameOwnership { 487 | own: Some(Name::Exact(String::from("org.freedesktop.DBus"))) 488 | }) 489 | ), 490 | ( 491 | Access::Allow, 492 | Operation::Own(NameOwnership { 493 | own: Some(Name::Prefix(String::from("org.freedesktop"))) 494 | }) 495 | ), 496 | ( 497 | Access::Allow, 498 | Operation::Connect(ConnectOperation { 499 | group: Some(String::from("wheel")), 500 | user: None, 501 | }) 502 | ), 503 | ( 504 | Access::Allow, 505 | Operation::Connect(ConnectOperation { 506 | group: None, 507 | user: Some(String::from("root")), 508 | }) 509 | ), 510 | ]), 511 | Policy::User( 512 | vec![ 513 | ( 514 | Access::Allow, 515 | Operation::Send(SendOperation { 516 | broadcast: Some(true), 517 | destination: Some(Name::Exact(String::from( 518 | "org.freedesktop.DBus" 519 | ))), 520 | error: Some(String::from("something bad")), 521 | interface: Some(String::from( 522 | "org.freedesktop.systemd1.Activator" 523 | )), 524 | max_fds: Some(128), 525 | member: Some(String::from("DoSomething")), 526 | min_fds: Some(12), 527 | path: Some(String::from("/org/freedesktop")), 528 | r#type: Some(MessageType::Signal), 529 | }) 530 | ), 531 | ( 532 | Access::Allow, 533 | Operation::Receive(ReceiveOperation { 534 | error: Some(String::from("something bad")), 535 | interface: Some(String::from( 536 | "org.freedesktop.systemd1.Activator" 537 | )), 538 | max_fds: Some(128), 539 | member: Some(String::from("DoSomething")), 540 | min_fds: Some(12), 541 | path: Some(String::from("/org/freedesktop")), 542 | sender: Some(String::from("org.freedesktop.DBus")), 543 | r#type: Some(MessageType::Signal), 544 | }) 545 | ) 546 | ], 547 | String::from("root") 548 | ), 549 | Policy::Group( 550 | vec![ 551 | ( 552 | Access::Allow, 553 | Operation::Send(SendOperation { 554 | broadcast: None, 555 | destination: Some(Name::Prefix(String::from( 556 | "org.freedesktop" 557 | ))), 558 | error: None, 559 | interface: None, 560 | max_fds: None, 561 | member: Some(String::from("DoSomething")), 562 | min_fds: None, 563 | path: None, 564 | r#type: None 565 | }) 566 | ), 567 | // ` 609 | 610 | 611 | 612 | 613 | 614 | "#; 615 | 616 | Config::parse(input).expect("should parse XML input"); 617 | } 618 | 619 | #[test] 620 | fn config_parse_with_policies_with_ignored_rules_and_rule_attributes_ok() { 621 | let input = r#" 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | "#; 638 | 639 | let config = Config::parse(input).expect("should parse XML input"); 640 | 641 | assert_eq!( 642 | config, 643 | Config { 644 | policies: vec![ 645 | Policy::DefaultContext(vec![ 646 | ( 647 | Access::Allow, 648 | // `eavesdrop="true"` is dropped, keep other attributes 649 | Operation::Send(SendOperation { 650 | broadcast: None, 651 | destination: Some(Name::Any), 652 | error: None, 653 | interface: None, 654 | max_fds: None, 655 | member: None, 656 | min_fds: None, 657 | path: None, 658 | r#type: None 659 | }) 660 | ), 661 | // `` has nothing left after dropping eavesdrop 662 | // `` is completely ignored 669 | ], 670 | ..Default::default() 671 | } 672 | ); 673 | } 674 | 675 | #[should_panic] 676 | #[test] 677 | fn config_parse_with_policies_with_own_and_own_prefix_error() { 678 | let input = r#" 680 | 681 | 682 | 683 | 684 | 685 | "#; 686 | 687 | Config::parse(input).expect("should parse XML input"); 688 | } 689 | 690 | #[should_panic] 691 | #[test] 692 | fn config_parse_with_policies_with_send_destination_and_send_destination_prefix_error() { 693 | let input = r#" 695 | 696 | 697 | 698 | 699 | 700 | "#; 701 | 702 | Config::parse(input).expect("should parse XML input"); 703 | } 704 | 705 | #[should_panic] 706 | #[test] 707 | fn config_parse_with_policies_with_send_and_receive_attributes_error() { 708 | let input = r#" 710 | 711 | 712 | 713 | 714 | 715 | "#; 716 | 717 | Config::parse(input).expect("should parse XML input"); 718 | } 719 | 720 | #[should_panic] 721 | #[test] 722 | fn config_parse_with_policies_without_attributes_error() { 723 | let input = r#" 725 | 726 | 727 | 728 | 729 | 730 | "#; 731 | 732 | Config::parse(input).expect("should parse XML input"); 733 | } 734 | 735 | #[test] 736 | fn config_parse_with_servicedir_and_standard_session_servicedirs_ok() { 737 | let input = r#" 739 | 740 | /example 741 | 742 | /anotherexample 743 | 744 | 745 | "#; 746 | 747 | let config = Config::parse(input).expect("should parse XML input"); 748 | 749 | // TODO: improve test: contents are dynamic depending upon environment variables 750 | assert_eq!(config.servicedirs.first(), Some(&PathBuf::from("/example"))); 751 | assert_eq!( 752 | config.servicedirs.last(), 753 | Some(&PathBuf::from("/usr/share/dbus-1/services")) 754 | ); 755 | } 756 | 757 | #[test] 758 | fn config_parse_with_servicedir_and_standard_system_servicedirs_ok() { 759 | let input = r#" 761 | 762 | /example 763 | 764 | /anotherexample 765 | 766 | 767 | "#; 768 | 769 | let config = Config::parse(input).expect("should parse XML input"); 770 | 771 | assert_eq!( 772 | config, 773 | Config { 774 | servicedirs: vec![ 775 | PathBuf::from("/example"), 776 | PathBuf::from("/usr/local/share/dbus-1/system-services"), 777 | PathBuf::from("/usr/share/dbus-1/system-services"), 778 | PathBuf::from("/lib/dbus-1/system-services"), 779 | PathBuf::from("/anotherexample"), 780 | PathBuf::from("/usr/local/share/dbus-1/system-services"), 781 | PathBuf::from("/usr/share/dbus-1/system-services"), 782 | PathBuf::from("/lib/dbus-1/system-services"), 783 | ], 784 | ..Default::default() 785 | } 786 | ); 787 | } 788 | 789 | #[test] 790 | fn config_parse_with_servicehelper_ok() { 791 | let input = r#" 793 | 794 | /example 795 | /anotherexample 796 | 797 | "#; 798 | 799 | let config = Config::parse(input).expect("should parse XML input"); 800 | 801 | assert_eq!( 802 | config, 803 | Config { 804 | servicehelper: Some(PathBuf::from("/anotherexample")), 805 | ..Default::default() 806 | } 807 | ); 808 | } 809 | 810 | #[test] 811 | fn config_parse_with_type_ok() { 812 | let input = r#" 814 | 815 | session 816 | system 817 | 818 | "#; 819 | 820 | let config = Config::parse(input).expect("should parse XML input"); 821 | 822 | assert_eq!( 823 | config, 824 | Config { 825 | r#type: Some(BusType::System), 826 | ..Default::default() 827 | } 828 | ); 829 | } 830 | 831 | #[test] 832 | fn config_parse_with_user_ok() { 833 | let input = r#" 835 | 836 | 1000 837 | alice 838 | 839 | "#; 840 | 841 | let config = Config::parse(input).expect("should parse XML input"); 842 | 843 | assert_eq!( 844 | config, 845 | Config { 846 | user: Some(String::from("alice")), 847 | ..Default::default() 848 | } 849 | ); 850 | } 851 | } 852 | -------------------------------------------------------------------------------- /src/config/policy.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use serde::Deserialize; 3 | 4 | use super::{ 5 | rule::{rules_try_from_rule_elements, Rule}, 6 | xml::{PolicyContext, PolicyElement}, 7 | }; 8 | 9 | #[derive(Clone, Debug, Deserialize, PartialEq)] 10 | pub enum Policy { 11 | DefaultContext(Vec), 12 | Group(Vec, String), 13 | MandatoryContext(Vec), 14 | User(Vec, String), 15 | } 16 | // TODO: implement Cmp/Ord to help stable-sort Policy values: 17 | // DefaultContext < Group < User < MandatoryContext 18 | 19 | pub type OptionalPolicy = Option; 20 | 21 | impl TryFrom for OptionalPolicy { 22 | type Error = Error; 23 | 24 | fn try_from(value: PolicyElement) -> std::result::Result { 25 | match value { 26 | PolicyElement { 27 | at_console: Some(_), 28 | context: None, 29 | group: None, 30 | user: None, 31 | .. 32 | } => Ok(None), 33 | PolicyElement { 34 | at_console: None, 35 | context: Some(c), 36 | group: None, 37 | rules, 38 | user: None, 39 | } => Ok(Some(match c { 40 | PolicyContext::Default => { 41 | Policy::DefaultContext(rules_try_from_rule_elements(rules)?) 42 | } 43 | PolicyContext::Mandatory => { 44 | Policy::MandatoryContext(rules_try_from_rule_elements(rules)?) 45 | } 46 | })), 47 | PolicyElement { 48 | at_console: None, 49 | context: None, 50 | group: Some(group), 51 | rules, 52 | user: None, 53 | } => Ok(Some(Policy::Group( 54 | rules_try_from_rule_elements(rules)?, 55 | group, 56 | ))), 57 | PolicyElement { 58 | at_console: None, 59 | context: None, 60 | group: None, 61 | rules, 62 | user: Some(user), 63 | } => Ok(Some(Policy::User( 64 | rules_try_from_rule_elements(rules)?, 65 | user, 66 | ))), 67 | _ => Err(Error::msg(format!( 68 | "policy contains conflicting attributes: {value:?}" 69 | ))), 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/config/rule.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Error, Result}; 2 | use serde::Deserialize; 3 | 4 | use super::{ 5 | xml::{RuleAttributes, RuleElement}, 6 | MessageType, Name, 7 | }; 8 | 9 | #[derive(Clone, Debug, Deserialize, PartialEq)] 10 | pub struct ConnectOperation { 11 | pub group: Option, 12 | pub user: Option, 13 | } 14 | 15 | impl From for ConnectOperation { 16 | fn from(value: RuleAttributes) -> Self { 17 | Self { 18 | group: value.group, 19 | user: value.user, 20 | } 21 | } 22 | } 23 | 24 | #[derive(Clone, Debug, Deserialize, PartialEq)] 25 | pub enum Operation { 26 | /// rules checked when a new connection to the message bus is established 27 | Connect(ConnectOperation), 28 | /// rules checked when a connection attempts to own a well-known bus names 29 | Own(NameOwnership), 30 | /// rules that are checked for each recipient of a message 31 | Receive(ReceiveOperation), 32 | /// rules that are checked when a connection attempts to send a message 33 | Send(SendOperation), 34 | } 35 | 36 | type OptionalOperation = Option; 37 | 38 | impl TryFrom for OptionalOperation { 39 | type Error = Error; 40 | 41 | fn try_from(value: RuleAttributes) -> std::result::Result { 42 | let has_connect = value.group.is_some() || value.user.is_some(); 43 | let has_own = value.own.is_some() || value.own_prefix.is_some(); 44 | let has_send = value.send_broadcast.is_some() 45 | || value.send_destination.is_some() 46 | || value.send_destination_prefix.is_some() 47 | || value.send_error.is_some() 48 | || value.send_interface.is_some() 49 | || value.send_member.is_some() 50 | || value.send_path.is_some() 51 | || value.send_requested_reply.is_some() 52 | || value.send_type.is_some(); 53 | let has_receive = value.receive_error.is_some() 54 | || value.receive_interface.is_some() 55 | || value.receive_member.is_some() 56 | || value.receive_path.is_some() 57 | || value.receive_sender.is_some() 58 | || value.receive_requested_reply.is_some() 59 | || value.receive_type.is_some(); 60 | 61 | let operations_count: i8 = vec![has_connect, has_own, has_receive, has_send] 62 | .into_iter() 63 | .map(i8::from) 64 | .sum(); 65 | 66 | if operations_count > 1 { 67 | return Err(Error::msg(format!("do not mix rule attributes for connect, own, receive, and/or send attributes in the same rule: {value:?}"))); 68 | } 69 | 70 | if has_connect { 71 | Ok(Some(Operation::Connect(ConnectOperation::from(value)))) 72 | } else if has_own { 73 | Ok(Some(Operation::Own(NameOwnership::from(value)))) 74 | } else if has_receive { 75 | Ok(Some(Operation::Receive(ReceiveOperation::from(value)))) 76 | } else if has_send { 77 | Ok(Some(Operation::Send(SendOperation::from(value)))) 78 | } else { 79 | Err(Error::msg(format!("rule must specify supported attributes for connect, own, receive, or send operations: {value:?}"))) 80 | } 81 | } 82 | } 83 | 84 | #[derive(Clone, Debug, Deserialize, PartialEq)] 85 | pub struct NameOwnership { 86 | pub own: Option, 87 | } 88 | 89 | impl From for NameOwnership { 90 | fn from(value: RuleAttributes) -> Self { 91 | let own = match value { 92 | RuleAttributes { 93 | own: Some(some), 94 | own_prefix: None, 95 | .. 96 | } if some == "*" => Some(Name::Any), 97 | RuleAttributes { 98 | own: Some(some), 99 | own_prefix: None, 100 | .. 101 | } => Some(Name::Exact(some)), 102 | RuleAttributes { 103 | own: None, 104 | own_prefix: Some(some), 105 | .. 106 | } => Some(Name::Prefix(some)), 107 | _ => None, 108 | }; 109 | Self { own } 110 | } 111 | } 112 | 113 | #[derive(Clone, Debug, Deserialize, PartialEq)] 114 | pub struct ReceiveOperation { 115 | pub error: Option, 116 | pub interface: Option, 117 | pub max_fds: Option, 118 | pub member: Option, 119 | pub min_fds: Option, 120 | pub path: Option, 121 | pub sender: Option, 122 | pub r#type: Option, 123 | } 124 | 125 | impl From for ReceiveOperation { 126 | fn from(value: RuleAttributes) -> Self { 127 | Self { 128 | error: value.receive_error, 129 | interface: value.receive_interface, 130 | max_fds: value.max_fds, 131 | member: value.receive_member, 132 | min_fds: value.min_fds, 133 | path: value.receive_path, 134 | sender: value.receive_sender, 135 | r#type: value.receive_type, 136 | } 137 | } 138 | } 139 | 140 | type OptionalRule = Option; 141 | 142 | impl TryFrom for OptionalRule { 143 | type Error = Error; 144 | 145 | fn try_from(value: RuleElement) -> std::result::Result { 146 | match value { 147 | RuleElement::Allow(RuleAttributes { 148 | group: Some(_), 149 | user: Some(_), 150 | .. 151 | }) 152 | | RuleElement::Deny(RuleAttributes { 153 | group: Some(_), 154 | user: Some(_), 155 | .. 156 | }) => Err(Error::msg(format!( 157 | "`group` cannot be combined with `user` in the same rule: {value:?}" 158 | ))), 159 | RuleElement::Allow(RuleAttributes { 160 | own: Some(_), 161 | own_prefix: Some(_), 162 | .. 163 | }) 164 | | RuleElement::Deny(RuleAttributes { 165 | own: Some(_), 166 | own_prefix: Some(_), 167 | .. 168 | }) => Err(Error::msg(format!( 169 | "`own_prefix` cannot be combined with `own` in the same rule: {value:?}" 170 | ))), 171 | RuleElement::Allow(RuleAttributes { 172 | send_destination: Some(_), 173 | send_destination_prefix: Some(_), 174 | .. 175 | }) 176 | | RuleElement::Deny(RuleAttributes { 177 | send_destination: Some(_), 178 | send_destination_prefix: Some(_), 179 | .. 180 | }) => Err(Error::msg(format!( 181 | "`send_destination_prefix` cannot be combined with `send_destination` in the same rule: {value:?}" 182 | ))), 183 | RuleElement::Allow(RuleAttributes { 184 | eavesdrop: Some(true), 185 | group: None, 186 | own: None, 187 | receive_requested_reply: None, 188 | receive_sender: None, 189 | send_broadcast: None, 190 | send_destination: None, 191 | send_destination_prefix: None, 192 | send_error: None, 193 | send_interface: None, 194 | send_member: None, 195 | send_path: None, 196 | send_requested_reply: None, 197 | send_type: None, 198 | user: None, 199 | .. 200 | }) => { 201 | // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 202 | Ok(None) 203 | } 204 | RuleElement::Allow( 205 | RuleAttributes { 206 | receive_requested_reply: Some(false), 207 | .. 208 | } 209 | | RuleAttributes { 210 | send_requested_reply: Some(false), 211 | .. 212 | }, 213 | ) => { 214 | // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 215 | Ok(None) 216 | } 217 | RuleElement::Allow(attrs) => { 218 | // if attrs.eavesdrop == Some(true) { 219 | // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 220 | // } 221 | match OptionalOperation::try_from(attrs)? { 222 | Some(some) => Ok(Some((Access::Allow, some))), 223 | None => Ok(None), 224 | } 225 | } 226 | RuleElement::Deny(RuleAttributes { 227 | eavesdrop: Some(true), 228 | .. 229 | }) => { 230 | // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 231 | Ok(None) 232 | } 233 | RuleElement::Deny( 234 | RuleAttributes { 235 | receive_requested_reply: Some(true), 236 | .. 237 | } 238 | | RuleAttributes { 239 | send_requested_reply: Some(true), 240 | .. 241 | }, 242 | ) => { 243 | // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 244 | Ok(None) 245 | } 246 | RuleElement::Deny(attrs) => match OptionalOperation::try_from(attrs)? { 247 | Some(some) => Ok(Some((Access::Deny, some))), 248 | None => Ok(None), 249 | }, 250 | } 251 | } 252 | } 253 | 254 | pub type Rule = (Access, Operation); 255 | 256 | #[derive(Clone, Debug, Deserialize, PartialEq)] 257 | pub enum Access { 258 | Allow, 259 | Deny, 260 | } 261 | 262 | #[derive(Clone, Debug, Deserialize, PartialEq)] 263 | pub struct SendOperation { 264 | pub broadcast: Option, 265 | pub destination: Option, 266 | pub error: Option, 267 | pub interface: Option, 268 | pub max_fds: Option, 269 | pub member: Option, 270 | pub min_fds: Option, 271 | pub path: Option, 272 | pub r#type: Option, 273 | } 274 | 275 | impl From for SendOperation { 276 | fn from(value: RuleAttributes) -> Self { 277 | let destination = match value { 278 | RuleAttributes { 279 | send_destination: Some(some), 280 | send_destination_prefix: None, 281 | .. 282 | } if some == "*" => Some(Name::Any), 283 | RuleAttributes { 284 | send_destination: Some(some), 285 | send_destination_prefix: None, 286 | .. 287 | } => Some(Name::Exact(some)), 288 | RuleAttributes { 289 | send_destination: None, 290 | send_destination_prefix: Some(some), 291 | .. 292 | } => Some(Name::Prefix(some)), 293 | _ => None, 294 | }; 295 | Self { 296 | broadcast: value.send_broadcast, 297 | destination, 298 | error: value.send_error, 299 | interface: value.send_interface, 300 | max_fds: value.max_fds, 301 | member: value.send_member, 302 | min_fds: value.min_fds, 303 | path: value.send_path, 304 | r#type: value.send_type, 305 | } 306 | } 307 | } 308 | 309 | pub fn rules_try_from_rule_elements(value: Vec) -> Result> { 310 | let mut rules = vec![]; 311 | for rule in value { 312 | let rule = OptionalRule::try_from(rule)?; 313 | if let Some(some) = rule { 314 | rules.push(some); 315 | } 316 | } 317 | Ok(rules) 318 | } 319 | -------------------------------------------------------------------------------- /src/config/xml.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::current_dir, 3 | ffi::OsString, 4 | fs::{read_dir, read_to_string}, 5 | path::{Path, PathBuf}, 6 | str::FromStr, 7 | }; 8 | 9 | use anyhow::{Error, Result}; 10 | use serde::Deserialize; 11 | use tracing::{error, warn}; 12 | 13 | use super::{BusType, MessageType}; 14 | 15 | /// The bus configuration. 16 | /// 17 | /// This is currently only loaded from the [XML configuration files] defined by the specification. 18 | /// We plan to add support for other formats (e.g JSON) in the future. 19 | /// 20 | /// [XML configuration files]: https://dbus.freedesktop.org/doc/dbus-daemon.1.html#configuration_file 21 | #[derive(Clone, Debug, Default, Deserialize, PartialEq)] 22 | pub struct Document { 23 | #[serde(rename = "$value", default)] 24 | pub busconfig: Vec, 25 | file_path: Option, 26 | } 27 | 28 | impl FromStr for Document { 29 | type Err = Error; 30 | 31 | fn from_str(s: &str) -> Result { 32 | quick_xml::de::from_str(s).map_err(Error::msg) 33 | } 34 | } 35 | 36 | impl Document { 37 | pub fn read_file(file_path: impl AsRef) -> Result { 38 | let text = read_to_string(file_path.as_ref())?; 39 | 40 | let mut doc = Document::from_str(&text)?; 41 | doc.file_path = Some(file_path.as_ref().to_path_buf()); 42 | doc.resolve_includedirs()?.resolve_includes() 43 | } 44 | 45 | fn resolve_includedirs(self) -> Result { 46 | let base_path = self.base_path()?; 47 | let Document { 48 | busconfig, 49 | file_path, 50 | } = self; 51 | 52 | let mut doc = Document { 53 | busconfig: vec![], 54 | file_path: None, 55 | }; 56 | 57 | for el in busconfig { 58 | match el { 59 | Element::Includedir(dir_path) => { 60 | let dir_path = resolve_include_path(&base_path, &dir_path); 61 | let dir_path = match dir_path.canonicalize() { 62 | Ok(ok) => ok, 63 | // we treat `` as though it has `ignore_missing="yes"` 64 | Err(err) => { 65 | warn!( 66 | "cannot resolve '{}' to an absolute path: {}", 67 | &dir_path.display(), 68 | err 69 | ); 70 | continue; 71 | } 72 | }; 73 | match read_dir(&dir_path) { 74 | Ok(ok) => { 75 | for entry in ok { 76 | let path = entry?.path(); 77 | if path.extension() == Some(&OsString::from("conf")) 78 | && path.is_file() 79 | { 80 | doc.busconfig.push(Element::Include(IncludeElement { 81 | file_path: path, 82 | ..Default::default() 83 | })); 84 | } 85 | } 86 | } 87 | // we treat `` as though it has `ignore_missing="yes"` 88 | Err(err) => { 89 | warn!( 90 | "cannot read '{}': {}", 91 | &dir_path.display(), 92 | err 93 | ); 94 | continue; 95 | } 96 | } 97 | } 98 | _ => doc.busconfig.push(el), 99 | } 100 | } 101 | 102 | doc.file_path = file_path; 103 | Ok(doc) 104 | } 105 | 106 | fn resolve_includes(self) -> Result { 107 | // TODO: implement protection against circular `` references 108 | let base_path = self.base_path()?; 109 | let Document { 110 | busconfig, 111 | file_path, 112 | } = self; 113 | 114 | let mut doc = Document { 115 | busconfig: vec![], 116 | file_path: None, 117 | }; 118 | 119 | for el in busconfig { 120 | match el { 121 | Element::Include(include) => { 122 | if include.if_selinux_enable == IncludeOption::Yes 123 | || include.selinux_root_relative == IncludeOption::Yes 124 | { 125 | // TODO: implement SELinux support 126 | continue; 127 | } 128 | 129 | let ignore_missing = include.ignore_missing == IncludeOption::Yes; 130 | let file_path = resolve_include_path(&base_path, &include.file_path); 131 | let file_path = match file_path.canonicalize().map_err(Error::msg) { 132 | Ok(ok) => ok, 133 | Err(err) => { 134 | let msg = format!( 135 | "cannot resolve '{}' to an absolute path: {}", 136 | &file_path.display(), 137 | err 138 | ); 139 | if ignore_missing { 140 | warn!(msg); 141 | continue; 142 | } 143 | error!(msg); 144 | return Err(err); 145 | } 146 | }; 147 | let mut included = match Document::read_file(&file_path) { 148 | Ok(ok) => ok, 149 | Err(err) => { 150 | let msg = format!( 151 | "'{}' should contain valid XML", 152 | include.file_path.display() 153 | ); 154 | if ignore_missing { 155 | warn!(msg); 156 | continue; 157 | } 158 | error!(msg); 159 | return Err(err); 160 | } 161 | }; 162 | doc.busconfig.append(&mut included.busconfig); 163 | } 164 | _ => doc.busconfig.push(el), 165 | } 166 | } 167 | 168 | doc.file_path = file_path; 169 | Ok(doc) 170 | } 171 | 172 | fn base_path(&self) -> Result { 173 | match &self.file_path { 174 | Some(some) => Ok(some 175 | .parent() 176 | .ok_or_else(|| Error::msg("`` path should contain a file name"))? 177 | .to_path_buf()), 178 | None => { 179 | warn!("cannot determine file path for this XML document, using current working directory"); 180 | current_dir().map_err(Error::msg) 181 | } 182 | } 183 | } 184 | } 185 | 186 | #[derive(Clone, Debug, Deserialize, PartialEq)] 187 | #[serde(rename_all = "snake_case")] 188 | pub enum Element { 189 | AllowAnonymous, 190 | Auth(String), 191 | Fork, 192 | /// Include a file at this point. If the filename is relative, it is located relative to the 193 | /// configuration file doing the including. 194 | Include(IncludeElement), 195 | /// Files in the directory are included in undefined order. 196 | /// Only files ending in ".conf" are included. 197 | Includedir(PathBuf), 198 | KeepUmask, 199 | Listen(String), 200 | Limit, 201 | Pidfile(PathBuf), 202 | Policy(PolicyElement), 203 | Servicedir(PathBuf), 204 | Servicehelper(PathBuf), 205 | /// Requests a standard set of session service directories. 206 | /// Its effect is similar to specifying a series of elements for each of the data 207 | /// directories, in the order given here. 208 | StandardSessionServicedirs, 209 | /// Specifies the standard system-wide activation directories that should be searched for 210 | /// service files. 211 | StandardSystemServicedirs, 212 | Syslog, 213 | Type(TypeElement), 214 | User(String), 215 | } 216 | 217 | #[derive(Clone, Debug, Default, Deserialize, PartialEq)] 218 | pub struct IncludeElement { 219 | #[serde(default, rename = "@ignore_missing")] 220 | ignore_missing: IncludeOption, 221 | 222 | // TODO: implement SELinux 223 | #[serde(default, rename = "@if_selinux_enabled")] 224 | if_selinux_enable: IncludeOption, 225 | #[serde(default, rename = "@selinux_root_relative")] 226 | selinux_root_relative: IncludeOption, 227 | 228 | #[serde(rename = "$value")] 229 | file_path: PathBuf, 230 | } 231 | 232 | #[derive(Clone, Debug, Default, Deserialize, PartialEq)] 233 | #[serde(rename_all = "lowercase")] 234 | pub enum IncludeOption { 235 | #[default] 236 | No, 237 | Yes, 238 | } 239 | 240 | #[derive(Clone, Debug, Deserialize, PartialEq)] 241 | #[serde(rename_all = "snake_case")] 242 | pub enum PolicyContext { 243 | Default, 244 | Mandatory, 245 | } 246 | 247 | #[derive(Clone, Debug, Default, Deserialize, PartialEq)] 248 | pub struct PolicyElement { 249 | #[serde(rename = "@at_console")] 250 | pub at_console: Option, 251 | #[serde(rename = "@context")] 252 | pub context: Option, 253 | #[serde(rename = "@group")] 254 | pub group: Option, 255 | #[serde(rename = "$value", default)] 256 | pub rules: Vec, 257 | #[serde(rename = "@user")] 258 | pub user: Option, 259 | } 260 | 261 | #[derive(Clone, Debug, Default, Deserialize, PartialEq)] 262 | pub struct RuleAttributes { 263 | #[serde(rename = "@max_fds")] 264 | pub max_fds: Option, 265 | #[serde(rename = "@min_fds")] 266 | pub min_fds: Option, 267 | 268 | #[serde(rename = "@receive_error")] 269 | pub receive_error: Option, 270 | #[serde(rename = "@receive_interface")] 271 | pub receive_interface: Option, 272 | /// deprecated and ignored 273 | #[serde(rename = "@receive_member")] 274 | pub receive_member: Option, 275 | #[serde(rename = "@receive_path")] 276 | pub receive_path: Option, 277 | #[serde(rename = "@receive_sender")] 278 | pub receive_sender: Option, 279 | #[serde(rename = "@receive_type")] 280 | pub receive_type: Option, 281 | 282 | #[serde(rename = "@send_broadcast")] 283 | pub send_broadcast: Option, 284 | #[serde(rename = "@send_destination")] 285 | pub send_destination: Option, 286 | #[serde(rename = "@send_destination_prefix")] 287 | pub send_destination_prefix: Option, 288 | #[serde(rename = "@send_error")] 289 | pub send_error: Option, 290 | #[serde(rename = "@send_interface")] 291 | pub send_interface: Option, 292 | #[serde(rename = "@send_member")] 293 | pub send_member: Option, 294 | #[serde(rename = "@send_path")] 295 | pub send_path: Option, 296 | #[serde(rename = "@send_type")] 297 | pub send_type: Option, 298 | 299 | /// deprecated and ignored 300 | #[serde(rename = "@receive_requested_reply")] 301 | pub receive_requested_reply: Option, 302 | /// deprecated and ignored 303 | #[serde(rename = "@send_requested_reply")] 304 | pub send_requested_reply: Option, 305 | 306 | /// deprecated and ignored 307 | #[serde(rename = "@eavesdrop")] 308 | pub eavesdrop: Option, 309 | 310 | #[serde(rename = "@own")] 311 | pub own: Option, 312 | #[serde(rename = "@own_prefix")] 313 | pub own_prefix: Option, 314 | 315 | #[serde(rename = "@group")] 316 | pub group: Option, 317 | #[serde(rename = "@user")] 318 | pub user: Option, 319 | } 320 | 321 | #[derive(Clone, Debug, Deserialize, PartialEq)] 322 | #[serde(rename_all = "snake_case")] 323 | pub enum RuleElement { 324 | Allow(RuleAttributes), 325 | Deny(RuleAttributes), 326 | } 327 | 328 | #[derive(Clone, Debug, Deserialize, PartialEq)] 329 | pub struct TypeElement { 330 | #[serde(rename = "$text")] 331 | pub r#type: BusType, 332 | } 333 | 334 | fn resolve_include_path(base_path: impl AsRef, include_path: impl AsRef) -> PathBuf { 335 | let p = include_path.as_ref(); 336 | if p.is_absolute() { 337 | return p.to_path_buf(); 338 | } 339 | 340 | base_path.as_ref().join(p) 341 | } 342 | -------------------------------------------------------------------------------- /src/fdo/dbus.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | sync::{Arc, Weak}, 4 | }; 5 | 6 | use enumflags2::BitFlags; 7 | use tokio::spawn; 8 | use tracing::warn; 9 | use zbus::{ 10 | fdo::{ 11 | ConnectionCredentials, Error, ReleaseNameReply, RequestNameFlags, RequestNameReply, Result, 12 | }, 13 | interface, message, 14 | names::{BusName, InterfaceName, OwnedBusName, OwnedUniqueName, UniqueName, WellKnownName}, 15 | object_server::{ResponseDispatchNotifier, SignalEmitter}, 16 | zvariant::Optional, 17 | OwnedGuid, OwnedMatchRule, 18 | }; 19 | 20 | use super::msg_sender; 21 | use crate::{peer::Peer, peers::Peers}; 22 | 23 | #[derive(Debug)] 24 | pub struct DBus { 25 | peers: Weak, 26 | guid: OwnedGuid, 27 | } 28 | 29 | impl DBus { 30 | pub const PATH: &'static str = "/org/freedesktop/DBus"; 31 | pub const INTERFACE: &'static str = "org.freedesktop.DBus"; 32 | 33 | pub fn new(peers: Arc, guid: OwnedGuid) -> Self { 34 | Self { 35 | peers: Arc::downgrade(&peers), 36 | guid, 37 | } 38 | } 39 | 40 | /// Helper for D-Bus methods that call a function on a peer. 41 | async fn call_mut_on_peer(&self, func: F, hdr: message::Header<'_>) -> Result 42 | where 43 | F: FnOnce(&mut Peer) -> Result, 44 | { 45 | let name = msg_sender(&hdr); 46 | let peers = self.peers()?; 47 | let mut peers = peers.peers_mut().await; 48 | let peer = peers 49 | .get_mut(name.as_str()) 50 | .ok_or_else(|| Error::NameHasNoOwner(format!("No such peer: {}", name)))?; 51 | 52 | func(peer) 53 | } 54 | 55 | fn peers(&self) -> Result> { 56 | self.peers 57 | .upgrade() 58 | // Can it happen in any other situation than the bus shutting down? 59 | .ok_or_else(|| Error::Failed("Bus shutting down.".to_string())) 60 | } 61 | } 62 | 63 | #[interface(interface = "org.freedesktop.DBus", introspection_docs = false)] 64 | impl DBus { 65 | /// This is already called & handled and we only need to handle it once. 66 | async fn hello( 67 | &self, 68 | #[zbus(header)] hdr: message::Header<'_>, 69 | #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, 70 | ) -> Result> { 71 | let name = msg_sender(&hdr); 72 | let peers = self.peers()?; 73 | let mut peers = peers.peers_mut().await; 74 | let peer = peers 75 | .get_mut(name.as_str()) 76 | .ok_or_else(|| Error::NameHasNoOwner(format!("No such peer: {}", name)))?; 77 | peer.hello().await?; 78 | 79 | // Notify name change in a separate task because we want: 80 | // 1. `Hello` to return ASAP and hence client connection to be esablished. 81 | // 2. The `Hello` response to arrive before the `NameAcquired` signal. 82 | let unique_name = peer.unique_name().clone(); 83 | let (response, listener) = ResponseDispatchNotifier::new(unique_name.clone()); 84 | let ctxt = emitter.to_owned(); 85 | spawn(async move { 86 | listener.await; 87 | let owner = UniqueName::from(unique_name); 88 | 89 | if let Err(e) = Self::name_owner_changed( 90 | &ctxt, 91 | owner.clone().into(), 92 | None.into(), 93 | Some(owner.clone()).into(), 94 | ) 95 | .await 96 | { 97 | warn!("Failed to notify peers of name change: {}", e); 98 | } 99 | 100 | let ctxt = ctxt.set_destination(owner.clone().into()); 101 | if let Err(e) = Self::name_acquired(&ctxt, owner.into()).await { 102 | warn!("Failed to send `NameAcquired` signal: {}", e); 103 | } 104 | }); 105 | 106 | Ok(response) 107 | } 108 | 109 | /// Ask the message bus to assign the given name to the method caller. 110 | async fn request_name( 111 | &self, 112 | name: WellKnownName<'_>, 113 | flags: BitFlags, 114 | #[zbus(header)] hdr: message::Header<'_>, 115 | ) -> Result { 116 | let unique_name = msg_sender(&hdr); 117 | let peers = self.peers()?; 118 | let (reply, name_owner_changed) = peers 119 | .name_registry_mut() 120 | .await 121 | .request_name(name, unique_name.clone(), flags) 122 | .await; 123 | if let Some(changed) = name_owner_changed { 124 | peers 125 | .notify_name_changes(changed) 126 | .await 127 | .map_err(|e| Error::Failed(e.to_string()))?; 128 | } 129 | 130 | Ok(reply) 131 | } 132 | 133 | /// Ask the message bus to release the method caller's claim to the given name. 134 | async fn release_name( 135 | &self, 136 | name: WellKnownName<'_>, 137 | #[zbus(header)] hdr: message::Header<'_>, 138 | ) -> Result { 139 | let unique_name = msg_sender(&hdr); 140 | let peers = self.peers()?; 141 | let (reply, name_owner_changed) = peers 142 | .name_registry_mut() 143 | .await 144 | .release_name(name, unique_name.clone()) 145 | .await; 146 | if let Some(changed) = name_owner_changed { 147 | peers 148 | .notify_name_changes(changed) 149 | .await 150 | .map_err(|e| Error::Failed(e.to_string()))?; 151 | } 152 | 153 | Ok(reply) 154 | } 155 | 156 | /// Returns the unique connection name of the primary owner of the name given. 157 | async fn get_name_owner(&self, name: BusName<'_>) -> Result { 158 | let peers = self.peers()?; 159 | match name { 160 | BusName::WellKnown(name) => peers.name_registry().await.lookup(name).ok_or_else(|| { 161 | Error::NameHasNoOwner("Name is not owned by anyone. Take it!".to_string()) 162 | }), 163 | BusName::Unique(name) => { 164 | if peers.peers().await.contains_key(&*name) { 165 | Ok(name.into()) 166 | } else { 167 | Err(Error::NameHasNoOwner( 168 | "Name is not owned by anyone.".to_string(), 169 | )) 170 | } 171 | } 172 | } 173 | } 174 | 175 | /// Adds a match rule to match messages going through the message bus 176 | async fn add_match( 177 | &self, 178 | rule: OwnedMatchRule, 179 | #[zbus(header)] hdr: message::Header<'_>, 180 | ) -> Result<()> { 181 | self.call_mut_on_peer( 182 | move |peer| { 183 | peer.add_match_rule(rule); 184 | 185 | Ok(()) 186 | }, 187 | hdr, 188 | ) 189 | .await 190 | } 191 | 192 | /// Removes the first rule that matches. 193 | async fn remove_match( 194 | &self, 195 | rule: OwnedMatchRule, 196 | #[zbus(header)] hdr: message::Header<'_>, 197 | ) -> Result<()> { 198 | self.call_mut_on_peer(move |peer| peer.remove_match_rule(rule), hdr) 199 | .await 200 | } 201 | 202 | /// Returns auditing data used by Solaris ADT, in an unspecified binary format. 203 | fn get_adt_audit_session_data(&self, _bus_name: BusName<'_>) -> Result> { 204 | Err(Error::NotSupported("Solaris really?".to_string())) 205 | } 206 | 207 | /// Returns as many credentials as possible for the process connected to the server. 208 | async fn get_connection_credentials( 209 | &self, 210 | bus_name: BusName<'_>, 211 | ) -> Result { 212 | let owner = self.get_name_owner(bus_name.clone()).await?; 213 | let peers = self.peers()?; 214 | let peers = peers.peers().await; 215 | let peer = peers 216 | .get(&owner) 217 | .ok_or_else(|| Error::Failed(format!("Peer `{}` not found", bus_name)))?; 218 | 219 | peer.conn().peer_credentials().await.map_err(|e| { 220 | Error::Failed(format!( 221 | "Failed to get peer credentials for `{}`: {}", 222 | bus_name, e 223 | )) 224 | }) 225 | } 226 | 227 | /// Returns the security context used by SELinux, in an unspecified format. 228 | #[zbus(name = "GetConnectionSELinuxSecurityContext")] 229 | async fn get_connection_selinux_security_context( 230 | &self, 231 | bus_name: BusName<'_>, 232 | ) -> Result> { 233 | self.get_connection_credentials(bus_name) 234 | .await 235 | .and_then(|c| { 236 | c.into_linux_security_label().ok_or_else(|| { 237 | Error::SELinuxSecurityContextUnknown("Unimplemented".to_string()) 238 | }) 239 | }) 240 | } 241 | 242 | /// Returns the Unix process ID of the process connected to the server. 243 | #[zbus(name = "GetConnectionUnixProcessID")] 244 | async fn get_connection_unix_process_id(&self, bus_name: BusName<'_>) -> Result { 245 | self.get_connection_credentials(bus_name.clone()) 246 | .await 247 | .and_then(|c| { 248 | c.process_id().ok_or_else(|| { 249 | Error::UnixProcessIdUnknown(format!( 250 | "Could not determine Unix user ID of `{bus_name}`" 251 | )) 252 | }) 253 | }) 254 | } 255 | 256 | /// Returns the Unix user ID of the process connected to the server. 257 | async fn get_connection_unix_user(&self, bus_name: BusName<'_>) -> Result { 258 | self.get_connection_credentials(bus_name.clone()) 259 | .await 260 | .and_then(|c| { 261 | c.unix_user_id().ok_or_else(|| { 262 | Error::Failed(format!("Could not determine Unix user ID of `{bus_name}`")) 263 | }) 264 | }) 265 | } 266 | 267 | /// Gets the unique ID of the bus. 268 | fn get_id(&self) -> &OwnedGuid { 269 | &self.guid 270 | } 271 | 272 | /// Returns a list of all names that can be activated on the bus. 273 | fn list_activatable_names(&self) -> &[OwnedBusName] { 274 | // TODO: Return actual list when we support service activation. 275 | &[] 276 | } 277 | 278 | /// Returns a list of all currently-owned names on the bus. 279 | async fn list_names(&self) -> Result> { 280 | let peers = self.peers()?; 281 | let mut names: Vec<_> = peers 282 | .peers() 283 | .await 284 | .keys() 285 | .cloned() 286 | .map(|n| BusName::Unique(n.into()).into()) 287 | .collect(); 288 | 289 | names.extend( 290 | peers 291 | .name_registry() 292 | .await 293 | .all_names() 294 | .keys() 295 | .map(|n| BusName::WellKnown(n.into()).into()), 296 | ); 297 | 298 | Ok(names) 299 | } 300 | 301 | /// List the connections currently queued for a bus name. 302 | async fn list_queued_owners(&self, name: WellKnownName<'_>) -> Result> { 303 | self.peers()? 304 | .name_registry() 305 | .await 306 | .waiting_list(name) 307 | .ok_or_else(|| { 308 | Error::NameHasNoOwner("Name is not owned by anyone. Take it!".to_string()) 309 | }) 310 | .map(|owners| owners.map(|o| o.unique_name()).cloned().collect()) 311 | } 312 | 313 | /// Checks if the specified name exists (currently has an owner). 314 | async fn name_has_owner(&self, name: BusName<'_>) -> Result { 315 | match self.get_name_owner(name).await { 316 | Ok(_) => Ok(true), 317 | Err(Error::NameHasNoOwner(_)) => Ok(false), 318 | Err(e) => Err(e), 319 | } 320 | } 321 | 322 | /// Tries to launch the executable associated with a name (service activation). 323 | fn start_service_by_name(&self, _name: WellKnownName<'_>, _flags: u32) -> Result { 324 | // TODO: Implement when we support service activation. 325 | Err(Error::Failed( 326 | "Service activation not yet supported".to_string(), 327 | )) 328 | } 329 | 330 | /// This method adds to or modifies that environment when activating services. 331 | fn update_activation_environment(&self, _environment: HashMap<&str, &str>) -> Result<()> { 332 | // TODO: Implement when we support service activation. 333 | Err(Error::Failed( 334 | "Service activation not yet supported".to_string(), 335 | )) 336 | } 337 | 338 | /// Reload server configuration. 339 | fn reload_config(&self) -> Result<()> { 340 | // TODO: Implement when we support configuration. 341 | Err(Error::Failed( 342 | "No server configuration to reload.".to_string(), 343 | )) 344 | } 345 | 346 | /// Easter egg method. 347 | fn dune(&self) -> &str { 348 | "I must not fear. Fear is the mind-killer. Fear is the little-death that brings total \ 349 | obliteration. I will face my fear. I will permit it to pass over me and through me. And \ 350 | when it has gone past I will turn the inner eye to see its path. Where the fear has gone \ 351 | there will be nothing. Only **I** will remain!" 352 | } 353 | 354 | // 355 | // Propertries 356 | // 357 | 358 | /// This property lists abstract “features” provided by the message bus, and can be used by 359 | /// clients to detect the capabilities of the message bus with which they are communicating. 360 | #[zbus(property)] 361 | fn features(&self) -> &[&str] { 362 | &[] 363 | } 364 | 365 | /// This property lists interfaces provided by the `/org/freedesktop/DBus` object, and can be 366 | /// used by clients to detect the capabilities of the message bus with which they are 367 | /// communicating. Unlike the standard Introspectable interface, querying this property does not 368 | /// require parsing XML. This property was added in version 1.11.x of the reference 369 | /// implementation of the message bus. 370 | /// 371 | /// The standard `org.freedesktop.DBus` and `org.freedesktop.DBus.Properties` interfaces are not 372 | /// included in the value of this property, because their presence can be inferred from the fact 373 | /// that a method call on `org.freedesktop.DBus.Properties` asking for properties of 374 | /// `org.freedesktop.DBus` was successful. The standard `org.freedesktop.DBus.Peer` and 375 | /// `org.freedesktop.DBus.Introspectable` interfaces are not included in the value of this 376 | /// property either, because they do not indicate features of the message bus implementation. 377 | #[zbus(property)] 378 | fn interfaces(&self) -> &[InterfaceName<'_>] { 379 | // TODO: List `org.freedesktop.DBus.Monitoring` when we support it. 380 | &[] 381 | } 382 | 383 | /// This signal indicates that the owner of a name has changed. 384 | /// 385 | /// It's also the signal to use to detect the appearance of new names on the bus. 386 | #[zbus(signal)] 387 | pub async fn name_owner_changed( 388 | emitter: &SignalEmitter<'_>, 389 | name: BusName<'_>, 390 | old_owner: Optional>, 391 | new_owner: Optional>, 392 | ) -> zbus::Result<()>; 393 | 394 | /// This signal is sent to a specific application when it loses ownership of a name. 395 | #[zbus(signal)] 396 | pub async fn name_lost(emitter: &SignalEmitter<'_>, name: BusName<'_>) -> zbus::Result<()>; 397 | 398 | /// This signal is sent to a specific application when it gains ownership of a name. 399 | #[zbus(signal)] 400 | pub async fn name_acquired(emitter: &SignalEmitter<'_>, name: BusName<'_>) -> zbus::Result<()>; 401 | } 402 | -------------------------------------------------------------------------------- /src/fdo/mod.rs: -------------------------------------------------------------------------------- 1 | mod dbus; 2 | pub use dbus::*; 3 | mod monitoring; 4 | pub use monitoring::*; 5 | use zbus::{message, names::UniqueName}; 6 | 7 | pub const BUS_NAME: &str = "org.freedesktop.DBus"; 8 | 9 | /// Helper for getting the peer name from a message header. 10 | fn msg_sender<'h>(hdr: &'h message::Header<'h>) -> &'h UniqueName<'h> { 11 | // SAFETY: The bus (that's us!) is supposed to ensure a valid sender on the message. 12 | hdr.sender().expect("Missing `sender` header") 13 | } 14 | -------------------------------------------------------------------------------- /src/fdo/monitoring.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Weak}; 2 | 3 | use tokio::spawn; 4 | use tracing::{debug, warn}; 5 | use zbus::{ 6 | fdo::{Error, Result}, 7 | interface, message, 8 | object_server::{ResponseDispatchNotifier, SignalEmitter}, 9 | zvariant::Optional, 10 | }; 11 | 12 | use super::msg_sender; 13 | use crate::{fdo::DBus, match_rules::MatchRules, peers::Peers}; 14 | 15 | #[derive(Debug)] 16 | pub struct Monitoring { 17 | peers: Weak, 18 | } 19 | 20 | impl Monitoring { 21 | pub const PATH: &'static str = "/org/freedesktop/DBus"; 22 | pub const INTERFACE: &'static str = "org.freedesktop.DBus.Monitoring"; 23 | 24 | pub fn new(peers: Arc) -> Self { 25 | Self { 26 | peers: Arc::downgrade(&peers), 27 | } 28 | } 29 | } 30 | 31 | #[interface( 32 | interface = "org.freedesktop.DBus.Monitoring", 33 | introspection_docs = false 34 | )] 35 | impl Monitoring { 36 | async fn become_monitor( 37 | &self, 38 | match_rules: MatchRules, 39 | _flags: u32, 40 | #[zbus(header)] hdr: message::Header<'_>, 41 | #[zbus(signal_emitter)] ctxt: SignalEmitter<'_>, 42 | ) -> Result> { 43 | let owner = msg_sender(&hdr).to_owned(); 44 | let peers = self 45 | .peers 46 | .upgrade() 47 | // Can it happen in any other situation than the bus shutting down? 48 | .ok_or_else(|| Error::Failed("Bus shutting down.".to_string()))?; 49 | if !peers.make_monitor(&owner, match_rules).await { 50 | return Err(Error::NameHasNoOwner(format!("No such peer: {}", owner))); 51 | } 52 | debug!("{} became a monitor", owner); 53 | 54 | // We want to emit the name change signals **after** the `BecomeMonitor` method returns. 55 | // Otherwise, some clients (e.g `busctl monitor`) can get confused. 56 | let (response, listener) = ResponseDispatchNotifier::new(()); 57 | let ctxt = ctxt.to_owned(); 58 | spawn(async move { 59 | listener.await; 60 | 61 | let names_changes = peers 62 | .name_registry_mut() 63 | .await 64 | .release_all(owner.clone()) 65 | .await; 66 | for changed in names_changes { 67 | if let Err(e) = DBus::name_owner_changed( 68 | &ctxt, 69 | changed.name.clone().into(), 70 | Some(owner.clone()).into(), 71 | Optional::default(), 72 | ) 73 | .await 74 | { 75 | warn!("Failed to notify peers of name change: {}", e); 76 | } 77 | 78 | let ctxt = ctxt.clone().set_destination(owner.clone().into()); 79 | if let Err(e) = DBus::name_lost(&ctxt, changed.name.into()).await { 80 | warn!("Failed to send `NameLost` signal: {}", e); 81 | } 82 | } 83 | 84 | if let Err(e) = DBus::name_owner_changed( 85 | &ctxt, 86 | owner.clone().into(), 87 | Some(owner.clone()).into(), 88 | None.into(), 89 | ) 90 | .await 91 | { 92 | warn!("Failed to notify peers of name change: {}", e); 93 | } 94 | 95 | let ctxt = ctxt.set_destination(owner.clone().into()); 96 | if let Err(e) = DBus::name_lost(&ctxt, owner.into()).await { 97 | warn!("Failed to send `NameLost` signal: {}", e); 98 | } 99 | }); 100 | 101 | Ok(response) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod bus; 2 | pub mod config; 3 | pub mod fdo; 4 | pub mod match_rules; 5 | pub mod name_registry; 6 | pub mod peer; 7 | pub mod peers; 8 | pub mod tracing_subscriber; 9 | -------------------------------------------------------------------------------- /src/match_rules.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use serde::Deserialize; 4 | use zbus::{names::BusName, zvariant::Type, OwnedMatchRule}; 5 | 6 | use crate::name_registry::NameRegistry; 7 | 8 | /// A collection of match rules. 9 | #[derive(Debug, Default, Deserialize, Type)] 10 | pub struct MatchRules(HashSet); 11 | 12 | impl MatchRules { 13 | /// Match the given message against the rules. 14 | /// 15 | /// # Panics 16 | /// 17 | /// if header, SENDER or DESTINATION is not set. 18 | pub fn matches(&self, msg: &zbus::Message, name_registry: &NameRegistry) -> bool { 19 | let hdr = msg.header(); 20 | 21 | let ret = self.0.iter().any(|rule| { 22 | // First make use of zbus API 23 | match rule.matches(msg) { 24 | Ok(false) => return false, 25 | Ok(true) => (), 26 | Err(e) => { 27 | tracing::warn!("error matching rule: {}", e); 28 | 29 | return false; 30 | } 31 | } 32 | 33 | // Then match sender and destination involving well-known names, manually. 34 | if let Some(sender) = rule.sender().cloned().and_then(|name| match name { 35 | BusName::WellKnown(name) => name_registry.lookup(name).as_deref().cloned(), 36 | // Unique name is already taken care of by the zbus API. 37 | BusName::Unique(_) => None, 38 | }) { 39 | if sender != hdr.sender().expect("SENDER field unset").clone() { 40 | return false; 41 | } 42 | } 43 | 44 | // The destination. 45 | if let Some(destination) = rule.destination() { 46 | match hdr.destination().expect("DESTINATION field unset").clone() { 47 | BusName::WellKnown(name) => match name_registry.lookup(name) { 48 | Some(name) if name == *destination => (), 49 | Some(_) => return false, 50 | None => return false, 51 | }, 52 | // Unique name is already taken care of by the zbus API. 53 | BusName::Unique(_) => {} 54 | } 55 | } 56 | 57 | true 58 | }); 59 | 60 | ret 61 | } 62 | 63 | pub fn add(&mut self, rule: OwnedMatchRule) { 64 | self.0.insert(rule); 65 | } 66 | 67 | /// Remove the first rule that matches. 68 | pub fn remove(&mut self, rule: OwnedMatchRule) -> zbus::fdo::Result<()> { 69 | if !self.0.remove(&rule) { 70 | return Err(zbus::fdo::Error::MatchRuleNotFound( 71 | "No such match rule".to_string(), 72 | )); 73 | } 74 | 75 | Ok(()) 76 | } 77 | 78 | pub fn is_empty(&self) -> bool { 79 | self.0.is_empty() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/name_registry.rs: -------------------------------------------------------------------------------- 1 | use enumflags2::BitFlags; 2 | use std::collections::{HashMap, VecDeque}; 3 | use zbus::{ 4 | fdo::{ReleaseNameReply, RequestNameFlags, RequestNameReply}, 5 | names::{ 6 | BusName, OwnedBusName, OwnedUniqueName, OwnedWellKnownName, UniqueName, WellKnownName, 7 | }, 8 | }; 9 | 10 | #[derive(Debug, Default)] 11 | pub struct NameRegistry { 12 | names: HashMap, 13 | } 14 | 15 | #[derive(Clone, Debug)] 16 | pub struct NameEntry { 17 | owner: NameOwner, 18 | waiting_list: VecDeque, 19 | } 20 | 21 | impl NameEntry { 22 | pub fn owner(&self) -> &NameOwner { 23 | &self.owner 24 | } 25 | 26 | pub fn waiting_list(&self) -> impl Iterator { 27 | self.waiting_list.iter() 28 | } 29 | } 30 | 31 | #[derive(Clone, Debug)] 32 | pub struct NameOwner { 33 | unique_name: OwnedUniqueName, 34 | allow_replacement: bool, 35 | } 36 | 37 | impl NameOwner { 38 | pub fn unique_name(&self) -> &OwnedUniqueName { 39 | &self.unique_name 40 | } 41 | } 42 | 43 | impl NameRegistry { 44 | pub async fn request_name( 45 | &mut self, 46 | name: WellKnownName<'_>, 47 | unique_name: UniqueName<'_>, 48 | flags: BitFlags, 49 | ) -> (RequestNameReply, Option) { 50 | match self.names.get_mut(&*name) { 51 | Some(entry) => { 52 | if entry.owner.unique_name == unique_name { 53 | (RequestNameReply::AlreadyOwner, None) 54 | } else if flags.contains(RequestNameFlags::ReplaceExisting) 55 | && entry.owner.allow_replacement 56 | { 57 | let old_owner = entry.owner.unique_name.clone(); 58 | let unique_name = OwnedUniqueName::from(unique_name.clone()); 59 | entry.owner = NameOwner { 60 | unique_name: unique_name.clone(), 61 | allow_replacement: flags.contains(RequestNameFlags::AllowReplacement), 62 | }; 63 | 64 | ( 65 | RequestNameReply::PrimaryOwner, 66 | Some(NameOwnerChanged { 67 | name: BusName::from(name).into(), 68 | old_owner: Some(old_owner), 69 | new_owner: Some(unique_name), 70 | }), 71 | ) 72 | } else if !flags.contains(RequestNameFlags::DoNotQueue) { 73 | let owner = NameOwner { 74 | unique_name: OwnedUniqueName::from(unique_name.clone()), 75 | allow_replacement: flags.contains(RequestNameFlags::AllowReplacement), 76 | }; 77 | entry.waiting_list.push_back(owner); 78 | 79 | (RequestNameReply::InQueue, None) 80 | } else { 81 | (RequestNameReply::Exists, None) 82 | } 83 | } 84 | None => { 85 | let unique_name = OwnedUniqueName::from(unique_name.clone()); 86 | let name = OwnedWellKnownName::from(name); 87 | let owner = NameOwner { 88 | unique_name: unique_name.clone(), 89 | allow_replacement: flags.contains(RequestNameFlags::AllowReplacement), 90 | }; 91 | 92 | self.names.insert( 93 | name.clone(), 94 | NameEntry { 95 | owner, 96 | waiting_list: VecDeque::new(), 97 | }, 98 | ); 99 | 100 | ( 101 | RequestNameReply::PrimaryOwner, 102 | Some(NameOwnerChanged { 103 | name: BusName::from(name.into_inner()).into(), 104 | old_owner: None, 105 | new_owner: Some(unique_name), 106 | }), 107 | ) 108 | } 109 | } 110 | } 111 | 112 | pub async fn release_name( 113 | &mut self, 114 | name: WellKnownName<'_>, 115 | owner: UniqueName<'_>, 116 | ) -> (ReleaseNameReply, Option) { 117 | match self.names.get_mut(name.as_str()) { 118 | Some(entry) => { 119 | if *entry.owner.unique_name == owner { 120 | let owner = entry.owner.unique_name.clone(); 121 | let new_owner_name = match entry.waiting_list.pop_front() { 122 | Some(owner) => { 123 | entry.owner = owner; 124 | Some(entry.owner.unique_name.clone()) 125 | } 126 | None => { 127 | self.names.remove(name.as_str()); 128 | 129 | None 130 | } 131 | }; 132 | 133 | ( 134 | ReleaseNameReply::Released, 135 | Some(NameOwnerChanged { 136 | name: BusName::from(name).into(), 137 | old_owner: Some(owner), 138 | new_owner: new_owner_name, 139 | }), 140 | ) 141 | } else { 142 | for (i, waiting) in entry.waiting_list.iter().enumerate() { 143 | if *waiting.unique_name == owner { 144 | entry.waiting_list.remove(i); 145 | 146 | return (ReleaseNameReply::Released, None); 147 | } 148 | } 149 | 150 | (ReleaseNameReply::NonExistent, None) 151 | } 152 | } 153 | None => (ReleaseNameReply::NonExistent, None), 154 | } 155 | } 156 | 157 | pub async fn release_all(&mut self, owner: UniqueName<'_>) -> Vec { 158 | // Find all names registered and queued by the given owner. 159 | let names: Vec<_> = self 160 | .names 161 | .iter() 162 | .filter_map(|(name, entry)| { 163 | if *entry.owner.unique_name == owner 164 | || entry 165 | .waiting_list 166 | .iter() 167 | .any(|waiting| *waiting.unique_name == owner) 168 | { 169 | Some(name.clone()) 170 | } else { 171 | None 172 | } 173 | }) 174 | .collect(); 175 | // Now release our claim or waiting list tickets from all these names. 176 | let mut all_changed = vec![]; 177 | for name in names { 178 | let (_, changed) = self.release_name(name.inner().clone(), owner.clone()).await; 179 | if let Some(changed) = changed { 180 | all_changed.push(changed); 181 | } 182 | } 183 | 184 | all_changed 185 | } 186 | 187 | pub fn lookup(&self, name: WellKnownName) -> Option { 188 | self.names 189 | .get(name.as_str()) 190 | .map(|e| e.owner.unique_name.clone()) 191 | } 192 | 193 | pub fn all_names(&self) -> &HashMap { 194 | &self.names 195 | } 196 | 197 | pub fn waiting_list( 198 | &self, 199 | name: WellKnownName<'_>, 200 | ) -> Option> { 201 | self.names.get(name.as_str()).map(|e| e.waiting_list.iter()) 202 | } 203 | } 204 | 205 | #[derive(Debug)] 206 | pub struct NameOwnerChanged { 207 | pub name: OwnedBusName, 208 | pub old_owner: Option, 209 | pub new_owner: Option, 210 | } 211 | -------------------------------------------------------------------------------- /src/peer/mod.rs: -------------------------------------------------------------------------------- 1 | mod stream; 2 | use event_listener::{Event, EventListener}; 3 | pub use stream::*; 4 | mod monitor; 5 | pub use monitor::*; 6 | 7 | use anyhow::Result; 8 | use tracing::trace; 9 | use zbus::{ 10 | connection, connection::socket::BoxedSplit, names::OwnedUniqueName, AuthMechanism, Connection, 11 | OwnedGuid, OwnedMatchRule, 12 | }; 13 | 14 | use crate::{fdo, match_rules::MatchRules, name_registry::NameRegistry}; 15 | 16 | /// A peer connection. 17 | #[derive(Debug)] 18 | pub struct Peer { 19 | conn: Connection, 20 | unique_name: OwnedUniqueName, 21 | match_rules: MatchRules, 22 | greeted: bool, 23 | canceled_event: Event, 24 | } 25 | 26 | impl Peer { 27 | pub async fn new( 28 | guid: OwnedGuid, 29 | id: usize, 30 | socket: BoxedSplit, 31 | auth_mechanism: AuthMechanism, 32 | ) -> Result { 33 | let unique_name = OwnedUniqueName::try_from(format!(":busd.{id}")).unwrap(); 34 | let conn = connection::Builder::socket(socket) 35 | .server(guid)? 36 | .p2p() 37 | .auth_mechanism(auth_mechanism) 38 | .build() 39 | .await?; 40 | trace!("created: {:?}", conn); 41 | 42 | Ok(Self { 43 | conn, 44 | unique_name, 45 | match_rules: MatchRules::default(), 46 | greeted: false, 47 | canceled_event: Event::new(), 48 | }) 49 | } 50 | 51 | // This the the bus itself, serving the FDO D-Bus API. 52 | pub async fn new_us(conn: Connection) -> Self { 53 | let unique_name = OwnedUniqueName::try_from(fdo::BUS_NAME).unwrap(); 54 | 55 | Self { 56 | conn, 57 | unique_name, 58 | match_rules: MatchRules::default(), 59 | greeted: true, 60 | canceled_event: Event::new(), 61 | } 62 | } 63 | 64 | pub fn unique_name(&self) -> &OwnedUniqueName { 65 | &self.unique_name 66 | } 67 | 68 | pub fn conn(&self) -> &Connection { 69 | &self.conn 70 | } 71 | 72 | pub fn stream(&self) -> Stream { 73 | Stream::for_peer(self) 74 | } 75 | 76 | pub fn listen_cancellation(&self) -> EventListener { 77 | self.canceled_event.listen() 78 | } 79 | 80 | /// # Panics 81 | /// 82 | /// Same as [`MatchRules::matches`]. 83 | pub fn interested(&self, msg: &zbus::Message, name_registry: &NameRegistry) -> bool { 84 | self.match_rules.matches(msg, name_registry) 85 | } 86 | 87 | pub fn add_match_rule(&mut self, rule: OwnedMatchRule) { 88 | self.match_rules.add(rule); 89 | } 90 | 91 | /// Remove the first rule that matches. 92 | pub fn remove_match_rule(&mut self, rule: OwnedMatchRule) -> zbus::fdo::Result<()> { 93 | self.match_rules.remove(rule) 94 | } 95 | 96 | /// This can only be called once. 97 | pub async fn hello(&mut self) -> zbus::fdo::Result<()> { 98 | if self.greeted { 99 | return Err(zbus::fdo::Error::Failed( 100 | "Can only call `Hello` method once".to_string(), 101 | )); 102 | } 103 | self.greeted = true; 104 | 105 | Result::Ok(()) 106 | } 107 | 108 | pub fn become_monitor(self, match_rules: MatchRules) -> Monitor { 109 | Monitor::new(self, match_rules) 110 | } 111 | } 112 | 113 | impl Drop for Peer { 114 | fn drop(&mut self) { 115 | self.canceled_event.notify(usize::MAX); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/peer/monitor.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | 3 | use futures_util::StreamExt; 4 | use tracing::warn; 5 | use zbus::{ 6 | names::{BusName, OwnedUniqueName}, 7 | Connection, MessageStream, 8 | }; 9 | 10 | use crate::{match_rules::MatchRules, name_registry::NameRegistry}; 11 | 12 | use super::Peer; 13 | 14 | /// A peer connection. 15 | #[derive(Debug)] 16 | pub struct Monitor { 17 | conn: Connection, 18 | unique_name: OwnedUniqueName, 19 | match_rules: MatchRules, 20 | } 21 | 22 | impl Monitor { 23 | pub fn conn(&self) -> &Connection { 24 | &self.conn 25 | } 26 | 27 | pub fn unique_name(&self) -> &OwnedUniqueName { 28 | &self.unique_name 29 | } 30 | 31 | /// # Panics 32 | /// 33 | /// Same as [`MatchRules::matches`]. 34 | pub fn interested(&self, msg: &zbus::Message, name_registry: &NameRegistry) -> bool { 35 | if self.match_rules.is_empty() 36 | || msg.header().destination().unwrap() == &BusName::from(&self.unique_name) 37 | { 38 | return true; 39 | } 40 | self.match_rules.matches(msg, name_registry) 41 | } 42 | 43 | /// Monitor the monitor. 44 | /// 45 | /// This method returns once the monitor connection is closed. 46 | pub fn monitor(&self) -> impl Future + 'static { 47 | let mut stream = MessageStream::from(&self.conn); 48 | let unique_name = self.unique_name.clone(); 49 | async move { 50 | if let Some(Ok(_)) = stream.next().await { 51 | warn!( 52 | "Monitor {} sent a message, which is against the rules.", 53 | unique_name 54 | ); 55 | } 56 | } 57 | } 58 | 59 | pub(super) fn new(peer: Peer, match_rules: MatchRules) -> Self { 60 | Self { 61 | conn: peer.conn().clone(), 62 | unique_name: peer.unique_name().clone(), 63 | match_rules, 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/peer/stream.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | 3 | use anyhow::{bail, Error, Result}; 4 | use futures_util::{Stream as FutureStream, TryStream, TryStreamExt}; 5 | use tracing::trace; 6 | use zbus::{message, Message, MessageStream}; 7 | 8 | use crate::peer::Peer; 9 | 10 | /// Message stream for a peer. 11 | /// 12 | /// This stream ensures the following for each message produced: 13 | /// 14 | /// * The destination field is present and readable for non-signals. 15 | /// * The sender field is present and set to the unique name of the peer. 16 | pub struct Stream { 17 | stream: Pin>, 18 | } 19 | 20 | type StreamInner = dyn TryStream> + Send; 21 | 22 | impl Stream { 23 | pub fn for_peer(peer: &Peer) -> Self { 24 | let unique_name = peer.unique_name().clone(); 25 | let stream = MessageStream::from(peer.conn()) 26 | .map_err(Into::into) 27 | .and_then(move |msg| { 28 | let unique_name = unique_name.clone(); 29 | async move { 30 | let header = msg.header(); 31 | 32 | // Ensure destination field is present and readable for non-signals. 33 | if msg.message_type() != message::Type::Signal && header.destination().is_none() 34 | { 35 | bail!("missing destination field"); 36 | } 37 | 38 | // Ensure sender field is present. If it is not we add it using the unique name 39 | // of the peer. 40 | match header.sender() { 41 | Some(sender) if *sender == unique_name => Ok(msg), 42 | Some(_) => bail!("failed to parse message: Invalid sender field"), 43 | None => { 44 | let signature = header.signature(); 45 | let body = msg.body(); 46 | let body_bytes = body.data(); 47 | let fds = body_bytes 48 | .fds() 49 | .iter() 50 | .map(|fd| fd.try_clone().map(Into::into)) 51 | .collect::>>()?; 52 | let builder = 53 | message::Builder::from(header.clone()).sender(&unique_name)?; 54 | let new_msg = 55 | unsafe { builder.build_raw_body(body_bytes, signature, fds)? }; 56 | trace!("Added sender field to message: {:?}", new_msg); 57 | 58 | Ok(new_msg) 59 | } 60 | } 61 | } 62 | }); 63 | 64 | Self { 65 | stream: Box::pin(stream), 66 | } 67 | } 68 | } 69 | 70 | impl FutureStream for Stream { 71 | type Item = Result; 72 | 73 | fn poll_next( 74 | self: Pin<&mut Self>, 75 | cx: &mut std::task::Context, 76 | ) -> std::task::Poll>> { 77 | FutureStream::poll_next(Pin::new(&mut self.get_mut().stream), cx) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/peers.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Context, Result}; 2 | use event_listener::EventListener; 3 | use futures_util::{ 4 | future::{select, Either}, 5 | stream::StreamExt, 6 | }; 7 | use std::{ 8 | collections::BTreeMap, 9 | ops::{Deref, DerefMut}, 10 | sync::Arc, 11 | }; 12 | use tokio::{spawn, sync::RwLock}; 13 | use tracing::{debug, trace, warn}; 14 | use zbus::{ 15 | connection::socket::BoxedSplit, 16 | message, 17 | names::{BusName, OwnedUniqueName, UniqueName}, 18 | zvariant::Optional, 19 | AuthMechanism, Message, OwnedGuid, 20 | }; 21 | 22 | use crate::{ 23 | fdo, 24 | match_rules::MatchRules, 25 | name_registry::{NameOwnerChanged, NameRegistry}, 26 | peer::{Monitor, Peer, Stream}, 27 | }; 28 | 29 | #[derive(Debug)] 30 | pub struct Peers { 31 | peers: RwLock>, 32 | monitors: RwLock>, 33 | name_registry: RwLock, 34 | } 35 | 36 | impl Peers { 37 | pub fn new() -> Arc { 38 | let name_registry = NameRegistry::default(); 39 | 40 | Arc::new(Self { 41 | peers: RwLock::new(BTreeMap::new()), 42 | monitors: RwLock::new(BTreeMap::new()), 43 | name_registry: RwLock::new(name_registry), 44 | }) 45 | } 46 | 47 | pub async fn add( 48 | self: &Arc, 49 | guid: &OwnedGuid, 50 | id: usize, 51 | socket: BoxedSplit, 52 | auth_mechanism: AuthMechanism, 53 | ) -> Result<()> { 54 | let mut peers = self.peers_mut().await; 55 | let peer = Peer::new(guid.clone(), id, socket, auth_mechanism).await?; 56 | let unique_name = peer.unique_name().clone(); 57 | match peers.get(&unique_name) { 58 | Some(peer) => panic!( 59 | "Unique name `{}` re-used. We're in deep trouble if this happens", 60 | peer.unique_name() 61 | ), 62 | None => { 63 | let peer_stream = peer.stream(); 64 | let listener = peer.listen_cancellation(); 65 | tokio::spawn( 66 | self.clone() 67 | .serve_peer(peer_stream, listener, unique_name.clone()), 68 | ); 69 | peers.insert(unique_name.clone(), peer); 70 | } 71 | } 72 | 73 | Ok(()) 74 | } 75 | 76 | pub async fn add_us(self: &Arc, conn: zbus::Connection) { 77 | let mut peers = self.peers_mut().await; 78 | let peer = Peer::new_us(conn).await; 79 | let unique_name = peer.unique_name().clone(); 80 | match peers.get(&unique_name) { 81 | Some(peer) => panic!( 82 | "Unique name `{}` re-used. We're in deep trouble if this happens", 83 | peer.unique_name() 84 | ), 85 | None => { 86 | let peer_stream = peer.stream(); 87 | let listener = peer.listen_cancellation(); 88 | tokio::spawn( 89 | self.clone() 90 | .serve_peer(peer_stream, listener, unique_name.clone()), 91 | ); 92 | peers.insert(unique_name.clone(), peer); 93 | } 94 | } 95 | } 96 | 97 | pub async fn peers(&self) -> impl Deref> + '_ { 98 | self.peers.read().await 99 | } 100 | 101 | pub async fn peers_mut(&self) -> impl DerefMut> + '_ { 102 | self.peers.write().await 103 | } 104 | 105 | pub async fn name_registry(&self) -> impl Deref + '_ { 106 | self.name_registry.read().await 107 | } 108 | 109 | pub async fn name_registry_mut(&self) -> impl DerefMut + '_ { 110 | self.name_registry.write().await 111 | } 112 | 113 | pub async fn make_monitor( 114 | self: &Arc, 115 | peer_name: &UniqueName<'_>, 116 | match_rules: MatchRules, 117 | ) -> bool { 118 | let monitor = { 119 | let mut peers = self.peers_mut().await; 120 | let peer = match peers.remove(peer_name.as_str()) { 121 | Some(peer) => peer, 122 | None => { 123 | return false; 124 | } 125 | }; 126 | 127 | peer.become_monitor(match_rules) 128 | }; 129 | 130 | let monitor_monitoring_fut = monitor.monitor(); 131 | let unique_name = monitor.unique_name().clone(); 132 | let peers = self.clone(); 133 | self.monitors 134 | .write() 135 | .await 136 | .insert(unique_name.clone(), monitor); 137 | 138 | spawn(async move { 139 | monitor_monitoring_fut.await; 140 | peers.monitors.write().await.remove(&unique_name); 141 | debug!("Monitor {} disconnected", unique_name); 142 | }); 143 | 144 | true 145 | } 146 | 147 | pub async fn notify_name_changes(&self, name_owner_changed: NameOwnerChanged) -> Result<()> { 148 | let name = BusName::from(name_owner_changed.name); 149 | let old_owner = name_owner_changed.old_owner.map(UniqueName::from); 150 | let new_owner = name_owner_changed.new_owner.map(UniqueName::from); 151 | 152 | // First broadcast the name change signal. 153 | let msg = Message::signal(fdo::DBus::PATH, fdo::DBus::INTERFACE, "NameOwnerChanged") 154 | .unwrap() 155 | .sender(fdo::BUS_NAME) 156 | .unwrap() 157 | .build(&( 158 | &name, 159 | Optional::from(old_owner.clone()), 160 | Optional::from(new_owner.clone()), 161 | ))?; 162 | self.broadcast_msg(msg).await; 163 | 164 | // Now unicast the appropriate signal to the old and new owners. 165 | if let Some(old_owner) = old_owner { 166 | let msg = Message::signal(fdo::DBus::PATH, fdo::DBus::INTERFACE, "NameLost") 167 | .unwrap() 168 | .sender(fdo::BUS_NAME) 169 | .unwrap() 170 | .destination(old_owner.clone()) 171 | .unwrap() 172 | .build(&name)?; 173 | if let Err(e) = self.send_msg_to_unique_name(msg, old_owner.clone()).await { 174 | warn!("Couldn't notify inexistant peer {old_owner} about loosing name {name}: {e}") 175 | } 176 | } 177 | if let Some(new_owner) = new_owner { 178 | let msg = Message::signal(fdo::DBus::PATH, fdo::DBus::INTERFACE, "NameAcquired") 179 | .unwrap() 180 | .sender(fdo::BUS_NAME) 181 | .unwrap() 182 | .destination(new_owner.clone()) 183 | .unwrap() 184 | .build(&name)?; 185 | if let Err(e) = self.send_msg_to_unique_name(msg, new_owner.clone()).await { 186 | warn!("Couldn't notify peer {new_owner} about acquiring name {name}: {e}") 187 | } 188 | } 189 | 190 | Ok(()) 191 | } 192 | 193 | async fn serve_peer( 194 | self: Arc, 195 | mut peer_stream: Stream, 196 | mut cancellation_listener: EventListener, 197 | unique_name: OwnedUniqueName, 198 | ) -> Result<()> { 199 | loop { 200 | let msg = match select(cancellation_listener, peer_stream.next()).await { 201 | Either::Left(_) | Either::Right((None, _)) => { 202 | trace!("Peer `{}` disconnected", unique_name); 203 | 204 | break; 205 | } 206 | Either::Right((Some(msg), listener)) => { 207 | cancellation_listener = listener; 208 | 209 | match msg { 210 | Ok(msg) => msg, 211 | Err(e) => { 212 | debug!("{e}"); 213 | 214 | continue; 215 | } 216 | } 217 | } 218 | }; 219 | 220 | match msg.message_type() { 221 | message::Type::Signal => self.broadcast_msg(msg).await, 222 | _ => match msg.header().destination() { 223 | Some(dest) => { 224 | if let Err(e) = self.send_msg(msg.clone(), dest.clone()).await { 225 | warn!("{}", e); 226 | } 227 | } 228 | // peer::Stream ensures a valid destination so this isn't exactly needed. 229 | _ => bail!("invalid message: {:?}", msg), 230 | }, 231 | }; 232 | } 233 | 234 | // Stream is done means the peer disconnected or it became a monitor. Remove it from the 235 | // list of peers. 236 | if self.peers_mut().await.remove(&unique_name).is_none() { 237 | // This means peer was turned into a monitor. `Monitoring` iface will emit the signals. 238 | return Ok(()); 239 | } 240 | let names_changes = self 241 | .name_registry_mut() 242 | .await 243 | .release_all(unique_name.inner().clone()) 244 | .await; 245 | for changed in names_changes { 246 | self.notify_name_changes(changed).await?; 247 | } 248 | self.notify_name_changes(NameOwnerChanged { 249 | name: BusName::from(unique_name.clone()).into(), 250 | old_owner: Some(unique_name.clone()), 251 | new_owner: None, 252 | }) 253 | .await?; 254 | 255 | Ok(()) 256 | } 257 | 258 | async fn send_msg(&self, msg: Message, destination: BusName<'_>) -> Result<()> { 259 | trace!( 260 | "Forwarding message: {:?}, destination: {}", 261 | msg, 262 | destination 263 | ); 264 | match destination { 265 | BusName::Unique(dest) => self.send_msg_to_unique_name(msg, dest.clone()).await, 266 | BusName::WellKnown(name) => { 267 | let dest = match self.name_registry().await.lookup(name.clone()) { 268 | Some(dest) => dest, 269 | None => bail!("unknown destination: {}", name), 270 | }; 271 | self.send_msg_to_unique_name(msg, (&*dest).into()).await 272 | } 273 | } 274 | } 275 | 276 | async fn send_msg_to_unique_name( 277 | &self, 278 | msg: Message, 279 | destination: UniqueName<'_>, 280 | ) -> Result<()> { 281 | let conn = self 282 | .peers 283 | .read() 284 | .await 285 | .get(destination.as_str()) 286 | .map(|peer| peer.conn().clone()); 287 | match conn { 288 | Some(conn) => conn.send(&msg).await.context("failed to send message")?, 289 | None => debug!("no peer for destination `{destination}`"), 290 | } 291 | let name_registry = self.name_registry().await; 292 | self.broadcast_to_monitors(msg, &name_registry).await; 293 | 294 | Ok(()) 295 | } 296 | 297 | async fn broadcast_msg(&self, msg: Message) { 298 | trace!("Broadcasting message: {:?}", msg); 299 | let name_registry = self.name_registry().await; 300 | for peer in self.peers.read().await.values() { 301 | if !peer.interested(&msg, &name_registry) { 302 | trace!("Peer {} not interested in {msg:?}", peer.unique_name()); 303 | continue; 304 | } 305 | 306 | if let Err(e) = peer 307 | .conn() 308 | .send(&msg) 309 | .await 310 | .context("failed to send message") 311 | { 312 | warn!("Error sending message: {}", e); 313 | } 314 | } 315 | 316 | self.broadcast_to_monitors(msg, &name_registry).await; 317 | } 318 | 319 | async fn broadcast_to_monitors(&self, msg: Message, name_registry: &NameRegistry) { 320 | let monitors = self.monitors.read().await; 321 | if monitors.is_empty() { 322 | return; 323 | } 324 | trace!( 325 | "Broadcasting message to {} monitors: {:?}", 326 | monitors.len(), 327 | msg 328 | ); 329 | for monitor in monitors.values() { 330 | if !monitor.interested(&msg, name_registry) { 331 | trace!( 332 | "Monitor {} not interested in {msg:?}", 333 | monitor.unique_name() 334 | ); 335 | continue; 336 | } 337 | 338 | if let Err(e) = monitor 339 | .conn() 340 | .send(&msg) 341 | .await 342 | .context("failed to send message") 343 | { 344 | warn!("Error sending message: {}", e); 345 | } 346 | } 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/tracing_subscriber.rs: -------------------------------------------------------------------------------- 1 | pub fn init() { 2 | #[cfg(all(feature = "tracing-subscriber", not(feature = "console-subscriber")))] 3 | { 4 | use tracing_subscriber::{util::SubscriberInitExt, EnvFilter, FmtSubscriber}; 5 | 6 | FmtSubscriber::builder() 7 | .with_env_filter(EnvFilter::from_default_env()) 8 | .finish() 9 | .init(); 10 | } 11 | 12 | #[cfg(feature = "console-subscriber")] 13 | console_subscriber::init(); 14 | } 15 | -------------------------------------------------------------------------------- /tests/config.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, str::FromStr}; 2 | 3 | use busd::config::{ 4 | Access, BusType, Config, ConnectOperation, MessageType, Name, NameOwnership, Operation, Policy, 5 | ReceiveOperation, SendOperation, 6 | }; 7 | use zbus::{Address, AuthMechanism}; 8 | 9 | #[test] 10 | fn config_read_file_with_includes_ok() { 11 | let got = 12 | Config::read_file("./tests/data/valid.conf").expect("should read and parse XML input"); 13 | 14 | assert_eq!( 15 | got, 16 | Config { 17 | auth: Some(AuthMechanism::External), 18 | listen: Some(Address::from_str("unix:path=/tmp/a").expect("should parse address")), 19 | policies: vec![ 20 | Policy::DefaultContext(vec![ 21 | ( 22 | Access::Allow, 23 | Operation::Own(NameOwnership { 24 | own: Some(Name::Any) 25 | }) 26 | ), 27 | ( 28 | Access::Deny, 29 | Operation::Own(NameOwnership { 30 | own: Some(Name::Any) 31 | }) 32 | ), 33 | ]), 34 | Policy::MandatoryContext(vec![ 35 | ( 36 | Access::Deny, 37 | Operation::Own(NameOwnership { 38 | own: Some(Name::Any) 39 | }) 40 | ), 41 | ( 42 | Access::Allow, 43 | Operation::Own(NameOwnership { 44 | own: Some(Name::Any) 45 | }) 46 | ), 47 | ],), 48 | ], 49 | ..Default::default() 50 | } 51 | ); 52 | } 53 | 54 | #[test] 55 | fn config_read_file_example_session_disable_stats_conf_ok() { 56 | let got = Config::read_file("./tests/data/example-session-disable-stats.conf") 57 | .expect("should read and parse XML input"); 58 | 59 | assert_eq!( 60 | got, 61 | Config { 62 | policies: vec![Policy::DefaultContext(vec![( 63 | Access::Deny, 64 | Operation::Send(SendOperation { 65 | broadcast: None, 66 | destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), 67 | error: None, 68 | interface: Some(String::from("org.freedesktop.DBus.Debug.Stats")), 69 | max_fds: None, 70 | member: None, 71 | min_fds: None, 72 | path: None, 73 | r#type: None 74 | }), 75 | ),]),], 76 | ..Default::default() 77 | } 78 | ); 79 | } 80 | 81 | #[test] 82 | fn config_read_file_example_system_enable_stats_conf_ok() { 83 | let got = Config::read_file("./tests/data/example-system-enable-stats.conf") 84 | .expect("should read and parse XML input"); 85 | 86 | assert_eq!( 87 | got, 88 | Config { 89 | policies: vec![Policy::User( 90 | vec![( 91 | Access::Allow, 92 | Operation::Send(SendOperation { 93 | broadcast: None, 94 | destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), 95 | error: None, 96 | interface: Some(String::from("org.freedesktop.DBus.Debug.Stats")), 97 | max_fds: None, 98 | member: None, 99 | min_fds: None, 100 | path: None, 101 | r#type: None 102 | }), 103 | )], 104 | String::from("USERNAME"), 105 | ),], 106 | ..Default::default() 107 | } 108 | ); 109 | } 110 | 111 | #[test] 112 | fn config_read_file_session_conf_ok() { 113 | let mut got = 114 | Config::read_file("./tests/data/session.conf").expect("should read and parse XML input"); 115 | 116 | assert!(!got.servicedirs.is_empty()); 117 | 118 | // nuking this to make it easier to `assert_eq!()` 119 | got.servicedirs = vec![]; 120 | 121 | assert_eq!( 122 | got, 123 | Config { 124 | listen: Some( 125 | Address::from_str("unix:path=/run/user/1000/bus").expect("should parse address") 126 | ), 127 | keep_umask: true, 128 | policies: vec![Policy::DefaultContext(vec![ 129 | ( 130 | Access::Allow, 131 | Operation::Send(SendOperation { 132 | broadcast: None, 133 | destination: Some(Name::Any), 134 | error: None, 135 | interface: None, 136 | max_fds: None, 137 | member: None, 138 | min_fds: None, 139 | path: None, 140 | r#type: None, 141 | }), 142 | ), 143 | ( 144 | Access::Allow, 145 | Operation::Own(NameOwnership { 146 | own: Some(Name::Any), 147 | }), 148 | ), 149 | ]),], 150 | r#type: Some(BusType::Session), 151 | ..Default::default() 152 | } 153 | ); 154 | } 155 | 156 | #[test] 157 | fn config_read_file_system_conf_ok() { 158 | let want = Config { 159 | auth: Some(AuthMechanism::External), 160 | fork: true, 161 | listen: Some( 162 | Address::from_str("unix:path=/var/run/dbus/system_bus_socket") 163 | .expect("should parse address"), 164 | ), 165 | pidfile: Some(PathBuf::from("@DBUS_SYSTEM_PID_FILE@")), 166 | policies: vec![ 167 | Policy::DefaultContext(vec![ 168 | ( 169 | Access::Allow, 170 | Operation::Connect(ConnectOperation { 171 | group: None, 172 | user: Some(String::from("*")), 173 | }), 174 | ), 175 | ( 176 | Access::Deny, 177 | Operation::Own(NameOwnership { 178 | own: Some(Name::Any), 179 | }), 180 | ), 181 | ( 182 | Access::Deny, 183 | Operation::Send(SendOperation { 184 | broadcast: None, 185 | destination: None, 186 | error: None, 187 | interface: None, 188 | max_fds: None, 189 | member: None, 190 | min_fds: None, 191 | path: None, 192 | r#type: Some(MessageType::MethodCall), 193 | }), 194 | ), 195 | ( 196 | Access::Allow, 197 | Operation::Send(SendOperation { 198 | broadcast: None, 199 | destination: None, 200 | error: None, 201 | interface: None, 202 | max_fds: None, 203 | member: None, 204 | min_fds: None, 205 | path: None, 206 | r#type: Some(MessageType::Signal), 207 | }), 208 | ), 209 | ( 210 | Access::Allow, 211 | Operation::Send(SendOperation { 212 | broadcast: None, 213 | destination: None, 214 | error: None, 215 | interface: None, 216 | max_fds: None, 217 | member: None, 218 | min_fds: None, 219 | path: None, 220 | r#type: Some(MessageType::MethodReturn), 221 | }), 222 | ), 223 | ( 224 | Access::Allow, 225 | Operation::Send(SendOperation { 226 | broadcast: None, 227 | destination: None, 228 | error: None, 229 | interface: None, 230 | max_fds: None, 231 | member: None, 232 | min_fds: None, 233 | path: None, 234 | r#type: Some(MessageType::Error), 235 | }), 236 | ), 237 | ( 238 | Access::Allow, 239 | Operation::Receive(ReceiveOperation { 240 | error: None, 241 | interface: None, 242 | max_fds: None, 243 | member: None, 244 | min_fds: None, 245 | path: None, 246 | sender: None, 247 | r#type: Some(MessageType::MethodCall), 248 | }), 249 | ), 250 | ( 251 | Access::Allow, 252 | Operation::Receive(ReceiveOperation { 253 | error: None, 254 | interface: None, 255 | max_fds: None, 256 | member: None, 257 | min_fds: None, 258 | path: None, 259 | sender: None, 260 | r#type: Some(MessageType::MethodReturn), 261 | }), 262 | ), 263 | ( 264 | Access::Allow, 265 | Operation::Receive(ReceiveOperation { 266 | error: None, 267 | interface: None, 268 | max_fds: None, 269 | member: None, 270 | min_fds: None, 271 | path: None, 272 | sender: None, 273 | r#type: Some(MessageType::Error), 274 | }), 275 | ), 276 | ( 277 | Access::Allow, 278 | Operation::Receive(ReceiveOperation { 279 | error: None, 280 | interface: None, 281 | max_fds: None, 282 | member: None, 283 | min_fds: None, 284 | path: None, 285 | sender: None, 286 | r#type: Some(MessageType::Signal), 287 | }), 288 | ), 289 | ( 290 | Access::Allow, 291 | Operation::Send(SendOperation { 292 | broadcast: None, 293 | destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), 294 | error: None, 295 | interface: Some(String::from("org.freedesktop.DBus")), 296 | max_fds: None, 297 | member: None, 298 | min_fds: None, 299 | path: None, 300 | r#type: None, 301 | }), 302 | ), 303 | ( 304 | Access::Allow, 305 | Operation::Send(SendOperation { 306 | broadcast: None, 307 | destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), 308 | error: None, 309 | interface: Some(String::from("org.freedesktop.DBus.Introspectable")), 310 | max_fds: None, 311 | member: None, 312 | min_fds: None, 313 | path: None, 314 | r#type: None, 315 | }), 316 | ), 317 | ( 318 | Access::Allow, 319 | Operation::Send(SendOperation { 320 | broadcast: None, 321 | destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), 322 | error: None, 323 | interface: Some(String::from("org.freedesktop.DBus.Properties")), 324 | max_fds: None, 325 | member: None, 326 | min_fds: None, 327 | path: None, 328 | r#type: None, 329 | }), 330 | ), 331 | ( 332 | Access::Allow, 333 | Operation::Send(SendOperation { 334 | broadcast: None, 335 | destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), 336 | error: None, 337 | interface: Some(String::from("org.freedesktop.DBus.Containers1")), 338 | max_fds: None, 339 | member: None, 340 | min_fds: None, 341 | path: None, 342 | r#type: None, 343 | }), 344 | ), 345 | ( 346 | Access::Deny, 347 | Operation::Send(SendOperation { 348 | broadcast: None, 349 | destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), 350 | error: None, 351 | interface: Some(String::from("org.freedesktop.DBus")), 352 | max_fds: None, 353 | member: Some(String::from("UpdateActivationEnvironment")), 354 | min_fds: None, 355 | path: None, 356 | r#type: None, 357 | }), 358 | ), 359 | ( 360 | Access::Deny, 361 | Operation::Send(SendOperation { 362 | broadcast: None, 363 | destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), 364 | error: None, 365 | interface: Some(String::from("org.freedesktop.DBus.Debug.Stats")), 366 | max_fds: None, 367 | member: None, 368 | min_fds: None, 369 | path: None, 370 | r#type: None, 371 | }), 372 | ), 373 | ( 374 | Access::Deny, 375 | Operation::Send(SendOperation { 376 | broadcast: None, 377 | destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), 378 | error: None, 379 | interface: Some(String::from("org.freedesktop.systemd1.Activator")), 380 | max_fds: None, 381 | member: None, 382 | min_fds: None, 383 | path: None, 384 | r#type: None, 385 | }), 386 | ), 387 | ]), 388 | Policy::User( 389 | vec![( 390 | Access::Allow, 391 | Operation::Send(SendOperation { 392 | broadcast: None, 393 | destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), 394 | error: None, 395 | interface: Some(String::from("org.freedesktop.systemd1.Activator")), 396 | max_fds: None, 397 | member: None, 398 | min_fds: None, 399 | path: None, 400 | r#type: None, 401 | }), 402 | )], 403 | String::from("root"), 404 | ), 405 | Policy::User( 406 | vec![( 407 | Access::Allow, 408 | Operation::Send(SendOperation { 409 | broadcast: None, 410 | destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), 411 | error: None, 412 | interface: Some(String::from("org.freedesktop.DBus.Monitoring")), 413 | max_fds: None, 414 | member: None, 415 | min_fds: None, 416 | path: None, 417 | r#type: None, 418 | }), 419 | )], 420 | String::from("root"), 421 | ), 422 | Policy::User( 423 | vec![( 424 | Access::Allow, 425 | Operation::Send(SendOperation { 426 | broadcast: None, 427 | destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), 428 | error: None, 429 | interface: Some(String::from("org.freedesktop.DBus.Debug.Stats")), 430 | max_fds: None, 431 | member: None, 432 | min_fds: None, 433 | path: None, 434 | r#type: None, 435 | }), 436 | )], 437 | String::from("root"), 438 | ), 439 | ], 440 | servicehelper: Some(PathBuf::from("@DBUS_LIBEXECDIR@/dbus-daemon-launch-helper")), 441 | syslog: true, 442 | r#type: Some(BusType::System), 443 | user: Some(String::from("@DBUS_USER@")), 444 | ..Default::default() 445 | }; 446 | 447 | let mut got = 448 | Config::read_file("./tests/data/system.conf").expect("should read and parse XML input"); 449 | 450 | assert!(!got.servicedirs.is_empty()); 451 | 452 | // nuking this to make it easier to `assert_eq!()` 453 | got.servicedirs = vec![]; 454 | 455 | assert_eq!(got, want,); 456 | } 457 | 458 | #[test] 459 | fn config_read_file_real_usr_share_dbus1_session_conf_ok() { 460 | let config_path = PathBuf::from("/usr/share/dbus-1/session.conf"); 461 | if !config_path.exists() { 462 | return; 463 | } 464 | Config::read_file(config_path).expect("should read and parse XML input"); 465 | } 466 | 467 | #[test] 468 | fn config_read_file_real_usr_share_dbus1_system_conf_ok() { 469 | let config_path = PathBuf::from("/usr/share/dbus-1/system.conf"); 470 | if !config_path.exists() { 471 | return; 472 | } 473 | Config::read_file(config_path).expect("should read and parse XML input"); 474 | } 475 | 476 | #[should_panic] 477 | #[test] 478 | fn config_read_file_with_missing_include_err() { 479 | Config::read_file("./tests/data/missing_include.conf") 480 | .expect("should read and parse XML input"); 481 | } 482 | 483 | #[should_panic] 484 | #[test] 485 | fn config_read_file_with_transitive_missing_include_err() { 486 | Config::read_file("./tests/data/transitive_missing_include.conf") 487 | .expect("should read and parse XML input"); 488 | } 489 | -------------------------------------------------------------------------------- /tests/data/example-session-disable-stats.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/data/example-system-enable-stats.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/data/includedir/a.conf: -------------------------------------------------------------------------------- 1 | 3 | 4 | unix:path=/tmp/a 5 | 6 | -------------------------------------------------------------------------------- /tests/data/includedir/not_included.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | unix:path=/tmp/not_included 5 | 6 | -------------------------------------------------------------------------------- /tests/data/missing_include.conf: -------------------------------------------------------------------------------- 1 | 3 | 4 | ./missing.conf 5 | 6 | -------------------------------------------------------------------------------- /tests/data/session.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 10 | 11 | 12 | session 13 | 14 | 16 | 17 | 18 | unix:path=/run/user/1000/bus 19 | 20 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | @SYSCONFDIR_FROM_PKGDATADIR@/dbus-1/session.conf 43 | 44 | 46 | 47 | 48 | 49 | 50 | 52 | @SYSCONFDIR_FROM_PKGDATADIR@/dbus-1/session-local.conf 53 | 54 | 55 | 56 | 63 | 64 | 65 | 1000000000 66 | 250000000 67 | 1000000000 68 | 250000000 69 | 1000000000 70 | 72 | 120000 73 | 240000 74 | 150000 75 | 100000 76 | 10000 77 | 100000 78 | 10000 79 | 50000 80 | 50000 81 | 50000 82 | 83 | 84 | -------------------------------------------------------------------------------- /tests/data/system.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 12 | 13 | 15 | 16 | 17 | 18 | system 19 | 20 | 21 | @DBUS_USER@ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | @DBUS_LIBEXECDIR@/dbus-daemon-launch-helper 31 | 32 | 33 | @DBUS_SYSTEM_PID_FILE@ 34 | 35 | 36 | 37 | 38 | 39 | EXTERNAL 40 | 41 | 45 | unix:path=/var/run/dbus/system_bus_socket 46 | 47 | 48 | 49 | 50 | 51 | 53 | 54 | 55 | 56 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 71 | 73 | 75 | 77 | 78 | 81 | 83 | 85 | 86 | 87 | 88 | 89 | 91 | 92 | 93 | 94 | 95 | 97 | 98 | 99 | 102 | 103 | 105 | 106 | 107 | 108 | @SYSCONFDIR_FROM_PKGDATADIR@/dbus-1/system.conf 109 | 110 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 135 | 136 | 137 | 138 | 139 | 141 | @SYSCONFDIR_FROM_PKGDATADIR@/dbus-1/system-local.conf 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /tests/data/transitive_missing_include.conf: -------------------------------------------------------------------------------- 1 | 3 | 4 | ./missing_include.conf 5 | 6 | -------------------------------------------------------------------------------- /tests/data/valid.conf: -------------------------------------------------------------------------------- 1 | 3 | 4 | ANONYMOUS 5 | unix:path=/tmp/foo 6 | 7 | 8 | 9 | 10 | ./valid_included.conf 11 | ./valid_missing.conf 12 | ./includedir 13 | 14 | -------------------------------------------------------------------------------- /tests/data/valid_included.conf: -------------------------------------------------------------------------------- 1 | 3 | 4 | EXTERNAL 5 | tcp:host=localhost,port=1234 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/fdo.rs: -------------------------------------------------------------------------------- 1 | use std::env::temp_dir; 2 | 3 | use anyhow::ensure; 4 | use busd::bus::Bus; 5 | use futures_util::stream::StreamExt; 6 | use ntest::timeout; 7 | use rand::{ 8 | distr::{Alphanumeric, SampleString}, 9 | rng, 10 | }; 11 | use tokio::{select, sync::oneshot::Sender}; 12 | use tracing::instrument; 13 | use zbus::{ 14 | connection, 15 | fdo::{self, DBusProxy, ReleaseNameReply, RequestNameFlags, RequestNameReply}, 16 | names::{BusName, WellKnownName}, 17 | proxy::CacheProperties, 18 | }; 19 | 20 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 21 | #[instrument] 22 | #[timeout(15000)] 23 | async fn name_ownership_changes() { 24 | busd::tracing_subscriber::init(); 25 | 26 | // Unix socket 27 | let s = Alphanumeric.sample_string(&mut rng(), 10); 28 | let path = temp_dir().join(s); 29 | let address = format!("unix:path={}", path.display()); 30 | name_ownership_changes_(&address).await; 31 | 32 | // TCP socket 33 | let address = "tcp:host=127.0.0.1,port=4242".to_string(); 34 | name_ownership_changes_(&address).await; 35 | } 36 | 37 | async fn name_ownership_changes_(address: &str) { 38 | let mut bus = Bus::for_address(Some(address)).await.unwrap(); 39 | let (tx, rx) = tokio::sync::oneshot::channel(); 40 | 41 | let handle = tokio::spawn(async move { 42 | select! { 43 | _ = rx => (), 44 | res = bus.run() => match res { 45 | Ok(()) => panic!("Bus exited unexpectedly"), 46 | Err(e) => panic!("Bus exited with an error: {}", e), 47 | } 48 | } 49 | 50 | bus 51 | }); 52 | 53 | let ret = name_ownership_changes_client(address, tx).await; 54 | let bus = handle.await.unwrap(); 55 | bus.cleanup().await.unwrap(); 56 | ret.unwrap(); 57 | } 58 | 59 | #[instrument] 60 | async fn name_ownership_changes_client(address: &str, tx: Sender<()>) -> anyhow::Result<()> { 61 | let conn = connection::Builder::address(address)?.build().await?; 62 | let conn_unique_name = conn.unique_name().unwrap().to_owned(); 63 | let dbus_proxy = DBusProxy::builder(&conn) 64 | .cache_properties(CacheProperties::No) 65 | .build() 66 | .await?; 67 | let name: WellKnownName = "org.blah".try_into()?; 68 | 69 | let mut name_changed_stream = dbus_proxy.receive_name_owner_changed().await?; 70 | let mut name_acquired_stream = dbus_proxy.receive_name_acquired().await?; 71 | // This should work. 72 | let ret = dbus_proxy 73 | .request_name(name.clone(), RequestNameFlags::AllowReplacement.into()) 74 | .await?; 75 | ensure!( 76 | ret == RequestNameReply::PrimaryOwner, 77 | "expected to become primary owner" 78 | ); 79 | // Ensure signals were emitted. 80 | let mut changed = name_changed_stream.next().await.unwrap(); 81 | if *changed.args()?.name() == *conn_unique_name { 82 | // In case we do happen to get the signal for our unique name, well-known name signal should 83 | // be next. 84 | changed = name_changed_stream.next().await.unwrap(); 85 | } 86 | ensure!( 87 | *changed.args()?.name() == name, 88 | "expected name owner changed signal for our name" 89 | ); 90 | ensure!( 91 | changed.args()?.old_owner.is_none(), 92 | "expected no old owner for our name" 93 | ); 94 | ensure!( 95 | changed.args()?.new_owner.as_ref().unwrap() == conn.unique_name().unwrap(), 96 | "expected new owner to be us" 97 | ); 98 | ensure!( 99 | changed.message().header().destination().is_none(), 100 | "expected no destination for our signal", 101 | ); 102 | let acquired = name_acquired_stream.next().await.unwrap(); 103 | ensure!( 104 | *acquired.args()?.name() == name, 105 | "expected name acquired signal for our name" 106 | ); 107 | ensure!( 108 | *acquired.message().header().destination().unwrap() 109 | == BusName::from(conn.unique_name().unwrap()), 110 | "expected name acquired signal to be unicasted to the acquiring connection", 111 | ); 112 | 113 | // This shouldn't and we should be told we already own the name. 114 | let ret = dbus_proxy 115 | .request_name(name.clone(), RequestNameFlags::AllowReplacement.into()) 116 | .await?; 117 | ensure!( 118 | ret == RequestNameReply::AlreadyOwner, 119 | "expected to be already primary owner" 120 | ); 121 | 122 | // Now we try with another connection and we should be queued. 123 | let conn2 = connection::Builder::address(address)?.build().await?; 124 | let conn2_unique_name = conn2.unique_name().unwrap().to_owned(); 125 | let changed = name_changed_stream.next().await.unwrap(); 126 | ensure!( 127 | *changed.args()?.name() == *conn2_unique_name, 128 | "expected name owner changed signal for the new connections gaining unique name" 129 | ); 130 | ensure!( 131 | changed.args()?.old_owner.is_none(), 132 | "expected no old owner for the unique name of the second connection" 133 | ); 134 | ensure!( 135 | *changed.args()?.new_owner.as_ref().unwrap() == conn2_unique_name, 136 | "expected new owner of the unique name of the second connection to be itself" 137 | ); 138 | let dbus_proxy2 = DBusProxy::builder(&conn2) 139 | .cache_properties(CacheProperties::No) 140 | .build() 141 | .await?; 142 | let ret = dbus_proxy2 143 | .request_name(name.clone(), Default::default()) 144 | .await?; 145 | 146 | // Check that first client is the primary owner before it releases the name. 147 | ensure!(ret == RequestNameReply::InQueue, "expected to be in queue"); 148 | let owner = dbus_proxy.get_name_owner(name.clone().into()).await?; 149 | let unique_name = conn.unique_name().unwrap().clone(); 150 | ensure!(owner == unique_name, "unexpected owner"); 151 | let owner = dbus_proxy 152 | .get_name_owner(unique_name.clone().into()) 153 | .await?; 154 | ensure!(owner == unique_name, "unexpected owner"); 155 | let res = dbus_proxy.get_name_owner(":1.3333".try_into()?).await; 156 | ensure!( 157 | matches!(res, Err(fdo::Error::NameHasNoOwner(_))), 158 | "expected error" 159 | ); 160 | 161 | let mut name_acquired_stream = dbus_proxy2.receive_name_acquired().await?; 162 | let mut name_lost_stream = dbus_proxy.receive_name_lost().await?; 163 | // Now the first client releases name. 164 | let ret = dbus_proxy.release_name(name.clone()).await?; 165 | ensure!( 166 | ret == ReleaseNameReply::Released, 167 | "expected name to be released" 168 | ); 169 | // Ensure signals were emitted. 170 | let changed = name_changed_stream.next().await.unwrap(); 171 | ensure!( 172 | *changed.args()?.name() == name, 173 | "expected name owner changed signal for our name" 174 | ); 175 | ensure!( 176 | changed.args()?.old_owner.as_ref().unwrap() == conn.unique_name().unwrap(), 177 | "expected old owner to be our first connection" 178 | ); 179 | ensure!( 180 | changed.args()?.new_owner.as_ref().unwrap() == conn2.unique_name().unwrap(), 181 | "expected new owner to be our second connection" 182 | ); 183 | ensure!( 184 | changed.message().header().destination().is_none(), 185 | "expected no destination for our signal", 186 | ); 187 | let lost = name_lost_stream.next().await.unwrap(); 188 | ensure!( 189 | *lost.args()?.name() == name, 190 | "expected name lost signal for our name" 191 | ); 192 | ensure!( 193 | *lost.message().header().destination().unwrap() 194 | == BusName::from(conn.unique_name().unwrap()), 195 | "expected name lost signal to be unicasted to the loosing connection", 196 | ); 197 | let acquired = name_acquired_stream.next().await.unwrap(); 198 | ensure!( 199 | *acquired.args()?.name() == name, 200 | "expected name acquired signal for our name" 201 | ); 202 | ensure!( 203 | *acquired.message().header().destination().unwrap() 204 | == BusName::from(conn2.unique_name().unwrap()), 205 | "expected name acquired signal to be unicasted to the acquiring connection", 206 | ); 207 | 208 | // Now the second client should be the primary owner. 209 | let owner = dbus_proxy.get_name_owner(name.clone().into()).await?; 210 | ensure!(owner == *conn2.unique_name().unwrap(), "unexpected owner"); 211 | 212 | drop(name_acquired_stream); 213 | drop(dbus_proxy2); 214 | drop(conn2); 215 | 216 | let mut unique_name_signaled = false; 217 | let mut well_known_name_signaled = false; 218 | while !unique_name_signaled && !well_known_name_signaled { 219 | let changed = name_changed_stream.next().await.unwrap(); 220 | if *changed.args()?.name() == *conn2_unique_name { 221 | ensure!( 222 | changed.args()?.new_owner.is_none(), 223 | "expected no new owner for our unique name" 224 | ); 225 | ensure!( 226 | *changed.args()?.old_owner.as_ref().unwrap() == conn2_unique_name, 227 | "expected old owner to be us" 228 | ); 229 | unique_name_signaled = true; 230 | } else if *changed.args()?.name() == name { 231 | ensure!( 232 | changed.args()?.new_owner.is_none(), 233 | "expected no new owner for our name" 234 | ); 235 | ensure!( 236 | *changed.args()?.old_owner.as_ref().unwrap() == conn2_unique_name, 237 | "expected old owner to be us" 238 | ); 239 | well_known_name_signaled = true; 240 | } else { 241 | panic!("unexpected name owner changed signal"); 242 | } 243 | } 244 | 245 | tx.send(()).unwrap(); 246 | 247 | Ok(()) 248 | } 249 | -------------------------------------------------------------------------------- /tests/greet.rs: -------------------------------------------------------------------------------- 1 | use std::{env::temp_dir, time::Duration}; 2 | 3 | use anyhow::anyhow; 4 | use busd::bus::Bus; 5 | use futures_util::{pin_mut, stream::StreamExt}; 6 | use ntest::timeout; 7 | use rand::{ 8 | distr::{Alphanumeric, SampleString}, 9 | rng, 10 | }; 11 | use tokio::{select, sync::mpsc::channel, time::timeout}; 12 | use tracing::instrument; 13 | use zbus::{ 14 | connection, 15 | fdo::{self, DBusProxy}, 16 | interface, message, 17 | object_server::SignalEmitter, 18 | proxy, 19 | proxy::CacheProperties, 20 | zvariant::ObjectPath, 21 | AsyncDrop, Connection, MatchRule, MessageStream, 22 | }; 23 | 24 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 25 | #[instrument] 26 | #[timeout(15000)] 27 | async fn greet() { 28 | busd::tracing_subscriber::init(); 29 | 30 | // Unix socket 31 | let s = Alphanumeric.sample_string(&mut rng(), 10); 32 | let path = temp_dir().join(s); 33 | let address = format!("unix:path={}", path.display()); 34 | greet_(&address).await; 35 | 36 | // TCP socket 37 | let address = "tcp:host=127.0.0.1,port=4248".to_string(); 38 | greet_(&address).await; 39 | } 40 | 41 | async fn greet_(socket_addr: &str) { 42 | let mut bus = Bus::for_address(Some(socket_addr)).await.unwrap(); 43 | let (tx, mut rx) = channel(1); 44 | 45 | let handle = tokio::spawn(async move { 46 | select! { 47 | _ = rx.recv() => (), 48 | res = bus.run() => match res { 49 | Ok(()) => panic!("Bus exited unexpectedly"), 50 | Err(e) => panic!("Bus exited with an error: {}", e), 51 | } 52 | } 53 | 54 | bus 55 | }); 56 | 57 | let ret = match greet_service(socket_addr).await { 58 | Ok(service_conn) => greet_client(socket_addr).await.map(|_| service_conn), 59 | Err(e) => Err(e), 60 | }; 61 | let _ = tx.send(()).await; 62 | let bus = handle.await.unwrap(); 63 | bus.cleanup().await.unwrap(); 64 | let _ = ret.unwrap(); 65 | } 66 | 67 | #[instrument] 68 | async fn greet_service(socket_addr: &str) -> anyhow::Result { 69 | struct Greeter { 70 | count: u64, 71 | } 72 | 73 | #[interface(name = "org.zbus.MyGreeter1")] 74 | impl Greeter { 75 | async fn say_hello( 76 | &mut self, 77 | name: &str, 78 | #[zbus(signal_emitter)] ctxt: SignalEmitter<'_>, 79 | #[zbus(header)] header: message::Header<'_>, 80 | ) -> fdo::Result { 81 | self.count += 1; 82 | let path = header.path().unwrap().clone(); 83 | Self::greeted(&ctxt, name, self.count, path).await?; 84 | Ok(format!( 85 | "Hello {}! I have been called {} times.", 86 | name, self.count 87 | )) 88 | } 89 | 90 | #[zbus(signal)] 91 | async fn greeted( 92 | ctxt: &SignalEmitter<'_>, 93 | name: &str, 94 | count: u64, 95 | path: ObjectPath<'_>, 96 | ) -> zbus::Result<()>; 97 | } 98 | 99 | let greeter = Greeter { count: 0 }; 100 | connection::Builder::address(socket_addr)? 101 | .name("org.zbus.MyGreeter")? 102 | .serve_at("/org/zbus/MyGreeter", greeter)? 103 | .build() 104 | .await 105 | .map_err(Into::into) 106 | } 107 | 108 | #[instrument] 109 | async fn greet_client(socket_addr: &str) -> anyhow::Result<()> { 110 | #[proxy( 111 | interface = "org.zbus.MyGreeter1", 112 | default_path = "/org/zbus/MyGreeter" 113 | )] 114 | trait MyGreeter { 115 | fn say_hello(&self, name: &str) -> zbus::Result; 116 | 117 | #[zbus(signal)] 118 | async fn greeted(name: &str, count: u64, path: ObjectPath<'_>); 119 | } 120 | 121 | let conn = connection::Builder::address(socket_addr)?.build().await?; 122 | 123 | let proxy = MyGreeterProxy::builder(&conn) 124 | .destination("org.zbus.MyGreeter")? 125 | .cache_properties(CacheProperties::No) 126 | .build() 127 | .await?; 128 | let mut greeted_stream = proxy.receive_greeted().await?; 129 | let reply = proxy.say_hello("Maria").await?; 130 | assert_eq!(reply, "Hello Maria! I have been called 1 times."); 131 | let signal = greeted_stream 132 | .next() 133 | .await 134 | .ok_or(anyhow!("stream ended unexpectedly"))?; 135 | let args = signal.args()?; 136 | assert_eq!(args.name, "Maria"); 137 | assert_eq!(args.count, 1); 138 | assert_eq!(args.path, "/org/zbus/MyGreeter"); 139 | 140 | // Now let's unsubcribe from the signal and ensure we don't receive it anymore. 141 | greeted_stream.async_drop().await; 142 | let msg_stream = MessageStream::from(&conn).filter_map(|msg| async { 143 | let msg = msg.ok()?; 144 | Greeted::from_message(msg) 145 | }); 146 | pin_mut!(msg_stream); 147 | let _ = proxy.say_hello("Maria").await?; 148 | timeout(Duration::from_millis(10), msg_stream.next()) 149 | .await 150 | .unwrap_err(); 151 | 152 | // Now let's try a manual subscription. 153 | let match_rule = MatchRule::builder() 154 | .interface("org.zbus.MyGreeter1")? 155 | .member("Greeted")? 156 | .add_arg("Maria")? 157 | .arg_path(2, "/org/zbus/MyGreeter")? 158 | .build(); 159 | DBusProxy::new(&conn) 160 | .await? 161 | .add_match_rule(match_rule) 162 | .await?; 163 | let _ = proxy.say_hello("Maria").await?; 164 | let signal = msg_stream.next().await.unwrap(); 165 | let args = signal.args()?; 166 | assert_eq!(args.name, "Maria"); 167 | 168 | Ok(()) 169 | } 170 | -------------------------------------------------------------------------------- /tests/monitor.rs: -------------------------------------------------------------------------------- 1 | use anyhow::ensure; 2 | use busd::bus::Bus; 3 | use futures_util::TryStreamExt; 4 | use ntest::timeout; 5 | use tokio::{select, sync::oneshot::Sender}; 6 | use tracing::instrument; 7 | use zbus::{ 8 | connection, 9 | fdo::{DBusProxy, MonitoringProxy, NameAcquired, NameLost, NameOwnerChanged, RequestNameFlags}, 10 | message, 11 | names::BusName, 12 | proxy::CacheProperties, 13 | MessageStream, 14 | }; 15 | 16 | #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 17 | #[instrument] 18 | #[timeout(15000)] 19 | async fn become_monitor() { 20 | busd::tracing_subscriber::init(); 21 | 22 | let address = "tcp:host=127.0.0.1,port=4242".to_string(); 23 | let mut bus = Bus::for_address(Some(&address)).await.unwrap(); 24 | let (tx, rx) = tokio::sync::oneshot::channel(); 25 | 26 | let handle = tokio::spawn(async move { 27 | select! { 28 | _ = rx => (), 29 | res = bus.run() => match res { 30 | Ok(()) => panic!("Bus exited unexpectedly"), 31 | Err(e) => panic!("Bus exited with an error: {}", e), 32 | } 33 | } 34 | 35 | bus 36 | }); 37 | 38 | let ret = become_monitor_client(&address, tx).await; 39 | let bus = handle.await.unwrap(); 40 | bus.cleanup().await.unwrap(); 41 | ret.unwrap(); 42 | } 43 | 44 | #[instrument] 45 | async fn become_monitor_client(address: &str, tx: Sender<()>) -> anyhow::Result<()> { 46 | // Create a monitor that wants all messages. 47 | let conn = connection::Builder::address(address)?.build().await?; 48 | let mut msg_stream = MessageStream::from(&conn); 49 | MonitoringProxy::builder(&conn) 50 | .cache_properties(CacheProperties::No) 51 | .build() 52 | .await? 53 | .become_monitor(&[], 0) 54 | .await?; 55 | let unique_name = BusName::from(conn.unique_name().unwrap().clone()); 56 | drop(conn); 57 | 58 | // Signals for the monitor loosing its unique name. 59 | let signal = loop { 60 | let msg = msg_stream.try_next().await?.unwrap(); 61 | // Ignore other messages (e.g `BecomeMonitor` method & reply) 62 | if let Some(signal) = NameOwnerChanged::from_message(msg) { 63 | break signal; 64 | } 65 | }; 66 | let args = signal.args()?; 67 | ensure!( 68 | *args.name() == unique_name, 69 | "expected NameOwnerChanged signal for monitor's unique_name" 70 | ); 71 | let signal = loop { 72 | let msg = msg_stream.try_next().await?.unwrap(); 73 | if let Some(signal) = NameLost::from_message(msg) { 74 | break signal; 75 | } 76 | }; 77 | let args = signal.args()?; 78 | ensure!( 79 | *args.name() == unique_name, 80 | "expected NameLost signal for monitor's unique_name" 81 | ); 82 | 83 | // Now a client that calls a method that triggers a signal. 84 | let conn = connection::Builder::address(address)?.build().await?; 85 | let name = "org.dbus2.MonitorTest"; 86 | DBusProxy::builder(&conn) 87 | .cache_properties(CacheProperties::No) 88 | .build() 89 | .await? 90 | .request_name( 91 | name.try_into()?, 92 | RequestNameFlags::ReplaceExisting | RequestNameFlags::DoNotQueue, 93 | ) 94 | .await?; 95 | 96 | // Now monitor should have received all messages. 97 | let mut num_received = 0; 98 | let mut hello_serial = None; 99 | let mut request_name_serial = None; 100 | while num_received < 8 { 101 | let msg = msg_stream.try_next().await?.unwrap(); 102 | let header = msg.header(); 103 | let member = header.member(); 104 | 105 | match msg.message_type() { 106 | message::Type::MethodCall => match member.unwrap().as_str() { 107 | "Hello" => { 108 | hello_serial = Some(msg.primary_header().serial_num()); 109 | } 110 | "RequestName" => { 111 | request_name_serial = Some(msg.primary_header().serial_num()); 112 | } 113 | method => panic!("unexpected method call: {}", method), 114 | }, 115 | message::Type::MethodReturn => { 116 | let serial = header.reply_serial(); 117 | if serial == hello_serial { 118 | hello_serial = None; 119 | } else if serial == request_name_serial { 120 | request_name_serial = None; 121 | } else { 122 | panic!("unexpected method return: {}", serial.unwrap()); 123 | } 124 | } 125 | message::Type::Signal => { 126 | if let Some(signal) = NameOwnerChanged::from_message(msg.clone()) { 127 | let args = signal.args()?; 128 | ensure!( 129 | *args.name() == BusName::from(conn.unique_name().unwrap()) 130 | || *args.name() == name, 131 | "expected NameOwnerChanged signal for one of client's names" 132 | ); 133 | } else if let Some(signal) = NameAcquired::from_message(msg) { 134 | let args = signal.args()?; 135 | ensure!( 136 | *args.name() == BusName::from(conn.unique_name().unwrap()) 137 | || *args.name() == name, 138 | "expected NameAcquired signal for one of client's names" 139 | ); 140 | } 141 | } 142 | _ => panic!("unexpected message type: {:?}", msg.message_type()), 143 | } 144 | 145 | num_received += 1; 146 | } 147 | 148 | tx.send(()).unwrap(); 149 | 150 | Ok(()) 151 | } 152 | -------------------------------------------------------------------------------- /tests/multiple_conns.rs: -------------------------------------------------------------------------------- 1 | use std::env::temp_dir; 2 | 3 | use busd::bus::Bus; 4 | use futures_util::future::join_all; 5 | use ntest::timeout; 6 | use rand::{ 7 | distr::{Alphanumeric, SampleString}, 8 | rng, 9 | }; 10 | use tokio::{select, sync::oneshot::channel}; 11 | use tracing::instrument; 12 | use zbus::connection; 13 | 14 | #[tokio::test(flavor = "multi_thread", worker_threads = 8)] 15 | #[instrument] 16 | #[timeout(15000)] 17 | async fn multi_conenct() { 18 | busd::tracing_subscriber::init(); 19 | 20 | // Unix socket 21 | let s = Alphanumeric.sample_string(&mut rng(), 10); 22 | let path = temp_dir().join(s); 23 | let address = format!("unix:path={}", path.display()); 24 | multi_conenct_(&address).await; 25 | 26 | // TCP socket 27 | let address = "tcp:host=127.0.0.1,port=4246".to_string(); 28 | multi_conenct_(&address).await; 29 | } 30 | 31 | async fn multi_conenct_(socket_addr: &str) { 32 | let mut bus = Bus::for_address(Some(socket_addr)).await.unwrap(); 33 | let (tx, rx) = channel(); 34 | 35 | let handle = tokio::spawn(async move { 36 | select! { 37 | _ = rx => (), 38 | res = bus.run() => match res { 39 | Ok(()) => panic!("Bus exited unexpectedly"), 40 | Err(e) => panic!("Bus exited with an error: {}", e), 41 | } 42 | } 43 | 44 | bus 45 | }); 46 | 47 | let ret = multi_clients_connect(socket_addr).await; 48 | let _ = tx.send(()); 49 | let bus = handle.await.unwrap(); 50 | bus.cleanup().await.unwrap(); 51 | ret.unwrap(); 52 | } 53 | 54 | #[instrument] 55 | async fn multi_clients_connect(socket_addr: &str) -> anyhow::Result<()> { 56 | // Create 10 connections simultaneously. 57 | let conns: Vec<_> = (0..10) 58 | .map(|_| connection::Builder::address(socket_addr).unwrap().build()) 59 | .collect(); 60 | join_all(conns).await; 61 | 62 | Ok(()) 63 | } 64 | --------------------------------------------------------------------------------