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 let col = address
130 .find(':')
131 .ok_or_else(|| Error::Address("address has no colon".to_owned()))?;
132 let transport = &address[..col];
133 let mut options = HashMap::new();
134
135 if address.len() > col + 1 {
136 for kv in address[col + 1..].split(',') {
137 let (k, v) = match kv.find('=') {
138 Some(eq) => (&kv[..eq], &kv[eq + 1..]),
139 None => {
140 return Err(Error::Address(
141 "missing = when parsing key/value".to_owned(),
142 ))
143 }
144 };
145 if options.insert(k, v).is_some() {
146 return Err(Error::Address(format!(
147 "Key `{k}` specified multiple times"
148 )));
149 }
150 }
151 }
152
153 Ok(Self {
154 guid: options
155 .remove("guid")
156 .map(|s| Guid::from_str(s).map(|guid| OwnedGuid::from(guid).to_owned()))
157 .transpose()?,
158 transport: Transport::from_options(transport, options)?,
159 })
160 }
161}
162
163impl TryFrom<&str> for Address {
164 type Error = Error;
165
166 fn try_from(value: &str) -> Result<Self> {
167 Self::from_str(value)
168 }
169}
170
171impl From<Transport> for Address {
172 fn from(transport: Transport) -> Self {
173 Self::new(transport)
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::{
180 transport::{Tcp, TcpTransportFamily, Transport},
181 Address,
182 };
183 #[cfg(target_os = "macos")]
184 use crate::address::transport::Launchd;
185 #[cfg(windows)]
186 use crate::address::transport::{Autolaunch, AutolaunchScope};
187 use crate::{
188 address::transport::{Unix, UnixSocket},
189 Error,
190 };
191 use std::str::FromStr;
192 use test_log::test;
193
194 #[test]
195 fn parse_dbus_addresses() {
196 match Address::from_str("").unwrap_err() {
197 Error::Address(e) => assert_eq!(e, "address has no colon"),
198 _ => panic!(),
199 }
200 match Address::from_str("foo").unwrap_err() {
201 Error::Address(e) => assert_eq!(e, "address has no colon"),
202 _ => panic!(),
203 }
204 match Address::from_str("foo:opt").unwrap_err() {
205 Error::Address(e) => assert_eq!(e, "missing = when parsing key/value"),
206 _ => panic!(),
207 }
208 match Address::from_str("foo:opt=1,opt=2").unwrap_err() {
209 Error::Address(e) => assert_eq!(e, "Key `opt` specified multiple times"),
210 _ => panic!(),
211 }
212 match Address::from_str("tcp:host=localhost").unwrap_err() {
213 Error::Address(e) => assert_eq!(e, "tcp address is missing `port`"),
214 _ => panic!(),
215 }
216 match Address::from_str("tcp:host=localhost,port=32f").unwrap_err() {
217 Error::Address(e) => assert_eq!(e, "invalid tcp `port`"),
218 _ => panic!(),
219 }
220 match Address::from_str("tcp:host=localhost,port=123,family=ipv7").unwrap_err() {
221 Error::Address(e) => assert_eq!(e, "invalid tcp address `family`: ipv7"),
222 _ => panic!(),
223 }
224 match Address::from_str("unix:foo=blah").unwrap_err() {
225 Error::Address(e) => assert_eq!(e, "unix: address is invalid"),
226 _ => panic!(),
227 }
228 #[cfg(target_os = "linux")]
229 match Address::from_str("unix:path=/tmp,abstract=foo").unwrap_err() {
230 Error::Address(e) => {
231 assert_eq!(e, "unix: address is invalid")
232 }
233 _ => panic!(),
234 }
235 assert_eq!(
236 Address::from_str("unix:path=/tmp/dbus-foo").unwrap(),
237 Transport::Unix(Unix::new(UnixSocket::File("/tmp/dbus-foo".into()))).into(),
238 );
239 #[cfg(target_os = "linux")]
240 assert_eq!(
241 Address::from_str("unix:abstract=/tmp/dbus-foo").unwrap(),
242 Transport::Unix(Unix::new(UnixSocket::Abstract("/tmp/dbus-foo".into()))).into(),
243 );
244 let guid = crate::Guid::generate();
245 assert_eq!(
246 Address::from_str(&format!("unix:path=/tmp/dbus-foo,guid={guid}")).unwrap(),
247 Address::from(Transport::Unix(Unix::new(UnixSocket::File(
248 "/tmp/dbus-foo".into()
249 ))))
250 .set_guid(guid.clone())
251 .unwrap(),
252 );
253 assert_eq!(
254 Address::from_str("tcp:host=localhost,port=4142").unwrap(),
255 Transport::Tcp(Tcp::new("localhost", 4142)).into(),
256 );
257 assert_eq!(
258 Address::from_str("tcp:host=localhost,port=4142,family=ipv4").unwrap(),
259 Transport::Tcp(Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv4)))
260 .into(),
261 );
262 assert_eq!(
263 Address::from_str("tcp:host=localhost,port=4142,family=ipv6").unwrap(),
264 Transport::Tcp(Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv6)))
265 .into(),
266 );
267 assert_eq!(
268 Address::from_str("tcp:host=localhost,port=4142,family=ipv6,noncefile=/a/file/path")
269 .unwrap(),
270 Transport::Tcp(
271 Tcp::new("localhost", 4142)
272 .set_family(Some(TcpTransportFamily::Ipv6))
273 .set_nonce_file(Some(b"/a/file/path".to_vec()))
274 )
275 .into(),
276 );
277 assert_eq!(
278 Address::from_str(
279 "nonce-tcp:host=localhost,port=4142,family=ipv6,noncefile=/a/file/path%20to%20file%201234"
280 )
281 .unwrap(),
282 Transport::Tcp(
283 Tcp::new("localhost", 4142)
284 .set_family(Some(TcpTransportFamily::Ipv6))
285 .set_nonce_file(Some(b"/a/file/path to file 1234".to_vec()))
286 ).into()
287 );
288 #[cfg(windows)]
289 assert_eq!(
290 Address::from_str("autolaunch:").unwrap(),
291 Transport::Autolaunch(Autolaunch::new()).into(),
292 );
293 #[cfg(windows)]
294 assert_eq!(
295 Address::from_str("autolaunch:scope=*my_cool_scope*").unwrap(),
296 Transport::Autolaunch(
297 Autolaunch::new()
298 .set_scope(Some(AutolaunchScope::Other("*my_cool_scope*".to_string())))
299 )
300 .into(),
301 );
302 #[cfg(target_os = "macos")]
303 assert_eq!(
304 Address::from_str("launchd:env=my_cool_env_key").unwrap(),
305 Transport::Launchd(Launchd::new("my_cool_env_key")).into(),
306 );
307
308 #[cfg(all(feature = "vsock", not(feature = "tokio")))]
309 assert_eq!(
310 Address::from_str(&format!("vsock:cid=98,port=2934,guid={guid}")).unwrap(),
311 Address::from(Transport::Vsock(super::transport::Vsock::new(98, 2934)))
312 .set_guid(guid)
313 .unwrap(),
314 );
315 assert_eq!(
316 Address::from_str("unix:dir=/some/dir").unwrap(),
317 Transport::Unix(Unix::new(UnixSocket::Dir("/some/dir".into()))).into(),
318 );
319 assert_eq!(
320 Address::from_str("unix:tmpdir=/some/dir").unwrap(),
321 Transport::Unix(Unix::new(UnixSocket::TmpDir("/some/dir".into()))).into(),
322 );
323 }
324
325 #[test]
326 fn stringify_dbus_addresses() {
327 assert_eq!(
328 Address::from(Transport::Unix(Unix::new(UnixSocket::File(
329 "/tmp/dbus-foo".into()
330 ))))
331 .to_string(),
332 "unix:path=/tmp/dbus-foo",
333 );
334 assert_eq!(
335 Address::from(Transport::Unix(Unix::new(UnixSocket::Dir(
336 "/tmp/dbus-foo".into()
337 ))))
338 .to_string(),
339 "unix:dir=/tmp/dbus-foo",
340 );
341 assert_eq!(
342 Address::from(Transport::Unix(Unix::new(UnixSocket::TmpDir(
343 "/tmp/dbus-foo".into()
344 ))))
345 .to_string(),
346 "unix:tmpdir=/tmp/dbus-foo"
347 );
348 #[cfg(target_os = "linux")]
350 assert_eq!(
351 Address::from(Transport::Unix(Unix::new(UnixSocket::Abstract(
352 "/tmp/dbus-foo".into()
353 ))))
354 .to_string(),
355 "unix:abstract=/tmp/dbus-foo"
356 );
357 assert_eq!(
358 Address::from(Transport::Tcp(Tcp::new("localhost", 4142))).to_string(),
359 "tcp:host=localhost,port=4142"
360 );
361 assert_eq!(
362 Address::from(Transport::Tcp(
363 Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv4))
364 ))
365 .to_string(),
366 "tcp:host=localhost,port=4142,family=ipv4"
367 );
368 assert_eq!(
369 Address::from(Transport::Tcp(
370 Tcp::new("localhost", 4142).set_family(Some(TcpTransportFamily::Ipv6))
371 ))
372 .to_string(),
373 "tcp:host=localhost,port=4142,family=ipv6"
374 );
375 assert_eq!(
376 Address::from(Transport::Tcp(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
402 #[cfg(all(feature = "vsock", not(feature = "tokio")))]
403 {
404 let guid = crate::Guid::generate();
405 assert_eq!(
406 Address::from(Transport::Vsock(super::transport::Vsock::new(98, 2934)))
407 .set_guid(guid.clone())
408 .unwrap()
409 .to_string(),
410 format!("vsock:cid=98,port=2934,guid={guid}"),
411 );
412 }
413 }
414
415 #[test]
416 fn connect_tcp() {
417 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
418 let port = listener.local_addr().unwrap().port();
419 let addr = Address::from_str(&format!("tcp:host=localhost,port={port}")).unwrap();
420 crate::utils::block_on(async { addr.connect().await }).unwrap();
421 }
422
423 #[test]
424 fn connect_nonce_tcp() {
425 struct PercentEncoded<'a>(&'a [u8]);
426
427 impl std::fmt::Display for PercentEncoded<'_> {
428 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
429 super::transport::encode_percents(f, self.0)
430 }
431 }
432
433 use std::io::Write;
434
435 const TEST_COOKIE: &[u8] = b"VERILY SECRETIVE";
436
437 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
438 let port = listener.local_addr().unwrap().port();
439
440 let mut cookie = tempfile::NamedTempFile::new().unwrap();
441 cookie.as_file_mut().write_all(TEST_COOKIE).unwrap();
442
443 let encoded_path = format!(
444 "{}",
445 PercentEncoded(cookie.path().to_str().unwrap().as_ref())
446 );
447
448 let addr = Address::from_str(&format!(
449 "nonce-tcp:host=localhost,port={port},noncefile={encoded_path}"
450 ))
451 .unwrap();
452
453 let (sender, receiver) = std::sync::mpsc::sync_channel(1);
454
455 std::thread::spawn(move || {
456 use std::io::Read;
457
458 let mut client = listener.incoming().next().unwrap().unwrap();
459
460 let mut buf = [0u8; 16];
461 client.read_exact(&mut buf).unwrap();
462
463 sender.send(buf == TEST_COOKIE).unwrap();
464 });
465
466 crate::utils::block_on(addr.connect()).unwrap();
467
468 let saw_cookie = receiver
469 .recv_timeout(std::time::Duration::from_millis(100))
470 .expect("nonce file content hasn't been received by server thread in time");
471
472 assert!(
473 saw_cookie,
474 "nonce file content has been received, but was invalid"
475 );
476 }
477}