1use std::default::Default;
3use std::net::SocketAddr;
4use std::option::Option;
5#[cfg(test)]
6use std::path::Path;
7use std::str::FromStr;
8use std::string::ToString;
9#[cfg(test)]
10use std::time::{Duration, Instant};
11
12use regex::Regex;
14#[cfg(test)]
15use serial_test::serial;
16
17use crate::legacy_tor_control_stream::*;
19#[cfg(test)]
20use crate::legacy_tor_process::*;
21use crate::legacy_tor_version::*;
22use crate::tor_crypto::*;
23
24#[derive(thiserror::Error, Debug)]
25pub enum Error {
26 #[error("response regex creation failed")]
27 ParsingRegexCreationFailed(#[source] regex::Error),
28
29 #[error("control stream read reply failed")]
30 ReadReplyFailed(#[source] crate::legacy_tor_control_stream::Error),
31
32 #[error("unexpected synchronous reply recieved")]
33 UnexpectedSynchonousReplyReceived(),
34
35 #[error("control stream write command failed")]
36 WriteCommandFailed(#[source] crate::legacy_tor_control_stream::Error),
37
38 #[error("invalid command arguments: {0}")]
39 InvalidCommandArguments(String),
40
41 #[error("command failed: {0} {}", .1.join("\n"))]
42 CommandFailed(u32, Vec<String>),
43
44 #[error("failed to parse command reply: {0}")]
45 CommandReplyParseFailed(String),
46
47 #[error("failed to parse received tor version")]
48 TorVersionParseFailed(#[source] crate::legacy_tor_version::Error),
49}
50
51#[derive(Default)]
53pub(crate) struct AddOnionFlags {
54 pub discard_pk: bool,
55 pub detach: bool,
56 pub v3_auth: bool,
57 pub non_anonymous: bool,
58 pub max_streams_close_circuit: bool,
59}
60
61#[derive(Default)]
62pub(crate) struct OnionClientAuthAddFlags {
63 pub permanent: bool,
64}
65
66pub(crate) enum AsyncEvent {
67 Unknown {
68 lines: Vec<String>,
69 },
70 StatusClient {
71 severity: String,
72 action: String,
73 arguments: Vec<(String, String)>,
74 },
75 HsDesc {
76 action: String,
77 hs_address: V3OnionServiceId,
78 },
79}
80
81pub(crate) struct LegacyTorController {
82 control_stream: LegacyControlStream,
84 async_replies: Vec<Reply>,
86 status_event_pattern: Regex,
88 status_event_argument_pattern: Regex,
89 hs_desc_pattern: Regex,
90}
91
92fn quoted_string(string: &str) -> String {
93 string.replace("\\", "\\\\").replace("\"", "\\\"")
96}
97
98impl LegacyTorController {
99 pub fn new(control_stream: LegacyControlStream) -> Result<LegacyTorController, Error> {
100 let status_event_pattern =
101 Regex::new(r#"^STATUS_CLIENT (?P<severity>NOTICE|WARN|ERR) (?P<action>[A-Za-z]+)"#)
102 .map_err(Error::ParsingRegexCreationFailed)?;
103 let status_event_argument_pattern =
104 Regex::new(r#"(?P<key>[A-Z]+)=(?P<value>[A-Za-z0-9_]+|"[^"]+")"#)
105 .map_err(Error::ParsingRegexCreationFailed)?;
106 let hs_desc_pattern = Regex::new(
107 r#"HS_DESC (?P<action>REQUESTED|UPLOAD|RECEIVED|UPLOADED|IGNORE|FAILED|CREATED) (?P<hsaddress>[a-z2-7]{56})"#
108 ).map_err(Error::ParsingRegexCreationFailed)?;
109
110 Ok(LegacyTorController {
111 control_stream,
112 async_replies: Default::default(),
113 status_event_pattern,
115 status_event_argument_pattern,
116 hs_desc_pattern,
117 })
118 }
119
120 fn wait_async_replies(&mut self) -> Result<Vec<Reply>, Error> {
123 let mut replies: Vec<Reply> = Default::default();
124 std::mem::swap(&mut self.async_replies, &mut replies);
126
127 loop {
129 if let Some(reply) = self
130 .control_stream
131 .read_reply()
132 .map_err(Error::ReadReplyFailed)?
133 {
134 replies.push(reply);
135 } else {
136 return Ok(replies);
138 }
139 }
140 }
141
142 fn reply_to_event(&self, reply: &mut Reply) -> Result<AsyncEvent, Error> {
143 if reply.status_code != 650u32 {
144 return Err(Error::UnexpectedSynchonousReplyReceived());
145 }
146
147 let reply_text = reply.reply_lines.join(" ");
149 if let Some(caps) = self.status_event_pattern.captures(&reply_text) {
150 let severity = match caps.name("severity") {
151 Some(severity) => severity.as_str(),
152 None => unreachable!(),
153 };
154 let action = match caps.name("action") {
155 Some(action) => action.as_str(),
156 None => unreachable!(),
157 };
158
159 let mut arguments: Vec<(String, String)> = Default::default();
160 for caps in self
161 .status_event_argument_pattern
162 .captures_iter(&reply_text)
163 {
164 let key = match caps.name("key") {
165 Some(key) => key.as_str(),
166 None => unreachable!(),
167 };
168 let value = {
169 let value = match caps.name("value") {
170 Some(value) => value.as_str(),
171 None => unreachable!(),
172 };
173 if value.starts_with('\"') && value.ends_with('\"') {
174 &value[1..value.len() - 1]
175 } else {
176 value
177 }
178 };
179 arguments.push((key.to_string(), value.to_string()));
180 }
181
182 return Ok(AsyncEvent::StatusClient {
183 severity: severity.to_string(),
184 action: action.to_string(),
185 arguments,
186 });
187 }
188
189 if let Some(caps) = self.hs_desc_pattern.captures(&reply_text) {
190 let action = match caps.name("action") {
191 Some(action) => action.as_str(),
192 None => unreachable!(),
193 };
194 let hs_address = match caps.name("hsaddress") {
195 Some(hs_address) => hs_address.as_str(),
196 None => unreachable!(),
197 };
198
199 if let Ok(hs_address) = V3OnionServiceId::from_string(hs_address) {
200 return Ok(AsyncEvent::HsDesc {
201 action: action.to_string(),
202 hs_address,
203 });
204 }
205 }
206
207 let mut reply_lines: Vec<String> = Default::default();
209 std::mem::swap(&mut reply_lines, &mut reply.reply_lines);
210
211 Ok(AsyncEvent::Unknown { lines: reply_lines })
212 }
213
214 pub fn wait_async_events(&mut self) -> Result<Vec<AsyncEvent>, Error> {
215 let mut async_replies = self.wait_async_replies()?;
216 let mut async_events: Vec<AsyncEvent> = Default::default();
217
218 for reply in async_replies.iter_mut() {
219 async_events.push(self.reply_to_event(reply)?);
220 }
221
222 Ok(async_events)
223 }
224
225 fn wait_sync_reply(&mut self) -> Result<Reply, Error> {
227 loop {
228 if let Some(reply) = self
229 .control_stream
230 .read_reply()
231 .map_err(Error::ReadReplyFailed)?
232 {
233 match reply.status_code {
234 650u32 => self.async_replies.push(reply),
235 _ => return Ok(reply),
236 }
237 }
238 }
239 }
240
241 fn write_command(&mut self, text: &str) -> Result<Reply, Error> {
242 self.control_stream
243 .write(text)
244 .map_err(Error::WriteCommandFailed)?;
245 self.wait_sync_reply()
246 }
247
248 fn setconf_cmd(&mut self, key_values: &[(&str, String)]) -> Result<Reply, Error> {
259 if key_values.is_empty() {
260 return Err(Error::InvalidCommandArguments(
261 "SETCONF key-value pairs list must not be empty".to_string(),
262 ));
263 }
264 let mut command_buffer = vec!["SETCONF".to_string()];
265
266 for (key, value) in key_values.iter() {
267 command_buffer.push(format!("{}=\"{}\"", key, quoted_string(value.trim())));
268 }
269 let command = command_buffer.join(" ");
270
271 self.write_command(&command)
272 }
273
274 #[cfg(test)]
276 fn getconf_cmd(&mut self, keywords: &[&str]) -> Result<Reply, Error> {
277 if keywords.is_empty() {
278 return Err(Error::InvalidCommandArguments(
279 "GETCONF keywords list must not be empty".to_string(),
280 ));
281 }
282 let command = format!("GETCONF {}", keywords.join(" "));
283
284 self.write_command(&command)
285 }
286
287 fn setevents_cmd(&mut self, event_codes: &[&str]) -> Result<Reply, Error> {
289 if event_codes.is_empty() {
290 return Err(Error::InvalidCommandArguments(
291 "SETEVENTS event codes list mut not be empty".to_string(),
292 ));
293 }
294 let command = format!("SETEVENTS {}", event_codes.join(" "));
295
296 self.write_command(&command)
297 }
298
299 fn authenticate_cmd(&mut self, password: &str) -> Result<Reply, Error> {
301 let command = format!("AUTHENTICATE \"{}\"", quoted_string(password));
302
303 self.write_command(&command)
304 }
305
306 fn getinfo_cmd(&mut self, keywords: &[&str]) -> Result<Reply, Error> {
308 if keywords.is_empty() {
309 return Err(Error::InvalidCommandArguments(
310 "GETINFO keywords list must not be empty".to_string(),
311 ));
312 }
313 let command = format!("GETINFO {}", keywords.join(" "));
314
315 self.write_command(&command)
316 }
317
318 fn add_onion_cmd(
320 &mut self,
321 key: Option<&Ed25519PrivateKey>,
322 flags: &AddOnionFlags,
323 max_streams: Option<u16>,
324 virt_port: u16,
325 target: Option<SocketAddr>,
326 client_auth: Option<&[X25519PublicKey]>,
327 ) -> Result<Reply, Error> {
328 let mut command_buffer = vec!["ADD_ONION".to_string()];
329
330 if let Some(key) = key {
332 command_buffer.push(key.to_key_blob());
333 } else {
334 command_buffer.push("NEW:ED25519-V3".to_string());
335 }
336
337 let mut flag_buffer: Vec<&str> = Default::default();
339 if flags.discard_pk {
340 flag_buffer.push("DiscardPK");
341 }
342 if flags.detach {
343 flag_buffer.push("Detach");
344 }
345 if flags.v3_auth {
346 flag_buffer.push("V3Auth");
347 }
348 if flags.non_anonymous {
349 flag_buffer.push("NonAnonymous");
350 }
351 if flags.max_streams_close_circuit {
352 flag_buffer.push("MaxStreamsCloseCircuit");
353 }
354
355 if !flag_buffer.is_empty() {
356 command_buffer.push(format!("Flags={}", flag_buffer.join(",")));
357 }
358
359 if let Some(max_streams) = max_streams {
361 command_buffer.push(format!("MaxStreams={}", max_streams));
362 }
363
364 if let Some(target) = target {
366 command_buffer.push(format!("Port={},{}", virt_port, target));
367 } else {
368 command_buffer.push(format!("Port={}", virt_port));
369 }
370 if let Some(client_auth) = client_auth {
372 for key in client_auth.iter() {
373 command_buffer.push(format!("ClientAuthV3={}", key.to_base32()));
374 }
375 }
376
377 let command = command_buffer.join(" ");
379
380 self.write_command(&command)
381 }
382
383 fn del_onion_cmd(&mut self, service_id: &V3OnionServiceId) -> Result<Reply, Error> {
385 let command = format!("DEL_ONION {}", service_id);
386
387 self.write_command(&command)
388 }
389
390 fn onion_client_auth_add_cmd(
392 &mut self,
393 service_id: &V3OnionServiceId,
394 private_key: &X25519PrivateKey,
395 client_name: Option<String>,
396 flags: &OnionClientAuthAddFlags,
397 ) -> Result<Reply, Error> {
398 let mut command_buffer = vec!["ONION_CLIENT_AUTH_ADD".to_string()];
399
400 command_buffer.push(service_id.to_string());
402
403 command_buffer.push(format!("x25519:{}", private_key.to_base64()));
405
406 if let Some(client_name) = client_name {
407 command_buffer.push(format!("ClientName={}", client_name));
408 }
409
410 if flags.permanent {
411 command_buffer.push("Flags=Permanent".to_string());
412 }
413
414 let command = command_buffer.join(" ");
416
417 self.write_command(&command)
418 }
419
420 fn onion_client_auth_remove_cmd(
422 &mut self,
423 service_id: &V3OnionServiceId,
424 ) -> Result<Reply, Error> {
425 let command = format!("ONION_CLIENT_AUTH_REMOVE {}", service_id);
426
427 self.write_command(&command)
428 }
429
430 pub fn setconf(&mut self, key_values: &[(&str, String)]) -> Result<(), Error> {
435 let reply = self.setconf_cmd(key_values)?;
436
437 match reply.status_code {
438 250u32 => Ok(()),
439 code => Err(Error::CommandFailed(code, reply.reply_lines)),
440 }
441 }
442
443 #[cfg(test)]
444 pub fn getconf(&mut self, keywords: &[&str]) -> Result<Vec<(String, String)>, Error> {
445 let reply = self.getconf_cmd(keywords)?;
446
447 match reply.status_code {
448 250u32 => {
449 let mut key_values: Vec<(String, String)> = Default::default();
450 for line in reply.reply_lines {
451 match line.find('=') {
452 Some(index) => key_values
453 .push((line[0..index].to_string(), line[index + 1..].to_string())),
454 None => key_values.push((line, String::new())),
455 }
456 }
457 Ok(key_values)
458 }
459 code => Err(Error::CommandFailed(code, reply.reply_lines)),
460 }
461 }
462
463 pub fn setevents(&mut self, events: &[&str]) -> Result<(), Error> {
464 let reply = self.setevents_cmd(events)?;
465
466 match reply.status_code {
467 250u32 => Ok(()),
468 code => Err(Error::CommandFailed(code, reply.reply_lines)),
469 }
470 }
471
472 pub fn authenticate(&mut self, password: &str) -> Result<(), Error> {
473 let reply = self.authenticate_cmd(password)?;
474
475 match reply.status_code {
476 250u32 => Ok(()),
477 code => Err(Error::CommandFailed(code, reply.reply_lines)),
478 }
479 }
480
481 pub fn getinfo(&mut self, keywords: &[&str]) -> Result<Vec<(String, String)>, Error> {
482 let reply = self.getinfo_cmd(keywords)?;
483
484 match reply.status_code {
485 250u32 => {
486 let mut key_values: Vec<(String, String)> = Default::default();
487 for line in reply.reply_lines {
488 match line.find('=') {
489 Some(index) => key_values
490 .push((line[0..index].to_string(), line[index + 1..].to_string())),
491 None => {
492 if line != "OK" {
493 key_values.push((line, String::new()))
494 }
495 }
496 }
497 }
498 Ok(key_values)
499 }
500 code => Err(Error::CommandFailed(code, reply.reply_lines)),
501 }
502 }
503
504 pub fn add_onion(
505 &mut self,
506 key: Option<&Ed25519PrivateKey>,
507 flags: &AddOnionFlags,
508 max_streams: Option<u16>,
509 virt_port: u16,
510 target: Option<SocketAddr>,
511 client_auth: Option<&[X25519PublicKey]>,
512 ) -> Result<(Option<Ed25519PrivateKey>, V3OnionServiceId), Error> {
513 let reply = self.add_onion_cmd(key, flags, max_streams, virt_port, target, client_auth)?;
514
515 let mut private_key: Option<Ed25519PrivateKey> = None;
516 let mut service_id: Option<V3OnionServiceId> = None;
517
518 match reply.status_code {
519 250u32 => {
520 for line in reply.reply_lines {
521 if let Some(mut index) = line.find("ServiceID=") {
522 if service_id.is_some() {
523 return Err(Error::CommandReplyParseFailed(
524 "received duplicate ServiceID entries".to_string(),
525 ));
526 }
527 index += "ServiceId=".len();
528 let service_id_string = &line[index..];
529 service_id = match V3OnionServiceId::from_string(service_id_string) {
530 Ok(service_id) => Some(service_id),
531 Err(_) => {
532 return Err(Error::CommandReplyParseFailed(format!(
533 "could not parse '{}' as V3OnionServiceId",
534 service_id_string
535 )))
536 }
537 }
538 } else if let Some(mut index) = line.find("PrivateKey=") {
539 if private_key.is_some() {
540 return Err(Error::CommandReplyParseFailed(
541 "received duplicate PrivateKey entries".to_string(),
542 ));
543 }
544 index += "PrivateKey=".len();
545 let key_blob_string = &line[index..];
546 private_key = match Ed25519PrivateKey::from_key_blob_legacy(key_blob_string)
547 {
548 Ok(private_key) => Some(private_key),
549 Err(_) => {
550 return Err(Error::CommandReplyParseFailed(format!(
551 "could not parse {} as Ed25519PrivateKey",
552 key_blob_string
553 )))
554 }
555 };
556 } else if line.contains("ClientAuthV3=") {
557 if client_auth.unwrap_or_default().is_empty() {
558 return Err(Error::CommandReplyParseFailed(
559 "recieved unexpected ClientAuthV3 keys".to_string(),
560 ));
561 }
562 } else if !line.contains("OK") {
563 return Err(Error::CommandReplyParseFailed(format!(
564 "received unexpected reply line '{}'",
565 line
566 )));
567 }
568 }
569 }
570 code => return Err(Error::CommandFailed(code, reply.reply_lines)),
571 }
572
573 if flags.discard_pk {
574 if private_key.is_some() {
575 return Err(Error::CommandReplyParseFailed(
576 "PrivateKey response should have been discard".to_string(),
577 ));
578 }
579 } else if private_key.is_none() {
580 return Err(Error::CommandReplyParseFailed(
581 "did not receive a PrivateKey".to_string(),
582 ));
583 }
584
585 match service_id {
586 Some(service_id) => Ok((private_key, service_id)),
587 None => Err(Error::CommandReplyParseFailed(
588 "did not receive a ServiceID".to_string(),
589 )),
590 }
591 }
592
593 pub fn del_onion(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> {
594 let reply = self.del_onion_cmd(service_id)?;
595
596 match reply.status_code {
597 250u32 => Ok(()),
598 code => Err(Error::CommandFailed(code, reply.reply_lines)),
599 }
600 }
601
602 pub fn getinfo_net_listeners_socks(&mut self) -> Result<Vec<SocketAddr>, Error> {
605 let response = self.getinfo(&["net/listeners/socks"])?;
606 for (key, value) in response.iter() {
607 if key.as_str() == "net/listeners/socks" {
608 if value.is_empty() {
609 return Ok(Default::default());
610 }
611 let listeners: Vec<&str> = value.split(' ').collect();
613 let mut result: Vec<SocketAddr> = Default::default();
614 for socket_addr in listeners.iter() {
615 if !socket_addr.starts_with('\"') || !socket_addr.ends_with('\"') {
616 return Err(Error::CommandReplyParseFailed(format!(
617 "could not parse '{}' as socket address",
618 socket_addr
619 )));
620 }
621
622 let stripped = &socket_addr[1..socket_addr.len() - 1];
624 result.push(match SocketAddr::from_str(stripped) {
625 Ok(result) => result,
626 Err(_) => {
627 return Err(Error::CommandReplyParseFailed(format!(
628 "could not parse '{}' as socket address",
629 socket_addr
630 )))
631 }
632 });
633 }
634 return Ok(result);
635 }
636 }
637 Err(Error::CommandReplyParseFailed(
638 "reply did not find a 'net/listeners/socks' key/value".to_string(),
639 ))
640 }
641
642 pub fn getinfo_version(&mut self) -> Result<LegacyTorVersion, Error> {
643 let response = self.getinfo(&["version"])?;
644 for (key, value) in response.iter() {
645 if key.as_str() == "version" {
646 return LegacyTorVersion::from_str(value).map_err(Error::TorVersionParseFailed);
647 }
648 }
649 Err(Error::CommandReplyParseFailed(
650 "did not find a 'version' key/value".to_string(),
651 ))
652 }
653
654 pub fn onion_client_auth_add(
655 &mut self,
656 service_id: &V3OnionServiceId,
657 private_key: &X25519PrivateKey,
658 client_name: Option<String>,
659 flags: &OnionClientAuthAddFlags,
660 ) -> Result<(), Error> {
661 let reply = self.onion_client_auth_add_cmd(service_id, private_key, client_name, flags)?;
662
663 match reply.status_code {
664 250u32..=252u32 => Ok(()),
665 code => Err(Error::CommandFailed(code, reply.reply_lines)),
666 }
667 }
668
669 #[allow(dead_code)]
670 pub fn onion_client_auth_remove(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> {
671 let reply = self.onion_client_auth_remove_cmd(service_id)?;
672
673 match reply.status_code {
674 250u32..=251u32 => Ok(()),
675 code => Err(Error::CommandFailed(code, reply.reply_lines)),
676 }
677 }
678}
679
680#[test]
681#[serial]
682fn test_tor_controller() -> anyhow::Result<()> {
683 let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?;
684 let mut data_path = std::env::temp_dir();
685 data_path.push("test_tor_controller");
686 let tor_process = LegacyTorProcess::new(&tor_path, &data_path)?;
687
688 {
690 let control_stream =
691 LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?;
692
693 let mut tor_controller = LegacyTorController::new(control_stream)?;
695 tor_controller.authenticate_cmd(tor_process.get_password())?;
696 assert!(
697 tor_controller
698 .authenticate_cmd("invalid password")?
699 .status_code
700 == 515u32
701 );
702
703 assert!(
705 tor_controller
706 .authenticate_cmd(tor_process.get_password())
707 .is_err(),
708 "expected failure due to closed connection"
709 );
710 assert!(tor_controller.control_stream.closed_by_remote());
711 }
712 {
714 let control_stream =
715 LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?;
716
717 let mut tor_controller = LegacyTorController::new(control_stream)?;
720 tor_controller.authenticate(tor_process.get_password())?;
721
722 let vals = tor_controller.getconf(&["SocksPort", "AvoidDiskWrites", "DisableNetwork"])?;
724 for (key, value) in vals.iter() {
725 let expected = match key.as_str() {
726 "SocksPort" => "auto",
727 "AvoidDiskWrites" => "1",
728 "DisableNetwork" => "1",
729 _ => panic!("unexpected returned key: {}", key),
730 };
731 assert!(value == expected);
732 }
733
734 let vals = tor_controller.getinfo(&["version", "config-file", "config-text"])?;
735 let mut expected_torrc_path = data_path.clone();
736 expected_torrc_path.push("torrc");
737 let mut expected_control_port_path = data_path.clone();
738 expected_control_port_path.push("control_port");
739 for (key, value) in vals.iter() {
740 match key.as_str() {
741 "version" => assert!(Regex::new(r"\d+\.\d+\.\d+\.\d+")?.is_match(&value)),
742 "config-file" => assert!(Path::new(&value) == expected_torrc_path),
743 "config-text" => assert!(
744 value.to_string()
745 == format!(
746 "\nControlPort auto\nControlPortWriteToFile {}\nDataDirectory {}",
747 expected_control_port_path.display(),
748 data_path.display()
749 )
750 ),
751 _ => panic!("unexpected returned key: {}", key),
752 }
753 }
754
755 tor_controller.setevents(&["STATUS_CLIENT"])?;
756 tor_controller.setconf(&[("DisableNetwork", "0".to_string())])?;
758
759 let (private_key, service_id) =
761 match tor_controller.add_onion(None, &Default::default(), None, 22, None, None)? {
762 (Some(private_key), service_id) => (private_key, service_id),
763 _ => panic!("add_onion did not return expected values"),
764 };
765 println!("private_key: {}", private_key.to_key_blob());
766 println!("service_id: {}", service_id.to_string());
767
768 assert!(
769 tor_controller
770 .del_onion(&V3OnionServiceId::from_string(
771 "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd"
772 )?)
773 .is_err(),
774 "deleting unknown onion should have failed"
775 );
776
777 tor_controller.del_onion(&service_id)?;
779
780 println!("listeners: ");
781 for sock_addr in tor_controller.getinfo_net_listeners_socks()?.iter() {
782 println!(" {}", sock_addr);
783 }
784
785 for (key, value) in tor_controller.getinfo(&["events/names"])?.iter() {
787 println!("{} : {}", key, value);
788 }
789
790 let stop_time = Instant::now() + std::time::Duration::from_secs(5);
791 while stop_time > Instant::now() {
792 for async_event in tor_controller.wait_async_events()?.iter() {
793 match async_event {
794 AsyncEvent::Unknown { lines } => {
795 println!("Unknown: {}", lines.join("\n"));
796 }
797 AsyncEvent::StatusClient {
798 severity,
799 action,
800 arguments,
801 } => {
802 println!("STATUS_CLIENT severity={}, action={}", severity, action);
803 for (key, value) in arguments.iter() {
804 println!(" {}='{}'", key, value);
805 }
806 }
807 AsyncEvent::HsDesc { action, hs_address } => {
808 println!(
809 "HS_DESC action={}, hsaddress={}",
810 action,
811 hs_address.to_string()
812 );
813 }
814 }
815 }
816 }
817 }
818 Ok(())
819}