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}