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 | |
12 | pub mod transport; |
13 | |
14 | use crate::{Error, Guid, OwnedGuid, Result}; |
15 | #[cfg (all(unix, not(target_os = "macos" )))] |
16 | use nix::unistd::Uid; |
17 | use std::{collections::HashMap, env, str::FromStr}; |
18 | |
19 | use std::fmt::{Display, Formatter}; |
20 | |
21 | use self::transport::Stream; |
22 | pub use self::transport::Transport; |
23 | |
24 | /// A bus address |
25 | #[derive (Clone, Debug, PartialEq, Eq)] |
26 | #[non_exhaustive ] |
27 | pub struct Address { |
28 | guid: Option<OwnedGuid>, |
29 | transport: Transport, |
30 | } |
31 | |
32 | impl 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 | self.transport.connect().await |
60 | } |
61 | |
62 | /// Get the address for session socket respecting the DBUS_SESSION_BUS_ADDRESS environment |
63 | /// variable. If we don't recognize the value (or it's not set) we fall back to |
64 | /// $XDG_RUNTIME_DIR/bus |
65 | 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 | /// Get the address for system bus respecting the DBUS_SYSTEM_BUS_ADDRESS environment |
88 | /// variable. If we don't recognize the value (or it's not set) we fall back to |
89 | /// /var/run/dbus/system_bus_socket |
90 | 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 | /// The GUID for this address, if known. |
107 | pub fn guid(&self) -> Option<&Guid<'_>> { |
108 | self.guid.as_ref().map(|guid| guid.inner()) |
109 | } |
110 | } |
111 | |
112 | impl Display for Address { |
113 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
114 | self.transport.fmt(f)?; |
115 | |
116 | if let Some(guid: &OwnedGuid) = &self.guid { |
117 | write!(f, ",guid= {}" , guid)?; |
118 | } |
119 | |
120 | Ok(()) |
121 | } |
122 | } |
123 | |
124 | impl FromStr for Address { |
125 | type Err = Error; |
126 | |
127 | /// Parse the transport part of a D-Bus address into a `Transport`. |
128 | 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 | |
163 | impl 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 | |
171 | impl From<Transport> for Address { |
172 | fn from(transport: Transport) -> Self { |
173 | Self::new(transport) |
174 | } |
175 | } |
176 | |
177 | #[cfg (test)] |
178 | mod 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 | // FIXME: figure out how to handle abstract on Windows |
349 | #[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 | } |
478 | |