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
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            // regex
114            status_event_pattern,
115            status_event_argument_pattern,
116            hs_desc_pattern,
117        })
118    }
119
120    // return curently available events, does not block waiting
121    // for an event
122    fn wait_async_replies(&mut self) -> Result<Vec<Reply>, Error> {
123        let mut replies: Vec<Reply> = Default::default();
124        // take any previously received async replies
125        std::mem::swap(&mut self.async_replies, &mut replies);
126
127        // and keep consuming until none are available
128        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                // no more replies immediately available so return
137                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        // not sure this is what we want but yolo
148        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        // no luck parsing reply, just return full text
208        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    // wait for a sync reply, save off async replies for later
226    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    //
249    // Tor Commands
250    //
251    // The section where we can find the specification in control-spec.txt
252    // for the underlying command is listed in parentheses
253    //
254    // Each of these command wrapper methods block until completion
255    //
256
257    // SETCONF (3.1)
258    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    // GETCONF (3.3)
275    #[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    // SETEVENTS (3.4)
288    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    // AUTHENTICATE (3.5)
300    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    // GETINFO (3.9)
307    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    // ADD_ONION (3.27)
319    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        // set our key or request a new one
331        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        // set our flags
338        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        // set max concurrent streams
360        if let Some(max_streams) = max_streams {
361            command_buffer.push(format!("MaxStreams={}", max_streams));
362        }
363
364        // set our onion service target
365        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        // setup client auth
371        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        // finally send the command
378        let command = command_buffer.join(" ");
379
380        self.write_command(&command)
381    }
382
383    // DEL_ONION (3.38)
384    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    // ONION_CLIENT_AUTH_ADD (3.30)
391    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        // set the onion service id
401        command_buffer.push(service_id.to_string());
402
403        // set our client's private key
404        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        // finally send command
415        let command = command_buffer.join(" ");
416
417        self.write_command(&command)
418    }
419
420    // ONION_CLIENT_AUTH_REMOVE (3.31)
421    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    //
431    // Public high-level typesafe command method wrappers
432    //
433
434    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    // more specific encapulsation of specific command invocations
603
604    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                // get our list of double-quoted strings
612                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                    // remove leading/trailing double quote
623                    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    // create a scope to ensure tor_controller is dropped
689    {
690        let control_stream =
691            LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?;
692
693        // create a tor controller and send authentication command
694        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        // tor controller should have shutdown the connection after failed authentication
704        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    // now create a second controller
713    {
714        let control_stream =
715            LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?;
716
717        // create a tor controller and send authentication command
718        // all async events are just printed to stdout
719        let mut tor_controller = LegacyTorController::new(control_stream)?;
720        tor_controller.authenticate(tor_process.get_password())?;
721
722        // ensure everything is matching our default_torrc settings
723        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        // begin bootstrap
757        tor_controller.setconf(&[("DisableNetwork", "0".to_string())])?;
758
759        // add an onoin service
760        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        // delete our new onion
778        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        // print our event names available to tor
786        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}