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 self.transport.connect().await
60 }
61
62 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 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 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 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 let key = alphanumeric1::<_, ()>;
139 let value = take_while(1.., |b| b != b',');
140 let kv = (key, b'=', value).map(|(k, _, v)| {
141 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 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 #[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}