diff --git a/Cargo.lock b/Cargo.lock index 28a0769..c1fac8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,6 +199,7 @@ dependencies = [ "futures-util", "nix", "ntest", + "quick-xml", "rand", "serde", "tokio", @@ -991,6 +992,16 @@ dependencies = [ "prost", ] +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.36" @@ -1419,9 +1430,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", diff --git a/Cargo.toml b/Cargo.toml index ca5bf6f..82f214d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ console-subscriber = { version = "0.4.0", optional = true } xdg-home = "1.1.0" event-listener = "5.3.0" fastrand = "2.2.0" +quick-xml = { version = "0.36.2", features = ["serialize"] } [target.'cfg(unix)'.dependencies] nix = { version = "0.29.0", features = ["user"] } diff --git a/src/bin/busd.rs b/src/bin/busd.rs index fdd1424..b7fb942 100644 --- a/src/bin/busd.rs +++ b/src/bin/busd.rs @@ -1,26 +1,32 @@ extern crate busd; +use std::path::PathBuf; #[cfg(unix)] use std::{fs::File, io::Write, os::fd::FromRawFd}; -use busd::bus; +use busd::{bus, config::Config}; use anyhow::Result; use clap::Parser; #[cfg(unix)] use tokio::{select, signal::unix::SignalKind}; -use tracing::error; #[cfg(unix)] -use tracing::{info, warn}; +use tracing::warn; +use tracing::{error, info}; /// A simple D-Bus broker. #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] struct Args { /// The address to listen on. + /// Takes precedence over any `` element in the configuration file. #[clap(short = 'a', long, value_parser)] address: Option, + /// Use the given configuration file. + #[clap(long)] + config: Option, + /// Print the address of the message bus to standard output. #[clap(long)] print_address: bool, @@ -36,6 +42,15 @@ struct Args { #[cfg(unix)] #[clap(long)] ready_fd: Option, + + /// Equivalent to `--config /usr/share/dbus-1/session.conf`. + /// This is the default if `--config` and `--system` are unspecified. + #[clap(long)] + session: bool, + + /// Equivalent to `--config /usr/share/dbus-1/system.conf`. + #[clap(long)] + system: bool, } #[tokio::main] @@ -44,7 +59,23 @@ async fn main() -> Result<()> { let args = Args::parse(); - let mut bus = bus::Bus::for_address(args.address.as_deref()).await?; + let config_path = if args.system { + PathBuf::from("/usr/share/dbus-1/system.conf") + } else if let Some(config_path) = args.config { + config_path + } else { + PathBuf::from("/usr/share/dbus-1/session.conf") + }; + info!("reading configuration file {} ...", config_path.display()); + let config = Config::read_file(&config_path)?; + + let address = if let Some(address) = args.address { + Some(address) + } else { + config.listen.map(|address| format!("{address}")) + }; + + let mut bus = bus::Bus::for_address(address.as_deref()).await?; #[cfg(unix)] if let Some(fd) = args.ready_fd { diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..60f713e --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,853 @@ +use std::{ + env::var, + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::{Error, Result}; +use policy::OptionalPolicy; +use serde::Deserialize; +use zbus::{Address, AuthMechanism}; + +pub mod policy; +pub mod rule; +mod xml; + +pub use policy::Policy; +pub use rule::{ + Access, ConnectOperation, NameOwnership, Operation, ReceiveOperation, SendOperation, +}; +use xml::{Document, Element, TypeElement}; + +/// The bus configuration. +/// +/// This is currently only loaded from the [XML configuration files] defined by the specification. +/// We plan to add support for other formats (e.g JSON) in the future. +/// +/// [XML configuration files]: https://dbus.freedesktop.org/doc/dbus-daemon.1.html#configuration_file +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct Config { + /// If `true`, connections that authenticated using the ANONYMOUS mechanism will be authorized + /// to connect. This option has no practical effect unless the ANONYMOUS mechanism has also + /// been enabled using the `auth` option. + pub allow_anonymous: bool, + + /// Lists permitted authorization mechanisms. + /// If this element doesn't exist, then all known mechanisms are allowed. + // TODO: warn when multiple `` elements are defined, as we only support one + // TODO: consider implementing `Deserialize` over in zbus crate, then removing this "skip..." + #[serde(default, skip_deserializing)] + pub auth: Option, + + /// If `true`, the bus daemon becomes a real daemon (forks into the background, etc.). + pub fork: bool, + + /// If `true`, the bus daemon keeps its original umask when forking. + /// This may be useful to avoid affecting the behavior of child processes. + pub keep_umask: bool, + + /// Address that the bus should listen on. + /// The address is in the standard D-Bus format that contains a transport name plus possible + /// parameters/options. + // TODO: warn when multiple `` elements are defined, as we only support one + // TODO: consider implementing `Deserialize` over in zbus crate, then removing this "skip..." + #[serde(default, skip_deserializing)] + pub listen: Option
, + + /// The bus daemon will write its pid to the specified file. + pub pidfile: Option, + + pub policies: Vec, + + /// Adds a directory to search for .service files, + /// which tell the dbus-daemon how to start a program to provide a particular well-known bus + /// name. + #[serde(default)] + pub servicedirs: Vec, + + /// Specifies the setuid helper that is used to launch system daemons with an alternate user. + pub servicehelper: Option, + + /// If `true`, the bus daemon will log to syslog. + pub syslog: bool, + + /// This element only controls which message bus specific environment variables are set in + /// activated clients. + pub r#type: Option, + + /// The user account the daemon should run as, as either a username or a UID. + /// If the daemon cannot change to this UID on startup, it will exit. + /// If this element is not present, the daemon will not change or care about its UID. + pub user: Option, +} + +impl TryFrom for Config { + type Error = Error; + + fn try_from(value: Document) -> std::result::Result { + let mut config = Config::default(); + + for element in value.busconfig { + match element { + Element::AllowAnonymous => config.allow_anonymous = true, + Element::Auth(auth) => { + config.auth = Some(AuthMechanism::from_str(&auth)?); + } + Element::Fork => config.fork = true, + Element::Include(_) => { + // NO-OP: removed during `Document::resolve_includes` + } + Element::Includedir(_) => { + // NO-OP: removed during `Document::resolve_includedirs` + } + Element::KeepUmask => config.keep_umask = true, + Element::Limit => { + // NO-OP: deprecated and ignored + } + Element::Listen(listen) => { + config.listen = Some(Address::from_str(&listen)?); + } + Element::Pidfile(p) => config.pidfile = Some(p), + Element::Policy(pe) => { + if let Some(p) = OptionalPolicy::try_from(pe)? { + config.policies.push(p); + } + } + Element::Servicedir(p) => { + config.servicedirs.push(p); + } + Element::Servicehelper(p) => { + // NOTE: we're assuming this has the same "last one wins" behaviour as `` + + // TODO: warn and then ignore if we aren't reading: + // /usr/share/dbus-1/system.conf + config.servicehelper = Some(p); + } + Element::StandardSessionServicedirs => { + // TODO: warn and then ignore if we aren't reading: /etc/dbus-1/session.conf + if let Ok(runtime_dir) = var("XDG_RUNTIME_DIR") { + config + .servicedirs + .push(PathBuf::from(runtime_dir).join("dbus-1/services")); + } + if let Ok(data_dir) = var("XDG_DATA_HOME") { + config + .servicedirs + .push(PathBuf::from(data_dir).join("dbus-1/services")); + } + let mut servicedirs_in_data_dirs = xdg_data_dirs() + .iter() + .map(|p| p.join("dbus-1/services")) + .map(PathBuf::from) + .collect(); + config.servicedirs.append(&mut servicedirs_in_data_dirs); + config + .servicedirs + .push(PathBuf::from("/usr/share/dbus-1/services")); + // TODO: add Windows-specific session directories + } + Element::StandardSystemServicedirs => { + // TODO: warn and then ignore if we aren't reading: + // /usr/share/dbus-1/system.conf + config + .servicedirs + .extend(STANDARD_SYSTEM_SERVICEDIRS.iter().map(PathBuf::from)); + } + Element::Syslog => config.syslog = true, + Element::Type(TypeElement { r#type: value }) => config.r#type = Some(value), + Element::User(s) => config.user = Some(s), + } + } + + Ok(config) + } +} + +impl Config { + pub fn parse(s: &str) -> Result { + // TODO: validate that our DOCTYPE and root element are correct + quick_xml::de::from_str::(s)?.try_into() + } + + pub fn read_file(file_path: impl AsRef) -> Result { + // TODO: error message should contain file path to missing `` + Document::read_file(&file_path)?.try_into() + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum BusType { + Session, + System, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum MessageType { + #[default] + #[serde(rename = "*")] + Any, + MethodCall, + MethodReturn, + Signal, + Error, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Name { + #[serde(rename = "*")] + Any, + Exact(String), + Prefix(String), +} + +const DEFAULT_DATA_DIRS: &[&str] = &["/usr/local/share", "/usr/share"]; + +const STANDARD_SYSTEM_SERVICEDIRS: &[&str] = &[ + "/usr/local/share/dbus-1/system-services", + "/usr/share/dbus-1/system-services", + "/lib/dbus-1/system-services", +]; + +fn xdg_data_dirs() -> Vec { + if let Ok(ok) = var("XDG_DATA_DIRS") { + return ok.split(":").map(PathBuf::from).collect(); + } + DEFAULT_DATA_DIRS.iter().map(PathBuf::from).collect() +} + +#[cfg(test)] +mod tests { + use rule::{ + Access, ConnectOperation, NameOwnership, Operation, ReceiveOperation, SendOperation, + }; + + use super::*; + + #[test] + fn config_parse_with_dtd_and_root_element_ok() { + let input = r#" + + "#; + Config::parse(input).expect("should parse XML input"); + } + + #[test] + #[should_panic] + fn config_parse_with_type_error() { + let input = r#" + + not-a-valid-message-bus-type + + "#; + Config::parse(input).expect("should parse XML input"); + } + + #[test] + fn config_parse_with_allow_anonymous_and_fork_and_keep_umask_and_syslog_ok() { + let input = r#" + + + + + + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + allow_anonymous: true, + fork: true, + keep_umask: true, + syslog: true, + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_auth_ok() { + let input = r#" + + ANONYMOUS + EXTERNAL + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + auth: Some(AuthMechanism::External), + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_limit_ok() { + let input = r#" + + 1000000000 + + "#; + + Config::parse(input).expect("should parse XML input"); + } + + #[test] + fn config_parse_with_listen_ok() { + let input = r#" + + unix:path=/tmp/foo + tcp:host=localhost,port=1234 + tcp:host=localhost,port=0,family=ipv4 + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + listen: Some( + Address::from_str("tcp:host=localhost,port=0,family=ipv4") + .expect("should parse address") + ), + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_overlapped_lists_ok() { + // confirm this works with/without quick-xml's [`overlapped-lists`] feature + // [`overlapped-lists`]: https://docs.rs/quick-xml/latest/quick_xml/#overlapped-lists + let input = r#" + + ANONYMOUS + unix:path=/tmp/foo + + + + + + EXTERNAL + tcp:host=localhost,port=1234 + + + + + + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + auth: Some(AuthMechanism::External), + listen: Some( + Address::from_str("tcp:host=localhost,port=1234") + .expect("should parse address") + ), + policies: vec![ + Policy::DefaultContext(vec![ + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ( + Access::Deny, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ]), + Policy::DefaultContext(vec![ + ( + Access::Deny, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ( + Access::Deny, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ]), + ], + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_pidfile_ok() { + let input = r#" + + /var/run/busd.pid + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + pidfile: Some(PathBuf::from("/var/run/busd.pid")), + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_policies_ok() { + let input = r#" + + + + + + + + + + + + + + + + + + + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + policies: vec![ + Policy::DefaultContext(vec![ + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Exact(String::from("org.freedesktop.DBus"))) + }) + ), + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Prefix(String::from("org.freedesktop"))) + }) + ), + ( + Access::Allow, + Operation::Connect(ConnectOperation { + group: Some(String::from("wheel")), + user: None, + }) + ), + ( + Access::Allow, + Operation::Connect(ConnectOperation { + group: None, + user: Some(String::from("root")), + }) + ), + ]), + Policy::User( + vec![ + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: Some(true), + destination: Some(Name::Exact(String::from( + "org.freedesktop.DBus" + ))), + error: Some(String::from("something bad")), + interface: Some(String::from( + "org.freedesktop.systemd1.Activator" + )), + max_fds: Some(128), + member: Some(String::from("DoSomething")), + min_fds: Some(12), + path: Some(String::from("/org/freedesktop")), + r#type: Some(MessageType::Signal), + }) + ), + ( + Access::Allow, + Operation::Receive(ReceiveOperation { + error: Some(String::from("something bad")), + interface: Some(String::from( + "org.freedesktop.systemd1.Activator" + )), + max_fds: Some(128), + member: Some(String::from("DoSomething")), + min_fds: Some(12), + path: Some(String::from("/org/freedesktop")), + sender: Some(String::from("org.freedesktop.DBus")), + r#type: Some(MessageType::Signal), + }) + ) + ], + String::from("root") + ), + Policy::Group( + vec![ + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Prefix(String::from( + "org.freedesktop" + ))), + error: None, + interface: None, + max_fds: None, + member: Some(String::from("DoSomething")), + min_fds: None, + path: None, + r#type: None + }) + ), + // ` + + + + + + "#; + + Config::parse(input).expect("should parse XML input"); + } + + #[test] + fn config_parse_with_policies_with_ignored_rules_and_rule_attributes_ok() { + let input = r#" + + + + + + + + + + + + + + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + policies: vec![ + Policy::DefaultContext(vec![ + ( + Access::Allow, + // `eavesdrop="true"` is dropped, keep other attributes + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Any), + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None + }) + ), + // `` has nothing left after dropping eavesdrop + // `` is completely ignored + ], + ..Default::default() + } + ); + } + + #[should_panic] + #[test] + fn config_parse_with_policies_with_own_and_own_prefix_error() { + let input = r#" + + + + + + "#; + + Config::parse(input).expect("should parse XML input"); + } + + #[should_panic] + #[test] + fn config_parse_with_policies_with_send_destination_and_send_destination_prefix_error() { + let input = r#" + + + + + + "#; + + Config::parse(input).expect("should parse XML input"); + } + + #[should_panic] + #[test] + fn config_parse_with_policies_with_send_and_receive_attributes_error() { + let input = r#" + + + + + + "#; + + Config::parse(input).expect("should parse XML input"); + } + + #[should_panic] + #[test] + fn config_parse_with_policies_without_attributes_error() { + let input = r#" + + + + + + "#; + + Config::parse(input).expect("should parse XML input"); + } + + #[test] + fn config_parse_with_servicedir_and_standard_session_servicedirs_ok() { + let input = r#" + + /example + + /anotherexample + + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + // TODO: improve test: contents are dynamic depending upon environment variables + assert_eq!(config.servicedirs.first(), Some(&PathBuf::from("/example"))); + assert_eq!( + config.servicedirs.last(), + Some(&PathBuf::from("/usr/share/dbus-1/services")) + ); + } + + #[test] + fn config_parse_with_servicedir_and_standard_system_servicedirs_ok() { + let input = r#" + + /example + + /anotherexample + + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + servicedirs: vec![ + PathBuf::from("/example"), + PathBuf::from("/usr/local/share/dbus-1/system-services"), + PathBuf::from("/usr/share/dbus-1/system-services"), + PathBuf::from("/lib/dbus-1/system-services"), + PathBuf::from("/anotherexample"), + PathBuf::from("/usr/local/share/dbus-1/system-services"), + PathBuf::from("/usr/share/dbus-1/system-services"), + PathBuf::from("/lib/dbus-1/system-services"), + ], + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_servicehelper_ok() { + let input = r#" + + /example + /anotherexample + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + servicehelper: Some(PathBuf::from("/anotherexample")), + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_type_ok() { + let input = r#" + + session + system + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + r#type: Some(BusType::System), + ..Default::default() + } + ); + } + + #[test] + fn config_parse_with_user_ok() { + let input = r#" + + 1000 + alice + + "#; + + let config = Config::parse(input).expect("should parse XML input"); + + assert_eq!( + config, + Config { + user: Some(String::from("alice")), + ..Default::default() + } + ); + } +} diff --git a/src/config/policy.rs b/src/config/policy.rs new file mode 100644 index 0000000..c8cbd27 --- /dev/null +++ b/src/config/policy.rs @@ -0,0 +1,72 @@ +use anyhow::Error; +use serde::Deserialize; + +use super::{ + rule::{rules_try_from_rule_elements, Rule}, + xml::{PolicyContext, PolicyElement}, +}; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub enum Policy { + DefaultContext(Vec), + Group(Vec, String), + MandatoryContext(Vec), + User(Vec, String), +} +// TODO: implement Cmp/Ord to help stable-sort Policy values: +// DefaultContext < Group < User < MandatoryContext + +pub type OptionalPolicy = Option; + +impl TryFrom for OptionalPolicy { + type Error = Error; + + fn try_from(value: PolicyElement) -> std::result::Result { + match value { + PolicyElement { + at_console: Some(_), + context: None, + group: None, + user: None, + .. + } => Ok(None), + PolicyElement { + at_console: None, + context: Some(c), + group: None, + rules, + user: None, + } => Ok(Some(match c { + PolicyContext::Default => { + Policy::DefaultContext(rules_try_from_rule_elements(rules)?) + } + PolicyContext::Mandatory => { + Policy::MandatoryContext(rules_try_from_rule_elements(rules)?) + } + })), + PolicyElement { + at_console: None, + context: None, + group: Some(group), + rules, + user: None, + } => Ok(Some(Policy::Group( + rules_try_from_rule_elements(rules)?, + group, + ))), + PolicyElement { + at_console: None, + context: None, + group: None, + rules, + user: Some(user), + } => Ok(Some(Policy::User( + rules_try_from_rule_elements(rules)?, + user, + ))), + _ => Err(Error::msg(format!( + "policy contains conflicting attributes: {value:?}" + ))), + } + } +} diff --git a/src/config/rule.rs b/src/config/rule.rs new file mode 100644 index 0000000..d2d3c8e --- /dev/null +++ b/src/config/rule.rs @@ -0,0 +1,318 @@ +use anyhow::{Error, Result}; +use serde::Deserialize; + +use super::{ + xml::{RuleAttributes, RuleElement}, + MessageType, Name, +}; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct ConnectOperation { + pub group: Option, + pub user: Option, +} + +impl From for ConnectOperation { + fn from(value: RuleAttributes) -> Self { + Self { + group: value.group, + user: value.user, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub enum Operation { + /// rules checked when a new connection to the message bus is established + Connect(ConnectOperation), + /// rules checked when a connection attempts to own a well-known bus names + Own(NameOwnership), + /// rules that are checked for each recipient of a message + Receive(ReceiveOperation), + /// rules that are checked when a connection attempts to send a message + Send(SendOperation), +} + +type OptionalOperation = Option; + +impl TryFrom for OptionalOperation { + type Error = Error; + + fn try_from(value: RuleAttributes) -> std::result::Result { + let has_connect = value.group.is_some() || value.user.is_some(); + let has_own = value.own.is_some() || value.own_prefix.is_some(); + let has_send = value.send_broadcast.is_some() + || value.send_destination.is_some() + || value.send_destination_prefix.is_some() + || value.send_error.is_some() + || value.send_interface.is_some() + || value.send_member.is_some() + || value.send_path.is_some() + || value.send_requested_reply.is_some() + || value.send_type.is_some(); + let has_receive = value.receive_error.is_some() + || value.receive_interface.is_some() + || value.receive_member.is_some() + || value.receive_path.is_some() + || value.receive_sender.is_some() + || value.receive_requested_reply.is_some() + || value.receive_type.is_some(); + + let operations_count: i8 = vec![has_connect, has_own, has_receive, has_send] + .into_iter() + .map(i8::from) + .sum(); + + if operations_count > 1 { + return Err(Error::msg(format!("do not mix rule attributes for connect, own, receive, and/or send attributes in the same rule: {value:?}"))); + } + + if has_connect { + Ok(Some(Operation::Connect(ConnectOperation::from(value)))) + } else if has_own { + Ok(Some(Operation::Own(NameOwnership::from(value)))) + } else if has_receive { + Ok(Some(Operation::Receive(ReceiveOperation::from(value)))) + } else if has_send { + Ok(Some(Operation::Send(SendOperation::from(value)))) + } else { + Err(Error::msg(format!("rule must specify supported attributes for connect, own, receive, or send operations: {value:?}"))) + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct NameOwnership { + pub own: Option, +} + +impl From for NameOwnership { + fn from(value: RuleAttributes) -> Self { + let own = match value { + RuleAttributes { + own: Some(some), + own_prefix: None, + .. + } if some == "*" => Some(Name::Any), + RuleAttributes { + own: Some(some), + own_prefix: None, + .. + } => Some(Name::Exact(some)), + RuleAttributes { + own: None, + own_prefix: Some(some), + .. + } => Some(Name::Prefix(some)), + _ => None, + }; + Self { own } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct ReceiveOperation { + pub error: Option, + pub interface: Option, + pub max_fds: Option, + pub member: Option, + pub min_fds: Option, + pub path: Option, + pub sender: Option, + pub r#type: Option, +} + +impl From for ReceiveOperation { + fn from(value: RuleAttributes) -> Self { + Self { + error: value.receive_error, + interface: value.receive_interface, + max_fds: value.max_fds, + member: value.receive_member, + min_fds: value.min_fds, + path: value.receive_path, + sender: value.receive_sender, + r#type: value.receive_type, + } + } +} + +type OptionalRule = Option; + +impl TryFrom for OptionalRule { + type Error = Error; + + fn try_from(value: RuleElement) -> std::result::Result { + match value { + RuleElement::Allow(RuleAttributes { + group: Some(_), + user: Some(_), + .. + }) + | RuleElement::Deny(RuleAttributes { + group: Some(_), + user: Some(_), + .. + }) => Err(Error::msg(format!( + "`group` cannot be combined with `user` in the same rule: {value:?}" + ))), + RuleElement::Allow(RuleAttributes { + own: Some(_), + own_prefix: Some(_), + .. + }) + | RuleElement::Deny(RuleAttributes { + own: Some(_), + own_prefix: Some(_), + .. + }) => Err(Error::msg(format!( + "`own_prefix` cannot be combined with `own` in the same rule: {value:?}" + ))), + RuleElement::Allow(RuleAttributes { + send_destination: Some(_), + send_destination_prefix: Some(_), + .. + }) + | RuleElement::Deny(RuleAttributes { + send_destination: Some(_), + send_destination_prefix: Some(_), + .. + }) => Err(Error::msg(format!( + "`send_destination_prefix` cannot be combined with `send_destination` in the same rule: {value:?}" + ))), + RuleElement::Allow(RuleAttributes { + eavesdrop: Some(true), + group: None, + own: None, + receive_requested_reply: None, + receive_sender: None, + send_broadcast: None, + send_destination: None, + send_destination_prefix: None, + send_error: None, + send_interface: None, + send_member: None, + send_path: None, + send_requested_reply: None, + send_type: None, + user: None, + .. + }) => { + // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 + Ok(None) + } + RuleElement::Allow( + RuleAttributes { + receive_requested_reply: Some(false), + .. + } + | RuleAttributes { + send_requested_reply: Some(false), + .. + }, + ) => { + // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 + Ok(None) + } + RuleElement::Allow(attrs) => { + // if attrs.eavesdrop == Some(true) { + // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 + // } + match OptionalOperation::try_from(attrs)? { + Some(some) => Ok(Some((Access::Allow, some))), + None => Ok(None), + } + } + RuleElement::Deny(RuleAttributes { + eavesdrop: Some(true), + .. + }) => { + // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 + Ok(None) + } + RuleElement::Deny( + RuleAttributes { + receive_requested_reply: Some(true), + .. + } + | RuleAttributes { + send_requested_reply: Some(true), + .. + }, + ) => { + // see: https://github.com/dbus2/busd/pull/146#issuecomment-2408429760 + Ok(None) + } + RuleElement::Deny(attrs) => match OptionalOperation::try_from(attrs)? { + Some(some) => Ok(Some((Access::Deny, some))), + None => Ok(None), + }, + } + } +} + +pub type Rule = (Access, Operation); + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub enum Access { + Allow, + Deny, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct SendOperation { + pub broadcast: Option, + pub destination: Option, + pub error: Option, + pub interface: Option, + pub max_fds: Option, + pub member: Option, + pub min_fds: Option, + pub path: Option, + pub r#type: Option, +} + +impl From for SendOperation { + fn from(value: RuleAttributes) -> Self { + let destination = match value { + RuleAttributes { + send_destination: Some(some), + send_destination_prefix: None, + .. + } if some == "*" => Some(Name::Any), + RuleAttributes { + send_destination: Some(some), + send_destination_prefix: None, + .. + } => Some(Name::Exact(some)), + RuleAttributes { + send_destination: None, + send_destination_prefix: Some(some), + .. + } => Some(Name::Prefix(some)), + _ => None, + }; + Self { + broadcast: value.send_broadcast, + destination, + error: value.send_error, + interface: value.send_interface, + max_fds: value.max_fds, + member: value.send_member, + min_fds: value.min_fds, + path: value.send_path, + r#type: value.send_type, + } + } +} + +pub fn rules_try_from_rule_elements(value: Vec) -> Result> { + let mut rules = vec![]; + for rule in value { + let rule = OptionalRule::try_from(rule)?; + if let Some(some) = rule { + rules.push(some); + } + } + Ok(rules) +} diff --git a/src/config/xml.rs b/src/config/xml.rs new file mode 100644 index 0000000..2cda981 --- /dev/null +++ b/src/config/xml.rs @@ -0,0 +1,341 @@ +use std::{ + env::current_dir, + ffi::OsString, + fs::{read_dir, read_to_string}, + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::{Error, Result}; +use serde::Deserialize; +use tracing::{error, warn}; + +use super::{BusType, MessageType}; + +/// The bus configuration. +/// +/// This is currently only loaded from the [XML configuration files] defined by the specification. +/// We plan to add support for other formats (e.g JSON) in the future. +/// +/// [XML configuration files]: https://dbus.freedesktop.org/doc/dbus-daemon.1.html#configuration_file +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct Document { + #[serde(rename = "$value", default)] + pub busconfig: Vec, + file_path: Option, +} + +impl FromStr for Document { + type Err = Error; + + fn from_str(s: &str) -> Result { + quick_xml::de::from_str(s).map_err(Error::msg) + } +} + +impl Document { + pub fn read_file(file_path: impl AsRef) -> Result { + let text = read_to_string(file_path.as_ref())?; + + let mut doc = Document::from_str(&text)?; + doc.file_path = Some(file_path.as_ref().to_path_buf()); + doc.resolve_includedirs()?.resolve_includes() + } + + fn resolve_includedirs(self) -> Result { + let base_path = self.base_path()?; + let Document { + busconfig, + file_path, + } = self; + + let mut doc = Document { + busconfig: vec![], + file_path: None, + }; + + for el in busconfig { + match el { + Element::Includedir(dir_path) => { + let dir_path = resolve_include_path(&base_path, &dir_path); + let dir_path = match dir_path.canonicalize() { + Ok(ok) => ok, + // we treat `` as though it has `ignore_missing="yes"` + Err(err) => { + warn!( + "cannot resolve '{}' to an absolute path: {}", + &dir_path.display(), + err + ); + continue; + } + }; + match read_dir(&dir_path) { + Ok(ok) => { + for entry in ok { + let path = entry?.path(); + if path.extension() == Some(&OsString::from("conf")) + && path.is_file() + { + doc.busconfig.push(Element::Include(IncludeElement { + file_path: path, + ..Default::default() + })); + } + } + } + // we treat `` as though it has `ignore_missing="yes"` + Err(err) => { + warn!( + "cannot read '{}': {}", + &dir_path.display(), + err + ); + continue; + } + } + } + _ => doc.busconfig.push(el), + } + } + + doc.file_path = file_path; + Ok(doc) + } + + fn resolve_includes(self) -> Result { + // TODO: implement protection against circular `` references + let base_path = self.base_path()?; + let Document { + busconfig, + file_path, + } = self; + + let mut doc = Document { + busconfig: vec![], + file_path: None, + }; + + for el in busconfig { + match el { + Element::Include(include) => { + if include.if_selinux_enable == IncludeOption::Yes + || include.selinux_root_relative == IncludeOption::Yes + { + // TODO: implement SELinux support + continue; + } + + let ignore_missing = include.ignore_missing == IncludeOption::Yes; + let file_path = resolve_include_path(&base_path, &include.file_path); + let file_path = match file_path.canonicalize().map_err(Error::msg) { + Ok(ok) => ok, + Err(err) => { + let msg = format!( + "cannot resolve '{}' to an absolute path: {}", + &file_path.display(), + err + ); + if ignore_missing { + warn!(msg); + continue; + } + error!(msg); + return Err(err); + } + }; + let mut included = match Document::read_file(&file_path) { + Ok(ok) => ok, + Err(err) => { + let msg = format!( + "'{}' should contain valid XML", + include.file_path.display() + ); + if ignore_missing { + warn!(msg); + continue; + } + error!(msg); + return Err(err); + } + }; + doc.busconfig.append(&mut included.busconfig); + } + _ => doc.busconfig.push(el), + } + } + + doc.file_path = file_path; + Ok(doc) + } + + fn base_path(&self) -> Result { + match &self.file_path { + Some(some) => Ok(some + .parent() + .ok_or_else(|| Error::msg("`` path should contain a file name"))? + .to_path_buf()), + None => { + warn!("cannot determine file path for this XML document, using current working directory"); + current_dir().map_err(Error::msg) + } + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum Element { + AllowAnonymous, + Auth(String), + Fork, + /// Include a file at this point. If the filename is relative, it is located relative to the + /// configuration file doing the including. + Include(IncludeElement), + /// Files in the directory are included in undefined order. + /// Only files ending in ".conf" are included. + Includedir(PathBuf), + KeepUmask, + Listen(String), + Limit, + Pidfile(PathBuf), + Policy(PolicyElement), + Servicedir(PathBuf), + Servicehelper(PathBuf), + /// Requests a standard set of session service directories. + /// Its effect is similar to specifying a series of elements for each of the data + /// directories, in the order given here. + StandardSessionServicedirs, + /// Specifies the standard system-wide activation directories that should be searched for + /// service files. + StandardSystemServicedirs, + Syslog, + Type(TypeElement), + User(String), +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct IncludeElement { + #[serde(default, rename = "@ignore_missing")] + ignore_missing: IncludeOption, + + // TODO: implement SELinux + #[serde(default, rename = "@if_selinux_enabled")] + if_selinux_enable: IncludeOption, + #[serde(default, rename = "@selinux_root_relative")] + selinux_root_relative: IncludeOption, + + #[serde(rename = "$value")] + file_path: PathBuf, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum IncludeOption { + #[default] + No, + Yes, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum PolicyContext { + Default, + Mandatory, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct PolicyElement { + #[serde(rename = "@at_console")] + pub at_console: Option, + #[serde(rename = "@context")] + pub context: Option, + #[serde(rename = "@group")] + pub group: Option, + #[serde(rename = "$value", default)] + pub rules: Vec, + #[serde(rename = "@user")] + pub user: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct RuleAttributes { + #[serde(rename = "@max_fds")] + pub max_fds: Option, + #[serde(rename = "@min_fds")] + pub min_fds: Option, + + #[serde(rename = "@receive_error")] + pub receive_error: Option, + #[serde(rename = "@receive_interface")] + pub receive_interface: Option, + /// deprecated and ignored + #[serde(rename = "@receive_member")] + pub receive_member: Option, + #[serde(rename = "@receive_path")] + pub receive_path: Option, + #[serde(rename = "@receive_sender")] + pub receive_sender: Option, + #[serde(rename = "@receive_type")] + pub receive_type: Option, + + #[serde(rename = "@send_broadcast")] + pub send_broadcast: Option, + #[serde(rename = "@send_destination")] + pub send_destination: Option, + #[serde(rename = "@send_destination_prefix")] + pub send_destination_prefix: Option, + #[serde(rename = "@send_error")] + pub send_error: Option, + #[serde(rename = "@send_interface")] + pub send_interface: Option, + #[serde(rename = "@send_member")] + pub send_member: Option, + #[serde(rename = "@send_path")] + pub send_path: Option, + #[serde(rename = "@send_type")] + pub send_type: Option, + + /// deprecated and ignored + #[serde(rename = "@receive_requested_reply")] + pub receive_requested_reply: Option, + /// deprecated and ignored + #[serde(rename = "@send_requested_reply")] + pub send_requested_reply: Option, + + /// deprecated and ignored + #[serde(rename = "@eavesdrop")] + pub eavesdrop: Option, + + #[serde(rename = "@own")] + pub own: Option, + #[serde(rename = "@own_prefix")] + pub own_prefix: Option, + + #[serde(rename = "@group")] + pub group: Option, + #[serde(rename = "@user")] + pub user: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum RuleElement { + Allow(RuleAttributes), + Deny(RuleAttributes), +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct TypeElement { + #[serde(rename = "$text")] + pub r#type: BusType, +} + +fn resolve_include_path(base_path: impl AsRef, include_path: impl AsRef) -> PathBuf { + let p = include_path.as_ref(); + if p.is_absolute() { + return p.to_path_buf(); + } + + base_path.as_ref().join(p) +} diff --git a/src/lib.rs b/src/lib.rs index 07a5f33..be211b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod bus; +pub mod config; pub mod fdo; pub mod match_rules; pub mod name_registry; diff --git a/tests/config.rs b/tests/config.rs new file mode 100644 index 0000000..eb336c2 --- /dev/null +++ b/tests/config.rs @@ -0,0 +1,490 @@ +use std::{path::PathBuf, str::FromStr}; + +use busd::config::{ + Access, BusType, Config, ConnectOperation, MessageType, Name, NameOwnership, Operation, Policy, + ReceiveOperation, SendOperation, +}; +use zbus::{Address, AuthMechanism}; + +#[test] +fn config_read_file_with_includes_ok() { + let got = + Config::read_file("./tests/data/valid.conf").expect("should read and parse XML input"); + + assert_eq!( + got, + Config { + auth: Some(AuthMechanism::External), + listen: Some(Address::from_str("unix:path=/tmp/a").expect("should parse address")), + policies: vec![ + Policy::DefaultContext(vec![ + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ( + Access::Deny, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ]), + Policy::MandatoryContext(vec![ + ( + Access::Deny, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Any) + }) + ), + ],), + ], + ..Default::default() + } + ); +} + +#[test] +fn config_read_file_example_session_disable_stats_conf_ok() { + let got = Config::read_file("./tests/data/example-session-disable-stats.conf") + .expect("should read and parse XML input"); + + assert_eq!( + got, + Config { + policies: vec![Policy::DefaultContext(vec![( + Access::Deny, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Debug.Stats")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None + }), + ),]),], + ..Default::default() + } + ); +} + +#[test] +fn config_read_file_example_system_enable_stats_conf_ok() { + let got = Config::read_file("./tests/data/example-system-enable-stats.conf") + .expect("should read and parse XML input"); + + assert_eq!( + got, + Config { + policies: vec![Policy::User( + vec![( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Debug.Stats")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None + }), + )], + String::from("USERNAME"), + ),], + ..Default::default() + } + ); +} + +#[test] +fn config_read_file_session_conf_ok() { + let mut got = + Config::read_file("./tests/data/session.conf").expect("should read and parse XML input"); + + assert!(!got.servicedirs.is_empty()); + + // nuking this to make it easier to `assert_eq!()` + got.servicedirs = vec![]; + + assert_eq!( + got, + Config { + listen: Some( + Address::from_str("unix:path=/run/user/1000/bus").expect("should parse address") + ), + keep_umask: true, + policies: vec![Policy::DefaultContext(vec![ + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Any), + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Allow, + Operation::Own(NameOwnership { + own: Some(Name::Any), + }), + ), + ]),], + r#type: Some(BusType::Session), + ..Default::default() + } + ); +} + +#[test] +fn config_read_file_system_conf_ok() { + let want = Config { + auth: Some(AuthMechanism::External), + fork: true, + listen: Some( + Address::from_str("unix:path=/var/run/dbus/system_bus_socket") + .expect("should parse address"), + ), + pidfile: Some(PathBuf::from("@DBUS_SYSTEM_PID_FILE@")), + policies: vec![ + Policy::DefaultContext(vec![ + ( + Access::Allow, + Operation::Connect(ConnectOperation { + group: None, + user: Some(String::from("*")), + }), + ), + ( + Access::Deny, + Operation::Own(NameOwnership { + own: Some(Name::Any), + }), + ), + ( + Access::Deny, + Operation::Send(SendOperation { + broadcast: None, + destination: None, + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: Some(MessageType::MethodCall), + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: None, + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: Some(MessageType::Signal), + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: None, + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: Some(MessageType::MethodReturn), + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: None, + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: Some(MessageType::Error), + }), + ), + ( + Access::Allow, + Operation::Receive(ReceiveOperation { + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + sender: None, + r#type: Some(MessageType::MethodCall), + }), + ), + ( + Access::Allow, + Operation::Receive(ReceiveOperation { + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + sender: None, + r#type: Some(MessageType::MethodReturn), + }), + ), + ( + Access::Allow, + Operation::Receive(ReceiveOperation { + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + sender: None, + r#type: Some(MessageType::Error), + }), + ), + ( + Access::Allow, + Operation::Receive(ReceiveOperation { + error: None, + interface: None, + max_fds: None, + member: None, + min_fds: None, + path: None, + sender: None, + r#type: Some(MessageType::Signal), + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Introspectable")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Properties")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Containers1")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Deny, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus")), + max_fds: None, + member: Some(String::from("UpdateActivationEnvironment")), + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Deny, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Debug.Stats")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ( + Access::Deny, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.systemd1.Activator")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + ), + ]), + Policy::User( + vec![( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.systemd1.Activator")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + )], + String::from("root"), + ), + Policy::User( + vec![( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Monitoring")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + )], + String::from("root"), + ), + Policy::User( + vec![( + Access::Allow, + Operation::Send(SendOperation { + broadcast: None, + destination: Some(Name::Exact(String::from("org.freedesktop.DBus"))), + error: None, + interface: Some(String::from("org.freedesktop.DBus.Debug.Stats")), + max_fds: None, + member: None, + min_fds: None, + path: None, + r#type: None, + }), + )], + String::from("root"), + ), + ], + servicehelper: Some(PathBuf::from("@DBUS_LIBEXECDIR@/dbus-daemon-launch-helper")), + syslog: true, + r#type: Some(BusType::System), + user: Some(String::from("@DBUS_USER@")), + ..Default::default() + }; + + let mut got = + Config::read_file("./tests/data/system.conf").expect("should read and parse XML input"); + + assert!(!got.servicedirs.is_empty()); + + // nuking this to make it easier to `assert_eq!()` + got.servicedirs = vec![]; + + assert_eq!(got, want,); +} + +#[cfg(unix)] +#[test] +fn config_read_file_real_usr_share_dbus1_session_conf_ok() { + let config_path = PathBuf::from("/usr/share/dbus-1/session.conf"); + if !config_path.exists() { + return; + } + Config::read_file(config_path).expect("should read and parse XML input"); +} + +#[cfg(unix)] +#[test] +fn config_read_file_real_usr_share_dbus1_system_conf_ok() { + let config_path = PathBuf::from("/usr/share/dbus-1/system.conf"); + if !config_path.exists() { + return; + } + Config::read_file(config_path).expect("should read and parse XML input"); +} + +#[should_panic] +#[test] +fn config_read_file_with_missing_include_err() { + Config::read_file("./tests/data/missing_include.conf") + .expect("should read and parse XML input"); +} + +#[should_panic] +#[test] +fn config_read_file_with_transitive_missing_include_err() { + Config::read_file("./tests/data/transitive_missing_include.conf") + .expect("should read and parse XML input"); +} diff --git a/tests/data/example-session-disable-stats.conf b/tests/data/example-session-disable-stats.conf new file mode 100644 index 0000000..baafb2d --- /dev/null +++ b/tests/data/example-session-disable-stats.conf @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/tests/data/example-system-enable-stats.conf b/tests/data/example-system-enable-stats.conf new file mode 100644 index 0000000..677f923 --- /dev/null +++ b/tests/data/example-system-enable-stats.conf @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/tests/data/includedir/a.conf b/tests/data/includedir/a.conf new file mode 100644 index 0000000..d602268 --- /dev/null +++ b/tests/data/includedir/a.conf @@ -0,0 +1,5 @@ + + + unix:path=/tmp/a + diff --git a/tests/data/includedir/not_included.xml b/tests/data/includedir/not_included.xml new file mode 100644 index 0000000..5af9363 --- /dev/null +++ b/tests/data/includedir/not_included.xml @@ -0,0 +1,5 @@ + + + unix:path=/tmp/not_included + diff --git a/tests/data/missing_include.conf b/tests/data/missing_include.conf new file mode 100644 index 0000000..67632af --- /dev/null +++ b/tests/data/missing_include.conf @@ -0,0 +1,5 @@ + + + ./missing.conf + diff --git a/tests/data/session.conf b/tests/data/session.conf new file mode 100644 index 0000000..aa4ddf9 --- /dev/null +++ b/tests/data/session.conf @@ -0,0 +1,83 @@ + + + + + + + + + session + + + + + unix:path=/run/user/1000/bus + + + + + + + + + + + + + + + + + @SYSCONFDIR_FROM_PKGDATADIR@/dbus-1/session.conf + + + + + + + + @SYSCONFDIR_FROM_PKGDATADIR@/dbus-1/session-local.conf + + + + + + + 1000000000 + 250000000 + 1000000000 + 250000000 + 1000000000 + + 120000 + 240000 + 150000 + 100000 + 10000 + 100000 + 10000 + 50000 + 50000 + 50000 + + diff --git a/tests/data/system.conf b/tests/data/system.conf new file mode 100644 index 0000000..a68b8fb --- /dev/null +++ b/tests/data/system.conf @@ -0,0 +1,145 @@ + + + + + + + + + + + + system + + + @DBUS_USER@ + + + + + + + + + @DBUS_LIBEXECDIR@/dbus-daemon-launch-helper + + + @DBUS_SYSTEM_PID_FILE@ + + + + + + EXTERNAL + + + unix:path=/var/run/dbus/system_bus_socket + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @SYSCONFDIR_FROM_PKGDATADIR@/dbus-1/system.conf + + + + + + + + + + + + + + + + + + + + + + + + + + @SYSCONFDIR_FROM_PKGDATADIR@/dbus-1/system-local.conf + + + + diff --git a/tests/data/transitive_missing_include.conf b/tests/data/transitive_missing_include.conf new file mode 100644 index 0000000..a317999 --- /dev/null +++ b/tests/data/transitive_missing_include.conf @@ -0,0 +1,5 @@ + + + ./missing_include.conf + diff --git a/tests/data/valid.conf b/tests/data/valid.conf new file mode 100644 index 0000000..f8e2a00 --- /dev/null +++ b/tests/data/valid.conf @@ -0,0 +1,13 @@ + + + ANONYMOUS + unix:path=/tmp/foo + + + + + ./valid_included.conf + ./valid_missing.conf + ./includedir + diff --git a/tests/data/valid_included.conf b/tests/data/valid_included.conf new file mode 100644 index 0000000..89ed48b --- /dev/null +++ b/tests/data/valid_included.conf @@ -0,0 +1,10 @@ + + + EXTERNAL + tcp:host=localhost,port=1234 + + + + +