1pub 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#[derive(Clone, Debug, PartialEq, Eq)]
26#[non_exhaustive]
27pub struct Address {
28 guid: Option<OwnedGuid>,
29 transport: Transport,
30}
31
32impl Address {
33 pub fn new(transport: Transport) -> Self {
35 Self {
36 transport,
37 guid: None,
38 }
39 }
40
41 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 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 let address = self.clone();
61 self.transport.connect(address).await
62 }
63
64 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 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 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 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 let key = alphanumeric1::<_, ()>;
141 let value = take_while(1.., |b| b != b',');
142 let kv = (key, b'=', value).map(|(k, _, v)| {
143 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 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 #[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}