1 | #[cfg (target_os = "macos" )] |
2 | use crate::process::run; |
3 | #[cfg (windows)] |
4 | use crate::win32::windows_autolaunch_bus_address; |
5 | use crate::{Error, Result}; |
6 | #[cfg (not(feature = "tokio" ))] |
7 | use async_io::Async; |
8 | #[cfg (all(unix, not(target_os = "macos" )))] |
9 | use nix::unistd::Uid; |
10 | #[cfg (not(feature = "tokio" ))] |
11 | use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; |
12 | #[cfg (all(unix, not(feature = "tokio" )))] |
13 | use std::os::unix::net::UnixStream; |
14 | use std::{collections::HashMap, convert::TryFrom, env, str::FromStr}; |
15 | #[cfg (feature = "tokio" )] |
16 | use tokio::net::TcpStream; |
17 | #[cfg (all(unix, feature = "tokio" ))] |
18 | use tokio::net::UnixStream; |
19 | #[cfg (feature = "tokio-vsock" )] |
20 | use tokio_vsock::VsockStream; |
21 | #[cfg (all(windows, not(feature = "tokio" )))] |
22 | use uds_windows::UnixStream; |
23 | #[cfg (all(feature = "vsock" , not(feature = "tokio" )))] |
24 | use vsock::VsockStream; |
25 | |
26 | use std::{ |
27 | ffi::OsString, |
28 | fmt::{Display, Formatter}, |
29 | str::from_utf8_unchecked, |
30 | }; |
31 | |
32 | /// A `tcp:` address family. |
33 | #[derive (Copy, Clone, Debug, PartialEq, Eq)] |
34 | pub enum TcpAddressFamily { |
35 | Ipv4, |
36 | Ipv6, |
37 | } |
38 | |
39 | /// A `tcp:` D-Bus address. |
40 | #[derive (Clone, Debug, PartialEq, Eq)] |
41 | pub struct TcpAddress { |
42 | pub(crate) host: String, |
43 | pub(crate) bind: Option<String>, |
44 | pub(crate) port: u16, |
45 | pub(crate) family: Option<TcpAddressFamily>, |
46 | } |
47 | |
48 | impl TcpAddress { |
49 | /// Returns the `tcp:` address `host` value. |
50 | pub fn host(&self) -> &str { |
51 | &self.host |
52 | } |
53 | |
54 | /// Returns the `tcp:` address `bind` value. |
55 | pub fn bind(&self) -> Option<&str> { |
56 | self.bind.as_deref() |
57 | } |
58 | |
59 | /// Returns the `tcp:` address `port` value. |
60 | pub fn port(&self) -> u16 { |
61 | self.port |
62 | } |
63 | |
64 | /// Returns the `tcp:` address `family` value. |
65 | pub fn family(&self) -> Option<TcpAddressFamily> { |
66 | self.family |
67 | } |
68 | |
69 | // Helper for FromStr |
70 | fn from_tcp(opts: HashMap<&str, &str>) -> Result<Self> { |
71 | let bind = None; |
72 | if opts.contains_key("bind" ) { |
73 | return Err(Error::Address("`bind` isn't yet supported" .into())); |
74 | } |
75 | |
76 | let host = opts |
77 | .get("host" ) |
78 | .ok_or_else(|| Error::Address("tcp address is missing `host`" .into()))? |
79 | .to_string(); |
80 | let port = opts |
81 | .get("port" ) |
82 | .ok_or_else(|| Error::Address("tcp address is missing `port`" .into()))?; |
83 | let port = port |
84 | .parse::<u16>() |
85 | .map_err(|_| Error::Address("invalid tcp `port`" .into()))?; |
86 | let family = opts |
87 | .get("family" ) |
88 | .map(|f| TcpAddressFamily::from_str(f)) |
89 | .transpose()?; |
90 | |
91 | Ok(Self { |
92 | host, |
93 | bind, |
94 | port, |
95 | family, |
96 | }) |
97 | } |
98 | |
99 | fn write_options(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
100 | f.write_str("host=" )?; |
101 | |
102 | encode_percents(f, self.host.as_ref())?; |
103 | |
104 | write!(f, ",port= {}" , self.port)?; |
105 | |
106 | if let Some(bind) = &self.bind { |
107 | f.write_str(",bind=" )?; |
108 | encode_percents(f, bind.as_ref())?; |
109 | } |
110 | |
111 | if let Some(family) = &self.family { |
112 | write!(f, ",family= {family}" )?; |
113 | } |
114 | |
115 | Ok(()) |
116 | } |
117 | } |
118 | |
119 | #[cfg (any( |
120 | all(feature = "vsock" , not(feature = "tokio" )), |
121 | feature = "tokio-vsock" |
122 | ))] |
123 | /// A `tcp:` D-Bus address. |
124 | #[derive (Clone, Debug, PartialEq, Eq)] |
125 | pub struct VsockAddress { |
126 | pub(crate) cid: u32, |
127 | pub(crate) port: u32, |
128 | } |
129 | |
130 | #[cfg (any( |
131 | all(feature = "vsock" , not(feature = "tokio" )), |
132 | feature = "tokio-vsock" |
133 | ))] |
134 | impl VsockAddress { |
135 | /// Create a new VSOCK address. |
136 | pub fn new(cid: u32, port: u32) -> Self { |
137 | Self { cid, port } |
138 | } |
139 | } |
140 | |
141 | /// A bus address |
142 | #[derive (Clone, Debug, PartialEq, Eq)] |
143 | #[non_exhaustive ] |
144 | pub enum Address { |
145 | /// A path on the filesystem |
146 | Unix(OsString), |
147 | /// TCP address details |
148 | Tcp(TcpAddress), |
149 | /// TCP address details with nonce file path |
150 | NonceTcp { |
151 | addr: TcpAddress, |
152 | nonce_file: Vec<u8>, |
153 | }, |
154 | /// Autolaunch address with optional scope |
155 | Autolaunch(Option<String>), |
156 | /// Launchd address with a required env key |
157 | Launchd(String), |
158 | #[cfg (any( |
159 | all(feature = "vsock" , not(feature = "tokio" )), |
160 | feature = "tokio-vsock" |
161 | ))] |
162 | /// VSOCK address |
163 | /// |
164 | /// This variant is only available when either `vsock` or `tokio-vsock` feature is enabled. The |
165 | /// type of `stream` is `vsock::VsockStream` with `vsock` feature and |
166 | /// `tokio_vsock::VsockStream` with `tokio-vsock` feature. |
167 | Vsock(VsockAddress), |
168 | } |
169 | |
170 | #[cfg (not(feature = "tokio" ))] |
171 | #[derive (Debug)] |
172 | pub(crate) enum Stream { |
173 | Unix(Async<UnixStream>), |
174 | Tcp(Async<TcpStream>), |
175 | #[cfg (feature = "vsock" )] |
176 | Vsock(Async<VsockStream>), |
177 | } |
178 | |
179 | #[cfg (feature = "tokio" )] |
180 | #[derive (Debug)] |
181 | pub(crate) enum Stream { |
182 | #[cfg (unix)] |
183 | Unix(UnixStream), |
184 | Tcp(TcpStream), |
185 | #[cfg (feature = "tokio-vsock" )] |
186 | Vsock(VsockStream), |
187 | } |
188 | |
189 | #[cfg (not(feature = "tokio" ))] |
190 | async fn connect_tcp(addr: TcpAddress) -> Result<Async<TcpStream>> { |
191 | let addrs = crate::Task::spawn_blocking( |
192 | move || -> Result<Vec<SocketAddr>> { |
193 | let addrs = (addr.host(), addr.port()).to_socket_addrs()?.filter(|a| { |
194 | if let Some(family) = addr.family() { |
195 | if family == TcpAddressFamily::Ipv4 { |
196 | a.is_ipv4() |
197 | } else { |
198 | a.is_ipv6() |
199 | } |
200 | } else { |
201 | true |
202 | } |
203 | }); |
204 | Ok(addrs.collect()) |
205 | }, |
206 | "connect tcp" , |
207 | ) |
208 | .await |
209 | .map_err(|e| Error::Address(format!("Failed to receive TCP addresses: {e}" )))?; |
210 | |
211 | // we could attempt connections in parallel? |
212 | let mut last_err = Error::Address("Failed to connect" .into()); |
213 | for addr in addrs { |
214 | match Async::<TcpStream>::connect(addr).await { |
215 | Ok(stream) => return Ok(stream), |
216 | Err(e) => last_err = e.into(), |
217 | } |
218 | } |
219 | |
220 | Err(last_err) |
221 | } |
222 | |
223 | #[cfg (feature = "tokio" )] |
224 | async fn connect_tcp(addr: TcpAddress) -> Result<TcpStream> { |
225 | TcpStream::connect((addr.host(), addr.port())) |
226 | .await |
227 | .map_err(|e| Error::InputOutput(e.into())) |
228 | } |
229 | |
230 | #[cfg (target_os = "macos" )] |
231 | pub(crate) async fn macos_launchd_bus_address(env_key: &str) -> Result<Address> { |
232 | let output = run("launchctl" , ["getenv" , env_key]) |
233 | .await |
234 | .expect("failed to wait on launchctl output" ); |
235 | |
236 | if !output.status.success() { |
237 | return Err(crate::Error::Address(format!( |
238 | "launchctl terminated with code: {}" , |
239 | output.status |
240 | ))); |
241 | } |
242 | |
243 | let addr = String::from_utf8(output.stdout).map_err(|e| { |
244 | crate::Error::Address(format!("Unable to parse launchctl output as UTF-8: {}" , e)) |
245 | })?; |
246 | |
247 | format!("unix:path= {}" , addr.trim()).parse() |
248 | } |
249 | |
250 | impl Address { |
251 | #[async_recursion::async_recursion ] |
252 | pub(crate) async fn connect(self) -> Result<Stream> { |
253 | match self { |
254 | Address::Unix(p) => { |
255 | #[cfg (not(feature = "tokio" ))] |
256 | { |
257 | #[cfg (windows)] |
258 | { |
259 | let stream = crate::Task::spawn_blocking( |
260 | move || UnixStream::connect(p), |
261 | "unix stream connection" , |
262 | ) |
263 | .await?; |
264 | Async::new(stream) |
265 | .map(Stream::Unix) |
266 | .map_err(|e| Error::InputOutput(e.into())) |
267 | } |
268 | |
269 | #[cfg (not(windows))] |
270 | { |
271 | Async::<UnixStream>::connect(p) |
272 | .await |
273 | .map(Stream::Unix) |
274 | .map_err(|e| Error::InputOutput(e.into())) |
275 | } |
276 | } |
277 | |
278 | #[cfg (feature = "tokio" )] |
279 | { |
280 | #[cfg (unix)] |
281 | { |
282 | UnixStream::connect(p) |
283 | .await |
284 | .map(Stream::Unix) |
285 | .map_err(|e| Error::InputOutput(e.into())) |
286 | } |
287 | |
288 | #[cfg (not(unix))] |
289 | { |
290 | let _ = p; |
291 | Err(Error::Unsupported) |
292 | } |
293 | } |
294 | } |
295 | |
296 | #[cfg (all(feature = "vsock" , not(feature = "tokio" )))] |
297 | Address::Vsock(addr) => { |
298 | let stream = VsockStream::connect_with_cid_port(addr.cid, addr.port)?; |
299 | Async::new(stream).map(Stream::Vsock).map_err(Into::into) |
300 | } |
301 | |
302 | #[cfg (feature = "tokio-vsock" )] |
303 | Address::Vsock(addr) => VsockStream::connect(addr.cid, addr.port) |
304 | .await |
305 | .map(Stream::Vsock) |
306 | .map_err(Into::into), |
307 | |
308 | Address::Tcp(addr) => connect_tcp(addr).await.map(Stream::Tcp), |
309 | |
310 | Address::NonceTcp { addr, nonce_file } => { |
311 | let mut stream = connect_tcp(addr).await?; |
312 | |
313 | #[cfg (unix)] |
314 | let nonce_file = { |
315 | use std::os::unix::ffi::OsStrExt; |
316 | std::ffi::OsStr::from_bytes(&nonce_file) |
317 | }; |
318 | |
319 | #[cfg (windows)] |
320 | let nonce_file = std::str::from_utf8(&nonce_file) |
321 | .map_err(|_| Error::Address("nonce file path is invalid UTF-8" .to_owned()))?; |
322 | |
323 | #[cfg (not(feature = "tokio" ))] |
324 | { |
325 | let nonce = std::fs::read(nonce_file)?; |
326 | let mut nonce = &nonce[..]; |
327 | |
328 | while !nonce.is_empty() { |
329 | let len = stream |
330 | .write_with_mut(|s| std::io::Write::write(s, nonce)) |
331 | .await?; |
332 | nonce = &nonce[len..]; |
333 | } |
334 | } |
335 | |
336 | #[cfg (feature = "tokio" )] |
337 | { |
338 | let nonce = tokio::fs::read(nonce_file).await?; |
339 | tokio::io::AsyncWriteExt::write_all(&mut stream, &nonce).await?; |
340 | } |
341 | |
342 | Ok(Stream::Tcp(stream)) |
343 | } |
344 | |
345 | #[cfg (not(windows))] |
346 | Address::Autolaunch(_) => Err(Error::Address( |
347 | "Autolaunch addresses are only supported on Windows" .to_owned(), |
348 | )), |
349 | |
350 | #[cfg (windows)] |
351 | Address::Autolaunch(Some(_)) => Err(Error::Address( |
352 | "Autolaunch scopes are currently unsupported" .to_owned(), |
353 | )), |
354 | |
355 | #[cfg (windows)] |
356 | Address::Autolaunch(None) => { |
357 | let addr = windows_autolaunch_bus_address()?; |
358 | addr.connect().await |
359 | } |
360 | |
361 | #[cfg (not(target_os = "macos" ))] |
362 | Address::Launchd(_) => Err(Error::Address( |
363 | "Launchd addresses are only supported on macOS" .to_owned(), |
364 | )), |
365 | |
366 | #[cfg (target_os = "macos" )] |
367 | Address::Launchd(env) => { |
368 | let addr = macos_launchd_bus_address(&env).await?; |
369 | addr.connect().await |
370 | } |
371 | } |
372 | } |
373 | |
374 | /// Get the address for session socket respecting the DBUS_SESSION_BUS_ADDRESS environment |
375 | /// variable. If we don't recognize the value (or it's not set) we fall back to |
376 | /// $XDG_RUNTIME_DIR/bus |
377 | pub fn session() -> Result<Self> { |
378 | match env::var("DBUS_SESSION_BUS_ADDRESS" ) { |
379 | Ok(val) => Self::from_str(&val), |
380 | _ => { |
381 | #[cfg (windows)] |
382 | { |
383 | #[cfg (feature = "windows-gdbus" )] |
384 | return Self::from_str("autolaunch:" ); |
385 | |
386 | #[cfg (not(feature = "windows-gdbus" ))] |
387 | return Self::from_str("autolaunch:scope=*user" ); |
388 | } |
389 | |
390 | #[cfg (all(unix, not(target_os = "macos" )))] |
391 | { |
392 | let runtime_dir = env::var("XDG_RUNTIME_DIR" ) |
393 | .unwrap_or_else(|_| format!("/run/user/ {}" , Uid::effective())); |
394 | let path = format!("unix:path= {runtime_dir}/bus" ); |
395 | |
396 | Self::from_str(&path) |
397 | } |
398 | |
399 | #[cfg (target_os = "macos" )] |
400 | return Self::from_str("launchd:env=DBUS_LAUNCHD_SESSION_BUS_SOCKET" ); |
401 | } |
402 | } |
403 | } |
404 | |
405 | /// Get the address for system bus respecting the DBUS_SYSTEM_BUS_ADDRESS environment |
406 | /// variable. If we don't recognize the value (or it's not set) we fall back to |
407 | /// /var/run/dbus/system_bus_socket |
408 | pub fn system() -> Result<Self> { |
409 | match env::var("DBUS_SYSTEM_BUS_ADDRESS" ) { |
410 | Ok(val) => Self::from_str(&val), |
411 | _ => { |
412 | #[cfg (all(unix, not(target_os = "macos" )))] |
413 | return Self::from_str("unix:path=/var/run/dbus/system_bus_socket" ); |
414 | |
415 | #[cfg (windows)] |
416 | return Self::from_str("autolaunch:" ); |
417 | |
418 | #[cfg (target_os = "macos" )] |
419 | return Self::from_str("launchd:env=DBUS_LAUNCHD_SESSION_BUS_SOCKET" ); |
420 | } |
421 | } |
422 | } |
423 | |
424 | // Helper for FromStr |
425 | #[cfg (any(unix, not(feature = "tokio" )))] |
426 | fn from_unix(opts: HashMap<&str, &str>) -> Result<Self> { |
427 | let path = if let Some(abs) = opts.get("abstract" ) { |
428 | if opts.get("path" ).is_some() { |
429 | return Err(Error::Address( |
430 | "`path` and `abstract` cannot be specified together" .into(), |
431 | )); |
432 | } |
433 | let mut s = OsString::from(" \0" ); |
434 | s.push(abs); |
435 | s |
436 | } else if let Some(path) = opts.get("path" ) { |
437 | OsString::from(path) |
438 | } else { |
439 | return Err(Error::Address( |
440 | "unix address is missing path or abstract" .to_owned(), |
441 | )); |
442 | }; |
443 | |
444 | Ok(Address::Unix(path)) |
445 | } |
446 | |
447 | #[cfg (all(feature = "vsock" , not(feature = "tokio" )))] |
448 | fn from_vsock(opts: HashMap<&str, &str>) -> Result<Self> { |
449 | let cid = opts |
450 | .get("cid" ) |
451 | .ok_or_else(|| Error::Address("VSOCK address is missing cid=" .into()))?; |
452 | let cid = cid |
453 | .parse::<u32>() |
454 | .map_err(|e| Error::Address(format!("Failed to parse VSOCK cid ` {}`: {}" , cid, e)))?; |
455 | let port = opts |
456 | .get("port" ) |
457 | .ok_or_else(|| Error::Address("VSOCK address is missing port=" .into()))?; |
458 | let port = port |
459 | .parse::<u32>() |
460 | .map_err(|e| Error::Address(format!("Failed to parse VSOCK port ` {}`: {}" , port, e)))?; |
461 | |
462 | Ok(Address::Vsock(VsockAddress { cid, port })) |
463 | } |
464 | } |
465 | |
466 | impl FromStr for TcpAddressFamily { |
467 | type Err = Error; |
468 | |
469 | fn from_str(family: &str) -> Result<Self> { |
470 | match family { |
471 | "ipv4" => Ok(Self::Ipv4), |
472 | "ipv6" => Ok(Self::Ipv6), |
473 | _ => Err(Error::Address(format!( |
474 | "invalid tcp address `family`: {family}" |
475 | ))), |
476 | } |
477 | } |
478 | } |
479 | |
480 | impl Display for TcpAddressFamily { |
481 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
482 | match self { |
483 | Self::Ipv4 => write!(f, "ipv4" ), |
484 | Self::Ipv6 => write!(f, "ipv6" ), |
485 | } |
486 | } |
487 | } |
488 | |
489 | fn decode_hex(c: char) -> Result<u8> { |
490 | match c { |
491 | '0' ..='9' => Ok(c as u8 - b'0' ), |
492 | 'a' ..='f' => Ok(c as u8 - b'a' + 10), |
493 | 'A' ..='F' => Ok(c as u8 - b'A' + 10), |
494 | |
495 | _ => Err(Error::Address( |
496 | "invalid hexadecimal character in percent-encoded sequence" .to_owned(), |
497 | )), |
498 | } |
499 | } |
500 | |
501 | fn decode_percents(value: &str) -> Result<Vec<u8>> { |
502 | let mut iter: Chars<'_> = value.chars(); |
503 | let mut decoded: Vec = Vec::new(); |
504 | |
505 | while let Some(c: char) = iter.next() { |
506 | if matches!(c, '-' | '0' ..='9' | 'A' ..='Z' | 'a' ..='z' | '_' | '/' | '.' | ' \\' | '*' ) { |
507 | decoded.push(c as u8) |
508 | } else if c == '%' { |
509 | decoded.push( |
510 | decode_hex(iter.next().ok_or_else(|| { |
511 | Error::Address("incomplete percent-encoded sequence" .to_owned()) |
512 | })?)? |
513 | << 4 |
514 | | decode_hex(iter.next().ok_or_else(|| { |
515 | Error::Address("incomplete percent-encoded sequence" .to_owned()) |
516 | })?)?, |
517 | ); |
518 | } else { |
519 | return Err(Error::Address("Invalid character in address" .to_owned())); |
520 | } |
521 | } |
522 | |
523 | Ok(decoded) |
524 | } |
525 | |
526 | fn encode_percents(f: &mut Formatter<'_>, mut value: &[u8]) -> std::fmt::Result { |
527 | const LOOKUP: &str = "\ |
528 | %00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f\ |
529 | %10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f\ |
530 | %20%21%22%23%24%25%26%27%28%29%2a%2b%2c%2d%2e%2f\ |
531 | %30%31%32%33%34%35%36%37%38%39%3a%3b%3c%3d%3e%3f\ |
532 | %40%41%42%43%44%45%46%47%48%49%4a%4b%4c%4d%4e%4f\ |
533 | %50%51%52%53%54%55%56%57%58%59%5a%5b%5c%5d%5e%5f\ |
534 | %60%61%62%63%64%65%66%67%68%69%6a%6b%6c%6d%6e%6f\ |
535 | %70%71%72%73%74%75%76%77%78%79%7a%7b%7c%7d%7e%7f\ |
536 | %80%81%82%83%84%85%86%87%88%89%8a%8b%8c%8d%8e%8f\ |
537 | %90%91%92%93%94%95%96%97%98%99%9a%9b%9c%9d%9e%9f\ |
538 | %a0%a1%a2%a3%a4%a5%a6%a7%a8%a9%aa%ab%ac%ad%ae%af\ |
539 | %b0%b1%b2%b3%b4%b5%b6%b7%b8%b9%ba%bb%bc%bd%be%bf\ |
540 | %c0%c1%c2%c3%c4%c5%c6%c7%c8%c9%ca%cb%cc%cd%ce%cf\ |
541 | %d0%d1%d2%d3%d4%d5%d6%d7%d8%d9%da%db%dc%dd%de%df\ |
542 | %e0%e1%e2%e3%e4%e5%e6%e7%e8%e9%ea%eb%ec%ed%ee%ef\ |
543 | %f0%f1%f2%f3%f4%f5%f6%f7%f8%f9%fa%fb%fc%fd%fe%ff" ; |
544 | |
545 | loop { |
546 | let pos = value.iter().position( |
547 | |c| !matches!(c, b'-' | b'0' ..=b'9' | b'A' ..=b'Z' | b'a' ..=b'z' | b'_' | b'/' | b'.' | b' \\' | b'*' ), |
548 | ); |
549 | |
550 | if let Some(pos) = pos { |
551 | // SAFETY: The above `position()` call made sure that only ASCII chars are in the string |
552 | // up to `pos` |
553 | f.write_str(unsafe { from_utf8_unchecked(&value[..pos]) })?; |
554 | |
555 | let c = value[pos]; |
556 | value = &value[pos + 1..]; |
557 | |
558 | let pos = c as usize * 3; |
559 | f.write_str(&LOOKUP[pos..pos + 3])?; |
560 | } else { |
561 | // SAFETY: The above `position()` call made sure that only ASCII chars are in the rest |
562 | // of the string |
563 | f.write_str(unsafe { from_utf8_unchecked(value) })?; |
564 | return Ok(()); |
565 | } |
566 | } |
567 | } |
568 | |
569 | impl Display for Address { |
570 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
571 | match self { |
572 | Self::Tcp(addr) => { |
573 | f.write_str("tcp:" )?; |
574 | addr.write_options(f)?; |
575 | } |
576 | |
577 | Self::NonceTcp { addr, nonce_file } => { |
578 | f.write_str("nonce-tcp:noncefile=" )?; |
579 | encode_percents(f, nonce_file)?; |
580 | f.write_str("," )?; |
581 | addr.write_options(f)?; |
582 | } |
583 | |
584 | Self::Unix(path) => { |
585 | #[cfg (unix)] |
586 | { |
587 | use std::os::unix::ffi::OsStrExt; |
588 | f.write_str("unix:path=" )?; |
589 | encode_percents(f, path.as_bytes())?; |
590 | } |
591 | |
592 | #[cfg (windows)] |
593 | write!(f, "unix:path= {}" , path.to_str().ok_or(std::fmt::Error)?)?; |
594 | } |
595 | |
596 | #[cfg (any( |
597 | all(feature = "vsock" , not(feature = "tokio" )), |
598 | feature = "tokio-vsock" |
599 | ))] |
600 | Self::Vsock(addr) => { |
601 | write!(f, "vsock:cid= {},port= {}" , addr.cid, addr.port)?; |
602 | } |
603 | |
604 | Self::Autolaunch(scope) => { |
605 | write!(f, "autolaunch:" )?; |
606 | if let Some(scope) = scope { |
607 | write!(f, "scope= {scope}" )?; |
608 | } |
609 | } |
610 | |
611 | Self::Launchd(env) => { |
612 | write!(f, "launchd:env= {}" , env)?; |
613 | } |
614 | } |
615 | |
616 | Ok(()) |
617 | } |
618 | } |
619 | |
620 | impl FromStr for Address { |
621 | type Err = Error; |
622 | |
623 | /// Parse a D-BUS address and return its path if we recognize it |
624 | fn from_str(address: &str) -> Result<Self> { |
625 | let col = address |
626 | .find(':' ) |
627 | .ok_or_else(|| Error::Address("address has no colon" .to_owned()))?; |
628 | let transport = &address[..col]; |
629 | let mut options = HashMap::new(); |
630 | |
631 | if address.len() > col + 1 { |
632 | for kv in address[col + 1..].split(',' ) { |
633 | let (k, v) = match kv.find('=' ) { |
634 | Some(eq) => (&kv[..eq], &kv[eq + 1..]), |
635 | None => { |
636 | return Err(Error::Address( |
637 | "missing = when parsing key/value" .to_owned(), |
638 | )) |
639 | } |
640 | }; |
641 | if options.insert(k, v).is_some() { |
642 | return Err(Error::Address(format!( |
643 | "Key ` {k}` specified multiple times" |
644 | ))); |
645 | } |
646 | } |
647 | } |
648 | |
649 | match transport { |
650 | #[cfg (any(unix, not(feature = "tokio" )))] |
651 | "unix" => Self::from_unix(options), |
652 | "tcp" => TcpAddress::from_tcp(options).map(Self::Tcp), |
653 | |
654 | "nonce-tcp" => Ok(Self::NonceTcp { |
655 | nonce_file: decode_percents( |
656 | options |
657 | .get("noncefile" ) |
658 | .ok_or_else(|| Error::Address("missing nonce file parameter" .into()))?, |
659 | )?, |
660 | addr: TcpAddress::from_tcp(options)?, |
661 | }), |
662 | #[cfg (all(feature = "vsock" , not(feature = "tokio" )))] |
663 | "vsock" => Self::from_vsock(options), |
664 | "autolaunch" => Ok(Self::Autolaunch( |
665 | options |
666 | .get("scope" ) |
667 | .map(|scope| -> Result<_> { |
668 | String::from_utf8(decode_percents(scope)?).map_err(|_| { |
669 | Error::Address("autolaunch scope is not valid UTF-8" .to_owned()) |
670 | }) |
671 | }) |
672 | .transpose()?, |
673 | )), |
674 | "launchd" => Ok(Self::Launchd( |
675 | options |
676 | .get("env" ) |
677 | .ok_or_else(|| Error::Address("missing env key" .into()))? |
678 | .to_string(), |
679 | )), |
680 | |
681 | _ => Err(Error::Address(format!( |
682 | "unsupported transport ' {transport}'" |
683 | ))), |
684 | } |
685 | } |
686 | } |
687 | |
688 | impl TryFrom<&str> for Address { |
689 | type Error = Error; |
690 | |
691 | fn try_from(value: &str) -> Result<Self> { |
692 | Self::from_str(value) |
693 | } |
694 | } |
695 | |
696 | #[cfg (test)] |
697 | mod tests { |
698 | use super::Address; |
699 | use crate::{Error, TcpAddress, TcpAddressFamily}; |
700 | use std::str::FromStr; |
701 | use test_log::test; |
702 | |
703 | #[test ] |
704 | fn parse_dbus_addresses() { |
705 | match Address::from_str("" ).unwrap_err() { |
706 | Error::Address(e) => assert_eq!(e, "address has no colon" ), |
707 | _ => panic!(), |
708 | } |
709 | match Address::from_str("foo" ).unwrap_err() { |
710 | Error::Address(e) => assert_eq!(e, "address has no colon" ), |
711 | _ => panic!(), |
712 | } |
713 | match Address::from_str("foo:opt" ).unwrap_err() { |
714 | Error::Address(e) => assert_eq!(e, "missing = when parsing key/value" ), |
715 | _ => panic!(), |
716 | } |
717 | match Address::from_str("foo:opt=1,opt=2" ).unwrap_err() { |
718 | Error::Address(e) => assert_eq!(e, "Key `opt` specified multiple times" ), |
719 | _ => panic!(), |
720 | } |
721 | match Address::from_str("tcp:host=localhost" ).unwrap_err() { |
722 | Error::Address(e) => assert_eq!(e, "tcp address is missing `port`" ), |
723 | _ => panic!(), |
724 | } |
725 | match Address::from_str("tcp:host=localhost,port=32f" ).unwrap_err() { |
726 | Error::Address(e) => assert_eq!(e, "invalid tcp `port`" ), |
727 | _ => panic!(), |
728 | } |
729 | match Address::from_str("tcp:host=localhost,port=123,family=ipv7" ).unwrap_err() { |
730 | Error::Address(e) => assert_eq!(e, "invalid tcp address `family`: ipv7" ), |
731 | _ => panic!(), |
732 | } |
733 | match Address::from_str("unix:foo=blah" ).unwrap_err() { |
734 | Error::Address(e) => assert_eq!(e, "unix address is missing path or abstract" ), |
735 | _ => panic!(), |
736 | } |
737 | match Address::from_str("unix:path=/tmp,abstract=foo" ).unwrap_err() { |
738 | Error::Address(e) => { |
739 | assert_eq!(e, "`path` and `abstract` cannot be specified together" ) |
740 | } |
741 | _ => panic!(), |
742 | } |
743 | assert_eq!( |
744 | Address::Unix("/tmp/dbus-foo" .into()), |
745 | Address::from_str("unix:path=/tmp/dbus-foo" ).unwrap() |
746 | ); |
747 | assert_eq!( |
748 | Address::Unix("/tmp/dbus-foo" .into()), |
749 | Address::from_str("unix:path=/tmp/dbus-foo,guid=123" ).unwrap() |
750 | ); |
751 | assert_eq!( |
752 | Address::Tcp(TcpAddress { |
753 | host: "localhost" .into(), |
754 | port: 4142, |
755 | bind: None, |
756 | family: None |
757 | }), |
758 | Address::from_str("tcp:host=localhost,port=4142" ).unwrap() |
759 | ); |
760 | assert_eq!( |
761 | Address::Tcp(TcpAddress { |
762 | host: "localhost" .into(), |
763 | port: 4142, |
764 | bind: None, |
765 | family: Some(TcpAddressFamily::Ipv4) |
766 | }), |
767 | Address::from_str("tcp:host=localhost,port=4142,family=ipv4" ).unwrap() |
768 | ); |
769 | assert_eq!( |
770 | Address::Tcp(TcpAddress { |
771 | host: "localhost" .into(), |
772 | port: 4142, |
773 | bind: None, |
774 | family: Some(TcpAddressFamily::Ipv6) |
775 | }), |
776 | Address::from_str("tcp:host=localhost,port=4142,family=ipv6" ).unwrap() |
777 | ); |
778 | assert_eq!( |
779 | Address::Tcp(TcpAddress { |
780 | host: "localhost" .into(), |
781 | port: 4142, |
782 | bind: None, |
783 | family: Some(TcpAddressFamily::Ipv6) |
784 | }), |
785 | Address::from_str("tcp:host=localhost,port=4142,family=ipv6,noncefile=/a/file/path" ) |
786 | .unwrap() |
787 | ); |
788 | assert_eq!( |
789 | Address::NonceTcp { |
790 | addr: TcpAddress { |
791 | host: "localhost" .into(), |
792 | port: 4142, |
793 | bind: None, |
794 | family: Some(TcpAddressFamily::Ipv6), |
795 | }, |
796 | nonce_file: b"/a/file/path to file 1234" .to_vec() |
797 | }, |
798 | Address::from_str( |
799 | "nonce-tcp:host=localhost,port=4142,family=ipv6,noncefile=/a/file/path%20to%20file%201234" |
800 | ) |
801 | .unwrap() |
802 | ); |
803 | assert_eq!( |
804 | Address::Autolaunch(None), |
805 | Address::from_str("autolaunch:" ).unwrap() |
806 | ); |
807 | assert_eq!( |
808 | Address::Autolaunch(Some("*my_cool_scope*" .to_owned())), |
809 | Address::from_str("autolaunch:scope=*my_cool_scope*" ).unwrap() |
810 | ); |
811 | assert_eq!( |
812 | Address::Launchd("my_cool_env_key" .to_owned()), |
813 | Address::from_str("launchd:env=my_cool_env_key" ).unwrap() |
814 | ); |
815 | |
816 | #[cfg (all(feature = "vsock" , not(feature = "tokio" )))] |
817 | assert_eq!( |
818 | Address::Vsock(crate::VsockAddress { |
819 | cid: 98, |
820 | port: 2934 |
821 | }), |
822 | Address::from_str("vsock:cid=98,port=2934,guid=123" ).unwrap() |
823 | ); |
824 | } |
825 | |
826 | #[test ] |
827 | fn stringify_dbus_addresses() { |
828 | assert_eq!( |
829 | Address::Unix("/tmp/dbus-foo" .into()).to_string(), |
830 | "unix:path=/tmp/dbus-foo" |
831 | ); |
832 | assert_eq!( |
833 | Address::Tcp(TcpAddress { |
834 | host: "localhost" .into(), |
835 | port: 4142, |
836 | bind: None, |
837 | family: None |
838 | }) |
839 | .to_string(), |
840 | "tcp:host=localhost,port=4142" |
841 | ); |
842 | assert_eq!( |
843 | Address::Tcp(TcpAddress { |
844 | host: "localhost" .into(), |
845 | port: 4142, |
846 | bind: None, |
847 | family: Some(TcpAddressFamily::Ipv4) |
848 | }) |
849 | .to_string(), |
850 | "tcp:host=localhost,port=4142,family=ipv4" |
851 | ); |
852 | assert_eq!( |
853 | Address::Tcp(TcpAddress { |
854 | host: "localhost" .into(), |
855 | port: 4142, |
856 | bind: None, |
857 | family: Some(TcpAddressFamily::Ipv6) |
858 | }) |
859 | .to_string(), |
860 | "tcp:host=localhost,port=4142,family=ipv6" |
861 | ); |
862 | assert_eq!( |
863 | Address::NonceTcp { |
864 | addr: TcpAddress { |
865 | host: "localhost" .into(), |
866 | port: 4142, |
867 | bind: None, |
868 | family: Some(TcpAddressFamily::Ipv6), |
869 | }, |
870 | nonce_file: b"/a/file/path to file 1234" .to_vec() |
871 | } |
872 | .to_string(), |
873 | "nonce-tcp:noncefile=/a/file/path%20to%20file%201234,host=localhost,port=4142,family=ipv6" |
874 | ); |
875 | assert_eq!(Address::Autolaunch(None).to_string(), "autolaunch:" ); |
876 | assert_eq!( |
877 | Address::Autolaunch(Some("*my_cool_scope*" .to_owned())).to_string(), |
878 | "autolaunch:scope=*my_cool_scope*" |
879 | ); |
880 | assert_eq!( |
881 | Address::Launchd("my_cool_key" .to_owned()).to_string(), |
882 | "launchd:env=my_cool_key" |
883 | ); |
884 | |
885 | #[cfg (all(feature = "vsock" , not(feature = "tokio" )))] |
886 | assert_eq!( |
887 | Address::Vsock(crate::VsockAddress { |
888 | cid: 98, |
889 | port: 2934 |
890 | }) |
891 | .to_string(), |
892 | "vsock:cid=98,port=2934,guid=123" , |
893 | ); |
894 | } |
895 | |
896 | #[test ] |
897 | fn connect_tcp() { |
898 | let listener = std::net::TcpListener::bind("127.0.0.1:0" ).unwrap(); |
899 | let port = listener.local_addr().unwrap().port(); |
900 | let addr = Address::from_str(&format!("tcp:host=localhost,port= {port}" )).unwrap(); |
901 | crate::utils::block_on(async { addr.connect().await }).unwrap(); |
902 | } |
903 | |
904 | #[test ] |
905 | fn connect_nonce_tcp() { |
906 | struct PercentEncoded<'a>(&'a [u8]); |
907 | |
908 | impl std::fmt::Display for PercentEncoded<'_> { |
909 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
910 | super::encode_percents(f, self.0) |
911 | } |
912 | } |
913 | |
914 | use std::io::Write; |
915 | |
916 | const TEST_COOKIE: &[u8] = b"VERILY SECRETIVE" ; |
917 | |
918 | let listener = std::net::TcpListener::bind("127.0.0.1:0" ).unwrap(); |
919 | let port = listener.local_addr().unwrap().port(); |
920 | |
921 | let mut cookie = tempfile::NamedTempFile::new().unwrap(); |
922 | cookie.as_file_mut().write_all(TEST_COOKIE).unwrap(); |
923 | |
924 | let encoded_path = format!( |
925 | " {}" , |
926 | PercentEncoded(cookie.path().to_str().unwrap().as_ref()) |
927 | ); |
928 | |
929 | let addr = Address::from_str(&format!( |
930 | "nonce-tcp:host=localhost,port= {port},noncefile= {encoded_path}" |
931 | )) |
932 | .unwrap(); |
933 | |
934 | let (sender, receiver) = std::sync::mpsc::sync_channel(1); |
935 | |
936 | std::thread::spawn(move || { |
937 | use std::io::Read; |
938 | |
939 | let mut client = listener.incoming().next().unwrap().unwrap(); |
940 | |
941 | let mut buf = [0u8; 16]; |
942 | client.read_exact(&mut buf).unwrap(); |
943 | |
944 | sender.send(buf == TEST_COOKIE).unwrap(); |
945 | }); |
946 | |
947 | crate::utils::block_on(addr.connect()).unwrap(); |
948 | |
949 | let saw_cookie = receiver |
950 | .recv_timeout(std::time::Duration::from_millis(100)) |
951 | .expect("nonce file content hasn't been received by server thread in time" ); |
952 | |
953 | assert!( |
954 | saw_cookie, |
955 | "nonce file content has been received, but was invalid" |
956 | ); |
957 | } |
958 | } |
959 | |