1use 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
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)] Null,
113 HashedPassword(String),
114 SafeCookie([u8; 32]),
115}
116
117fn quoted_string(string: &str) -> String {
118 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 status_event_pattern,
158 status_event_argument_pattern,
159 hs_desc_pattern,
160 authchallenge_pattern,
161 })
162 }
163
164 fn wait_async_replies(&mut self) -> Result<Vec<Reply>, Error> {
167 let mut replies: Vec<Reply> = Default::default();
168 std::mem::swap(&mut self.async_replies, &mut replies);
170
171 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 if let Some(max_streams) = max_streams {
426 command_buffer.push(format!("MaxStreams={}", max_streams));
427 }
428
429 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 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 let command = command_buffer.join(" ");
444
445 self.write_command(&command)
446 }
447
448 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 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 command_buffer.push(service_id.to_string());
467
468 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 let command = command_buffer.join(" ");
481
482 self.write_command(&command)
483 }
484
485 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 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 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 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 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 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 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 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 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 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 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 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 {
805 let control_stream =
806 LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?;
807
808 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 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 {
829 let control_stream =
830 LegacyControlStream::new(tor_process.get_control_addr(), Duration::from_millis(16))?;
831
832 let mut tor_controller = LegacyTorController::new(control_stream)?;
835 tor_controller.authenticate_cmd(AuthenticateMethod::HashedPassword(tor_process.get_password().to_string()))?;
836
837 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 tor_controller.setconf(&[("DisableNetwork", "0".to_string())])?;
873
874 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 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 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}