tor_interface/
legacy_tor_controller.rs

1// standard
2use std::default::Default;
3use std::io::{Read, Write};
4use std::net::SocketAddr;
5use std::option::Option;
6use std::path::PathBuf;
7use std::str::FromStr;
8use std::string::ToString;
9#[cfg(test)]
10use std::time::{Duration, Instant};
11
12// extern crates
13use data_encoding::{HEXLOWER, HEXUPPER};
14use hmac::Mac;
15use rand::RngCore;
16use regex::Regex;
17#[cfg(test)]
18use serial_test::serial;
19use zeroize::Zeroize;
20
21// internal crates
22use crate::legacy_tor_control_stream::*;
23#[cfg(test)]
24use crate::legacy_tor_process::*;
25use crate::legacy_tor_version::*;
26use crate::tor_crypto::*;
27
28#[derive(thiserror::Error, Debug)]
29pub enum Error {
30    #[error("response regex creation failed")]
31    ParsingRegexCreationFailed(#[source] regex::Error),
32
33    #[error("control stream read reply failed")]
34    ReadReplyFailed(#[source] crate::legacy_tor_control_stream::Error),
35
36    #[error("unexpected synchronous reply recieved")]
37    UnexpectedSynchonousReplyReceived(),
38
39    #[error("control stream write command failed")]
40    WriteCommandFailed(#[source] crate::legacy_tor_control_stream::Error),
41
42    #[error("invalid command arguments: {0}")]
43    InvalidCommandArguments(String),
44
45    #[error("command failed: {0} {}", .1.join("\n"))]
46    CommandFailed(u32, Vec<String>),
47
48    #[error("failed to parse command reply: {0}")]
49    CommandReplyParseFailed(String),
50
51    #[error("failed to parse received tor version")]
52    TorVersionParseFailed(#[source] crate::legacy_tor_version::Error),
53
54    #[error("unable to read cookie file: {1:?}")]
55    CookieFileReadFailed(#[source] std::io::Error, PathBuf),
56
57    #[error("cookie file invalid")]
58    CookieFileInvalid(PathBuf),
59
60    #[error("received serverhash invalid")]
61    ServerHashInvalid(),
62}
63
64// Per-command data
65#[derive(Default)]
66pub(crate) struct AddOnionFlags {
67    pub discard_pk: bool,
68    pub detach: bool,
69    pub v3_auth: bool,
70    pub non_anonymous: bool,
71    pub max_streams_close_circuit: bool,
72}
73
74#[derive(Default)]
75pub(crate) struct OnionClientAuthAddFlags {
76    pub permanent: bool,
77}
78
79pub(crate) enum AsyncEvent {
80    Unknown {
81        lines: Vec<String>,
82    },
83    StatusClient {
84        severity: String,
85        action: String,
86        arguments: Vec<(String, String)>,
87    },
88    HsDesc {
89        action: String,
90        hs_address: V3OnionServiceId,
91    },
92}
93
94pub(crate) struct LegacyTorController {
95    // underlying control stream
96    control_stream: LegacyControlStream,
97    // list of async replies to be handled
98    async_replies: Vec<Reply>,
99    // regex for parsing events
100    status_event_pattern: Regex,
101    status_event_argument_pattern: Regex,
102    hs_desc_pattern: Regex,
103    authchallenge_pattern: Regex,
104}
105
106// tor authenticatin methods
107// SAFECOOKIE support was introduced in version 0.2.3.13-alpha which is
108// much older than our required version 0.4.6.1, so lets not bother
109// with the older COOKIEFILE method
110#[derive(Zeroize)]
111enum AuthenticateMethod {
112    #[zeroize(skip)] Null,
113    HashedPassword(String),
114    SafeCookie([u8; 32]),
115}
116
117fn quoted_string(string: &str) -> String {
118    // replace \ with \\ and " with \"
119    // see: https://spec.torproject.org/control-spec/message-format.html?highlight=QuotedString#description-format
120    string.replace("\\", "\\\\").replace("\"", "\\\"")
121}
122
123fn hmac_sha256(key: &str, blob1: &[u8], blob2: &[u8], blob3: &[u8]) -> hmac::Hmac<sha2::Sha256> {
124    let mut hmac = hmac::Hmac::new_from_slice(key.as_bytes()).unwrap();
125    hmac.update(blob1);
126    hmac.update(blob2);
127    hmac.update(blob3);
128    hmac
129}
130
131fn reply_ok(reply: Reply) -> Result<Reply, Error> {
132    match reply.status_code {
133        250u32 => Ok(reply),
134        code => Err(Error::CommandFailed(code, reply.reply_lines)),
135    }
136}
137
138impl LegacyTorController {
139    pub fn new(control_stream: LegacyControlStream) -> Result<LegacyTorController, Error> {
140        let status_event_pattern =
141            Regex::new(r#"^STATUS_CLIENT (?P<severity>NOTICE|WARN|ERR) (?P<action>[A-Za-z]+)"#)
142                .map_err(Error::ParsingRegexCreationFailed)?;
143        let status_event_argument_pattern =
144            Regex::new(r#"(?P<key>[A-Z]+)=(?P<value>[A-Za-z0-9_]+|"[^"]+")"#)
145                .map_err(Error::ParsingRegexCreationFailed)?;
146        let hs_desc_pattern =
147            Regex::new(r#"HS_DESC (?P<action>REQUESTED|UPLOAD|RECEIVED|UPLOADED|IGNORE|FAILED|CREATED) (?P<hsaddress>[a-z2-7]{56})"#)
148                .map_err(Error::ParsingRegexCreationFailed)?;
149        let authchallenge_pattern =
150            Regex::new(r#"AUTHCHALLENGE SERVERHASH=(?P<serverhash>[A-F0-9]{64}) SERVERNONCE=(?P<servernonce>[A-F0-9]{64})"#)
151                .map_err(Error::ParsingRegexCreationFailed)?;
152
153        Ok(LegacyTorController {
154            control_stream,
155            async_replies: Default::default(),
156            // regex
157            status_event_pattern,
158            status_event_argument_pattern,
159            hs_desc_pattern,
160            authchallenge_pattern,
161        })
162    }
163
164    // return curently available events, does not block waiting
165    // for an event
166    fn wait_async_replies(&mut self) -> Result<Vec<Reply>, Error> {
167        let mut replies: Vec<Reply> = Default::default();
168        // take any previously received async replies
169        std::mem::swap(&mut self.async_replies, &mut replies);
170
171        // and keep consuming until none are available
172        loop {
173            if let Some(reply) = self
174                .control_stream
175                .read_reply()
176                .map_err(Error::ReadReplyFailed)?
177            {
178                replies.push(reply);
179            } else {
180                // no more replies immediately available so return
181                return Ok(replies);
182            }
183        }
184    }
185
186    fn reply_to_event(&self, reply: &mut Reply) -> Result<AsyncEvent, Error> {
187        if reply.status_code != 650u32 {
188            return Err(Error::UnexpectedSynchonousReplyReceived());
189        }
190
191        // not sure this is what we want but yolo
192        let reply_text = reply.reply_lines.join(" ");
193        if let Some(caps) = self.status_event_pattern.captures(&reply_text) {
194            let severity = match caps.name("severity") {
195                Some(severity) => severity.as_str(),
196                None => unreachable!(),
197            };
198            let action = match caps.name("action") {
199                Some(action) => action.as_str(),
200                None => unreachable!(),
201            };
202
203            let mut arguments: Vec<(String, String)> = Default::default();
204            for caps in self
205                .status_event_argument_pattern
206                .captures_iter(&reply_text)
207            {
208                let key = match caps.name("key") {
209                    Some(key) => key.as_str(),
210                    None => unreachable!(),
211                };
212                let value = {
213                    let value = match caps.name("value") {
214                        Some(value) => value.as_str(),
215                        None => unreachable!(),
216                    };
217                    if value.starts_with('\"') && value.ends_with('\"') {
218                        &value[1..value.len() - 1]
219                    } else {
220                        value
221                    }
222                };
223                arguments.push((key.to_string(), value.to_string()));
224            }
225
226            return Ok(AsyncEvent::StatusClient {
227                severity: severity.to_string(),
228                action: action.to_string(),
229                arguments,
230            });
231        }
232
233        if let Some(caps) = self.hs_desc_pattern.captures(&reply_text) {
234            let action = match caps.name("action") {
235                Some(action) => action.as_str(),
236                None => unreachable!(),
237            };
238            let hs_address = match caps.name("hsaddress") {
239                Some(hs_address) => hs_address.as_str(),
240                None => unreachable!(),
241            };
242
243            if let Ok(hs_address) = V3OnionServiceId::from_string(hs_address) {
244                return Ok(AsyncEvent::HsDesc {
245                    action: action.to_string(),
246                    hs_address,
247                });
248            }
249        }
250
251        // no luck parsing reply, just return full text
252        let mut reply_lines: Vec<String> = Default::default();
253        std::mem::swap(&mut reply_lines, &mut reply.reply_lines);
254
255        Ok(AsyncEvent::Unknown { lines: reply_lines })
256    }
257
258    pub fn wait_async_events(&mut self) -> Result<Vec<AsyncEvent>, Error> {
259        let mut async_replies = self.wait_async_replies()?;
260        let mut async_events: Vec<AsyncEvent> = Default::default();
261
262        for reply in async_replies.iter_mut() {
263            async_events.push(self.reply_to_event(reply)?);
264        }
265
266        Ok(async_events)
267    }
268
269    // wait for a sync reply, save off async replies for later
270    fn wait_sync_reply(&mut self) -> Result<Reply, Error> {
271        loop {
272            if let Some(reply) = self
273                .control_stream
274                .read_reply()
275                .map_err(Error::ReadReplyFailed)?
276            {
277                match reply.status_code {
278                    650u32 => self.async_replies.push(reply),
279                    _ => return Ok(reply),
280                }
281            }
282        }
283    }
284
285    fn write_command(&mut self, text: &str) -> Result<Reply, Error> {
286        self.control_stream
287            .write(text)
288            .map_err(Error::WriteCommandFailed)?;
289        self.wait_sync_reply()
290    }
291
292    //
293    // Tor Commands
294    //
295    // The section where we can find the specification in control-spec.txt
296    // for the underlying command is listed in parentheses
297    //
298    // Each of these command wrapper methods block until completion
299    //
300
301    // SETCONF (3.1)
302    fn setconf_cmd(&mut self, key_values: &[(&str, String)]) -> Result<Reply, Error> {
303        if key_values.is_empty() {
304            return Err(Error::InvalidCommandArguments(
305                "SETCONF key-value pairs list must not be empty".to_string(),
306            ));
307        }
308        let mut command_buffer = vec!["SETCONF".to_string()];
309
310        for (key, value) in key_values.iter() {
311            command_buffer.push(format!("{}=\"{}\"", key, quoted_string(value.trim())));
312        }
313        let command = command_buffer.join(" ");
314
315        self.write_command(&command)
316    }
317
318    // GETCONF (3.3)
319    #[cfg(test)]
320    fn getconf_cmd(&mut self, keywords: &[&str]) -> Result<Reply, Error> {
321        if keywords.is_empty() {
322            return Err(Error::InvalidCommandArguments(
323                "GETCONF keywords list must not be empty".to_string(),
324            ));
325        }
326        let command = format!("GETCONF {}", keywords.join(" "));
327
328        self.write_command(&command)
329    }
330
331    // SETEVENTS (3.4)
332    fn setevents_cmd(&mut self, event_codes: &[&str]) -> Result<Reply, Error> {
333        if event_codes.is_empty() {
334            return Err(Error::InvalidCommandArguments(
335                "SETEVENTS event codes list mut not be empty".to_string(),
336            ));
337        }
338        let command = format!("SETEVENTS {}", event_codes.join(" "));
339
340        self.write_command(&command)
341    }
342
343    // AUTHENTICATE (3.5)
344    fn authenticate_cmd(&mut self, authenticate_method: AuthenticateMethod) -> Result<Reply, Error> {
345        let mut command = match authenticate_method {
346            AuthenticateMethod::Null => "AUTHENTICATE".to_string(),
347            AuthenticateMethod::HashedPassword(password) => {
348                let mut password = quoted_string(&password);
349                let command = format!("AUTHENTICATE \"{password}\"");
350                password.zeroize();
351                command
352            },
353            AuthenticateMethod::SafeCookie(clienthash) => {
354                let clienthash = HEXLOWER.encode(&clienthash);
355                format!("AUTHENTICATE {clienthash}")
356            }
357        };
358        let result = self.write_command(&command);
359        command.zeroize();
360        result
361    }
362
363    // AUTHCHALLENGE (3.24)
364    fn authchallenge_cmd(&mut self, client_nonce: [u8; 32]) -> Result<Reply, Error> {
365        let client_nonce = HEXLOWER.encode(&client_nonce);
366        let command = format!("AUTHCHALLENGE SAFECOOKIE {client_nonce}");
367
368        self.write_command(&command)
369    }
370
371    // GETINFO (3.9)
372    fn getinfo_cmd(&mut self, keywords: &[&str]) -> Result<Reply, Error> {
373        if keywords.is_empty() {
374            return Err(Error::InvalidCommandArguments(
375                "GETINFO keywords list must not be empty".to_string(),
376            ));
377        }
378        let command = format!("GETINFO {}", keywords.join(" "));
379
380        self.write_command(&command)
381    }
382
383    // ADD_ONION (3.27)
384    fn add_onion_cmd(
385        &mut self,
386        key: Option<&Ed25519PrivateKey>,
387        flags: &AddOnionFlags,
388        max_streams: Option<u16>,
389        virt_port: u16,
390        target: Option<SocketAddr>,
391        client_auth: Option<&[X25519PublicKey]>,
392    ) -> Result<Reply, Error> {
393        let mut command_buffer = vec!["ADD_ONION".to_string()];
394
395        // set our key or request a new one
396        if let Some(key) = key {
397            command_buffer.push(key.to_key_blob());
398        } else {
399            command_buffer.push("NEW:ED25519-V3".to_string());
400        }
401
402        // set our flags
403        let mut flag_buffer: Vec<&str> = Default::default();
404        if flags.discard_pk {
405            flag_buffer.push("DiscardPK");
406        }
407        if flags.detach {
408            flag_buffer.push("Detach");
409        }
410        if flags.v3_auth {
411            flag_buffer.push("V3Auth");
412        }
413        if flags.non_anonymous {
414            flag_buffer.push("NonAnonymous");
415        }
416        if flags.max_streams_close_circuit {
417            flag_buffer.push("MaxStreamsCloseCircuit");
418        }
419
420        if !flag_buffer.is_empty() {
421            command_buffer.push(format!("Flags={}", flag_buffer.join(",")));
422        }
423
424        // set max concurrent streams
425        if let Some(max_streams) = max_streams {
426            command_buffer.push(format!("MaxStreams={}", max_streams));
427        }
428
429        // set our onion service target
430        if let Some(target) = target {
431            command_buffer.push(format!("Port={},{}", virt_port, target));
432        } else {
433            command_buffer.push(format!("Port={}", virt_port));
434        }
435        // setup client auth
436        if let Some(client_auth) = client_auth {
437            for key in client_auth.iter() {
438                command_buffer.push(format!("ClientAuthV3={}", key.to_base32()));
439            }
440        }
441
442        // finally send the command
443        let command = command_buffer.join(" ");
444
445        self.write_command(&command)
446    }
447
448    // DEL_ONION (3.38)
449    fn del_onion_cmd(&mut self, service_id: &V3OnionServiceId) -> Result<Reply, Error> {
450        let command = format!("DEL_ONION {}", service_id);
451
452        self.write_command(&command)
453    }
454
455    // ONION_CLIENT_AUTH_ADD (3.30)
456    fn onion_client_auth_add_cmd(
457        &mut self,
458        service_id: &V3OnionServiceId,
459        private_key: &X25519PrivateKey,
460        client_name: Option<String>,
461        flags: &OnionClientAuthAddFlags,
462    ) -> Result<Reply, Error> {
463        let mut command_buffer = vec!["ONION_CLIENT_AUTH_ADD".to_string()];
464
465        // set the onion service id
466        command_buffer.push(service_id.to_string());
467
468        // set our client's private key
469        command_buffer.push(format!("x25519:{}", private_key.to_base64()));
470
471        if let Some(client_name) = client_name {
472            command_buffer.push(format!("ClientName={}", client_name));
473        }
474
475        if flags.permanent {
476            command_buffer.push("Flags=Permanent".to_string());
477        }
478
479        // finally send command
480        let command = command_buffer.join(" ");
481
482        self.write_command(&command)
483    }
484
485    // ONION_CLIENT_AUTH_REMOVE (3.31)
486    fn onion_client_auth_remove_cmd(
487        &mut self,
488        service_id: &V3OnionServiceId,
489    ) -> Result<Reply, Error> {
490        let command = format!("ONION_CLIENT_AUTH_REMOVE {}", service_id);
491
492        self.write_command(&command)
493    }
494
495    //
496    // Public high-level typesafe command method wrappers
497    //
498
499    pub fn setconf(&mut self, key_values: &[(&str, String)]) -> Result<(), Error> {
500        self.setconf_cmd(key_values).and_then(reply_ok).map(|_| ())
501    }
502
503    #[cfg(test)]
504    pub fn getconf(&mut self, keywords: &[&str]) -> Result<Vec<(String, String)>, Error> {
505        let reply = self.getconf_cmd(keywords).and_then(reply_ok)?;
506
507        let mut key_values: Vec<(String, String)> = Default::default();
508        for line in reply.reply_lines {
509            match line.find('=') {
510                Some(index) => key_values
511                    .push((line[0..index].to_string(), line[index + 1..].to_string())),
512                None => key_values.push((line, String::new())),
513            }
514        }
515        Ok(key_values)
516    }
517
518    pub fn setevents(&mut self, events: &[&str]) -> Result<(), Error> {
519        self.setevents_cmd(events).and_then(reply_ok).map(|_| ())
520    }
521
522    pub fn authenticate(&mut self) -> Result<(), Error> {
523        self.authenticate_cmd(AuthenticateMethod::Null).and_then(reply_ok).map(|_| ())
524    }
525
526    pub fn authenticate_password(&mut self, password: String) -> Result<(), Error> {
527        self.authenticate_cmd(AuthenticateMethod::HashedPassword(password)).and_then(reply_ok).map(|_| ())
528    }
529
530    fn read_cookie_file(cookie_file_path: PathBuf) -> Result<[u8; 32], Error> {
531        // All authentication cookies are 32 bytes long.  Controllers MUST NOT
532        // use the contents of a non-32-byte-long file as an authentication
533        // cookie./
534        let mut cookie_file = match std::fs::File::open(&cookie_file_path) {
535            Ok(cookie_file) => cookie_file,
536            Err(e) => return Err(Error::CookieFileReadFailed(e, cookie_file_path)),
537        };
538        // read first 32 bytes
539        let mut cookie = [0u8; 32];
540        match cookie_file.read_exact(&mut cookie) {
541            Ok(()) => (),
542            Err(_) => return Err(Error::CookieFileInvalid(cookie_file_path)),
543        }
544        // ensure no more bytes to read
545        let mut nonce = [0u8; 1];
546        match cookie_file.read_exact(&mut nonce) {
547            Ok(()) => return Err(Error::CookieFileInvalid(cookie_file_path)),
548            Err(_) => (),
549        }
550
551        Ok(cookie)
552    }
553
554    pub fn authenticate_safecookie(&mut self, cookiefile_path: PathBuf) -> Result<(), Error> {
555        let mut cookie = Self::read_cookie_file(cookiefile_path)?;
556
557        let mut clientnonce = [0u8; 32];
558        let csprng = &mut tor_llcrypto::rng::CautiousRng;
559        csprng.fill_bytes(&mut clientnonce);
560
561        // get the AUTHCHALLENGE response
562        let mut reply = self.authchallenge_cmd(clientnonce).and_then(reply_ok)?;
563
564        let reply_text = match reply.reply_lines.len() {
565            1 => reply.reply_lines.remove(0),
566            _ => return Err(Error::CommandReplyParseFailed("unexpected number of reply lines".to_string()))
567        };
568
569        // parse SERVERHASH and SERVERNONCE
570        let (serverhash, servernonce) = if let Some(caps) = self.authchallenge_pattern.captures(&reply_text) {
571            let serverhash = match caps.name("serverhash") {
572                Some(serverhash) => serverhash.as_str(),
573                None => unreachable!(),
574            };
575            let servernonce = match caps.name("servernonce") {
576                Some(servernonce) => servernonce.as_str(),
577                None => unreachable!(),
578            };
579            (serverhash, servernonce)
580        } else {
581            return Err(Error::CommandReplyParseFailed(format!("failed to parse AUTHCHALLENGE reply: {reply_text}")));
582        };
583
584        let serverhash = match HEXUPPER.decode(serverhash.as_bytes()) {
585            Ok(serverhash) => serverhash,
586            Err(_) => return Err(Error::CommandReplyParseFailed(format!("failed to parse AUTHCHALLENGE reply's SERVERHASH: {serverhash}"))),
587        };
588        let serverhash: [u8; 32] = serverhash.try_into().map_err(|_| Error::CommandReplyParseFailed("SERVERHASH wrong length".to_string()))?;
589
590        let servernonce = match HEXUPPER.decode(servernonce.as_bytes()) {
591            Ok(servernonce) => servernonce,
592            Err(_) => return Err(Error::CommandReplyParseFailed(format!("failed to parse AUTHCHALLENGE reply's SERVERNONCE: {servernonce}"))),
593        };
594        let servernonce: [u8; 32] = servernonce.try_into().map_err(|_| Error::CommandReplyParseFailed("SERVERNONCE wrong length".to_string()))?;
595
596        // verify the received SERVERHASH
597        const SERVER_TO_CONTROLLER_KEY: &str = "Tor safe cookie authentication server-to-controller hash";
598        let hmac = hmac_sha256(SERVER_TO_CONTROLLER_KEY, &cookie, &clientnonce, &servernonce);
599        hmac.verify_slice(&serverhash).map_err(|_| Error::ServerHashInvalid())?;
600
601        // construct CLIENTHASH
602        const CONTROLLER_TO_SERVER_KEY: &str = "Tor safe cookie authentication controller-to-server hash";
603        let hmac = hmac_sha256(CONTROLLER_TO_SERVER_KEY, &cookie, &clientnonce, &servernonce);
604        let clienthash: [u8; 32] = hmac.finalize().into_bytes().try_into().expect("");
605
606        cookie.zeroize();
607
608        self.authenticate_cmd(AuthenticateMethod::SafeCookie(clienthash)).and_then(reply_ok).map(|_| ())
609    }
610
611    pub fn getinfo(&mut self, keywords: &[&str]) -> Result<Vec<(String, String)>, Error> {
612        let reply = self.getinfo_cmd(keywords).and_then(reply_ok)?;
613
614        let mut key_values: Vec<(String, String)> = Default::default();
615        for line in reply.reply_lines {
616            match line.find('=') {
617                Some(index) => key_values
618                    .push((line[0..index].to_string(), line[index + 1..].to_string())),
619                None => {
620                    if line != "OK" {
621                        key_values.push((line, String::new()))
622                    }
623                }
624            }
625        }
626        Ok(key_values)
627    }
628
629    pub fn add_onion(
630        &mut self,
631        key: Option<&Ed25519PrivateKey>,
632        flags: &AddOnionFlags,
633        max_streams: Option<u16>,
634        virt_port: u16,
635        target: Option<SocketAddr>,
636        client_auth: Option<&[X25519PublicKey]>,
637    ) -> Result<(Option<Ed25519PrivateKey>, V3OnionServiceId), Error> {
638        let reply = self.add_onion_cmd(key, flags, max_streams, virt_port, target, client_auth).and_then(reply_ok)?;
639
640        let mut private_key: Option<Ed25519PrivateKey> = None;
641        let mut service_id: Option<V3OnionServiceId> = None;
642
643        for line in reply.reply_lines {
644            if let Some(mut index) = line.find("ServiceID=") {
645                if service_id.is_some() {
646                    return Err(Error::CommandReplyParseFailed(
647                        "received duplicate ServiceID entries".to_string(),
648                    ));
649                }
650                index += "ServiceId=".len();
651                let service_id_string = &line[index..];
652                service_id = match V3OnionServiceId::from_string(service_id_string) {
653                    Ok(service_id) => Some(service_id),
654                    Err(_) => {
655                        return Err(Error::CommandReplyParseFailed(format!(
656                            "could not parse '{}' as V3OnionServiceId",
657                            service_id_string
658                        )))
659                    }
660                }
661            } else if let Some(mut index) = line.find("PrivateKey=") {
662                if private_key.is_some() {
663                    return Err(Error::CommandReplyParseFailed(
664                        "received duplicate PrivateKey entries".to_string(),
665                    ));
666                }
667                index += "PrivateKey=".len();
668                let key_blob_string = &line[index..];
669                private_key = match Ed25519PrivateKey::from_key_blob_legacy(key_blob_string)
670                {
671                    Ok(private_key) => Some(private_key),
672                    Err(_) => {
673                        return Err(Error::CommandReplyParseFailed(format!(
674                            "could not parse {} as Ed25519PrivateKey",
675                            key_blob_string
676                        )))
677                    }
678                };
679            } else if line.contains("ClientAuthV3=") {
680                if client_auth.unwrap_or_default().is_empty() {
681                    return Err(Error::CommandReplyParseFailed(
682                        "recieved unexpected ClientAuthV3 keys".to_string(),
683                    ));
684                }
685            } else if !line.contains("OK") {
686                return Err(Error::CommandReplyParseFailed(format!(
687                    "received unexpected reply line '{}'",
688                    line
689                )));
690            }
691        }
692
693        if flags.discard_pk {
694            if private_key.is_some() {
695                return Err(Error::CommandReplyParseFailed(
696                    "PrivateKey response should have been discard".to_string(),
697                ));
698            }
699        } else if private_key.is_none() {
700            return Err(Error::CommandReplyParseFailed(
701                "did not receive a PrivateKey".to_string(),
702            ));
703        }
704
705        match service_id {
706            Some(service_id) => Ok((private_key, service_id)),
707            None => Err(Error::CommandReplyParseFailed(
708                "did not receive a ServiceID".to_string(),
709            )),
710        }
711    }
712
713    pub fn del_onion(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> {
714        self.del_onion_cmd(service_id).and_then(reply_ok).map(|_| ())
715    }
716
717    // more specific encapulsation of specific command invocations
718
719    pub fn getinfo_net_listeners_socks(&mut self) -> Result<Vec<SocketAddr>, Error> {
720        let response = self.getinfo(&["net/listeners/socks"])?;
721        for (key, value) in response.iter() {
722            if key.as_str() == "net/listeners/socks" {
723                if value.is_empty() {
724                    return Ok(Default::default());
725                }
726                // get our list of double-quoted strings
727                let listeners: Vec<&str> = value.split(' ').collect();
728                let mut result: Vec<SocketAddr> = Default::default();
729                for socket_addr in listeners.iter() {
730                    if !socket_addr.starts_with('\"') || !socket_addr.ends_with('\"') {
731                        return Err(Error::CommandReplyParseFailed(format!(
732                            "could not parse '{}' as socket address",
733                            socket_addr
734                        )));
735                    }
736
737                    // remove leading/trailing double quote
738                    let stripped = &socket_addr[1..socket_addr.len() - 1];
739                    result.push(match SocketAddr::from_str(stripped) {
740                        Ok(result) => result,
741                        Err(_) => {
742                            return Err(Error::CommandReplyParseFailed(format!(
743                                "could not parse '{}' as socket address",
744                                socket_addr
745                            )))
746                        }
747                    });
748                }
749                return Ok(result);
750            }
751        }
752        Err(Error::CommandReplyParseFailed(
753            "reply did not find a 'net/listeners/socks' key/value".to_string(),
754        ))
755    }
756
757    pub fn getinfo_version(&mut self) -> Result<LegacyTorVersion, Error> {
758        let response = self.getinfo(&["version"])?;
759        for (key, value) in response.iter() {
760            if key.as_str() == "version" {
761                return LegacyTorVersion::from_str(value).map_err(Error::TorVersionParseFailed);
762            }
763        }
764        Err(Error::CommandReplyParseFailed(
765            "did not find a 'version' key/value".to_string(),
766        ))
767    }
768
769    pub fn onion_client_auth_add(
770        &mut self,
771        service_id: &V3OnionServiceId,
772        private_key: &X25519PrivateKey,
773        client_name: Option<String>,
774        flags: &OnionClientAuthAddFlags,
775    ) -> Result<(), Error> {
776        let reply = self.onion_client_auth_add_cmd(service_id, private_key, client_name, flags)?;
777
778        match reply.status_code {
779            250u32..=252u32 => Ok(()),
780            code => Err(Error::CommandFailed(code, reply.reply_lines)),
781        }
782    }
783
784    #[allow(dead_code)]
785    pub fn onion_client_auth_remove(&mut self, service_id: &V3OnionServiceId) -> Result<(), Error> {
786        let reply = self.onion_client_auth_remove_cmd(service_id)?;
787
788        match reply.status_code {
789            250u32..=251u32 => Ok(()),
790            code => Err(Error::CommandFailed(code, reply.reply_lines)),
791        }
792    }
793}
794
795#[test]
796#[serial]
797fn test_tor_controller() -> anyhow::Result<()> {
798    let tor_path = which::which(format!("tor{}", std::env::consts::EXE_SUFFIX))?;
799    let mut data_path = std::env::temp_dir();
800    data_path.push("test_tor_controller");
801    let tor_process = LegacyTorProcess::new(&tor_path, &data_path)?;
802
803    // create a scope to ensure tor_controller is dropped
804    {
805        let control_stream =
806            LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?;
807
808        // create a tor controller and send authentication command
809        let mut tor_controller = LegacyTorController::new(control_stream)?;
810        tor_controller.authenticate_cmd(AuthenticateMethod::HashedPassword(tor_process.get_password().to_string()))?;
811        assert!(
812            tor_controller
813                .authenticate_cmd(AuthenticateMethod::HashedPassword("invalid password".to_string()))?
814                .status_code
815                == 515u32
816        );
817
818        // tor controller should have shutdown the connection after failed authentication
819        assert!(
820            tor_controller
821                .authenticate_cmd(AuthenticateMethod::HashedPassword(tor_process.get_password().to_string()))
822                .is_err(),
823            "expected failure due to closed connection"
824        );
825        assert!(tor_controller.control_stream.closed_by_remote());
826    }
827    // now create a second controller
828    {
829        let control_stream =
830            LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?;
831
832        // create a tor controller and send authentication command
833        // all async events are just printed to stdout
834        let mut tor_controller = LegacyTorController::new(control_stream)?;
835        tor_controller.authenticate_cmd(AuthenticateMethod::HashedPassword(tor_process.get_password().to_string()))?;
836
837        // ensure everything is matching our default_torrc settings
838        let vals = tor_controller.getconf(&["SocksPort", "AvoidDiskWrites", "DisableNetwork"])?;
839        for (key, value) in vals.iter() {
840            let expected = match key.as_str() {
841                "SocksPort" => "auto",
842                "AvoidDiskWrites" => "1",
843                "DisableNetwork" => "1",
844                _ => panic!("unexpected returned key: {}", key),
845            };
846            assert!(value == expected);
847        }
848
849        let vals = tor_controller.getinfo(&["version", "config-file", "config-text"])?;
850        let mut expected_torrc_path = data_path.clone();
851        expected_torrc_path.push("torrc");
852        let mut expected_control_port_path = data_path.clone();
853        expected_control_port_path.push("control_port");
854        for (key, value) in vals.iter() {
855            match key.as_str() {
856                "version" => assert!(Regex::new(r"\d+\.\d+\.\d+\.\d+")?.is_match(&value)),
857                "config-file" => assert!(std::path::Path::new(&value) == expected_torrc_path),
858                "config-text" => assert!(
859                    value.to_string()
860                        == format!(
861                            "\nControlPort auto\nControlPortWriteToFile {}\nDataDirectory {}",
862                            expected_control_port_path.display(),
863                            data_path.display()
864                        )
865                ),
866                _ => panic!("unexpected returned key: {}", key),
867            }
868        }
869
870        tor_controller.setevents(&["STATUS_CLIENT"])?;
871        // begin bootstrap
872        tor_controller.setconf(&[("DisableNetwork", "0".to_string())])?;
873
874        // add an onoin service
875        let (private_key, service_id) =
876            match tor_controller.add_onion(None, &Default::default(), None, 22, None, None)? {
877                (Some(private_key), service_id) => (private_key, service_id),
878                _ => panic!("add_onion did not return expected values"),
879            };
880        println!("private_key: {}", private_key.to_key_blob());
881        println!("service_id: {}", service_id.to_string());
882
883        assert!(
884            tor_controller
885                .del_onion(&V3OnionServiceId::from_string(
886                    "6l62fw7tqctlu5fesdqukvpoxezkaxbzllrafa2ve6ewuhzphxczsjyd"
887                )?)
888                .is_err(),
889            "deleting unknown onion should have failed"
890        );
891
892        // delete our new onion
893        tor_controller.del_onion(&service_id)?;
894
895        println!("listeners: ");
896        for sock_addr in tor_controller.getinfo_net_listeners_socks()?.iter() {
897            println!(" {}", sock_addr);
898        }
899
900        // print our event names available to tor
901        for (key, value) in tor_controller.getinfo(&["events/names"])?.iter() {
902            println!("{} : {}", key, value);
903        }
904
905        let stop_time = Instant::now() + std::time::Duration::from_secs(5);
906        while stop_time > Instant::now() {
907            for async_event in tor_controller.wait_async_events()?.iter() {
908                match async_event {
909                    AsyncEvent::Unknown { lines } => {
910                        println!("Unknown: {}", lines.join("\n"));
911                    }
912                    AsyncEvent::StatusClient {
913                        severity,
914                        action,
915                        arguments,
916                    } => {
917                        println!("STATUS_CLIENT severity={}, action={}", severity, action);
918                        for (key, value) in arguments.iter() {
919                            println!(" {}='{}'", key, value);
920                        }
921                    }
922                    AsyncEvent::HsDesc { action, hs_address } => {
923                        println!(
924                            "HS_DESC action={}, hsaddress={}",
925                            action,
926                            hs_address.to_string()
927                        );
928                    }
929                }
930            }
931        }
932    }
933    Ok(())
934}