Skip to main content

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        // FIXME: Avoid the unconditional clone of the whole `Address`.
60        let address = self.clone();
61        self.transport.connect(address).await
62    }
63
64    /// Get the address for the session socket respecting the `DBUS_SESSION_BUS_ADDRESS` environment
65    /// variable. If we don't recognize the value (or it's not set) we fall back to
66    /// `$XDG_RUNTIME_DIR/bus`.
67    pub fn session() -> Result<Self> {
68        match env::var("DBUS_SESSION_BUS_ADDRESS") {
69            Ok(val) => Self::from_str(&val),
70            _ => {
71                #[cfg(windows)]
72                return Self::from_str("autolaunch:");
73
74                #[cfg(all(unix, not(target_os = "macos")))]
75                {
76                    let runtime_dir = env::var("XDG_RUNTIME_DIR")
77                        .unwrap_or_else(|_| format!("/run/user/{}", geteuid().as_raw()));
78                    let path = format!("unix:path={runtime_dir}/bus");
79
80                    Self::from_str(&path)
81                }
82
83                #[cfg(target_os = "macos")]
84                return Self::from_str("launchd:env=DBUS_LAUNCHD_SESSION_BUS_SOCKET");
85            }
86        }
87    }
88
89    /// Get the address for the system bus respecting the `DBUS_SYSTEM_BUS_ADDRESS` environment
90    /// variable. If we don't recognize the value (or it's not set) we fall back to
91    /// `/var/run/dbus/system_bus_socket`.
92    pub fn system() -> Result<Self> {
93        match env::var("DBUS_SYSTEM_BUS_ADDRESS") {
94            Ok(val) => Self::from_str(&val),
95            _ => {
96                #[cfg(all(unix, not(target_os = "macos")))]
97                return Self::from_str("unix:path=/var/run/dbus/system_bus_socket");
98
99                #[cfg(windows)]
100                return Self::from_str("autolaunch:");
101
102                #[cfg(target_os = "macos")]
103                return Self::from_str("launchd:env=DBUS_LAUNCHD_SESSION_BUS_SOCKET");
104            }
105        }
106    }
107
108    /// The GUID for this address, if known.
109    pub fn guid(&self) -> Option<&Guid<'_>> {
110        self.guid.as_ref().map(|guid| guid.inner())
111    }
112}
113
114impl Display for Address {
115    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
116        self.transport.fmt(f)?;
117
118        if let Some(guid) = &self.guid {
119            write!(f, ",guid={guid}")?;
120        }
121
122        Ok(())
123    }
124}
125
126impl FromStr for Address {
127    type Err = Error;
128
129    /// Parse the transport part of a D-Bus address into a `Transport`.
130    fn from_str(address: &str) -> Result<Self> {
131        use std::str::from_utf8_unchecked;
132        use winnow::{
133            Parser,
134            ascii::alphanumeric1,
135            combinator::separated,
136            token::{take_until, take_while},
137        };
138
139        // All currently defined keys are alphanumber only. Change the paser when/if this changes.
140        let key = alphanumeric1::<_, ()>;
141        let value = take_while(1.., |b| b != b',');
142        let kv = (key, b'=', value).map(|(k, _, v)| {
143            // SAFETY: We got the bytes off a `&str` so they're guaranteed to be UTF-8 only.
144            unsafe { (from_utf8_unchecked(k), from_utf8_unchecked(v)) }
145        });
146        let options_parse = separated(0.., kv, b',');
147
148        let transport_parse = take_until(1.., b':').map(|bytes| {
149            // SAFETY: We got the bytes off a `&str` so they're guaranteed to be UTF-8 only.
150            unsafe { from_utf8_unchecked(bytes) }
151        });
152
153        (transport_parse, b':', options_parse)
154            .parse(address.as_bytes())
155            .map_err(|_| {
156                Error::Address(
157                    "Invalid address. \
158                    See https://dbus.freedesktop.org/doc/dbus-specification.html#addresses"
159                        .to_string(),
160                )
161            })
162            .and_then(|(transport, _, opts): (_, _, HashMap<_, _>)| {
163                let guid = opts
164                    .get("guid")
165                    .map(|s| Guid::from_str(s).map(|guid| OwnedGuid::from(guid).to_owned()))
166                    .transpose()?;
167                let transport = Transport::from_options(transport, opts)?;
168
169                Ok(Address { guid, transport })
170            })
171    }
172}
173
174impl TryFrom<&str> for Address {
175    type Error = Error;
176
177    fn try_from(value: &str) -> Result<Self> {
178        Self::from_str(value)
179    }
180}
181
182impl From<Transport> for Address {
183    fn from(transport: Transport) -> Self {
184        Self::new(transport)
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::{
191        Address,
192        transport::{Tcp, TcpTransportFamily, Transport},
193    };
194    #[cfg(target_os = "macos")]
195    use crate::address::transport::Launchd;
196    #[cfg(unix)]
197    use crate::address::transport::Unixexec;
198    #[cfg(windows)]
199    use crate::address::transport::{Autolaunch, AutolaunchScope};
200    use crate::address::transport::{Unix, UnixSocket};
201    use std::str::FromStr;
202    use test_log::test;
203
204    #[test]
205    fn parse_dbus_addresses() {
206        assert!(Address::from_str("").is_err());
207        assert!(Address::from_str("foo").is_err());
208        assert!(Address::from_str("foo:opt").is_err());
209        assert!(Address::from_str("foo:opt=1,opt=2").is_err());
210        assert!(Address::from_str("tcp:host=localhost").is_err());
211        assert!(Address::from_str("tcp:host=localhost,port=32f").is_err());
212        assert!(Address::from_str("tcp:host=localhost,port=123,family=ipv7").is_err());
213        assert!(Address::from_str("unix:foo=blah").is_err());
214        #[cfg(target_os = "linux")]
215        assert!(Address::from_str("unix:path=/tmp,abstract=foo").is_err());
216        #[cfg(unix)]
217        assert!(Address::from_str("unixexec:foo=blah").is_err());
218        assert_eq!(
219            Address::from_str("unix:path=/tmp/dbus-foo").unwrap(),
220            Transport::Unix(Unix::new(UnixSocket::File("/tmp/dbus-foo".into()))).into(),
221        );
222        #[cfg(target_os = "linux")]
223        assert_eq!(
224            Address::from_str("unix:abstract=/tmp/dbus-foo").unwrap(),
225            Transport::Unix(Unix::new(UnixSocket::Abstract("/tmp/dbus-foo".into()))).into(),
226        );
227        #[cfg(feature = "p2p")]
228        {
229            let guid = crate::Guid::generate();
230            assert_eq!(
231                Address::from_str(&format!("unix:path=/tmp/dbus-foo,guid={guid}")).unwrap(),
232                Address::from(Transport::Unix(Unix::new(UnixSocket::File(
233                    "/tmp/dbus-foo".into()
234                ))))
235                .set_guid(guid.clone())
236                .unwrap(),
237            );
238        }
239        #[cfg(unix)]
240        assert_eq!(
241            Address::from_str("unixexec:path=/tmp/dbus-foo").unwrap(),
242            Transport::Unixexec(Unixexec::new("/tmp/dbus-foo".into(), None, Vec::new())).into(),
243        );
244        assert_eq!(
245            Address::from_str("tcp:host=localhost,port=4142").unwrap(),
246            Transport::Tcp(Tcp::new("localhost", 4142)).into(),
247        );
248        assert_eq!(
249            Address::from_str("tcp:host=localhost,port=4142,family=ipv4").unwrap(),
250            Transport::Tcp(Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv4)))
251                .into(),
252        );
253        assert_eq!(
254            Address::from_str("tcp:host=localhost,port=4142,family=ipv6").unwrap(),
255            Transport::Tcp(Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv6)))
256                .into(),
257        );
258        assert_eq!(
259            Address::from_str("tcp:host=localhost,port=4142,family=ipv6,noncefile=/a/file/path")
260                .unwrap(),
261            Transport::Tcp(
262                Tcp::new("localhost", 4142)
263                    .set_family(Some(TcpTransportFamily::Ipv6))
264                    .set_nonce_file(Some(b"/a/file/path".to_vec()))
265            )
266            .into(),
267        );
268        assert_eq!(
269            Address::from_str(
270                "nonce-tcp:host=localhost,port=4142,family=ipv6,noncefile=/a/file/path%20to%20file%201234"
271            )
272            .unwrap(),
273            Transport::Tcp(
274                Tcp::new("localhost", 4142)
275                    .set_family(Some(TcpTransportFamily::Ipv6))
276                    .set_nonce_file(Some(b"/a/file/path to file 1234".to_vec()))
277            ).into()
278        );
279        #[cfg(windows)]
280        assert_eq!(
281            Address::from_str("autolaunch:").unwrap(),
282            Transport::Autolaunch(Autolaunch::new()).into(),
283        );
284        #[cfg(windows)]
285        assert_eq!(
286            Address::from_str("autolaunch:scope=*my_cool_scope*").unwrap(),
287            Transport::Autolaunch(
288                Autolaunch::new()
289                    .set_scope(Some(AutolaunchScope::Other("*my_cool_scope*".to_string())))
290            )
291            .into(),
292        );
293        #[cfg(target_os = "macos")]
294        assert_eq!(
295            Address::from_str("launchd:env=my_cool_env_key").unwrap(),
296            Transport::Launchd(Launchd::new("my_cool_env_key")).into(),
297        );
298        #[cfg(unix)]
299        assert_eq!(
300            Address::from_str("ibus:").unwrap(),
301            Transport::Ibus(crate::address::transport::Ibus::new()).into(),
302        );
303
304        #[cfg(all(any(feature = "vsock", feature = "tokio-vsock"), feature = "p2p"))]
305        {
306            let guid = crate::Guid::generate();
307            assert_eq!(
308                Address::from_str(&format!("vsock:cid=98,port=2934,guid={guid}")).unwrap(),
309                Address::from(Transport::Vsock(super::transport::Vsock::new(98, 2934)))
310                    .set_guid(guid)
311                    .unwrap(),
312            );
313        }
314        assert_eq!(
315            Address::from_str("unix:dir=/some/dir").unwrap(),
316            Transport::Unix(Unix::new(UnixSocket::Dir("/some/dir".into()))).into(),
317        );
318        assert_eq!(
319            Address::from_str("unix:tmpdir=/some/dir").unwrap(),
320            Transport::Unix(Unix::new(UnixSocket::TmpDir("/some/dir".into()))).into(),
321        );
322    }
323
324    #[test]
325    fn stringify_dbus_addresses() {
326        assert_eq!(
327            Address::from(Transport::Unix(Unix::new(UnixSocket::File(
328                "/tmp/dbus-foo".into()
329            ))))
330            .to_string(),
331            "unix:path=/tmp/dbus-foo",
332        );
333        assert_eq!(
334            Address::from(Transport::Unix(Unix::new(UnixSocket::Dir(
335                "/tmp/dbus-foo".into()
336            ))))
337            .to_string(),
338            "unix:dir=/tmp/dbus-foo",
339        );
340        assert_eq!(
341            Address::from(Transport::Unix(Unix::new(UnixSocket::TmpDir(
342                "/tmp/dbus-foo".into()
343            ))))
344            .to_string(),
345            "unix:tmpdir=/tmp/dbus-foo"
346        );
347        // FIXME: figure out how to handle abstract on Windows
348        #[cfg(target_os = "linux")]
349        assert_eq!(
350            Address::from(Transport::Unix(Unix::new(UnixSocket::Abstract(
351                "/tmp/dbus-foo".into()
352            ))))
353            .to_string(),
354            "unix:abstract=/tmp/dbus-foo"
355        );
356        assert_eq!(
357            Address::from(Transport::Tcp(Tcp::new("localhost", 4142))).to_string(),
358            "tcp:host=localhost,port=4142"
359        );
360        assert_eq!(
361            Address::from(Transport::Tcp(
362                Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv4))
363            ))
364            .to_string(),
365            "tcp:host=localhost,port=4142,family=ipv4"
366        );
367        assert_eq!(
368            Address::from(Transport::Tcp(
369                Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv6))
370            ))
371            .to_string(),
372            "tcp:host=localhost,port=4142,family=ipv6"
373        );
374        assert_eq!(
375            Address::from(Transport::Tcp(
376                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        #[cfg(unix)]
402        assert_eq!(
403            Address::from(Transport::Ibus(crate::address::transport::Ibus::new())).to_string(),
404            "ibus:"
405        );
406
407        #[cfg(all(any(feature = "vsock", feature = "tokio-vsock"), feature = "p2p"))]
408        {
409            let guid = crate::Guid::generate();
410            assert_eq!(
411                Address::from(Transport::Vsock(super::transport::Vsock::new(98, 2934)))
412                    .set_guid(guid.clone())
413                    .unwrap()
414                    .to_string(),
415                format!("vsock:cid=98,port=2934,guid={guid}"),
416            );
417        }
418    }
419
420    #[test]
421    fn connect_tcp() {
422        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
423        let port = listener.local_addr().unwrap().port();
424        let addr = Address::from_str(&format!("tcp:host=localhost,port={port}")).unwrap();
425        crate::utils::block_on(async { addr.connect().await }).unwrap();
426    }
427
428    #[test]
429    fn connect_nonce_tcp() {
430        struct PercentEncoded<'a>(&'a [u8]);
431
432        impl std::fmt::Display for PercentEncoded<'_> {
433            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434                super::transport::encode_percents(f, self.0)
435            }
436        }
437
438        use std::io::Write;
439
440        const TEST_COOKIE: &[u8] = b"VERILY SECRETIVE";
441
442        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
443        let port = listener.local_addr().unwrap().port();
444
445        let mut cookie = tempfile::NamedTempFile::new().unwrap();
446        cookie.as_file_mut().write_all(TEST_COOKIE).unwrap();
447
448        let encoded_path = format!(
449            "{}",
450            PercentEncoded(cookie.path().to_str().unwrap().as_ref())
451        );
452
453        let addr = Address::from_str(&format!(
454            "nonce-tcp:host=localhost,port={port},noncefile={encoded_path}"
455        ))
456        .unwrap();
457
458        let (sender, receiver) = std::sync::mpsc::sync_channel(1);
459
460        std::thread::spawn(move || {
461            use std::io::Read;
462
463            let mut client = listener.incoming().next().unwrap().unwrap();
464
465            let mut buf = [0u8; 16];
466            client.read_exact(&mut buf).unwrap();
467
468            sender.send(buf == TEST_COOKIE).unwrap();
469        });
470
471        crate::utils::block_on(addr.connect()).unwrap();
472
473        let saw_cookie = receiver
474            .recv_timeout(std::time::Duration::from_millis(100))
475            .expect("nonce file content hasn't been received by server thread in time");
476
477        assert!(
478            saw_cookie,
479            "nonce file content has been received, but was invalid"
480        );
481    }
482}