stem_rs/response/protocolinfo.rs
1//! PROTOCOLINFO response parsing.
2//!
3//! This module parses responses from the `PROTOCOLINFO` command, which provides
4//! information about available authentication methods and the Tor version.
5//! This is typically the first command sent after connecting to determine
6//! how to authenticate.
7//!
8//! # Response Format
9//!
10//! A typical PROTOCOLINFO response:
11//!
12//! ```text
13//! 250-PROTOCOLINFO 1
14//! 250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE="/home/user/.tor/control_auth_cookie"
15//! 250-VERSION Tor="0.4.7.1"
16//! 250 OK
17//! ```
18//!
19//! # Authentication Methods
20//!
21//! | Method | Description |
22//! |--------|-------------|
23//! | `NULL` | No authentication required |
24//! | `HASHEDPASSWORD` | Password authentication |
25//! | `COOKIE` | Cookie file authentication |
26//! | `SAFECOOKIE` | HMAC-based cookie authentication (most secure) |
27//!
28//! # Example
29//!
30//! ```rust
31//! use stem_rs::response::{ControlMessage, ProtocolInfoResponse, AuthMethod};
32//!
33//! let response_text = "250-PROTOCOLINFO 1\r\n\
34//! 250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"/tmp/cookie\"\r\n\
35//! 250-VERSION Tor=\"0.4.7.1\"\r\n\
36//! 250 OK\r\n";
37//! let msg = ControlMessage::from_str(response_text, None, false).unwrap();
38//! let response = ProtocolInfoResponse::from_message(&msg).unwrap();
39//!
40//! assert_eq!(response.protocol_version, 1);
41//! assert!(response.auth_methods.contains(&AuthMethod::Cookie));
42//! assert!(response.auth_methods.contains(&AuthMethod::SafeCookie));
43//! ```
44//!
45//! # See Also
46//!
47//! - [`crate::auth::get_protocol_info`]: High-level API for getting protocol info
48//! - [`crate::auth::authenticate`]: Uses this response to select auth method
49//! - [Tor Control Protocol: PROTOCOLINFO](https://spec.torproject.org/control-spec/commands.html#protocolinfo)
50
51use std::path::PathBuf;
52
53use super::{ControlLine, ControlMessage};
54use crate::version::Version;
55use crate::Error;
56
57/// Authentication methods supported by Tor's control protocol.
58///
59/// These correspond to the methods listed in the PROTOCOLINFO response's
60/// AUTH METHODS field.
61///
62/// # Security Comparison
63///
64/// | Method | Security | Use Case |
65/// |--------|----------|----------|
66/// | [`None`](AuthMethod::None) | None | Testing only |
67/// | [`Password`](AuthMethod::Password) | Low | Simple setups |
68/// | [`Cookie`](AuthMethod::Cookie) | Medium | Local connections |
69/// | [`SafeCookie`](AuthMethod::SafeCookie) | High | Recommended for local |
70///
71/// # Example
72///
73/// ```rust
74/// use stem_rs::response::{ControlMessage, ProtocolInfoResponse, AuthMethod};
75///
76/// let msg = ControlMessage::from_str(
77/// "250-PROTOCOLINFO 1\r\n\
78/// 250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"/tmp/cookie\"\r\n\
79/// 250 OK\r\n",
80/// None,
81/// false
82/// ).unwrap();
83///
84/// let response = ProtocolInfoResponse::from_message(&msg).unwrap();
85///
86/// // Check which methods are available
87/// if response.auth_methods.contains(&AuthMethod::SafeCookie) {
88/// println!("SafeCookie authentication available (recommended)");
89/// }
90/// ```
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum AuthMethod {
93 /// No authentication required (`NULL` in protocol).
94 ///
95 /// The control port is open without any authentication. This is
96 /// insecure and should only be used for testing.
97 None,
98
99 /// Password authentication (`HASHEDPASSWORD` in protocol).
100 ///
101 /// Requires the password configured via `HashedControlPassword` in torrc.
102 /// The password is sent in cleartext, so this is less secure than
103 /// cookie-based methods for local connections.
104 Password,
105
106 /// Cookie file authentication (`COOKIE` in protocol).
107 ///
108 /// Authenticates by proving access to a cookie file on disk.
109 /// The cookie path is provided in the PROTOCOLINFO response.
110 Cookie,
111
112 /// HMAC-based cookie authentication (`SAFECOOKIE` in protocol).
113 ///
114 /// The most secure authentication method for local connections.
115 /// Uses HMAC-SHA256 challenge-response to prove cookie knowledge
116 /// without transmitting the cookie itself.
117 SafeCookie,
118
119 /// An unrecognized authentication method.
120 ///
121 /// This variant is used when Tor advertises an authentication method
122 /// that this library doesn't recognize. The actual method name is
123 /// stored in [`ProtocolInfoResponse::unknown_auth_methods`].
124 Unknown,
125}
126
127impl AuthMethod {
128 /// Parses an authentication method from its protocol string.
129 fn from_str(s: &str) -> Self {
130 match s.to_uppercase().as_str() {
131 "NULL" => AuthMethod::None,
132 "HASHEDPASSWORD" => AuthMethod::Password,
133 "COOKIE" => AuthMethod::Cookie,
134 "SAFECOOKIE" => AuthMethod::SafeCookie,
135 _ => AuthMethod::Unknown,
136 }
137 }
138}
139
140/// Parsed response from the PROTOCOLINFO command.
141///
142/// Contains information about the Tor version and available authentication
143/// methods. This is typically used to determine how to authenticate with
144/// the control port.
145///
146/// # Example
147///
148/// ```rust
149/// use stem_rs::response::{ControlMessage, ProtocolInfoResponse, AuthMethod};
150///
151/// let msg = ControlMessage::from_str(
152/// "250-PROTOCOLINFO 1\r\n\
153/// 250-AUTH METHODS=COOKIE COOKIEFILE=\"/home/user/.tor/control_auth_cookie\"\r\n\
154/// 250-VERSION Tor=\"0.4.7.1\"\r\n\
155/// 250 OK\r\n",
156/// None,
157/// false
158/// ).unwrap();
159///
160/// let response = ProtocolInfoResponse::from_message(&msg).unwrap();
161///
162/// println!("Protocol version: {}", response.protocol_version);
163/// if let Some(ref version) = response.tor_version {
164/// println!("Tor version: {}", version);
165/// }
166/// println!("Auth methods: {:?}", response.auth_methods);
167/// if let Some(ref path) = response.cookie_path {
168/// println!("Cookie file: {}", path.display());
169/// }
170/// ```
171#[derive(Debug, Clone)]
172pub struct ProtocolInfoResponse {
173 /// The protocol version (typically 1).
174 ///
175 /// This indicates the version of the PROTOCOLINFO response format.
176 /// Currently, version 1 is the only defined version.
177 pub protocol_version: u32,
178
179 /// The Tor version, if provided.
180 ///
181 /// Parsed from the VERSION line of the response. May be `None` if
182 /// the VERSION line was not present.
183 pub tor_version: Option<Version>,
184
185 /// Available authentication methods.
186 ///
187 /// Lists all authentication methods that Tor will accept. Use this
188 /// to determine which authentication method to use.
189 pub auth_methods: Vec<AuthMethod>,
190
191 /// Unrecognized authentication method names.
192 ///
193 /// Contains the raw strings of any authentication methods that
194 /// weren't recognized. Useful for debugging or future compatibility.
195 pub unknown_auth_methods: Vec<String>,
196
197 /// Path to the authentication cookie file, if applicable.
198 ///
199 /// Present when COOKIE or SAFECOOKIE authentication is available.
200 /// This file must be readable to authenticate using cookie methods.
201 pub cookie_path: Option<PathBuf>,
202}
203
204impl ProtocolInfoResponse {
205 /// Parses a PROTOCOLINFO response from a control message.
206 ///
207 /// Extracts the protocol version, Tor version, authentication methods,
208 /// and cookie file path from the response.
209 ///
210 /// # Arguments
211 ///
212 /// * `message` - The control message to parse
213 ///
214 /// # Errors
215 ///
216 /// Returns [`Error::Protocol`](crate::Error::Protocol) if:
217 /// - The response status is not OK
218 /// - The response doesn't start with "PROTOCOLINFO"
219 /// - The protocol version is missing or non-numeric
220 /// - The AUTH line is missing the METHODS mapping
221 /// - The VERSION line is missing the Tor version mapping
222 /// - The Tor version string is invalid
223 ///
224 /// # Example
225 ///
226 /// ```rust
227 /// use stem_rs::response::{ControlMessage, ProtocolInfoResponse, AuthMethod};
228 ///
229 /// // Minimal response (just protocol version)
230 /// let msg = ControlMessage::from_str(
231 /// "250-PROTOCOLINFO 1\r\n250 OK\r\n",
232 /// None,
233 /// false
234 /// ).unwrap();
235 /// let response = ProtocolInfoResponse::from_message(&msg).unwrap();
236 /// assert_eq!(response.protocol_version, 1);
237 /// assert!(response.auth_methods.is_empty());
238 ///
239 /// // Full response with all fields
240 /// let msg = ControlMessage::from_str(
241 /// "250-PROTOCOLINFO 1\r\n\
242 /// 250-AUTH METHODS=NULL,HASHEDPASSWORD,COOKIE,SAFECOOKIE COOKIEFILE=\"/tmp/cookie\"\r\n\
243 /// 250-VERSION Tor=\"0.4.7.1\"\r\n\
244 /// 250 OK\r\n",
245 /// None,
246 /// false
247 /// ).unwrap();
248 /// let response = ProtocolInfoResponse::from_message(&msg).unwrap();
249 /// assert_eq!(response.auth_methods.len(), 4);
250 /// ```
251 pub fn from_message(message: &ControlMessage) -> Result<Self, Error> {
252 let mut protocol_version = None;
253 let mut tor_version = None;
254 let mut auth_methods = Vec::new();
255 let mut unknown_auth_methods = Vec::new();
256 let mut cookie_path = None;
257
258 let lines: Vec<String> = message.iter().map(|l| l.to_string()).collect();
259
260 let last_line = lines.last().map(|s| s.as_str()).unwrap_or("");
261 if !message.is_ok() || last_line != "OK" {
262 return Err(Error::Protocol(format!(
263 "PROTOCOLINFO response didn't have an OK status:\n{}",
264 message
265 )));
266 }
267
268 if lines.is_empty() || !lines[0].starts_with("PROTOCOLINFO") {
269 return Err(Error::Protocol(format!(
270 "Message is not a PROTOCOLINFO response:\n{}",
271 message
272 )));
273 }
274
275 for line_str in &lines {
276 if line_str == "OK" {
277 continue;
278 }
279
280 let mut line = ControlLine::new(line_str);
281 let line_type = line.pop(false, false)?;
282
283 match line_type.as_str() {
284 "PROTOCOLINFO" => {
285 if line.is_empty() {
286 return Err(Error::Protocol(format!(
287 "PROTOCOLINFO response's initial line is missing the protocol version: {}",
288 line_str
289 )));
290 }
291
292 let version_str = line.pop(false, false)?;
293 protocol_version = Some(version_str.parse().map_err(|_| {
294 Error::Protocol(format!(
295 "PROTOCOLINFO response version is non-numeric: {}",
296 line_str
297 ))
298 })?);
299 }
300 "AUTH" => {
301 if !line.is_next_mapping(Some("METHODS"), false, false) {
302 return Err(Error::Protocol(format!(
303 "PROTOCOLINFO response's AUTH line is missing its mandatory 'METHODS' mapping: {}",
304 line_str
305 )));
306 }
307
308 let (_, methods_str) = line.pop_mapping(false, false)?;
309 for method in methods_str.split(',') {
310 let auth_method = AuthMethod::from_str(method);
311 if auth_method == AuthMethod::Unknown {
312 unknown_auth_methods.push(method.to_string());
313 if !auth_methods.contains(&AuthMethod::Unknown) {
314 auth_methods.push(AuthMethod::Unknown);
315 }
316 } else if !auth_methods.contains(&auth_method) {
317 auth_methods.push(auth_method);
318 }
319 }
320
321 if line.is_next_mapping(Some("COOKIEFILE"), true, true) {
322 let (_, path_bytes) = line.pop_mapping_bytes(true, true)?;
323 let path_str = String::from_utf8_lossy(&path_bytes);
324 cookie_path = Some(PathBuf::from(path_str.to_string()));
325 }
326 }
327 "VERSION" => {
328 if !line.is_next_mapping(Some("Tor"), true, false) {
329 return Err(Error::Protocol(format!(
330 "PROTOCOLINFO response's VERSION line is missing its mandatory tor version mapping: {}",
331 line_str
332 )));
333 }
334
335 let (_, version_str) = line.pop_mapping(true, false)?;
336 tor_version = Some(
337 Version::parse(&version_str)
338 .map_err(|e| Error::Protocol(format!("Invalid Tor version: {}", e)))?,
339 );
340 }
341 _ => {}
342 }
343 }
344
345 Ok(Self {
346 protocol_version: protocol_version.unwrap_or(1),
347 tor_version,
348 auth_methods,
349 unknown_auth_methods,
350 cookie_path,
351 })
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 fn create_message(lines: Vec<&str>) -> ControlMessage {
360 let parsed: Vec<(String, char, Vec<u8>)> = lines
361 .iter()
362 .enumerate()
363 .map(|(i, line)| {
364 let divider = if i == lines.len() - 1 { ' ' } else { '-' };
365 ("250".to_string(), divider, line.as_bytes().to_vec())
366 })
367 .collect();
368 let raw = lines.join("\r\n");
369 ControlMessage::new(parsed, raw.into_bytes(), None).unwrap()
370 }
371
372 #[test]
373 fn test_protocolinfo_no_auth() {
374 let msg = create_message(vec![
375 "PROTOCOLINFO 1",
376 "AUTH METHODS=NULL",
377 "VERSION Tor=\"0.2.1.30\"",
378 "OK",
379 ]);
380 let response = ProtocolInfoResponse::from_message(&msg).unwrap();
381 assert_eq!(response.protocol_version, 1);
382 assert_eq!(
383 response.tor_version,
384 Some(Version::parse("0.2.1.30").unwrap())
385 );
386 assert_eq!(response.auth_methods, vec![AuthMethod::None]);
387 assert!(response.cookie_path.is_none());
388 }
389
390 #[test]
391 fn test_protocolinfo_password_auth() {
392 let msg = create_message(vec![
393 "PROTOCOLINFO 1",
394 "AUTH METHODS=HASHEDPASSWORD",
395 "VERSION Tor=\"0.2.1.30\"",
396 "OK",
397 ]);
398 let response = ProtocolInfoResponse::from_message(&msg).unwrap();
399 assert_eq!(response.auth_methods, vec![AuthMethod::Password]);
400 }
401
402 #[test]
403 fn test_protocolinfo_cookie_auth() {
404 let msg = create_message(vec![
405 "PROTOCOLINFO 1",
406 "AUTH METHODS=COOKIE COOKIEFILE=\"/home/atagar/.tor/control_auth_cookie\"",
407 "VERSION Tor=\"0.2.1.30\"",
408 "OK",
409 ]);
410 let response = ProtocolInfoResponse::from_message(&msg).unwrap();
411 assert_eq!(response.auth_methods, vec![AuthMethod::Cookie]);
412 assert_eq!(
413 response.cookie_path,
414 Some(PathBuf::from("/home/atagar/.tor/control_auth_cookie"))
415 );
416 }
417
418 #[test]
419 fn test_protocolinfo_cookie_auth_with_escape() {
420 let msg = create_message(vec![
421 "PROTOCOLINFO 1",
422 "AUTH METHODS=COOKIE COOKIEFILE=\"/tmp/my data\\\\\\\"dir//control_auth_cookie\"",
423 "VERSION Tor=\"0.2.1.30\"",
424 "OK",
425 ]);
426 let response = ProtocolInfoResponse::from_message(&msg).unwrap();
427 assert_eq!(response.auth_methods, vec![AuthMethod::Cookie]);
428 let path = response.cookie_path.unwrap();
429 assert_eq!(
430 path.to_str().unwrap(),
431 "/tmp/my data\\\"dir//control_auth_cookie"
432 );
433 }
434
435 #[test]
436 fn test_protocolinfo_multiple_auth_methods() {
437 let msg = create_message(vec![
438 "PROTOCOLINFO 1",
439 "AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE=\"/home/atagar/.tor/control_auth_cookie\"",
440 "VERSION Tor=\"0.2.1.30\"",
441 "OK",
442 ]);
443 let response = ProtocolInfoResponse::from_message(&msg).unwrap();
444 assert!(response.auth_methods.contains(&AuthMethod::Cookie));
445 assert!(response.auth_methods.contains(&AuthMethod::Password));
446 }
447
448 #[test]
449 fn test_protocolinfo_safecookie_auth() {
450 let msg = create_message(vec![
451 "PROTOCOLINFO 1",
452 "AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"/var/run/tor/control.authcookie\"",
453 "VERSION Tor=\"0.4.2.6\"",
454 "OK",
455 ]);
456 let response = ProtocolInfoResponse::from_message(&msg).unwrap();
457 assert!(response.auth_methods.contains(&AuthMethod::Cookie));
458 assert!(response.auth_methods.contains(&AuthMethod::SafeCookie));
459 }
460
461 #[test]
462 fn test_protocolinfo_all_auth_methods() {
463 let msg = create_message(vec![
464 "PROTOCOLINFO 1",
465 "AUTH METHODS=NULL,HASHEDPASSWORD,COOKIE,SAFECOOKIE COOKIEFILE=\"/tmp/cookie\"",
466 "VERSION Tor=\"0.4.7.1\"",
467 "OK",
468 ]);
469 let response = ProtocolInfoResponse::from_message(&msg).unwrap();
470 assert_eq!(response.auth_methods.len(), 4);
471 assert!(response.auth_methods.contains(&AuthMethod::None));
472 assert!(response.auth_methods.contains(&AuthMethod::Password));
473 assert!(response.auth_methods.contains(&AuthMethod::Cookie));
474 assert!(response.auth_methods.contains(&AuthMethod::SafeCookie));
475 }
476
477 #[test]
478 fn test_protocolinfo_minimum_response() {
479 let msg = create_message(vec!["PROTOCOLINFO 5", "OK"]);
480 let response = ProtocolInfoResponse::from_message(&msg).unwrap();
481 assert_eq!(response.protocol_version, 5);
482 assert!(response.tor_version.is_none());
483 assert!(response.auth_methods.is_empty());
484 assert!(response.cookie_path.is_none());
485 }
486
487 #[test]
488 fn test_protocolinfo_unknown_auth_method() {
489 let msg = create_message(vec![
490 "PROTOCOLINFO 1",
491 "AUTH METHODS=NULL,NEWMETHOD",
492 "VERSION Tor=\"0.4.7.1\"",
493 "OK",
494 ]);
495 let response = ProtocolInfoResponse::from_message(&msg).unwrap();
496 assert!(response.auth_methods.contains(&AuthMethod::None));
497 assert!(response.auth_methods.contains(&AuthMethod::Unknown));
498 assert!(response
499 .unknown_auth_methods
500 .contains(&"NEWMETHOD".to_string()));
501 }
502
503 #[test]
504 fn test_protocolinfo_error_response() {
505 let parsed = vec![(
506 "515".to_string(),
507 ' ',
508 "Authentication required".as_bytes().to_vec(),
509 )];
510 let msg = ControlMessage::new(parsed, "515 Authentication required".into(), None).unwrap();
511 assert!(ProtocolInfoResponse::from_message(&msg).is_err());
512 }
513
514 #[test]
515 fn test_protocolinfo_missing_version() {
516 let msg = create_message(vec!["PROTOCOLINFO", "OK"]);
517 assert!(ProtocolInfoResponse::from_message(&msg).is_err());
518 }
519
520 #[test]
521 fn test_protocolinfo_multiple_unknown_auth_methods() {
522 let msg = create_message(vec![
523 "PROTOCOLINFO 1",
524 "AUTH METHODS=MAGIC,HASHEDPASSWORD,PIXIE_DUST",
525 "VERSION Tor=\"0.2.1.30\"",
526 "OK",
527 ]);
528 let response = ProtocolInfoResponse::from_message(&msg).unwrap();
529 assert!(response.auth_methods.contains(&AuthMethod::Unknown));
530 assert!(response.auth_methods.contains(&AuthMethod::Password));
531 assert!(response.unknown_auth_methods.contains(&"MAGIC".to_string()));
532 assert!(response
533 .unknown_auth_methods
534 .contains(&"PIXIE_DUST".to_string()));
535 }
536
537 #[test]
538 fn test_protocolinfo_relative_cookie_path() {
539 let msg = create_message(vec![
540 "PROTOCOLINFO 1",
541 "AUTH METHODS=COOKIE COOKIEFILE=\"./tor-browser_en-US/Data/control_auth_cookie\"",
542 "VERSION Tor=\"0.2.1.30\"",
543 "OK",
544 ]);
545 let response = ProtocolInfoResponse::from_message(&msg).unwrap();
546 assert_eq!(
547 response.cookie_path,
548 Some(PathBuf::from(
549 "./tor-browser_en-US/Data/control_auth_cookie"
550 ))
551 );
552 }
553
554 #[test]
555 fn test_protocolinfo_not_protocolinfo_message() {
556 let msg = create_message(vec!["BW 32326 2856", "OK"]);
557 assert!(ProtocolInfoResponse::from_message(&msg).is_err());
558 }
559
560 #[test]
561 fn test_protocolinfo_missing_auth_methods() {
562 let msg = create_message(vec![
563 "PROTOCOLINFO 1",
564 "AUTH",
565 "VERSION Tor=\"0.2.1.30\"",
566 "OK",
567 ]);
568 assert!(ProtocolInfoResponse::from_message(&msg).is_err());
569 }
570
571 #[test]
572 fn test_protocolinfo_missing_tor_version_mapping() {
573 let msg = create_message(vec!["PROTOCOLINFO 1", "AUTH METHODS=NULL", "VERSION", "OK"]);
574 assert!(ProtocolInfoResponse::from_message(&msg).is_err());
575 }
576}