zbus/address/
mod.rs

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
12pub mod transport;
13
14use crate::{Error, Guid, OwnedGuid, Result};
15#[cfg(all(unix, not(target_os = "macos")))]
16use nix::unistd::Uid;
17use std::{collections::HashMap, env, str::FromStr};
18
19use std::fmt::{Display, Formatter};
20
21use self::transport::Stream;
22pub use self::transport::Transport;
23
24/// A bus address
25#[derive(Clone, Debug, PartialEq, Eq)]
26#[non_exhaustive]
27pub struct Address {
28    guid: Option<OwnedGuid>,
29    transport: Transport,
30}
31
32impl 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
112impl Display for Address {
113    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
114        self.transport.fmt(f)?;
115
116        if let Some(guid) = &self.guid {
117            write!(f, ",guid={}", guid)?;
118        }
119
120        Ok(())
121    }
122}
123
124impl 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
163impl 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
171impl From<Transport> for Address {
172    fn from(transport: Transport) -> Self {
173        Self::new(transport)
174    }
175}
176
177#[cfg(test)]
178mod 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}