1use 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
12use data_encoding::{HEXLOWER, HEXUPPER};
14use hmac::Mac;
15use rand::RngCore;
16use regex::Regex;
17#[cfg(test)]
18use serial_test::serial;
19use zeroize::Zeroize;
20
21use 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#[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 control_stream: LegacyControlStream,
97 async_replies: Vec<Reply>,
99 status_event_pattern: Regex,
101 status_event_argument_pattern: Regex,
102 hs_desc_pattern: Regex,
103 authchallenge_pattern: Regex,
104}
105
106#[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 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 status_event_pattern,
159 status_event_argument_pattern,
160 hs_desc_pattern,
161 authchallenge_pattern,
162 })
163 }
164
165 fn wait_async_replies(&mut self) -> Result<Vec<Reply>, Error> {
168 let mut replies: Vec<Reply> = Default::default();
169 std::mem::swap(&mut self.async_replies, &mut replies);
171
172 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 if let Some(max_streams) = max_streams {
430 command_buffer.push(format!("MaxStreams={}", max_streams));
431 }
432
433 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 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 let command = command_buffer.join(" ");
448
449 self.write_command(&command)
450 }
451
452 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 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 command_buffer.push(service_id.to_string());
471
472 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 let command = command_buffer.join(" ");
485
486 self.write_command(&command)
487 }
488
489 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 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 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 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 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 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 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 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 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 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 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 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 {
851 let control_stream =
852 LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?;
853
854 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 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 {
881 let control_stream =
882 LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?;
883
884 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 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 tor_controller.setconf(&[("DisableNetwork", "0".to_string())])?;
927
928 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 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 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}