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 the 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 the 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        use std::str::from_utf8_unchecked;
130        use winnow::{
131            ascii::alphanumeric1,
132            combinator::separated,
133            token::{take_until, take_while},
134            Parser,
135        };
136
137        // All currently defined keys are alphanumber only. Change the paser when/if this changes.
138        let key = alphanumeric1::<_, ()>;
139        let value = take_while(1.., |b| b != b',');
140        let kv = (key, b'=', value).map(|(k, _, v)| {
141            // SAFETY: We got the bytes off a `&str` so they're guaranteed to be UTF-8 only.
142            unsafe { (from_utf8_unchecked(k), from_utf8_unchecked(v)) }
143        });
144        let options_parse = separated(1.., kv, b',');
145
146        let transport_parse = take_until(1.., b':').map(|bytes| {
147            // SAFETY: We got the bytes off a `&str` so they're guaranteed to be UTF-8 only.
148            unsafe { from_utf8_unchecked(bytes) }
149        });
150
151        (transport_parse, b':', options_parse)
152            .parse(address.as_bytes())
153            .map_err(|_| {
154                Error::Address(
155                    "Invalid address. \
156                    See https://dbus.freedesktop.org/doc/dbus-specification.html#addresses"
157                        .to_string(),
158                )
159            })
160            .and_then(|(transport, _, opts): (_, _, HashMap<_, _>)| {
161                let guid = opts
162                    .get("guid")
163                    .map(|s| Guid::from_str(s).map(|guid| OwnedGuid::from(guid).to_owned()))
164                    .transpose()?;
165                let transport = Transport::from_options(transport, opts)?;
166
167                Ok(Address { guid, transport })
168            })
169    }
170}
171
172impl TryFrom<&str> for Address {
173    type Error = Error;
174
175    fn try_from(value: &str) -> Result<Self> {
176        Self::from_str(value)
177    }
178}
179
180impl From<Transport> for Address {
181    fn from(transport: Transport) -> Self {
182        Self::new(transport)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::{
189        transport::{Tcp, TcpTransportFamily, Transport},
190        Address,
191    };
192    #[cfg(target_os = "macos")]
193    use crate::address::transport::Launchd;
194    #[cfg(unix)]
195    use crate::address::transport::Unixexec;
196    #[cfg(windows)]
197    use crate::address::transport::{Autolaunch, AutolaunchScope};
198    use crate::address::transport::{Unix, UnixSocket};
199    use std::str::FromStr;
200    use test_log::test;
201
202    #[test]
203    fn parse_dbus_addresses() {
204        assert!(Address::from_str("").is_err());
205        assert!(Address::from_str("foo").is_err());
206        assert!(Address::from_str("foo:opt").is_err());
207        assert!(Address::from_str("foo:opt=1,opt=2").is_err());
208        assert!(Address::from_str("tcp:host=localhost").is_err());
209        assert!(Address::from_str("tcp:host=localhost,port=32f").is_err());
210        assert!(Address::from_str("tcp:host=localhost,port=123,family=ipv7").is_err());
211        assert!(Address::from_str("unix:foo=blah").is_err());
212        #[cfg(target_os = "linux")]
213        assert!(Address::from_str("unix:path=/tmp,abstract=foo").is_err());
214        #[cfg(unix)]
215        assert!(Address::from_str("unixexec:foo=blah").is_err());
216        assert_eq!(
217            Address::from_str("unix:path=/tmp/dbus-foo").unwrap(),
218            Transport::Unix(Unix::new(UnixSocket::File("/tmp/dbus-foo".into()))).into(),
219        );
220        #[cfg(target_os = "linux")]
221        assert_eq!(
222            Address::from_str("unix:abstract=/tmp/dbus-foo").unwrap(),
223            Transport::Unix(Unix::new(UnixSocket::Abstract("/tmp/dbus-foo".into()))).into(),
224        );
225        #[cfg(feature = "p2p")]
226        {
227            let guid = crate::Guid::generate();
228            assert_eq!(
229                Address::from_str(&format!("unix:path=/tmp/dbus-foo,guid={guid}")).unwrap(),
230                Address::from(Transport::Unix(Unix::new(UnixSocket::File(
231                    "/tmp/dbus-foo".into()
232                ))))
233                .set_guid(guid.clone())
234                .unwrap(),
235            );
236        }
237        #[cfg(unix)]
238        assert_eq!(
239            Address::from_str("unixexec:path=/tmp/dbus-foo").unwrap(),
240            Transport::Unixexec(Unixexec::new("/tmp/dbus-foo".into(), None, Vec::new())).into(),
241        );
242        assert_eq!(
243            Address::from_str("tcp:host=localhost,port=4142").unwrap(),
244            Transport::Tcp(Tcp::new("localhost", 4142)).into(),
245        );
246        assert_eq!(
247            Address::from_str("tcp:host=localhost,port=4142,family=ipv4").unwrap(),
248            Transport::Tcp(Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv4)))
249                .into(),
250        );
251        assert_eq!(
252            Address::from_str("tcp:host=localhost,port=4142,family=ipv6").unwrap(),
253            Transport::Tcp(Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv6)))
254                .into(),
255        );
256        assert_eq!(
257            Address::from_str("tcp:host=localhost,port=4142,family=ipv6,noncefile=/a/file/path")
258                .unwrap(),
259            Transport::Tcp(
260                Tcp::new("localhost", 4142)
261                    .set_family(Some(TcpTransportFamily::Ipv6))
262                    .set_nonce_file(Some(b"/a/file/path".to_vec()))
263            )
264            .into(),
265        );
266        assert_eq!(
267            Address::from_str(
268                "nonce-tcp:host=localhost,port=4142,family=ipv6,noncefile=/a/file/path%20to%20file%201234"
269            )
270            .unwrap(),
271            Transport::Tcp(
272                Tcp::new("localhost", 4142)
273                    .set_family(Some(TcpTransportFamily::Ipv6))
274                    .set_nonce_file(Some(b"/a/file/path to file 1234".to_vec()))
275            ).into()
276        );
277        #[cfg(windows)]
278        assert_eq!(
279            Address::from_str("autolaunch:").unwrap(),
280            Transport::Autolaunch(Autolaunch::new()).into(),
281        );
282        #[cfg(windows)]
283        assert_eq!(
284            Address::from_str("autolaunch:scope=*my_cool_scope*").unwrap(),
285            Transport::Autolaunch(
286                Autolaunch::new()
287                    .set_scope(Some(AutolaunchScope::Other("*my_cool_scope*".to_string())))
288            )
289            .into(),
290        );
291        #[cfg(target_os = "macos")]
292        assert_eq!(
293            Address::from_str("launchd:env=my_cool_env_key").unwrap(),
294            Transport::Launchd(Launchd::new("my_cool_env_key")).into(),
295        );
296
297        #[cfg(all(feature = "vsock", feature = "p2p", not(feature = "tokio")))]
298        {
299            let guid = crate::Guid::generate();
300            assert_eq!(
301                Address::from_str(&format!("vsock:cid=98,port=2934,guid={guid}")).unwrap(),
302                Address::from(Transport::Vsock(super::transport::Vsock::new(98, 2934)))
303                    .set_guid(guid)
304                    .unwrap(),
305            );
306        }
307        assert_eq!(
308            Address::from_str("unix:dir=/some/dir").unwrap(),
309            Transport::Unix(Unix::new(UnixSocket::Dir("/some/dir".into()))).into(),
310        );
311        assert_eq!(
312            Address::from_str("unix:tmpdir=/some/dir").unwrap(),
313            Transport::Unix(Unix::new(UnixSocket::TmpDir("/some/dir".into()))).into(),
314        );
315    }
316
317    #[test]
318    fn stringify_dbus_addresses() {
319        assert_eq!(
320            Address::from(Transport::Unix(Unix::new(UnixSocket::File(
321                "/tmp/dbus-foo".into()
322            ))))
323            .to_string(),
324            "unix:path=/tmp/dbus-foo",
325        );
326        assert_eq!(
327            Address::from(Transport::Unix(Unix::new(UnixSocket::Dir(
328                "/tmp/dbus-foo".into()
329            ))))
330            .to_string(),
331            "unix:dir=/tmp/dbus-foo",
332        );
333        assert_eq!(
334            Address::from(Transport::Unix(Unix::new(UnixSocket::TmpDir(
335                "/tmp/dbus-foo".into()
336            ))))
337            .to_string(),
338            "unix:tmpdir=/tmp/dbus-foo"
339        );
340        // FIXME: figure out how to handle abstract on Windows
341        #[cfg(target_os = "linux")]
342        assert_eq!(
343            Address::from(Transport::Unix(Unix::new(UnixSocket::Abstract(
344                "/tmp/dbus-foo".into()
345            ))))
346            .to_string(),
347            "unix:abstract=/tmp/dbus-foo"
348        );
349        assert_eq!(
350            Address::from(Transport::Tcp(Tcp::new("localhost", 4142))).to_string(),
351            "tcp:host=localhost,port=4142"
352        );
353        assert_eq!(
354            Address::from(Transport::Tcp(
355                Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv4))
356            ))
357            .to_string(),
358            "tcp:host=localhost,port=4142,family=ipv4"
359        );
360        assert_eq!(
361            Address::from(Transport::Tcp(
362                Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv6))
363            ))
364            .to_string(),
365            "tcp:host=localhost,port=4142,family=ipv6"
366        );
367        assert_eq!(
368            Address::from(Transport::Tcp(Tcp::new("localhost", 4142)
369                .set_family(Some(TcpTransportFamily::Ipv6))
370                .set_nonce_file(Some(b"/a/file/path to file 1234".to_vec())
371            )))
372            .to_string(),
373            "nonce-tcp:noncefile=/a/file/path%20to%20file%201234,host=localhost,port=4142,family=ipv6"
374        );
375        #[cfg(windows)]
376        assert_eq!(
377            Address::from(Transport::Autolaunch(Autolaunch::new())).to_string(),
378            "autolaunch:"
379        );
380        #[cfg(windows)]
381        assert_eq!(
382            Address::from(Transport::Autolaunch(Autolaunch::new().set_scope(Some(
383                AutolaunchScope::Other("*my_cool_scope*".to_string())
384            ))))
385            .to_string(),
386            "autolaunch:scope=*my_cool_scope*"
387        );
388        #[cfg(target_os = "macos")]
389        assert_eq!(
390            Address::from(Transport::Launchd(Launchd::new("my_cool_key"))).to_string(),
391            "launchd:env=my_cool_key"
392        );
393
394        #[cfg(all(feature = "vsock", feature = "p2p", not(feature = "tokio")))]
395        {
396            let guid = crate::Guid::generate();
397            assert_eq!(
398                Address::from(Transport::Vsock(super::transport::Vsock::new(98, 2934)))
399                    .set_guid(guid.clone())
400                    .unwrap()
401                    .to_string(),
402                format!("vsock:cid=98,port=2934,guid={guid}"),
403            );
404        }
405    }
406
407    #[test]
408    fn connect_tcp() {
409        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
410        let port = listener.local_addr().unwrap().port();
411        let addr = Address::from_str(&format!("tcp:host=localhost,port={port}")).unwrap();
412        crate::utils::block_on(async { addr.connect().await }).unwrap();
413    }
414
415    #[test]
416    fn connect_nonce_tcp() {
417        struct PercentEncoded<'a>(&'a [u8]);
418
419        impl std::fmt::Display for PercentEncoded<'_> {
420            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
421                super::transport::encode_percents(f, self.0)
422            }
423        }
424
425        use std::io::Write;
426
427        const TEST_COOKIE: &[u8] = b"VERILY SECRETIVE";
428
429        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
430        let port = listener.local_addr().unwrap().port();
431
432        let mut cookie = tempfile::NamedTempFile::new().unwrap();
433        cookie.as_file_mut().write_all(TEST_COOKIE).unwrap();
434
435        let encoded_path = format!(
436            "{}",
437            PercentEncoded(cookie.path().to_str().unwrap().as_ref())
438        );
439
440        let addr = Address::from_str(&format!(
441            "nonce-tcp:host=localhost,port={port},noncefile={encoded_path}"
442        ))
443        .unwrap();
444
445        let (sender, receiver) = std::sync::mpsc::sync_channel(1);
446
447        std::thread::spawn(move || {
448            use std::io::Read;
449
450            let mut client = listener.incoming().next().unwrap().unwrap();
451
452            let mut buf = [0u8; 16];
453            client.read_exact(&mut buf).unwrap();
454
455            sender.send(buf == TEST_COOKIE).unwrap();
456        });
457
458        crate::utils::block_on(addr.connect()).unwrap();
459
460        let saw_cookie = receiver
461            .recv_timeout(std::time::Duration::from_millis(100))
462            .expect("nonce file content hasn't been received by server thread in time");
463
464        assert!(
465            saw_cookie,
466            "nonce file content has been received, but was invalid"
467        );
468    }
469}