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 rustix::process::geteuid;
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/{}", geteuid().as_raw()));
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            Parser,
132            ascii::alphanumeric1,
133            combinator::separated,
134            token::{take_until, take_while},
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(0.., 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        Address,
190        transport::{Tcp, TcpTransportFamily, Transport},
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        #[cfg(unix)]
297        assert_eq!(
298            Address::from_str("ibus:").unwrap(),
299            Transport::Ibus(crate::address::transport::Ibus::new()).into(),
300        );
301
302        #[cfg(all(feature = "vsock", feature = "p2p", not(feature = "tokio")))]
303        {
304            let guid = crate::Guid::generate();
305            assert_eq!(
306                Address::from_str(&format!("vsock:cid=98,port=2934,guid={guid}")).unwrap(),
307                Address::from(Transport::Vsock(super::transport::Vsock::new(98, 2934)))
308                    .set_guid(guid)
309                    .unwrap(),
310            );
311        }
312        assert_eq!(
313            Address::from_str("unix:dir=/some/dir").unwrap(),
314            Transport::Unix(Unix::new(UnixSocket::Dir("/some/dir".into()))).into(),
315        );
316        assert_eq!(
317            Address::from_str("unix:tmpdir=/some/dir").unwrap(),
318            Transport::Unix(Unix::new(UnixSocket::TmpDir("/some/dir".into()))).into(),
319        );
320    }
321
322    #[test]
323    fn stringify_dbus_addresses() {
324        assert_eq!(
325            Address::from(Transport::Unix(Unix::new(UnixSocket::File(
326                "/tmp/dbus-foo".into()
327            ))))
328            .to_string(),
329            "unix:path=/tmp/dbus-foo",
330        );
331        assert_eq!(
332            Address::from(Transport::Unix(Unix::new(UnixSocket::Dir(
333                "/tmp/dbus-foo".into()
334            ))))
335            .to_string(),
336            "unix:dir=/tmp/dbus-foo",
337        );
338        assert_eq!(
339            Address::from(Transport::Unix(Unix::new(UnixSocket::TmpDir(
340                "/tmp/dbus-foo".into()
341            ))))
342            .to_string(),
343            "unix:tmpdir=/tmp/dbus-foo"
344        );
345        // FIXME: figure out how to handle abstract on Windows
346        #[cfg(target_os = "linux")]
347        assert_eq!(
348            Address::from(Transport::Unix(Unix::new(UnixSocket::Abstract(
349                "/tmp/dbus-foo".into()
350            ))))
351            .to_string(),
352            "unix:abstract=/tmp/dbus-foo"
353        );
354        assert_eq!(
355            Address::from(Transport::Tcp(Tcp::new("localhost", 4142))).to_string(),
356            "tcp:host=localhost,port=4142"
357        );
358        assert_eq!(
359            Address::from(Transport::Tcp(
360                Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv4))
361            ))
362            .to_string(),
363            "tcp:host=localhost,port=4142,family=ipv4"
364        );
365        assert_eq!(
366            Address::from(Transport::Tcp(
367                Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv6))
368            ))
369            .to_string(),
370            "tcp:host=localhost,port=4142,family=ipv6"
371        );
372        assert_eq!(
373            Address::from(Transport::Tcp(
374                Tcp::new("localhost", 4142)
375                    .set_family(Some(TcpTransportFamily::Ipv6))
376                    .set_nonce_file(Some(b"/a/file/path to file 1234".to_vec()))
377            ))
378            .to_string(),
379            "nonce-tcp:noncefile=/a/file/path%20to%20file%201234,host=localhost,port=4142,family=ipv6"
380        );
381        #[cfg(windows)]
382        assert_eq!(
383            Address::from(Transport::Autolaunch(Autolaunch::new())).to_string(),
384            "autolaunch:"
385        );
386        #[cfg(windows)]
387        assert_eq!(
388            Address::from(Transport::Autolaunch(Autolaunch::new().set_scope(Some(
389                AutolaunchScope::Other("*my_cool_scope*".to_string())
390            ))))
391            .to_string(),
392            "autolaunch:scope=*my_cool_scope*"
393        );
394        #[cfg(target_os = "macos")]
395        assert_eq!(
396            Address::from(Transport::Launchd(Launchd::new("my_cool_key"))).to_string(),
397            "launchd:env=my_cool_key"
398        );
399        #[cfg(unix)]
400        assert_eq!(
401            Address::from(Transport::Ibus(crate::address::transport::Ibus::new())).to_string(),
402            "ibus:"
403        );
404
405        #[cfg(all(feature = "vsock", feature = "p2p", not(feature = "tokio")))]
406        {
407            let guid = crate::Guid::generate();
408            assert_eq!(
409                Address::from(Transport::Vsock(super::transport::Vsock::new(98, 2934)))
410                    .set_guid(guid.clone())
411                    .unwrap()
412                    .to_string(),
413                format!("vsock:cid=98,port=2934,guid={guid}"),
414            );
415        }
416    }
417
418    #[test]
419    fn connect_tcp() {
420        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
421        let port = listener.local_addr().unwrap().port();
422        let addr = Address::from_str(&format!("tcp:host=localhost,port={port}")).unwrap();
423        crate::utils::block_on(async { addr.connect().await }).unwrap();
424    }
425
426    #[test]
427    fn connect_nonce_tcp() {
428        struct PercentEncoded<'a>(&'a [u8]);
429
430        impl std::fmt::Display for PercentEncoded<'_> {
431            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432                super::transport::encode_percents(f, self.0)
433            }
434        }
435
436        use std::io::Write;
437
438        const TEST_COOKIE: &[u8] = b"VERILY SECRETIVE";
439
440        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
441        let port = listener.local_addr().unwrap().port();
442
443        let mut cookie = tempfile::NamedTempFile::new().unwrap();
444        cookie.as_file_mut().write_all(TEST_COOKIE).unwrap();
445
446        let encoded_path = format!(
447            "{}",
448            PercentEncoded(cookie.path().to_str().unwrap().as_ref())
449        );
450
451        let addr = Address::from_str(&format!(
452            "nonce-tcp:host=localhost,port={port},noncefile={encoded_path}"
453        ))
454        .unwrap();
455
456        let (sender, receiver) = std::sync::mpsc::sync_channel(1);
457
458        std::thread::spawn(move || {
459            use std::io::Read;
460
461            let mut client = listener.incoming().next().unwrap().unwrap();
462
463            let mut buf = [0u8; 16];
464            client.read_exact(&mut buf).unwrap();
465
466            sender.send(buf == TEST_COOKIE).unwrap();
467        });
468
469        crate::utils::block_on(addr.connect()).unwrap();
470
471        let saw_cookie = receiver
472            .recv_timeout(std::time::Duration::from_millis(100))
473            .expect("nonce file content hasn't been received by server thread in time");
474
475        assert!(
476            saw_cookie,
477            "nonce file content has been received, but was invalid"
478        );
479    }
480}