1use std::fmt;
7use std::path::{Path, PathBuf};
8
9use serde::Serialize;
10use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
11use tokio::net::TcpStream;
12#[cfg(unix)]
13use tokio::net::UnixStream;
14use tracing::debug;
15
16#[derive(Debug)]
22pub enum TorControlError {
23 ConnectionFailed(String),
25 AuthFailed(String),
27 ProtocolError(String),
29 Io(std::io::Error),
31}
32
33impl fmt::Display for TorControlError {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::ConnectionFailed(msg) => write!(f, "control port connection failed: {}", msg),
37 Self::AuthFailed(msg) => write!(f, "control port auth failed: {}", msg),
38 Self::ProtocolError(msg) => write!(f, "control protocol error: {}", msg),
39 Self::Io(e) => write!(f, "control port I/O error: {}", e),
40 }
41 }
42}
43
44impl std::error::Error for TorControlError {}
45
46impl From<std::io::Error> for TorControlError {
47 fn from(e: std::io::Error) -> Self {
48 Self::Io(e)
49 }
50}
51
52#[derive(Debug, Clone)]
58pub enum ControlAuth {
59 Cookie(PathBuf),
61 Password(String),
63}
64
65impl ControlAuth {
66 pub fn from_config(auth_str: &str, default_cookie_path: &str) -> Result<Self, TorControlError> {
71 if auth_str == "cookie" {
72 Ok(Self::Cookie(PathBuf::from(default_cookie_path)))
73 } else if let Some(path) = auth_str.strip_prefix("cookie:") {
74 Ok(Self::Cookie(PathBuf::from(path)))
75 } else if let Some(password) = auth_str.strip_prefix("password:") {
76 Ok(Self::Password(password.to_string()))
77 } else {
78 Err(TorControlError::AuthFailed(format!(
79 "unknown control_auth format '{}': expected 'cookie', 'cookie:/path', or 'password:secret'",
80 auth_str
81 )))
82 }
83 }
84}
85
86#[derive(Debug, Clone, Serialize)]
92pub struct TorMonitoringInfo {
93 pub bootstrap: u8,
95 pub circuit_established: bool,
97 pub traffic_read: u64,
99 pub traffic_written: u64,
101 pub network_liveness: String,
103 pub version: String,
105 pub dormant: bool,
107}
108
109pub struct TorControlClient {
120 reader: BufReader<Box<dyn AsyncRead + Unpin + Send>>,
121 writer: Box<dyn AsyncWrite + Unpin + Send>,
122}
123
124impl fmt::Debug for TorControlClient {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 f.debug_struct("TorControlClient").finish_non_exhaustive()
127 }
128}
129
130impl TorControlClient {
131 pub async fn connect(addr: &str) -> Result<Self, TorControlError> {
141 #[cfg(unix)]
142 if is_unix_socket_path(addr) {
143 return Self::connect_unix(addr).await;
144 }
145 Self::connect_tcp(addr).await
146 }
147
148 async fn connect_tcp(addr: &str) -> Result<Self, TorControlError> {
150 let stream = TcpStream::connect(addr).await.map_err(|e| {
151 TorControlError::ConnectionFailed(format!(
152 "failed to connect to control port {}: {}",
153 addr, e
154 ))
155 })?;
156
157 let (read_half, write_half) = stream.into_split();
158
159 debug!(addr = %addr, transport = "tcp", "Connected to Tor control port");
160
161 Ok(Self {
162 reader: BufReader::new(Box::new(read_half)),
163 writer: Box::new(write_half),
164 })
165 }
166
167 #[cfg(unix)]
169 async fn connect_unix(path: &str) -> Result<Self, TorControlError> {
170 let stream = UnixStream::connect(path).await.map_err(|e| {
171 TorControlError::ConnectionFailed(format!(
172 "failed to connect to control socket {}: {}",
173 path, e
174 ))
175 })?;
176
177 let (read_half, write_half) = stream.into_split();
178
179 debug!(path = %path, transport = "unix", "Connected to Tor control port");
180
181 Ok(Self {
182 reader: BufReader::new(Box::new(read_half)),
183 writer: Box::new(write_half),
184 })
185 }
186
187 pub async fn authenticate(&mut self, auth: &ControlAuth) -> Result<(), TorControlError> {
189 let command = match auth {
190 ControlAuth::Cookie(path) => {
191 let cookie = read_cookie_file(path)?;
192 format!("AUTHENTICATE {}\r\n", hex::encode(cookie))
193 }
194 ControlAuth::Password(password) => {
195 let escaped = password.replace('\\', "\\\\").replace('"', "\\\"");
197 format!("AUTHENTICATE \"{}\"\r\n", escaped)
198 }
199 };
200
201 self.send_command(&command).await?;
202 let response = self.read_response().await?;
203
204 if response.code != 250 {
205 return Err(TorControlError::AuthFailed(format!(
206 "AUTHENTICATE failed: {} {}",
207 response.code, response.message
208 )));
209 }
210
211 debug!("Authenticated with Tor control port");
212 Ok(())
213 }
214
215 async fn getinfo(&mut self, key: &str) -> Result<String, TorControlError> {
224 let command = format!("GETINFO {}\r\n", key);
225 self.send_command(&command).await?;
226 let response = self.read_response().await?;
227
228 if response.code != 250 {
229 return Err(TorControlError::ProtocolError(format!(
230 "GETINFO {} failed: {} {}",
231 key, response.code, response.message
232 )));
233 }
234
235 let prefix = format!("{}=", key);
236 for line in &response.data_lines {
237 if let Some(value) = line.strip_prefix(&prefix) {
238 return Ok(value.to_string());
239 }
240 }
241
242 Err(TorControlError::ProtocolError(format!(
243 "GETINFO response missing key '{}'",
244 key
245 )))
246 }
247
248 pub async fn get_bootstrap_phase(&mut self) -> Result<u8, TorControlError> {
250 let raw = self.getinfo("status/bootstrap-phase").await?;
251
252 if let Some(progress_start) = raw.find("PROGRESS=") {
254 let after = &raw[progress_start + 9..];
255 let digits: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
256 if let Ok(progress) = digits.parse::<u8>() {
257 return Ok(progress);
258 }
259 }
260
261 Err(TorControlError::ProtocolError(
262 "could not parse bootstrap progress".into(),
263 ))
264 }
265
266 pub async fn is_circuit_established(&mut self) -> Result<bool, TorControlError> {
270 let value = self.getinfo("status/circuit-established").await?;
271 Ok(value.trim() == "1")
272 }
273
274 pub async fn traffic_read(&mut self) -> Result<u64, TorControlError> {
276 let value = self.getinfo("traffic/read").await?;
277 value.trim().parse::<u64>().map_err(|_| {
278 TorControlError::ProtocolError(format!("invalid traffic/read value: '{}'", value))
279 })
280 }
281
282 pub async fn traffic_written(&mut self) -> Result<u64, TorControlError> {
284 let value = self.getinfo("traffic/written").await?;
285 value.trim().parse::<u64>().map_err(|_| {
286 TorControlError::ProtocolError(format!("invalid traffic/written value: '{}'", value))
287 })
288 }
289
290 pub async fn network_liveness(&mut self) -> Result<String, TorControlError> {
294 self.getinfo("network-liveness").await
295 }
296
297 pub async fn version(&mut self) -> Result<String, TorControlError> {
299 self.getinfo("version").await
300 }
301
302 pub async fn is_dormant(&mut self) -> Result<bool, TorControlError> {
304 let value = self.getinfo("dormant").await?;
305 Ok(value.trim() == "1")
306 }
307
308 pub async fn socks_listeners(&mut self) -> Result<Vec<String>, TorControlError> {
312 let value = self.getinfo("net/listeners/socks").await?;
313 Ok(value
314 .split_whitespace()
315 .map(|s| s.trim_matches('"').to_string())
316 .collect())
317 }
318
319 pub async fn monitoring_snapshot(&mut self) -> Result<TorMonitoringInfo, TorControlError> {
321 let bootstrap = self.get_bootstrap_phase().await.unwrap_or(0);
322 let circuit_established = self.is_circuit_established().await.unwrap_or(false);
323 let traffic_read = self.traffic_read().await.unwrap_or(0);
324 let traffic_written = self.traffic_written().await.unwrap_or(0);
325 let network_liveness = self
326 .network_liveness()
327 .await
328 .unwrap_or_else(|_| "unknown".into());
329 let version = self.version().await.unwrap_or_else(|_| "unknown".into());
330 let dormant = self.is_dormant().await.unwrap_or(false);
331
332 Ok(TorMonitoringInfo {
333 bootstrap,
334 circuit_established,
335 traffic_read,
336 traffic_written,
337 network_liveness,
338 version,
339 dormant,
340 })
341 }
342
343 async fn send_command(&mut self, command: &str) -> Result<(), TorControlError> {
349 self.writer.write_all(command.as_bytes()).await?;
350 self.writer.flush().await?;
351 Ok(())
352 }
353
354 async fn read_response(&mut self) -> Result<ControlResponse, TorControlError> {
363 let mut data_lines = Vec::new();
364 let mut line_buf = String::new();
365
366 loop {
367 line_buf.clear();
368 let n = self.reader.read_line(&mut line_buf).await?;
369 if n == 0 {
370 return Err(TorControlError::ProtocolError(
371 "control port connection closed".into(),
372 ));
373 }
374
375 let line = line_buf.trim_end_matches(['\r', '\n']);
376
377 if line.len() < 4 {
378 return Err(TorControlError::ProtocolError(format!(
379 "response line too short: '{}'",
380 line
381 )));
382 }
383
384 let code: u16 = line[..3].parse().map_err(|_| {
385 TorControlError::ProtocolError(format!("invalid response code in: '{}'", line))
386 })?;
387
388 let separator = line.as_bytes()[3];
389 let content = &line[4..];
390
391 match separator {
392 b'-' => {
393 data_lines.push(content.to_string());
395 }
396 b' ' => {
397 return Ok(ControlResponse {
399 code,
400 message: content.to_string(),
401 data_lines,
402 });
403 }
404 b'+' => {
405 data_lines.push(content.to_string());
407 loop {
408 line_buf.clear();
409 let n = self.reader.read_line(&mut line_buf).await?;
410 if n == 0 {
411 return Err(TorControlError::ProtocolError(
412 "connection closed during multi-line response".into(),
413 ));
414 }
415 let dot_line = line_buf.trim_end_matches(['\r', '\n']);
416 if dot_line == "." {
417 break;
418 }
419 let unescaped = dot_line.strip_prefix('.').unwrap_or(dot_line);
421 data_lines.push(unescaped.to_string());
422 }
423 }
424 _ => {
425 return Err(TorControlError::ProtocolError(format!(
426 "unexpected separator '{}' in: '{}'",
427 separator as char, line
428 )));
429 }
430 }
431 }
432 }
433}
434
435struct ControlResponse {
437 code: u16,
439 message: String,
441 data_lines: Vec<String>,
443}
444
445fn read_cookie_file(path: &Path) -> Result<Vec<u8>, TorControlError> {
451 let data = std::fs::read(path).map_err(|e| {
452 TorControlError::AuthFailed(format!(
453 "failed to read cookie file '{}': {}",
454 path.display(),
455 e
456 ))
457 })?;
458
459 if data.len() != 32 {
460 return Err(TorControlError::AuthFailed(format!(
461 "cookie file '{}' has {} bytes, expected 32",
462 path.display(),
463 data.len()
464 )));
465 }
466
467 Ok(data)
468}
469
470#[cfg(unix)]
479fn is_unix_socket_path(addr: &str) -> bool {
480 addr.starts_with('/') || addr.starts_with("./")
481}
482
483mod hex {
488 const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
489
490 pub fn encode(data: Vec<u8>) -> String {
491 let mut s = String::with_capacity(data.len() * 2);
492 for byte in data {
493 s.push(HEX_CHARS[(byte >> 4) as usize] as char);
494 s.push(HEX_CHARS[(byte & 0x0f) as usize] as char);
495 }
496 s
497 }
498}
499
500#[cfg(test)]
505mod tests {
506 use super::*;
507 use crate::transport::tor::mock_control::{self, MockTorControlServer};
508 use tempfile::TempDir;
509
510 #[test]
513 fn test_control_auth_cookie_default() {
514 let auth = ControlAuth::from_config("cookie", "/var/run/tor/cookie").unwrap();
515 match auth {
516 ControlAuth::Cookie(path) => assert_eq!(path, Path::new("/var/run/tor/cookie")),
517 _ => panic!("expected Cookie"),
518 }
519 }
520
521 #[test]
522 fn test_control_auth_cookie_custom_path() {
523 let auth = ControlAuth::from_config("cookie:/tmp/my_cookie", "/default").unwrap();
524 match auth {
525 ControlAuth::Cookie(path) => assert_eq!(path, Path::new("/tmp/my_cookie")),
526 _ => panic!("expected Cookie"),
527 }
528 }
529
530 #[test]
531 fn test_control_auth_password() {
532 let auth = ControlAuth::from_config("password:mypass", "/default").unwrap();
533 match auth {
534 ControlAuth::Password(p) => assert_eq!(p, "mypass"),
535 _ => panic!("expected Password"),
536 }
537 }
538
539 #[test]
540 fn test_control_auth_invalid() {
541 let result = ControlAuth::from_config("unknown", "/default");
542 assert!(result.is_err());
543 }
544
545 #[test]
548 fn test_hex_encode() {
549 assert_eq!(hex::encode(vec![0xde, 0xad, 0xbe, 0xef]), "deadbeef");
550 assert_eq!(hex::encode(vec![0x00, 0xff]), "00ff");
551 }
552
553 #[cfg(unix)]
556 #[test]
557 fn test_is_unix_socket_path() {
558 assert!(is_unix_socket_path("/run/tor/control"));
559 assert!(is_unix_socket_path("/var/run/tor/control"));
560 assert!(is_unix_socket_path("./tor-control.sock"));
561 assert!(!is_unix_socket_path("127.0.0.1:9051"));
562 assert!(!is_unix_socket_path("tor-daemon:9051"));
563 assert!(!is_unix_socket_path("localhost:9051"));
564 }
565
566 #[cfg(unix)]
567 #[tokio::test]
568 async fn test_connect_unix_socket_nonexistent() {
569 let result = TorControlClient::connect("/tmp/nonexistent-tor-control.sock").await;
570 assert!(result.is_err());
571 let err = format!("{}", result.unwrap_err());
572 assert!(err.contains("control socket"));
573 }
574
575 #[cfg(unix)]
576 #[tokio::test]
577 async fn test_connect_unix_socket_roundtrip() {
578 let dir = TempDir::new().unwrap();
580 let sock_path = dir.path().join("control.sock");
581 let sock_path_str = sock_path.to_str().unwrap().to_string();
582
583 let listener = tokio::net::UnixListener::bind(&sock_path).unwrap();
584
585 let handle = tokio::spawn(async move {
587 let (stream, _) = listener.accept().await.unwrap();
588 let (reader, mut writer) = stream.into_split();
589 let mut reader = tokio::io::BufReader::new(reader);
590 let mut line = String::new();
591
592 reader.read_line(&mut line).await.unwrap();
594 assert!(line.starts_with("AUTHENTICATE"));
595
596 use tokio::io::AsyncWriteExt;
597 writer.write_all(b"250 OK\r\n").await.unwrap();
598 writer.flush().await.unwrap();
599 });
600
601 let mut client = TorControlClient::connect(&sock_path_str).await.unwrap();
602 let auth = ControlAuth::Password("test".to_string());
603 client.authenticate(&auth).await.unwrap();
604
605 handle.await.unwrap();
606 }
607
608 #[test]
611 fn test_read_cookie_file_valid() {
612 let dir = TempDir::new().unwrap();
613 let path = dir.path().join("cookie");
614 let cookie_data = vec![0xAA; 32];
615 std::fs::write(&path, &cookie_data).unwrap();
616
617 let loaded = read_cookie_file(&path).unwrap();
618 assert_eq!(loaded, cookie_data);
619 }
620
621 #[test]
622 fn test_read_cookie_file_wrong_size() {
623 let dir = TempDir::new().unwrap();
624 let path = dir.path().join("cookie");
625 std::fs::write(&path, [0u8; 16]).unwrap();
626
627 assert!(read_cookie_file(&path).is_err());
628 }
629
630 #[test]
631 fn test_read_cookie_file_nonexistent() {
632 assert!(read_cookie_file(Path::new("/nonexistent/cookie")).is_err());
633 }
634
635 #[tokio::test]
638 async fn test_authenticate_password() {
639 let mock = MockTorControlServer::start().await;
640 let mut client = TorControlClient::connect(&mock.addr().to_string())
641 .await
642 .unwrap();
643
644 let auth = ControlAuth::Password("testpass".to_string());
645 client.authenticate(&auth).await.unwrap();
646 }
647
648 #[tokio::test]
649 async fn test_authenticate_cookie() {
650 let mock = MockTorControlServer::start().await;
651
652 let dir = TempDir::new().unwrap();
654 let cookie_path = dir.path().join("cookie");
655 std::fs::write(&cookie_path, [0xAA; 32]).unwrap();
656
657 let mut client = TorControlClient::connect(&mock.addr().to_string())
658 .await
659 .unwrap();
660 let auth = ControlAuth::Cookie(cookie_path);
661 client.authenticate(&auth).await.unwrap();
662 }
663
664 #[tokio::test]
665 async fn test_get_bootstrap_phase() {
666 let mock = MockTorControlServer::start().await;
667 let mut client = TorControlClient::connect(&mock.addr().to_string())
668 .await
669 .unwrap();
670
671 let auth = ControlAuth::Password("testpass".to_string());
672 client.authenticate(&auth).await.unwrap();
673
674 let progress = client.get_bootstrap_phase().await.unwrap();
675 assert_eq!(progress, 100);
676 }
677
678 #[tokio::test]
679 async fn test_auth_failure() {
680 let mock = MockTorControlServer::start_with_options(mock_control::MockOptions {
681 reject_auth: true,
682 })
683 .await;
684 let mut client = TorControlClient::connect(&mock.addr().to_string())
685 .await
686 .unwrap();
687
688 let auth = ControlAuth::Password("wrongpass".to_string());
689 let result = client.authenticate(&auth).await;
690 assert!(result.is_err());
691 }
692
693 #[tokio::test]
694 async fn test_connect_to_closed_port() {
695 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
697 let addr = listener.local_addr().unwrap();
698 drop(listener);
699
700 let result = TorControlClient::connect(&addr.to_string()).await;
701 assert!(result.is_err());
702 }
703
704 #[tokio::test]
707 async fn test_is_circuit_established() {
708 let mock = MockTorControlServer::start().await;
709 let mut client = TorControlClient::connect(&mock.addr().to_string())
710 .await
711 .unwrap();
712 client
713 .authenticate(&ControlAuth::Password("test".into()))
714 .await
715 .unwrap();
716
717 assert!(client.is_circuit_established().await.unwrap());
718 }
719
720 #[tokio::test]
721 async fn test_traffic_counters() {
722 let mock = MockTorControlServer::start().await;
723 let mut client = TorControlClient::connect(&mock.addr().to_string())
724 .await
725 .unwrap();
726 client
727 .authenticate(&ControlAuth::Password("test".into()))
728 .await
729 .unwrap();
730
731 assert_eq!(client.traffic_read().await.unwrap(), 1048576);
732 assert_eq!(client.traffic_written().await.unwrap(), 524288);
733 }
734
735 #[tokio::test]
736 async fn test_network_liveness() {
737 let mock = MockTorControlServer::start().await;
738 let mut client = TorControlClient::connect(&mock.addr().to_string())
739 .await
740 .unwrap();
741 client
742 .authenticate(&ControlAuth::Password("test".into()))
743 .await
744 .unwrap();
745
746 assert_eq!(client.network_liveness().await.unwrap(), "up");
747 }
748
749 #[tokio::test]
750 async fn test_version() {
751 let mock = MockTorControlServer::start().await;
752 let mut client = TorControlClient::connect(&mock.addr().to_string())
753 .await
754 .unwrap();
755 client
756 .authenticate(&ControlAuth::Password("test".into()))
757 .await
758 .unwrap();
759
760 assert_eq!(client.version().await.unwrap(), "0.4.8.10");
761 }
762
763 #[tokio::test]
764 async fn test_dormant() {
765 let mock = MockTorControlServer::start().await;
766 let mut client = TorControlClient::connect(&mock.addr().to_string())
767 .await
768 .unwrap();
769 client
770 .authenticate(&ControlAuth::Password("test".into()))
771 .await
772 .unwrap();
773
774 assert!(!client.is_dormant().await.unwrap());
775 }
776
777 #[tokio::test]
778 async fn test_socks_listeners() {
779 let mock = MockTorControlServer::start().await;
780 let mut client = TorControlClient::connect(&mock.addr().to_string())
781 .await
782 .unwrap();
783 client
784 .authenticate(&ControlAuth::Password("test".into()))
785 .await
786 .unwrap();
787
788 let listeners = client.socks_listeners().await.unwrap();
789 assert_eq!(listeners, vec!["127.0.0.1:9050"]);
790 }
791
792 #[tokio::test]
793 async fn test_monitoring_snapshot() {
794 let mock = MockTorControlServer::start().await;
795 let mut client = TorControlClient::connect(&mock.addr().to_string())
796 .await
797 .unwrap();
798 client
799 .authenticate(&ControlAuth::Password("test".into()))
800 .await
801 .unwrap();
802
803 let info = client.monitoring_snapshot().await.unwrap();
804 assert_eq!(info.bootstrap, 100);
805 assert!(info.circuit_established);
806 assert_eq!(info.traffic_read, 1048576);
807 assert_eq!(info.traffic_written, 524288);
808 assert_eq!(info.network_liveness, "up");
809 assert_eq!(info.version, "0.4.8.10");
810 assert!(!info.dormant);
811 }
812}