| 1 | //! D-Bus address handling. |
| 2 | //! |
| 3 | //! Server addresses consist of a transport name followed by a colon, and then an optional, |
| 4 | //! comma-separated list of keys and values in the form key=value. |
| 5 | //! |
| 6 | //! See also: |
| 7 | //! |
| 8 | //! * [Server addresses] in the D-Bus specification. |
| 9 | //! |
| 10 | //! [Server addresses]: https://dbus.freedesktop.org/doc/dbus-specification.html#addresses |
| 11 | |
| 12 | pub mod transport; |
| 13 | |
| 14 | use crate::{Error, Guid, OwnedGuid, Result}; |
| 15 | #[cfg (all(unix, not(target_os = "macos" )))] |
| 16 | use nix::unistd::Uid; |
| 17 | use std::{collections::HashMap, env, str::FromStr}; |
| 18 | |
| 19 | use std::fmt::{Display, Formatter}; |
| 20 | |
| 21 | use self::transport::Stream; |
| 22 | pub use self::transport::Transport; |
| 23 | |
| 24 | /// A bus address |
| 25 | #[derive (Clone, Debug, PartialEq, Eq)] |
| 26 | #[non_exhaustive ] |
| 27 | pub struct Address { |
| 28 | guid: Option<OwnedGuid>, |
| 29 | transport: Transport, |
| 30 | } |
| 31 | |
| 32 | impl Address { |
| 33 | /// Create a new `Address` from a `Transport`. |
| 34 | pub fn new(transport: Transport) -> Self { |
| 35 | Self { |
| 36 | transport, |
| 37 | guid: None, |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | /// Set the GUID for this address. |
| 42 | pub fn set_guid<G>(mut self, guid: G) -> Result<Self> |
| 43 | where |
| 44 | G: TryInto<OwnedGuid>, |
| 45 | G::Error: Into<crate::Error>, |
| 46 | { |
| 47 | self.guid = Some(guid.try_into().map_err(Into::into)?); |
| 48 | |
| 49 | Ok(self) |
| 50 | } |
| 51 | |
| 52 | /// The transport details for this address. |
| 53 | pub fn transport(&self) -> &Transport { |
| 54 | &self.transport |
| 55 | } |
| 56 | |
| 57 | #[cfg_attr (any(target_os = "macos" , windows), async_recursion::async_recursion)] |
| 58 | pub(crate) async fn connect(self) -> Result<Stream> { |
| 59 | self.transport.connect().await |
| 60 | } |
| 61 | |
| 62 | /// Get the address for session socket respecting the DBUS_SESSION_BUS_ADDRESS environment |
| 63 | /// variable. If we don't recognize the value (or it's not set) we fall back to |
| 64 | /// $XDG_RUNTIME_DIR/bus |
| 65 | pub fn session() -> Result<Self> { |
| 66 | match env::var("DBUS_SESSION_BUS_ADDRESS" ) { |
| 67 | Ok(val) => Self::from_str(&val), |
| 68 | _ => { |
| 69 | #[cfg (windows)] |
| 70 | return Self::from_str("autolaunch:" ); |
| 71 | |
| 72 | #[cfg (all(unix, not(target_os = "macos" )))] |
| 73 | { |
| 74 | let runtime_dir = env::var("XDG_RUNTIME_DIR" ) |
| 75 | .unwrap_or_else(|_| format!("/run/user/ {}" , Uid::effective())); |
| 76 | let path = format!("unix:path= {runtime_dir}/bus" ); |
| 77 | |
| 78 | Self::from_str(&path) |
| 79 | } |
| 80 | |
| 81 | #[cfg (target_os = "macos" )] |
| 82 | return Self::from_str("launchd:env=DBUS_LAUNCHD_SESSION_BUS_SOCKET" ); |
| 83 | } |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | /// Get the address for system bus respecting the DBUS_SYSTEM_BUS_ADDRESS environment |
| 88 | /// variable. If we don't recognize the value (or it's not set) we fall back to |
| 89 | /// /var/run/dbus/system_bus_socket |
| 90 | pub fn system() -> Result<Self> { |
| 91 | match env::var("DBUS_SYSTEM_BUS_ADDRESS" ) { |
| 92 | Ok(val) => Self::from_str(&val), |
| 93 | _ => { |
| 94 | #[cfg (all(unix, not(target_os = "macos" )))] |
| 95 | return Self::from_str("unix:path=/var/run/dbus/system_bus_socket" ); |
| 96 | |
| 97 | #[cfg (windows)] |
| 98 | return Self::from_str("autolaunch:" ); |
| 99 | |
| 100 | #[cfg (target_os = "macos" )] |
| 101 | return Self::from_str("launchd:env=DBUS_LAUNCHD_SESSION_BUS_SOCKET" ); |
| 102 | } |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | /// The GUID for this address, if known. |
| 107 | pub fn guid(&self) -> Option<&Guid<'_>> { |
| 108 | self.guid.as_ref().map(|guid| guid.inner()) |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | impl Display for Address { |
| 113 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
| 114 | self.transport.fmt(f)?; |
| 115 | |
| 116 | if let Some(guid: &OwnedGuid) = &self.guid { |
| 117 | write!(f, ",guid= {}" , guid)?; |
| 118 | } |
| 119 | |
| 120 | Ok(()) |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | impl FromStr for Address { |
| 125 | type Err = Error; |
| 126 | |
| 127 | /// Parse the transport part of a D-Bus address into a `Transport`. |
| 128 | fn from_str(address: &str) -> Result<Self> { |
| 129 | let col = address |
| 130 | .find(':' ) |
| 131 | .ok_or_else(|| Error::Address("address has no colon" .to_owned()))?; |
| 132 | let transport = &address[..col]; |
| 133 | let mut options = HashMap::new(); |
| 134 | |
| 135 | if address.len() > col + 1 { |
| 136 | for kv in address[col + 1..].split(',' ) { |
| 137 | let (k, v) = match kv.find('=' ) { |
| 138 | Some(eq) => (&kv[..eq], &kv[eq + 1..]), |
| 139 | None => { |
| 140 | return Err(Error::Address( |
| 141 | "missing = when parsing key/value" .to_owned(), |
| 142 | )) |
| 143 | } |
| 144 | }; |
| 145 | if options.insert(k, v).is_some() { |
| 146 | return Err(Error::Address(format!( |
| 147 | "Key ` {k}` specified multiple times" |
| 148 | ))); |
| 149 | } |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | Ok(Self { |
| 154 | guid: options |
| 155 | .remove("guid" ) |
| 156 | .map(|s| Guid::from_str(s).map(|guid| OwnedGuid::from(guid).to_owned())) |
| 157 | .transpose()?, |
| 158 | transport: Transport::from_options(transport, options)?, |
| 159 | }) |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | impl TryFrom<&str> for Address { |
| 164 | type Error = Error; |
| 165 | |
| 166 | fn try_from(value: &str) -> Result<Self> { |
| 167 | Self::from_str(value) |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | impl From<Transport> for Address { |
| 172 | fn from(transport: Transport) -> Self { |
| 173 | Self::new(transport) |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | #[cfg (test)] |
| 178 | mod tests { |
| 179 | use super::{ |
| 180 | transport::{Tcp, TcpTransportFamily, Transport}, |
| 181 | Address, |
| 182 | }; |
| 183 | #[cfg (target_os = "macos" )] |
| 184 | use crate::address::transport::Launchd; |
| 185 | #[cfg (windows)] |
| 186 | use crate::address::transport::{Autolaunch, AutolaunchScope}; |
| 187 | use crate::{ |
| 188 | address::transport::{Unix, UnixSocket}, |
| 189 | Error, |
| 190 | }; |
| 191 | use std::str::FromStr; |
| 192 | use test_log::test; |
| 193 | |
| 194 | #[test ] |
| 195 | fn parse_dbus_addresses() { |
| 196 | match Address::from_str("" ).unwrap_err() { |
| 197 | Error::Address(e) => assert_eq!(e, "address has no colon" ), |
| 198 | _ => panic!(), |
| 199 | } |
| 200 | match Address::from_str("foo" ).unwrap_err() { |
| 201 | Error::Address(e) => assert_eq!(e, "address has no colon" ), |
| 202 | _ => panic!(), |
| 203 | } |
| 204 | match Address::from_str("foo:opt" ).unwrap_err() { |
| 205 | Error::Address(e) => assert_eq!(e, "missing = when parsing key/value" ), |
| 206 | _ => panic!(), |
| 207 | } |
| 208 | match Address::from_str("foo:opt=1,opt=2" ).unwrap_err() { |
| 209 | Error::Address(e) => assert_eq!(e, "Key `opt` specified multiple times" ), |
| 210 | _ => panic!(), |
| 211 | } |
| 212 | match Address::from_str("tcp:host=localhost" ).unwrap_err() { |
| 213 | Error::Address(e) => assert_eq!(e, "tcp address is missing `port`" ), |
| 214 | _ => panic!(), |
| 215 | } |
| 216 | match Address::from_str("tcp:host=localhost,port=32f" ).unwrap_err() { |
| 217 | Error::Address(e) => assert_eq!(e, "invalid tcp `port`" ), |
| 218 | _ => panic!(), |
| 219 | } |
| 220 | match Address::from_str("tcp:host=localhost,port=123,family=ipv7" ).unwrap_err() { |
| 221 | Error::Address(e) => assert_eq!(e, "invalid tcp address `family`: ipv7" ), |
| 222 | _ => panic!(), |
| 223 | } |
| 224 | match Address::from_str("unix:foo=blah" ).unwrap_err() { |
| 225 | Error::Address(e) => assert_eq!(e, "unix: address is invalid" ), |
| 226 | _ => panic!(), |
| 227 | } |
| 228 | #[cfg (target_os = "linux" )] |
| 229 | match Address::from_str("unix:path=/tmp,abstract=foo" ).unwrap_err() { |
| 230 | Error::Address(e) => { |
| 231 | assert_eq!(e, "unix: address is invalid" ) |
| 232 | } |
| 233 | _ => panic!(), |
| 234 | } |
| 235 | assert_eq!( |
| 236 | Address::from_str("unix:path=/tmp/dbus-foo" ).unwrap(), |
| 237 | Transport::Unix(Unix::new(UnixSocket::File("/tmp/dbus-foo" .into()))).into(), |
| 238 | ); |
| 239 | #[cfg (target_os = "linux" )] |
| 240 | assert_eq!( |
| 241 | Address::from_str("unix:abstract=/tmp/dbus-foo" ).unwrap(), |
| 242 | Transport::Unix(Unix::new(UnixSocket::Abstract("/tmp/dbus-foo" .into()))).into(), |
| 243 | ); |
| 244 | let guid = crate::Guid::generate(); |
| 245 | assert_eq!( |
| 246 | Address::from_str(&format!("unix:path=/tmp/dbus-foo,guid={guid}" )).unwrap(), |
| 247 | Address::from(Transport::Unix(Unix::new(UnixSocket::File( |
| 248 | "/tmp/dbus-foo" .into() |
| 249 | )))) |
| 250 | .set_guid(guid.clone()) |
| 251 | .unwrap(), |
| 252 | ); |
| 253 | assert_eq!( |
| 254 | Address::from_str("tcp:host=localhost,port=4142" ).unwrap(), |
| 255 | Transport::Tcp(Tcp::new("localhost" , 4142)).into(), |
| 256 | ); |
| 257 | assert_eq!( |
| 258 | Address::from_str("tcp:host=localhost,port=4142,family=ipv4" ).unwrap(), |
| 259 | Transport::Tcp(Tcp::new("localhost" , 4142).set_family(Some(TcpTransportFamily::Ipv4))) |
| 260 | .into(), |
| 261 | ); |
| 262 | assert_eq!( |
| 263 | Address::from_str("tcp:host=localhost,port=4142,family=ipv6" ).unwrap(), |
| 264 | Transport::Tcp(Tcp::new("localhost" , 4142).set_family(Some(TcpTransportFamily::Ipv6))) |
| 265 | .into(), |
| 266 | ); |
| 267 | assert_eq!( |
| 268 | Address::from_str("tcp:host=localhost,port=4142,family=ipv6,noncefile=/a/file/path" ) |
| 269 | .unwrap(), |
| 270 | Transport::Tcp( |
| 271 | Tcp::new("localhost" , 4142) |
| 272 | .set_family(Some(TcpTransportFamily::Ipv6)) |
| 273 | .set_nonce_file(Some(b"/a/file/path" .to_vec())) |
| 274 | ) |
| 275 | .into(), |
| 276 | ); |
| 277 | assert_eq!( |
| 278 | Address::from_str( |
| 279 | "nonce-tcp:host=localhost,port=4142,family=ipv6,noncefile=/a/file/path%20to%20file%201234" |
| 280 | ) |
| 281 | .unwrap(), |
| 282 | Transport::Tcp( |
| 283 | Tcp::new("localhost" , 4142) |
| 284 | .set_family(Some(TcpTransportFamily::Ipv6)) |
| 285 | .set_nonce_file(Some(b"/a/file/path to file 1234" .to_vec())) |
| 286 | ).into() |
| 287 | ); |
| 288 | #[cfg (windows)] |
| 289 | assert_eq!( |
| 290 | Address::from_str("autolaunch:" ).unwrap(), |
| 291 | Transport::Autolaunch(Autolaunch::new()).into(), |
| 292 | ); |
| 293 | #[cfg (windows)] |
| 294 | assert_eq!( |
| 295 | Address::from_str("autolaunch:scope=*my_cool_scope*" ).unwrap(), |
| 296 | Transport::Autolaunch( |
| 297 | Autolaunch::new() |
| 298 | .set_scope(Some(AutolaunchScope::Other("*my_cool_scope*" .to_string()))) |
| 299 | ) |
| 300 | .into(), |
| 301 | ); |
| 302 | #[cfg (target_os = "macos" )] |
| 303 | assert_eq!( |
| 304 | Address::from_str("launchd:env=my_cool_env_key" ).unwrap(), |
| 305 | Transport::Launchd(Launchd::new("my_cool_env_key" )).into(), |
| 306 | ); |
| 307 | |
| 308 | #[cfg (all(feature = "vsock" , not(feature = "tokio" )))] |
| 309 | assert_eq!( |
| 310 | Address::from_str(&format!("vsock:cid=98,port=2934,guid={guid}" )).unwrap(), |
| 311 | Address::from(Transport::Vsock(super::transport::Vsock::new(98, 2934))) |
| 312 | .set_guid(guid) |
| 313 | .unwrap(), |
| 314 | ); |
| 315 | assert_eq!( |
| 316 | Address::from_str("unix:dir=/some/dir" ).unwrap(), |
| 317 | Transport::Unix(Unix::new(UnixSocket::Dir("/some/dir" .into()))).into(), |
| 318 | ); |
| 319 | assert_eq!( |
| 320 | Address::from_str("unix:tmpdir=/some/dir" ).unwrap(), |
| 321 | Transport::Unix(Unix::new(UnixSocket::TmpDir("/some/dir" .into()))).into(), |
| 322 | ); |
| 323 | } |
| 324 | |
| 325 | #[test ] |
| 326 | fn stringify_dbus_addresses() { |
| 327 | assert_eq!( |
| 328 | Address::from(Transport::Unix(Unix::new(UnixSocket::File( |
| 329 | "/tmp/dbus-foo" .into() |
| 330 | )))) |
| 331 | .to_string(), |
| 332 | "unix:path=/tmp/dbus-foo" , |
| 333 | ); |
| 334 | assert_eq!( |
| 335 | Address::from(Transport::Unix(Unix::new(UnixSocket::Dir( |
| 336 | "/tmp/dbus-foo" .into() |
| 337 | )))) |
| 338 | .to_string(), |
| 339 | "unix:dir=/tmp/dbus-foo" , |
| 340 | ); |
| 341 | assert_eq!( |
| 342 | Address::from(Transport::Unix(Unix::new(UnixSocket::TmpDir( |
| 343 | "/tmp/dbus-foo" .into() |
| 344 | )))) |
| 345 | .to_string(), |
| 346 | "unix:tmpdir=/tmp/dbus-foo" |
| 347 | ); |
| 348 | // FIXME: figure out how to handle abstract on Windows |
| 349 | #[cfg (target_os = "linux" )] |
| 350 | assert_eq!( |
| 351 | Address::from(Transport::Unix(Unix::new(UnixSocket::Abstract( |
| 352 | "/tmp/dbus-foo" .into() |
| 353 | )))) |
| 354 | .to_string(), |
| 355 | "unix:abstract=/tmp/dbus-foo" |
| 356 | ); |
| 357 | assert_eq!( |
| 358 | Address::from(Transport::Tcp(Tcp::new("localhost" , 4142))).to_string(), |
| 359 | "tcp:host=localhost,port=4142" |
| 360 | ); |
| 361 | assert_eq!( |
| 362 | Address::from(Transport::Tcp( |
| 363 | Tcp::new("localhost" , 4142).set_family(Some(TcpTransportFamily::Ipv4)) |
| 364 | )) |
| 365 | .to_string(), |
| 366 | "tcp:host=localhost,port=4142,family=ipv4" |
| 367 | ); |
| 368 | assert_eq!( |
| 369 | Address::from(Transport::Tcp( |
| 370 | Tcp::new("localhost" , 4142).set_family(Some(TcpTransportFamily::Ipv6)) |
| 371 | )) |
| 372 | .to_string(), |
| 373 | "tcp:host=localhost,port=4142,family=ipv6" |
| 374 | ); |
| 375 | assert_eq!( |
| 376 | Address::from(Transport::Tcp(Tcp::new("localhost" , 4142) |
| 377 | .set_family(Some(TcpTransportFamily::Ipv6)) |
| 378 | .set_nonce_file(Some(b"/a/file/path to file 1234" .to_vec()) |
| 379 | ))) |
| 380 | .to_string(), |
| 381 | "nonce-tcp:noncefile=/a/file/path%20to%20file%201234,host=localhost,port=4142,family=ipv6" |
| 382 | ); |
| 383 | #[cfg (windows)] |
| 384 | assert_eq!( |
| 385 | Address::from(Transport::Autolaunch(Autolaunch::new())).to_string(), |
| 386 | "autolaunch:" |
| 387 | ); |
| 388 | #[cfg (windows)] |
| 389 | assert_eq!( |
| 390 | Address::from(Transport::Autolaunch(Autolaunch::new().set_scope(Some( |
| 391 | AutolaunchScope::Other("*my_cool_scope*" .to_string()) |
| 392 | )))) |
| 393 | .to_string(), |
| 394 | "autolaunch:scope=*my_cool_scope*" |
| 395 | ); |
| 396 | #[cfg (target_os = "macos" )] |
| 397 | assert_eq!( |
| 398 | Address::from(Transport::Launchd(Launchd::new("my_cool_key" ))).to_string(), |
| 399 | "launchd:env=my_cool_key" |
| 400 | ); |
| 401 | |
| 402 | #[cfg (all(feature = "vsock" , not(feature = "tokio" )))] |
| 403 | { |
| 404 | let guid = crate::Guid::generate(); |
| 405 | assert_eq!( |
| 406 | Address::from(Transport::Vsock(super::transport::Vsock::new(98, 2934))) |
| 407 | .set_guid(guid.clone()) |
| 408 | .unwrap() |
| 409 | .to_string(), |
| 410 | format!("vsock:cid=98,port=2934,guid={guid}" ), |
| 411 | ); |
| 412 | } |
| 413 | } |
| 414 | |
| 415 | #[test ] |
| 416 | fn connect_tcp() { |
| 417 | let listener = std::net::TcpListener::bind("127.0.0.1:0" ).unwrap(); |
| 418 | let port = listener.local_addr().unwrap().port(); |
| 419 | let addr = Address::from_str(&format!("tcp:host=localhost,port={port}" )).unwrap(); |
| 420 | crate::utils::block_on(async { addr.connect().await }).unwrap(); |
| 421 | } |
| 422 | |
| 423 | #[test ] |
| 424 | fn connect_nonce_tcp() { |
| 425 | struct PercentEncoded<'a>(&'a [u8]); |
| 426 | |
| 427 | impl std::fmt::Display for PercentEncoded<'_> { |
| 428 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 429 | super::transport::encode_percents(f, self.0) |
| 430 | } |
| 431 | } |
| 432 | |
| 433 | use std::io::Write; |
| 434 | |
| 435 | const TEST_COOKIE: &[u8] = b"VERILY SECRETIVE" ; |
| 436 | |
| 437 | let listener = std::net::TcpListener::bind("127.0.0.1:0" ).unwrap(); |
| 438 | let port = listener.local_addr().unwrap().port(); |
| 439 | |
| 440 | let mut cookie = tempfile::NamedTempFile::new().unwrap(); |
| 441 | cookie.as_file_mut().write_all(TEST_COOKIE).unwrap(); |
| 442 | |
| 443 | let encoded_path = format!( |
| 444 | "{}" , |
| 445 | PercentEncoded(cookie.path().to_str().unwrap().as_ref()) |
| 446 | ); |
| 447 | |
| 448 | let addr = Address::from_str(&format!( |
| 449 | "nonce-tcp:host=localhost,port={port},noncefile={encoded_path}" |
| 450 | )) |
| 451 | .unwrap(); |
| 452 | |
| 453 | let (sender, receiver) = std::sync::mpsc::sync_channel(1); |
| 454 | |
| 455 | std::thread::spawn(move || { |
| 456 | use std::io::Read; |
| 457 | |
| 458 | let mut client = listener.incoming().next().unwrap().unwrap(); |
| 459 | |
| 460 | let mut buf = [0u8; 16]; |
| 461 | client.read_exact(&mut buf).unwrap(); |
| 462 | |
| 463 | sender.send(buf == TEST_COOKIE).unwrap(); |
| 464 | }); |
| 465 | |
| 466 | crate::utils::block_on(addr.connect()).unwrap(); |
| 467 | |
| 468 | let saw_cookie = receiver |
| 469 | .recv_timeout(std::time::Duration::from_millis(100)) |
| 470 | .expect("nonce file content hasn't been received by server thread in time" ); |
| 471 | |
| 472 | assert!( |
| 473 | saw_cookie, |
| 474 | "nonce file content has been received, but was invalid" |
| 475 | ); |
| 476 | } |
| 477 | } |
| 478 | |