tor_interface/
legacy_tor_controller.rs

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