tor_interface/
legacy_tor_controller.rs

1// standard
2use 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
12// extern crates
13use regex::Regex;
14#[cfg(test)]
15use serial_test::serial;
16
17// internal crates
18use 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// Per-command data
52#[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    // underlying control stream
83    control_stream: LegacyControlStream,
84    // list of async replies to be handled
85    async_replies: Vec<Reply>,
86    // regex for parsing events
87    status_event_pattern: Regex,
88    status_event_argument_pattern: Regex,
89    hs_desc_pattern: Regex,
90}
91
92fn quoted_string(string: &str) -> String {
93    // replace \ with \\ and " with \"
94    // see: https://spec.torproject.org/control-spec/message-format.html?highlight=QuotedString#description-format
95    string.replace("\\", "\\\\").replace("\"", "\\\"")
96}
97
98fn reply_ok(reply: Reply) -> Result<Reply, Error> {
99    match reply.status_code {
100        250u32 => Ok(reply),
101        code => Err(Error::CommandFailed(code, reply.reply_lines)),
102    }
103}
104
105impl LegacyTorController {
106    pub fn new(control_stream: LegacyControlStream) -> Result<LegacyTorController, Error> {
107        let status_event_pattern =
108            Regex::new(r#"^STATUS_CLIENT (?P<severity>NOTICE|WARN|ERR) (?P<action>[A-Za-z]+)"#)
109                .map_err(Error::ParsingRegexCreationFailed)?;
110        let status_event_argument_pattern =
111            Regex::new(r#"(?P<key>[A-Z]+)=(?P<value>[A-Za-z0-9_]+|"[^"]+")"#)
112                .map_err(Error::ParsingRegexCreationFailed)?;
113        let hs_desc_pattern = Regex::new(
114            r#"HS_DESC (?P<action>REQUESTED|UPLOAD|RECEIVED|UPLOADED|IGNORE|FAILED|CREATED) (?P<hsaddress>[a-z2-7]{56})"#
115        ).map_err(Error::ParsingRegexCreationFailed)?;
116
117        Ok(LegacyTorController {
118            control_stream,
119            async_replies: Default::default(),
120            // regex
121            status_event_pattern,
122            status_event_argument_pattern,
123            hs_desc_pattern,
124        })
125    }
126
127    // return curently available events, does not block waiting
128    // for an event
129    fn wait_async_replies(&mut self) -> Result<Vec<Reply>, Error> {
130        let mut replies: Vec<Reply> = Default::default();
131        // take any previously received async replies
132        std::mem::swap(&mut self.async_replies, &mut replies);
133
134        // and keep consuming until none are available
135        loop {
136            if let Some(reply) = self
137                .control_stream
138                .read_reply()
139                .map_err(Error::ReadReplyFailed)?
140            {
141                replies.push(reply);
142            } else {
143                // no more replies immediately available so return
144                return Ok(replies);
145            }
146        }
147    }
148
149    fn reply_to_event(&self, reply: &mut Reply) -> Result<AsyncEvent, Error> {
150        if reply.status_code != 650u32 {
151            return Err(Error::UnexpectedSynchonousReplyReceived());
152        }
153
154        // not sure this is what we want but yolo
155        let reply_text = reply.reply_lines.join(" ");
156        if let Some(caps) = self.status_event_pattern.captures(&reply_text) {
157            let severity = match caps.name("severity") {
158                Some(severity) => severity.as_str(),
159                None => unreachable!(),
160            };
161            let action = match caps.name("action") {
162                Some(action) => action.as_str(),
163                None => unreachable!(),
164            };
165
166            let mut arguments: Vec<(String, String)> = Default::default();
167            for caps in self
168                .status_event_argument_pattern
169                .captures_iter(&reply_text)
170            {
171                let key = match caps.name("key") {
172                    Some(key) => key.as_str(),
173                    None => unreachable!(),
174                };
175                let value = {
176                    let value = match caps.name("value") {
177                        Some(value) => value.as_str(),
178                        None => unreachable!(),
179                    };
180                    if value.starts_with('\"') && value.ends_with('\"') {
181                        &value[1..value.len() - 1]
182                    } else {
183                        value
184                    }
185                };
186                arguments.push((key.to_string(), value.to_string()));
187            }
188
189            return Ok(AsyncEvent::StatusClient {
190                severity: severity.to_string(),
191                action: action.to_string(),
192                arguments,
193            });
194        }
195
196        if let Some(caps) = self.hs_desc_pattern.captures(&reply_text) {
197            let action = match caps.name("action") {
198                Some(action) => action.as_str(),
199                None => unreachable!(),
200            };
201            let hs_address = match caps.name("hsaddress") {
202                Some(hs_address) => hs_address.as_str(),
203                None => unreachable!(),
204            };
205
206            if let Ok(hs_address) = V3OnionServiceId::from_string(hs_address) {
207                return Ok(AsyncEvent::HsDesc {
208                    action: action.to_string(),
209                    hs_address,
210                });
211            }
212        }
213
214        // no luck parsing reply, just return full text
215        let mut reply_lines: Vec<String> = Default::default();
216        std::mem::swap(&mut reply_lines, &mut reply.reply_lines);
217
218        Ok(AsyncEvent::Unknown { lines: reply_lines })
219    }
220
221    pub fn wait_async_events(&mut self) -> Result<Vec<AsyncEvent>, Error> {
222        let mut async_replies = self.wait_async_replies()?;
223        let mut async_events: Vec<AsyncEvent> = Default::default();
224
225        for reply in async_replies.iter_mut() {
226            async_events.push(self.reply_to_event(reply)?);
227        }
228
229        Ok(async_events)
230    }
231
232    // wait for a sync reply, save off async replies for later
233    fn wait_sync_reply(&mut self) -> Result<Reply, Error> {
234        loop {
235            if let Some(reply) = self
236                .control_stream
237                .read_reply()
238                .map_err(Error::ReadReplyFailed)?
239            {
240                match reply.status_code {
241                    650u32 => self.async_replies.push(reply),
242                    _ => return Ok(reply),
243                }
244            }
245        }
246    }
247
248    fn write_command(&mut self, text: &str) -> Result<Reply, Error> {
249        self.control_stream
250            .write(text)
251            .map_err(Error::WriteCommandFailed)?;
252        self.wait_sync_reply()
253    }
254
255    //
256    // Tor Commands
257    //
258    // The section where we can find the specification in control-spec.txt
259    // for the underlying command is listed in parentheses
260    //
261    // Each of these command wrapper methods block until completion
262    //
263
264    // SETCONF (3.1)
265    fn setconf_cmd(&mut self, key_values: &[(&str, String)]) -> Result<Reply, Error> {
266        if key_values.is_empty() {
267            return Err(Error::InvalidCommandArguments(
268                "SETCONF key-value pairs list must not be empty".to_string(),
269            ));
270        }
271        let mut command_buffer = vec!["SETCONF".to_string()];
272
273        for (key, value) in key_values.iter() {
274            command_buffer.push(format!("{}=\"{}\"", key, quoted_string(value.trim())));
275        }
276        let command = command_buffer.join(" ");
277
278        self.write_command(&command)
279    }
280
281    // GETCONF (3.3)
282    #[cfg(test)]
283    fn getconf_cmd(&mut self, keywords: &[&str]) -> Result<Reply, Error> {
284        if keywords.is_empty() {
285            return Err(Error::InvalidCommandArguments(
286                "GETCONF keywords list must not be empty".to_string(),
287            ));
288        }
289        let command = format!("GETCONF {}", keywords.join(" "));
290
291        self.write_command(&command)
292    }
293
294    // SETEVENTS (3.4)
295    fn setevents_cmd(&mut self, event_codes: &[&str]) -> Result<Reply, Error> {
296        if event_codes.is_empty() {
297            return Err(Error::InvalidCommandArguments(
298                "SETEVENTS event codes list mut not be empty".to_string(),
299            ));
300        }
301        let command = format!("SETEVENTS {}", event_codes.join(" "));
302
303        self.write_command(&command)
304    }
305
306    // AUTHENTICATE (3.5)
307    fn authenticate_cmd(&mut self, password: &str) -> Result<Reply, Error> {
308        let command = format!("AUTHENTICATE \"{}\"", quoted_string(password));
309
310        self.write_command(&command)
311    }
312
313    // GETINFO (3.9)
314    fn getinfo_cmd(&mut self, keywords: &[&str]) -> Result<Reply, Error> {
315        if keywords.is_empty() {
316            return Err(Error::InvalidCommandArguments(
317                "GETINFO keywords list must not be empty".to_string(),
318            ));
319        }
320        let command = format!("GETINFO {}", keywords.join(" "));
321
322        self.write_command(&command)
323    }
324
325    // ADD_ONION (3.27)
326    fn add_onion_cmd(
327        &mut self,
328        key: Option<&Ed25519PrivateKey>,
329        flags: &AddOnionFlags,
330        max_streams: Option<u16>,
331        virt_port: u16,
332        target: Option<SocketAddr>,
333        client_auth: Option<&[X25519PublicKey]>,
334    ) -> Result<Reply, Error> {
335        let mut command_buffer = vec!["ADD_ONION".to_string()];
336
337        // set our key or request a new one
338        if let Some(key) = key {
339            command_buffer.push(key.to_key_blob());
340        } else {
341            command_buffer.push("NEW:ED25519-V3".to_string());
342        }
343
344        // set our flags
345        let mut flag_buffer: Vec<&str> = Default::default();
346        if flags.discard_pk {
347            flag_buffer.push("DiscardPK");
348        }
349        if flags.detach {
350            flag_buffer.push("Detach");
351        }
352        if flags.v3_auth {
353            flag_buffer.push("V3Auth");
354        }
355        if flags.non_anonymous {
356            flag_buffer.push("NonAnonymous");
357        }
358        if flags.max_streams_close_circuit {
359            flag_buffer.push("MaxStreamsCloseCircuit");
360        }
361
362        if !flag_buffer.is_empty() {
363            command_buffer.push(format!("Flags={}", flag_buffer.join(",")));
364        }
365
366        // set max concurrent streams
367        if let Some(max_streams) = max_streams {
368            command_buffer.push(format!("MaxStreams={}", max_streams));
369        }
370
371        // set our onion service target
372        if let Some(target) = target {
373            command_buffer.push(format!("Port={},{}", virt_port, target));
374        } else {
375            command_buffer.push(format!("Port={}", virt_port));
376        }
377        // setup client auth
378        if let Some(client_auth) = client_auth {
379            for key in client_auth.iter() {
380                command_buffer.push(format!("ClientAuthV3={}", key.to_base32()));
381            }
382        }
383
384        // finally send the command
385        let command = command_buffer.join(" ");
386
387        self.write_command(&command)
388    }
389
390    // DEL_ONION (3.38)
391    fn del_onion_cmd(&mut self, service_id: &V3OnionServiceId) -> Result<Reply, Error> {
392        let command = format!("DEL_ONION {}", service_id);
393
394        self.write_command(&command)
395    }
396
397    // ONION_CLIENT_AUTH_ADD (3.30)
398    fn onion_client_auth_add_cmd(
399        &mut self,
400        service_id: &V3OnionServiceId,
401        private_key: &X25519PrivateKey,
402        client_name: Option<String>,
403        flags: &OnionClientAuthAddFlags,
404    ) -> Result<Reply, Error> {
405        let mut command_buffer = vec!["ONION_CLIENT_AUTH_ADD".to_string()];
406
407        // set the onion service id
408        command_buffer.push(service_id.to_string());
409
410        // set our client's private key
411        command_buffer.push(format!("x25519:{}", private_key.to_base64()));
412
413        if let Some(client_name) = client_name {
414            command_buffer.push(format!("ClientName={}", client_name));
415        }
416
417        if flags.permanent {
418            command_buffer.push("Flags=Permanent".to_string());
419        }
420
421        // finally send command
422        let command = command_buffer.join(" ");
423
424        self.write_command(&command)
425    }
426
427    // ONION_CLIENT_AUTH_REMOVE (3.31)
428    fn onion_client_auth_remove_cmd(
429        &mut self,
430        service_id: &V3OnionServiceId,
431    ) -> Result<Reply, Error> {
432        let command = format!("ONION_CLIENT_AUTH_REMOVE {}", service_id);
433
434        self.write_command(&command)
435    }
436
437    //
438    // Public high-level typesafe command method wrappers
439    //
440
441    pub fn setconf(&mut self, key_values: &[(&str, String)]) -> Result<(), Error> {
442        self.setconf_cmd(key_values).and_then(reply_ok).map(|_| ())
443    }
444
445    #[cfg(test)]
446    pub fn getconf(&mut self, keywords: &[&str]) -> Result<Vec<(String, String)>, Error> {
447        let reply = self.getconf_cmd(keywords).and_then(reply_ok)?;
448
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
460    pub fn setevents(&mut self, events: &[&str]) -> Result<(), Error> {
461        self.setevents_cmd(events).and_then(reply_ok).map(|_| ())
462    }
463
464    pub fn authenticate(&mut self, password: &str) -> Result<(), Error> {
465        self.authenticate_cmd(password).and_then(reply_ok).map(|_| ())
466    }
467
468    pub fn getinfo(&mut self, keywords: &[&str]) -> Result<Vec<(String, String)>, Error> {
469        let reply = self.getinfo_cmd(keywords).and_then(reply_ok)?;
470
471        let mut key_values: Vec<(String, String)> = Default::default();
472        for line in reply.reply_lines {
473            match line.find('=') {
474                Some(index) => key_values
475                    .push((line[0..index].to_string(), line[index + 1..].to_string())),
476                None => {
477                    if line != "OK" {
478                        key_values.push((line, String::new()))
479                    }
480                }
481            }
482        }
483        Ok(key_values)
484    }
485
486    pub fn add_onion(
487        &mut self,
488        key: Option<&Ed25519PrivateKey>,
489        flags: &AddOnionFlags,
490        max_streams: Option<u16>,
491        virt_port: u16,
492        target: Option<SocketAddr>,
493        client_auth: Option<&[X25519PublicKey]>,
494    ) -> Result<(Option<Ed25519PrivateKey>, V3OnionServiceId), Error> {
495        let reply = self.add_onion_cmd(key, flags, max_streams, virt_port, target, client_auth).and_then(reply_ok)?;
496
497        let mut private_key: Option<Ed25519PrivateKey> = None;
498        let mut service_id: Option<V3OnionServiceId> = None;
499
500        for line in reply.reply_lines {
501            if let Some(mut index) = line.find("ServiceID=") {
502                if service_id.is_some() {
503                    return Err(Error::CommandReplyParseFailed(
504                        "received duplicate ServiceID entries".to_string(),
505                    ));
506                }
507                index += "ServiceId=".len();
508                let service_id_string = &line[index..];
509                service_id = match V3OnionServiceId::from_string(service_id_string) {
510                    Ok(service_id) => Some(service_id),
511                    Err(_) => {
512                        return Err(Error::CommandReplyParseFailed(format!(
513                            "could not parse '{}' as V3OnionServiceId",
514                            service_id_string
515                        )))
516                    }
517                }
518            } else if let Some(mut index) = line.find("PrivateKey=") {
519                if private_key.is_some() {
520                    return Err(Error::CommandReplyParseFailed(
521                        "received duplicate PrivateKey entries".to_string(),
522                    ));
523                }
524                index += "PrivateKey=".len();
525                let key_blob_string = &line[index..];
526                private_key = match Ed25519PrivateKey::from_key_blob_legacy(key_blob_string)
527                {
528                    Ok(private_key) => Some(private_key),
529                    Err(_) => {
530                        return Err(Error::CommandReplyParseFailed(format!(
531                            "could not parse {} as Ed25519PrivateKey",
532                            key_blob_string
533                        )))
534                    }
535                };
536            } else if line.contains("ClientAuthV3=") {
537                if client_auth.unwrap_or_default().is_empty() {
538                    return Err(Error::CommandReplyParseFailed(
539                        "recieved unexpected ClientAuthV3 keys".to_string(),
540                    ));
541                }
542            } else if !line.contains("OK") {
543                return Err(Error::CommandReplyParseFailed(format!(
544                    "received unexpected reply line '{}'",
545                    line
546                )));
547            }
548        }
549
550        if flags.discard_pk {
551            if private_key.is_some() {
552                return Err(Error::CommandReplyParseFailed(
553                    "PrivateKey response should have been discard".to_string(),
554                ));
555            }
556        } else if private_key.is_none() {
557            return Err(Error::CommandReplyParseFailed(
558                "did not receive a PrivateKey".to_string(),
559            ));
560        }
561
562        match service_id {
563            Some(service_id) => Ok((private_key, service_id)),
564            None => Err(Error::CommandReplyParseFailed(
565                "did not receive a ServiceID".to_string(),
566            )),
567        }
568    }
569
570    pub fn del_onion(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> {
571        self.del_onion_cmd(service_id).and_then(reply_ok).map(|_| ())
572    }
573
574    // more specific encapulsation of specific command invocations
575
576    pub fn getinfo_net_listeners_socks(&mut self) -> Result<Vec<SocketAddr>, Error> {
577        let response = self.getinfo(&["net/listeners/socks"])?;
578        for (key, value) in response.iter() {
579            if key.as_str() == "net/listeners/socks" {
580                if value.is_empty() {
581                    return Ok(Default::default());
582                }
583                // get our list of double-quoted strings
584                let listeners: Vec<&str> = value.split(' ').collect();
585                let mut result: Vec<SocketAddr> = Default::default();
586                for socket_addr in listeners.iter() {
587                    if !socket_addr.starts_with('\"') || !socket_addr.ends_with('\"') {
588                        return Err(Error::CommandReplyParseFailed(format!(
589                            "could not parse '{}' as socket address",
590                            socket_addr
591                        )));
592                    }
593
594                    // remove leading/trailing double quote
595                    let stripped = &socket_addr[1..socket_addr.len() - 1];
596                    result.push(match SocketAddr::from_str(stripped) {
597                        Ok(result) => result,
598                        Err(_) => {
599                            return Err(Error::CommandReplyParseFailed(format!(
600                                "could not parse '{}' as socket address",
601                                socket_addr
602                            )))
603                        }
604                    });
605                }
606                return Ok(result);
607            }
608        }
609        Err(Error::CommandReplyParseFailed(
610            "reply did not find a 'net/listeners/socks' key/value".to_string(),
611        ))
612    }
613
614    pub fn getinfo_version(&mut self) -> Result<LegacyTorVersion, Error> {
615        let response = self.getinfo(&["version"])?;
616        for (key, value) in response.iter() {
617            if key.as_str() == "version" {
618                return LegacyTorVersion::from_str(value).map_err(Error::TorVersionParseFailed);
619            }
620        }
621        Err(Error::CommandReplyParseFailed(
622            "did not find a 'version' key/value".to_string(),
623        ))
624    }
625
626    pub fn onion_client_auth_add(
627        &mut self,
628        service_id: &V3OnionServiceId,
629        private_key: &X25519PrivateKey,
630        client_name: Option<String>,
631        flags: &OnionClientAuthAddFlags,
632    ) -> Result<(), Error> {
633        let reply = self.onion_client_auth_add_cmd(service_id, private_key, client_name, flags)?;
634
635        match reply.status_code {
636            250u32..=252u32 => Ok(()),
637            code => Err(Error::CommandFailed(code, reply.reply_lines)),
638        }
639    }
640
641    #[allow(dead_code)]
642    pub fn onion_client_auth_remove(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> {
643        let reply = self.onion_client_auth_remove_cmd(service_id)?;
644
645        match reply.status_code {
646            250u32..=251u32 => Ok(()),
647            code => Err(Error::CommandFailed(code, reply.reply_lines)),
648        }
649    }
650}
651
652#[test]
653#[serial]
654fn test_tor_controller() -> anyhow::Result<()> {
655    let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?;
656    let mut data_path = std::env::temp_dir();
657    data_path.push("test_tor_controller");
658    let tor_process = LegacyTorProcess::new(&tor_path, &data_path)?;
659
660    // create a scope to ensure tor_controller is dropped
661    {
662        let control_stream =
663            LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?;
664
665        // create a tor controller and send authentication command
666        let mut tor_controller = LegacyTorController::new(control_stream)?;
667        tor_controller.authenticate_cmd(tor_process.get_password())?;
668        assert!(
669            tor_controller
670                .authenticate_cmd("invalid password")?
671                .status_code
672                == 515u32
673        );
674
675        // tor controller should have shutdown the connection after failed authentication
676        assert!(
677            tor_controller
678                .authenticate_cmd(tor_process.get_password())
679                .is_err(),
680            "expected failure due to closed connection"
681        );
682        assert!(tor_controller.control_stream.closed_by_remote());
683    }
684    // now create a second controller
685    {
686        let control_stream =
687            LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?;
688
689        // create a tor controller and send authentication command
690        // all async events are just printed to stdout
691        let mut tor_controller = LegacyTorController::new(control_stream)?;
692        tor_controller.authenticate(tor_process.get_password())?;
693
694        // ensure everything is matching our default_torrc settings
695        let vals = tor_controller.getconf(&["SocksPort", "AvoidDiskWrites", "DisableNetwork"])?;
696        for (key, value) in vals.iter() {
697            let expected = match key.as_str() {
698                "SocksPort" => "auto",
699                "AvoidDiskWrites" => "1",
700                "DisableNetwork" => "1",
701                _ => panic!("unexpected returned key: {}", key),
702            };
703            assert!(value == expected);
704        }
705
706        let vals = tor_controller.getinfo(&["version", "config-file", "config-text"])?;
707        let mut expected_torrc_path = data_path.clone();
708        expected_torrc_path.push("torrc");
709        let mut expected_control_port_path = data_path.clone();
710        expected_control_port_path.push("control_port");
711        for (key, value) in vals.iter() {
712            match key.as_str() {
713                "version" => assert!(Regex::new(r"\d+\.\d+\.\d+\.\d+")?.is_match(&value)),
714                "config-file" => assert!(Path::new(&value) == expected_torrc_path),
715                "config-text" => assert!(
716                    value.to_string()
717                        == format!(
718                            "\nControlPort auto\nControlPortWriteToFile {}\nDataDirectory {}",
719                            expected_control_port_path.display(),
720                            data_path.display()
721                        )
722                ),
723                _ => panic!("unexpected returned key: {}", key),
724            }
725        }
726
727        tor_controller.setevents(&["STATUS_CLIENT"])?;
728        // begin bootstrap
729        tor_controller.setconf(&[("DisableNetwork", "0".to_string())])?;
730
731        // add an onoin service
732        let (private_key, service_id) =
733            match tor_controller.add_onion(None, &Default::default(), None, 22, None, None)? {
734                (Some(private_key), service_id) => (private_key, service_id),
735                _ => panic!("add_onion did not return expected values"),
736            };
737        println!("private_key: {}", private_key.to_key_blob());
738        println!("service_id: {}", service_id.to_string());
739
740        assert!(
741            tor_controller
742                .del_onion(&V3OnionServiceId::from_string(
743                    "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd"
744                )?)
745                .is_err(),
746            "deleting unknown onion should have failed"
747        );
748
749        // delete our new onion
750        tor_controller.del_onion(&service_id)?;
751
752        println!("listeners: ");
753        for sock_addr in tor_controller.getinfo_net_listeners_socks()?.iter() {
754            println!(" {}", sock_addr);
755        }
756
757        // print our event names available to tor
758        for (key, value) in tor_controller.getinfo(&["events/names"])?.iter() {
759            println!("{} : {}", key, value);
760        }
761
762        let stop_time = Instant::now() + std::time::Duration::from_secs(5);
763        while stop_time > Instant::now() {
764            for async_event in tor_controller.wait_async_events()?.iter() {
765                match async_event {
766                    AsyncEvent::Unknown { lines } => {
767                        println!("Unknown: {}", lines.join("\n"));
768                    }
769                    AsyncEvent::StatusClient {
770                        severity,
771                        action,
772                        arguments,
773                    } => {
774                        println!("STATUS_CLIENT severity={}, action={}", severity, action);
775                        for (key, value) in arguments.iter() {
776                            println!(" {}='{}'", key, value);
777                        }
778                    }
779                    AsyncEvent::HsDesc { action, hs_address } => {
780                        println!(
781                            "HS_DESC action={}, hsaddress={}",
782                            action,
783                            hs_address.to_string()
784                        );
785                    }
786                }
787            }
788        }
789    }
790    Ok(())
791}