1pub 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#[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/{}", 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 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 ascii::alphanumeric1,
132 combinator::separated,
133 token::{take_until, take_while},
134 Parser,
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(1.., 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 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 #[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}