stem_rs/descriptor/remote.rs
1//! Remote descriptor downloading from directory authorities and mirrors.
2//!
3//! This module provides functionality to download Tor descriptors from
4//! directory authorities and fallback mirrors. It enables fetching various
5//! types of network data including consensus documents, server descriptors,
6//! microdescriptors, and key certificates.
7//!
8//! # Overview
9//!
10//! The Tor network publishes its state through a distributed directory system.
11//! Directory authorities are trusted servers that vote on the network consensus,
12//! while directory mirrors cache and serve this data to reduce load on authorities.
13//!
14//! This module provides:
15//! - A list of known directory authorities
16//! - Functions to download specific descriptor types
17//! - Support for compression (gzip, zstd, lzma)
18//! - Configurable timeouts and retry logic
19//!
20//! # Descriptor Types
21//!
22//! | Type | Function | Description |
23//! |------|----------|-------------|
24//! | Consensus | [`download_consensus()`] | Network status document |
25//! | Server Descriptors | [`download_server_descriptors()`] | Full relay metadata |
26//! | Extra-Info | [`download_extrainfo_descriptors()`] | Bandwidth statistics |
27//! | Microdescriptors | [`download_microdescriptors()`] | Compact client descriptors |
28//! | Key Certificates | [`download_key_certificates()`] | Authority signing keys |
29//! | Bandwidth File | [`download_bandwidth_file()`] | Bandwidth measurements |
30//!
31//! # Example
32//!
33//! ```rust,no_run
34//! use stem_rs::descriptor::remote::{download_consensus, DirPort};
35//! use std::time::Duration;
36//!
37//! # async fn example() -> Result<(), stem_rs::Error> {
38//! // Download the current consensus with a 30-second timeout
39//! let result = download_consensus(
40//! false, // full consensus, not microdescriptor
41//! None, // use default authorities
42//! Some(Duration::from_secs(30)),
43//! ).await?;
44//!
45//! println!("Downloaded {} bytes from {:?}", result.content.len(), result.source);
46//! println!("Download took {:?}", result.runtime);
47//! # Ok(())
48//! # }
49//! ```
50//!
51//! # Rate Limits
52//!
53//! The Tor directory protocol has limits on how many descriptors can be
54//! requested at once:
55//! - Maximum 96 fingerprints per request for server/extra-info descriptors
56//! - Maximum 90 hashes per request for microdescriptors
57//!
58//! These limits exist due to URL length restrictions in proxy servers.
59//!
60//! # Compression
61//!
62//! Downloads support multiple compression formats to reduce bandwidth:
63//! - **gzip**: Widely supported, good compression
64//! - **zstd**: Better compression ratio, faster decompression
65//! - **lzma**: Best compression, slower
66//! - **identity**: No compression (plaintext)
67//!
68//! The server will use the best mutually supported format.
69//!
70//! # Error Handling
71//!
72//! Download functions try multiple endpoints and return the first successful
73//! result. If all endpoints fail, the last error is returned.
74//!
75//! # See Also
76//!
77//! - [`consensus`](super::consensus): Parsing downloaded consensus documents
78//! - [`server`](super::server): Parsing server descriptors
79//! - [`micro`](super::micro): Parsing microdescriptors
80//! - [`key_cert`](super::key_cert): Parsing key certificates
81
82use crate::Error;
83use std::net::{IpAddr, SocketAddr};
84use std::time::Duration;
85use tokio::io::{AsyncReadExt, AsyncWriteExt};
86use tokio::net::TcpStream;
87use tokio::time::timeout;
88
89/// User agent string sent with HTTP requests.
90const USER_AGENT: &str = "stem-rs/0.1.0";
91
92/// Maximum number of fingerprints that can be requested at once.
93///
94/// This limit exists due to URL length restrictions in proxy servers
95/// that may sit between clients and directory servers.
96const MAX_FINGERPRINTS: usize = 96;
97
98/// Maximum number of microdescriptor hashes that can be requested at once.
99///
100/// Microdescriptor hashes are longer than fingerprints, so fewer can
101/// fit in a single request URL.
102const MAX_MICRODESCRIPTOR_HASHES: usize = 90;
103
104/// Compression formats supported for descriptor downloads.
105///
106/// Directory servers can compress responses to reduce bandwidth usage.
107/// Clients advertise which formats they support, and the server uses
108/// the best mutually supported format.
109///
110/// # Compression Comparison
111///
112/// | Format | Compression | Speed | Support |
113/// |--------|-------------|-------|---------|
114/// | Plaintext | None | Fastest | Universal |
115/// | Gzip | Good | Fast | Universal |
116/// | Zstd | Better | Faster | Modern Tor |
117/// | Lzma | Best | Slower | Modern Tor |
118///
119/// # Example
120///
121/// ```rust
122/// use stem_rs::descriptor::remote::Compression;
123///
124/// let formats = [Compression::Zstd, Compression::Gzip, Compression::Plaintext];
125/// for fmt in &formats {
126/// println!("Encoding: {}", fmt.encoding());
127/// }
128/// ```
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum Compression {
131 /// No compression (identity encoding).
132 ///
133 /// Data is transferred as-is without any compression.
134 /// Always supported but uses the most bandwidth.
135 Plaintext,
136
137 /// Gzip compression (RFC 1952).
138 ///
139 /// Good compression ratio with fast decompression.
140 /// Universally supported by all Tor versions.
141 Gzip,
142
143 /// Zstandard compression.
144 ///
145 /// Better compression ratio than gzip with faster decompression.
146 /// Supported by modern Tor versions (0.3.1+).
147 Zstd,
148
149 /// LZMA compression.
150 ///
151 /// Best compression ratio but slower decompression.
152 /// Supported by modern Tor versions.
153 Lzma,
154}
155
156impl Compression {
157 /// Returns the HTTP Accept-Encoding value for this compression format.
158 ///
159 /// This is the string used in HTTP headers to indicate support
160 /// for this compression format.
161 ///
162 /// # Returns
163 ///
164 /// The encoding name as used in HTTP headers.
165 ///
166 /// # Example
167 ///
168 /// ```rust
169 /// use stem_rs::descriptor::remote::Compression;
170 ///
171 /// assert_eq!(Compression::Gzip.encoding(), "gzip");
172 /// assert_eq!(Compression::Plaintext.encoding(), "identity");
173 /// ```
174 pub fn encoding(&self) -> &'static str {
175 match self {
176 Compression::Plaintext => "identity",
177 Compression::Gzip => "gzip",
178 Compression::Zstd => "zstd",
179 Compression::Lzma => "x-tor-lzma",
180 }
181 }
182}
183
184/// A directory port endpoint for downloading descriptors.
185///
186/// Directory ports (DirPorts) are HTTP endpoints where Tor relays and
187/// authorities serve directory information. This struct represents
188/// the address and port of such an endpoint.
189///
190/// # Example
191///
192/// ```rust
193/// use stem_rs::descriptor::remote::DirPort;
194/// use std::net::IpAddr;
195///
196/// let addr: IpAddr = "128.31.0.39".parse().unwrap();
197/// let dirport = DirPort::new(addr, 9131);
198///
199/// println!("Connecting to {}", dirport.socket_addr());
200/// ```
201#[derive(Debug, Clone)]
202pub struct DirPort {
203 /// IP address of the directory server.
204 ///
205 /// Can be either IPv4 or IPv6.
206 pub address: IpAddr,
207
208 /// Port number for the directory service.
209 ///
210 /// Common values are 80, 443, or 9030.
211 pub port: u16,
212}
213
214impl DirPort {
215 /// Creates a new directory port endpoint.
216 ///
217 /// # Arguments
218 ///
219 /// * `address` - IP address of the directory server
220 /// * `port` - Port number for the directory service
221 ///
222 /// # Example
223 ///
224 /// ```rust
225 /// use stem_rs::descriptor::remote::DirPort;
226 /// use std::net::IpAddr;
227 ///
228 /// let addr: IpAddr = "127.0.0.1".parse().unwrap();
229 /// let dirport = DirPort::new(addr, 9030);
230 /// ```
231 pub fn new(address: IpAddr, port: u16) -> Self {
232 Self { address, port }
233 }
234
235 /// Returns the socket address for this endpoint.
236 ///
237 /// Combines the IP address and port into a `SocketAddr` suitable
238 /// for use with network operations.
239 ///
240 /// # Returns
241 ///
242 /// A `SocketAddr` combining the address and port.
243 pub fn socket_addr(&self) -> SocketAddr {
244 SocketAddr::new(self.address, self.port)
245 }
246}
247
248/// Information about a Tor directory authority.
249///
250/// Directory authorities are trusted servers that vote on the network
251/// consensus. They are hardcoded into Tor clients and are the root of
252/// trust for the Tor network.
253///
254/// # Fields
255///
256/// Each authority has:
257/// - A nickname for identification
258/// - Network addresses (IP, DirPort, ORPort)
259/// - A fingerprint (identity key hash)
260/// - A v3ident (v3 authority identity) for consensus voting
261///
262/// # Example
263///
264/// ```rust
265/// use stem_rs::descriptor::remote::get_authorities;
266///
267/// for auth in get_authorities() {
268/// println!("{}: {}:{}", auth.nickname, auth.address, auth.dir_port);
269/// }
270/// ```
271#[derive(Debug, Clone)]
272pub struct DirectoryAuthority {
273 /// Human-readable name of the authority.
274 ///
275 /// Examples: "moria1", "tor26", "gabelmoo"
276 pub nickname: String,
277
278 /// IP address of the authority.
279 pub address: IpAddr,
280
281 /// Port for directory requests (HTTP).
282 ///
283 /// Used for downloading descriptors and consensus documents.
284 pub dir_port: u16,
285
286 /// Port for onion routing connections (TLS).
287 ///
288 /// Used for relay-to-relay communication.
289 pub or_port: u16,
290
291 /// SHA-1 fingerprint of the authority's identity key.
292 ///
293 /// A 40-character hexadecimal string.
294 pub fingerprint: String,
295
296 /// V3 directory authority identity.
297 ///
298 /// Used for signing votes and the consensus.
299 /// Some authorities may not have this if they don't participate
300 /// in v3 consensus voting.
301 pub v3ident: Option<String>,
302}
303
304/// Returns the list of known directory authorities.
305///
306/// These are the trusted servers that vote on the Tor network consensus.
307/// The list is hardcoded and matches the authorities configured in the
308/// official Tor client.
309///
310/// # Returns
311///
312/// A vector of [`DirectoryAuthority`] structs for all known authorities.
313///
314/// # Note
315///
316/// Some authorities (like "tor26") intentionally throttle their DirPort
317/// to discourage abuse. The download functions automatically skip these
318/// when using default endpoints.
319///
320/// # Example
321///
322/// ```rust
323/// use stem_rs::descriptor::remote::get_authorities;
324///
325/// let authorities = get_authorities();
326/// println!("Known authorities: {}", authorities.len());
327///
328/// for auth in &authorities {
329/// println!(" {} - {}", auth.nickname, auth.fingerprint);
330/// }
331/// ```
332pub fn get_authorities() -> Vec<DirectoryAuthority> {
333 vec![
334 DirectoryAuthority {
335 nickname: "moria1".into(),
336 address: "128.31.0.39".parse().unwrap(),
337 dir_port: 9131,
338 or_port: 9101,
339 fingerprint: "9695DFC35FFEB861329B9F1AB04C46397020CE31".into(),
340 v3ident: Some("D586D18309DED4CD6D57C18FDB97EFA96D330566".into()),
341 },
342 DirectoryAuthority {
343 nickname: "tor26".into(),
344 address: "86.59.21.38".parse().unwrap(),
345 dir_port: 80,
346 or_port: 443,
347 fingerprint: "847B1F850344D7876491A54892F904934E4EB85D".into(),
348 v3ident: Some("14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4".into()),
349 },
350 DirectoryAuthority {
351 nickname: "dizum".into(),
352 address: "45.66.33.45".parse().unwrap(),
353 dir_port: 80,
354 or_port: 443,
355 fingerprint: "7EA6EAD6FD83083C538F44038BBFA077587DD755".into(),
356 v3ident: Some("E8A9C45EDE6D711294FADF8E7951F4DE6CA56B58".into()),
357 },
358 DirectoryAuthority {
359 nickname: "gabelmoo".into(),
360 address: "131.188.40.189".parse().unwrap(),
361 dir_port: 80,
362 or_port: 443,
363 fingerprint: "F2044413DAC2E02E3D6BCF4735A19BCA1DE97281".into(),
364 v3ident: Some("ED03BB616EB2F60BEC80151114BB25CEF515B226".into()),
365 },
366 DirectoryAuthority {
367 nickname: "dannenberg".into(),
368 address: "193.23.244.244".parse().unwrap(),
369 dir_port: 80,
370 or_port: 443,
371 fingerprint: "7BE683E65D48141321C5ED92F075C55364AC7123".into(),
372 v3ident: Some("0232AF901C31A04EE9848595AF9BB7620D4C5B2E".into()),
373 },
374 DirectoryAuthority {
375 nickname: "maatuska".into(),
376 address: "171.25.193.9".parse().unwrap(),
377 dir_port: 443,
378 or_port: 80,
379 fingerprint: "BD6A829255CB08E66FBE7D3748363586E46B3810".into(),
380 v3ident: Some("49015F787433103580E3B66A1707A00E60F2D15B".into()),
381 },
382 DirectoryAuthority {
383 nickname: "Faravahar".into(),
384 address: "154.35.175.225".parse().unwrap(),
385 dir_port: 80,
386 or_port: 443,
387 fingerprint: "CF6D0AAFB385BE71B8E111FC5CFF4B47923733BC".into(),
388 v3ident: Some("EFCBE720AB3A82B99F9E953CD5BF50F7EEFC7B97".into()),
389 },
390 DirectoryAuthority {
391 nickname: "longclaw".into(),
392 address: "199.58.81.140".parse().unwrap(),
393 dir_port: 80,
394 or_port: 443,
395 fingerprint: "74A910646BCEEFBCD2E874FC1DC997430F968145".into(),
396 v3ident: Some("23D15D965BC35114467363C165C4F724B64B4F66".into()),
397 },
398 DirectoryAuthority {
399 nickname: "bastet".into(),
400 address: "204.13.164.118".parse().unwrap(),
401 dir_port: 80,
402 or_port: 443,
403 fingerprint: "24E2F139121D4394C54B5BCC368B3B411857C413".into(),
404 v3ident: Some("27102BC123E7AF1D4741AE047E160C91ADC76B21".into()),
405 },
406 ]
407}
408
409/// Result of a successful descriptor download.
410///
411/// Contains the downloaded content along with metadata about the
412/// download including the source endpoint and timing information.
413///
414/// # Example
415///
416/// ```rust,no_run
417/// use stem_rs::descriptor::remote::download_consensus;
418/// use std::time::Duration;
419///
420/// # async fn example() -> Result<(), stem_rs::Error> {
421/// let result = download_consensus(false, None, Some(Duration::from_secs(30))).await?;
422///
423/// println!("Downloaded {} bytes", result.content.len());
424/// println!("From: {:?}", result.source.socket_addr());
425/// println!("Time: {:?}", result.runtime);
426/// # Ok(())
427/// # }
428/// ```
429#[derive(Debug)]
430pub struct DownloadResult {
431 /// The downloaded content as raw bytes.
432 ///
433 /// This may be compressed depending on what the server sent.
434 /// Use the appropriate decompression based on the Content-Encoding
435 /// header (not currently exposed).
436 pub content: Vec<u8>,
437
438 /// The endpoint that provided this content.
439 ///
440 /// Useful for debugging and logging which server was used.
441 pub source: DirPort,
442
443 /// How long the download took.
444 ///
445 /// Includes connection time, request/response, and data transfer.
446 pub runtime: Duration,
447}
448
449/// Downloads a resource from a directory port.
450///
451/// This is the low-level download function that handles HTTP communication
452/// with a single endpoint. Higher-level functions like [`download_consensus()`]
453/// use this internally with retry logic.
454///
455/// # Arguments
456///
457/// * `endpoint` - The directory port to download from
458/// * `resource` - The URL path to request (e.g., "/tor/status-vote/current/consensus")
459/// * `compression` - List of acceptable compression formats, in preference order
460/// * `request_timeout` - Optional timeout for the entire request
461///
462/// # Returns
463///
464/// A [`DownloadResult`] containing the response body and metadata.
465///
466/// # Errors
467///
468/// Returns [`Error::Download`] if:
469/// - Connection to the endpoint fails
470/// - The HTTP response indicates an error (non-200 status)
471/// - The response is malformed
472///
473/// Returns [`Error::DownloadTimeout`] if the request exceeds the timeout.
474///
475/// # Example
476///
477/// ```rust,no_run
478/// use stem_rs::descriptor::remote::{download_from_dirport, DirPort, Compression};
479/// use std::time::Duration;
480///
481/// # async fn example() -> Result<(), stem_rs::Error> {
482/// let endpoint = DirPort::new("128.31.0.39".parse().unwrap(), 9131);
483/// let result = download_from_dirport(
484/// &endpoint,
485/// "/tor/status-vote/current/consensus",
486/// &[Compression::Gzip, Compression::Plaintext],
487/// Some(Duration::from_secs(30)),
488/// ).await?;
489///
490/// println!("Downloaded {} bytes", result.content.len());
491/// # Ok(())
492/// # }
493/// ```
494pub async fn download_from_dirport(
495 endpoint: &DirPort,
496 resource: &str,
497 compression: &[Compression],
498 request_timeout: Option<Duration>,
499) -> Result<DownloadResult, Error> {
500 let start = std::time::Instant::now();
501 let socket_addr = endpoint.socket_addr();
502
503 let accept_encoding = compression
504 .iter()
505 .map(|c| c.encoding())
506 .collect::<Vec<_>>()
507 .join(", ");
508
509 let request = format!(
510 "GET {} HTTP/1.0\r\nHost: {}:{}\r\nAccept-Encoding: {}\r\nUser-Agent: {}\r\n\r\n",
511 resource, endpoint.address, endpoint.port, accept_encoding, USER_AGENT
512 );
513
514 let connect_and_download = async {
515 let mut stream = TcpStream::connect(socket_addr)
516 .await
517 .map_err(|e| Error::Download {
518 url: format!("http://{}:{}{}", endpoint.address, endpoint.port, resource),
519 reason: e.to_string(),
520 })?;
521
522 stream
523 .write_all(request.as_bytes())
524 .await
525 .map_err(|e| Error::Download {
526 url: format!("http://{}:{}{}", endpoint.address, endpoint.port, resource),
527 reason: e.to_string(),
528 })?;
529
530 let mut response = Vec::new();
531 stream
532 .read_to_end(&mut response)
533 .await
534 .map_err(|e| Error::Download {
535 url: format!("http://{}:{}{}", endpoint.address, endpoint.port, resource),
536 reason: e.to_string(),
537 })?;
538
539 Ok::<Vec<u8>, Error>(response)
540 };
541
542 let response = match request_timeout {
543 Some(t) => {
544 timeout(t, connect_and_download)
545 .await
546 .map_err(|_| Error::DownloadTimeout {
547 url: format!("http://{}:{}{}", endpoint.address, endpoint.port, resource),
548 })??
549 }
550 None => connect_and_download.await?,
551 };
552
553 let content = extract_http_body(&response)?;
554 let runtime = start.elapsed();
555
556 Ok(DownloadResult {
557 content,
558 source: endpoint.clone(),
559 runtime,
560 })
561}
562
563/// Extracts the HTTP body from a raw HTTP response.
564///
565/// Parses the HTTP response, validates the status code, and returns
566/// just the body content.
567fn extract_http_body(response: &[u8]) -> Result<Vec<u8>, Error> {
568 let response_str = String::from_utf8_lossy(response);
569 if let Some(pos) = response_str.find("\r\n\r\n") {
570 let header = &response_str[..pos];
571 if !header.starts_with("HTTP/1.") {
572 return Err(Error::Download {
573 url: String::new(),
574 reason: "Invalid HTTP response".into(),
575 });
576 }
577 let status_line = header.lines().next().unwrap_or("");
578 if !status_line.contains(" 200 ") {
579 return Err(Error::Download {
580 url: String::new(),
581 reason: format!("HTTP error: {}", status_line),
582 });
583 }
584 Ok(response[pos + 4..].to_vec())
585 } else {
586 Err(Error::Download {
587 url: String::new(),
588 reason: "Invalid HTTP response: no body separator".into(),
589 })
590 }
591}
592
593/// Downloads the current network consensus.
594///
595/// The consensus is the agreed-upon view of the Tor network, containing
596/// information about all known relays. It is signed by a majority of
597/// directory authorities.
598///
599/// # Arguments
600///
601/// * `microdescriptor` - If `true`, downloads the microdescriptor consensus
602/// (smaller, used by clients). If `false`, downloads the full consensus.
603/// * `endpoints` - Optional list of endpoints to try. If `None`, uses
604/// default directory authorities.
605/// * `request_timeout` - Optional timeout for each download attempt.
606///
607/// # Returns
608///
609/// A [`DownloadResult`] containing the consensus document.
610///
611/// # Errors
612///
613/// Returns [`Error::Download`] if all endpoints fail.
614/// Returns [`Error::DownloadTimeout`] if all attempts timeout.
615///
616/// # Example
617///
618/// ```rust,no_run
619/// use stem_rs::descriptor::remote::download_consensus;
620/// use std::time::Duration;
621///
622/// # async fn example() -> Result<(), stem_rs::Error> {
623/// // Download microdescriptor consensus (smaller, for clients)
624/// let result = download_consensus(true, None, Some(Duration::from_secs(60))).await?;
625/// println!("Consensus size: {} bytes", result.content.len());
626/// # Ok(())
627/// # }
628/// ```
629pub async fn download_consensus(
630 microdescriptor: bool,
631 endpoints: Option<&[DirPort]>,
632 request_timeout: Option<Duration>,
633) -> Result<DownloadResult, Error> {
634 let resource = if microdescriptor {
635 "/tor/status-vote/current/consensus-microdesc"
636 } else {
637 "/tor/status-vote/current/consensus"
638 };
639
640 download_resource(resource, endpoints, request_timeout).await
641}
642
643/// Downloads server descriptors.
644///
645/// Server descriptors contain detailed information about relays including
646/// their keys, exit policies, and capabilities.
647///
648/// # Arguments
649///
650/// * `fingerprints` - Optional list of relay fingerprints to fetch.
651/// If `None`, downloads all server descriptors (large!).
652/// Maximum 96 fingerprints per request.
653/// * `endpoints` - Optional list of endpoints to try.
654/// * `request_timeout` - Optional timeout for each download attempt.
655///
656/// # Returns
657///
658/// A [`DownloadResult`] containing the server descriptors.
659///
660/// # Errors
661///
662/// Returns [`Error::InvalidRequest`] if more than 96 fingerprints are requested.
663/// Returns [`Error::Download`] if all endpoints fail.
664///
665/// # Example
666///
667/// ```rust,no_run
668/// use stem_rs::descriptor::remote::download_server_descriptors;
669/// use std::time::Duration;
670///
671/// # async fn example() -> Result<(), stem_rs::Error> {
672/// // Download specific relay descriptors
673/// let fingerprints = ["9695DFC35FFEB861329B9F1AB04C46397020CE31"];
674/// let result = download_server_descriptors(
675/// Some(&fingerprints.iter().map(|s| *s).collect::<Vec<_>>()),
676/// None,
677/// Some(Duration::from_secs(30)),
678/// ).await?;
679/// # Ok(())
680/// # }
681/// ```
682pub async fn download_server_descriptors(
683 fingerprints: Option<&[&str]>,
684 endpoints: Option<&[DirPort]>,
685 request_timeout: Option<Duration>,
686) -> Result<DownloadResult, Error> {
687 let resource = match fingerprints {
688 Some(fps) => {
689 if fps.len() > MAX_FINGERPRINTS {
690 return Err(Error::InvalidRequest(format!(
691 "Cannot request more than {} descriptors at a time",
692 MAX_FINGERPRINTS
693 )));
694 }
695 format!("/tor/server/fp/{}", fps.join("+"))
696 }
697 None => "/tor/server/all".to_string(),
698 };
699
700 download_resource(&resource, endpoints, request_timeout).await
701}
702
703/// Downloads extra-info descriptors.
704///
705/// Extra-info descriptors contain additional relay information not included
706/// in server descriptors, such as bandwidth statistics and transport details.
707///
708/// # Arguments
709///
710/// * `fingerprints` - Optional list of relay fingerprints to fetch.
711/// If `None`, downloads all extra-info descriptors.
712/// Maximum 96 fingerprints per request.
713/// * `endpoints` - Optional list of endpoints to try.
714/// * `request_timeout` - Optional timeout for each download attempt.
715///
716/// # Returns
717///
718/// A [`DownloadResult`] containing the extra-info descriptors.
719///
720/// # Errors
721///
722/// Returns [`Error::InvalidRequest`] if more than 96 fingerprints are requested.
723/// Returns [`Error::Download`] if all endpoints fail.
724pub async fn download_extrainfo_descriptors(
725 fingerprints: Option<&[&str]>,
726 endpoints: Option<&[DirPort]>,
727 request_timeout: Option<Duration>,
728) -> Result<DownloadResult, Error> {
729 let resource = match fingerprints {
730 Some(fps) => {
731 if fps.len() > MAX_FINGERPRINTS {
732 return Err(Error::InvalidRequest(format!(
733 "Cannot request more than {} descriptors at a time",
734 MAX_FINGERPRINTS
735 )));
736 }
737 format!("/tor/extra/fp/{}", fps.join("+"))
738 }
739 None => "/tor/extra/all".to_string(),
740 };
741
742 download_resource(&resource, endpoints, request_timeout).await
743}
744
745/// Downloads microdescriptors by their hashes.
746///
747/// Microdescriptors are compact relay descriptions used by Tor clients.
748/// They are identified by their digest (hash) rather than fingerprint.
749///
750/// # Arguments
751///
752/// * `hashes` - List of microdescriptor digests to fetch.
753/// Maximum 90 hashes per request.
754/// * `endpoints` - Optional list of endpoints to try.
755/// * `request_timeout` - Optional timeout for each download attempt.
756///
757/// # Returns
758///
759/// A [`DownloadResult`] containing the microdescriptors.
760///
761/// # Errors
762///
763/// Returns [`Error::InvalidRequest`] if more than 90 hashes are requested.
764/// Returns [`Error::Download`] if all endpoints fail.
765///
766/// # Note
767///
768/// Microdescriptor hashes are obtained from the microdescriptor consensus.
769/// Each router status entry contains the hash of its microdescriptor.
770pub async fn download_microdescriptors(
771 hashes: &[&str],
772 endpoints: Option<&[DirPort]>,
773 request_timeout: Option<Duration>,
774) -> Result<DownloadResult, Error> {
775 if hashes.len() > MAX_MICRODESCRIPTOR_HASHES {
776 return Err(Error::InvalidRequest(format!(
777 "Cannot request more than {} microdescriptors at a time",
778 MAX_MICRODESCRIPTOR_HASHES
779 )));
780 }
781
782 let resource = format!("/tor/micro/d/{}", hashes.join("-"));
783 download_resource(&resource, endpoints, request_timeout).await
784}
785
786/// Downloads authority key certificates.
787///
788/// Key certificates bind directory authority identity keys to their
789/// signing keys. They are needed to verify consensus signatures.
790///
791/// # Arguments
792///
793/// * `v3idents` - Optional list of v3 authority identities to fetch.
794/// If `None`, downloads all key certificates.
795/// * `endpoints` - Optional list of endpoints to try.
796/// * `request_timeout` - Optional timeout for each download attempt.
797///
798/// # Returns
799///
800/// A [`DownloadResult`] containing the key certificates.
801///
802/// # Errors
803///
804/// Returns [`Error::Download`] if all endpoints fail.
805///
806/// # See Also
807///
808/// - [`super::key_cert::KeyCertificate`] for parsing the downloaded certificates
809pub async fn download_key_certificates(
810 v3idents: Option<&[&str]>,
811 endpoints: Option<&[DirPort]>,
812 request_timeout: Option<Duration>,
813) -> Result<DownloadResult, Error> {
814 let resource = match v3idents {
815 Some(ids) => format!("/tor/keys/fp/{}", ids.join("+")),
816 None => "/tor/keys/all".to_string(),
817 };
818
819 download_resource(&resource, endpoints, request_timeout).await
820}
821
822/// Downloads the bandwidth file for the next consensus.
823///
824/// Bandwidth files contain measurements used by authorities to assign
825/// bandwidth weights in the consensus. These are produced by bandwidth
826/// measurement systems like sbws or Torflow.
827///
828/// # Arguments
829///
830/// * `endpoints` - Optional list of endpoints to try.
831/// * `request_timeout` - Optional timeout for each download attempt.
832///
833/// # Returns
834///
835/// A [`DownloadResult`] containing the bandwidth file.
836///
837/// # Errors
838///
839/// Returns [`Error::Download`] if all endpoints fail.
840///
841/// # See Also
842///
843/// - [`super::bandwidth_file::BandwidthFile`] for parsing the downloaded file
844pub async fn download_bandwidth_file(
845 endpoints: Option<&[DirPort]>,
846 request_timeout: Option<Duration>,
847) -> Result<DownloadResult, Error> {
848 download_resource(
849 "/tor/status-vote/next/bandwidth",
850 endpoints,
851 request_timeout,
852 )
853 .await
854}
855
856/// Downloads detached signatures for the next consensus.
857///
858/// Detached signatures are authority signatures collected during the
859/// consensus voting process. They are used to create the final signed
860/// consensus document.
861///
862/// # Arguments
863///
864/// * `endpoints` - Optional list of endpoints to try.
865/// * `request_timeout` - Optional timeout for each download attempt.
866///
867/// # Returns
868///
869/// A [`DownloadResult`] containing the detached signatures.
870///
871/// # Errors
872///
873/// Returns [`Error::Download`] if all endpoints fail.
874///
875/// # Note
876///
877/// This is primarily useful for directory authority operators and
878/// researchers studying the consensus process.
879pub async fn download_detached_signatures(
880 endpoints: Option<&[DirPort]>,
881 request_timeout: Option<Duration>,
882) -> Result<DownloadResult, Error> {
883 download_resource(
884 "/tor/status-vote/next/consensus-signatures",
885 endpoints,
886 request_timeout,
887 )
888 .await
889}
890
891/// Internal function to download a resource with retry logic.
892///
893/// Tries each endpoint in order until one succeeds. Automatically
894/// skips known problematic authorities (tor26, Serge).
895async fn download_resource(
896 resource: &str,
897 endpoints: Option<&[DirPort]>,
898 request_timeout: Option<Duration>,
899) -> Result<DownloadResult, Error> {
900 let authorities = get_authorities();
901 let default_endpoints: Vec<DirPort> = authorities
902 .iter()
903 .filter(|a| a.nickname != "tor26" && a.nickname != "Serge")
904 .map(|a| DirPort::new(a.address, a.dir_port))
905 .collect();
906
907 let endpoints = endpoints.unwrap_or(&default_endpoints);
908 let compression = vec![Compression::Gzip, Compression::Plaintext];
909
910 let mut last_error = None;
911
912 for endpoint in endpoints {
913 match download_from_dirport(endpoint, resource, &compression, request_timeout).await {
914 Ok(result) => return Ok(result),
915 Err(e) => {
916 last_error = Some(e);
917 continue;
918 }
919 }
920 }
921
922 Err(last_error.unwrap_or_else(|| Error::Download {
923 url: resource.to_string(),
924 reason: "No endpoints available".into(),
925 }))
926}
927
928#[cfg(test)]
929mod tests {
930 use super::*;
931
932 #[test]
933 fn test_get_authorities() {
934 let authorities = get_authorities();
935 assert!(!authorities.is_empty());
936 assert!(authorities.iter().any(|a| a.nickname == "moria1"));
937 }
938
939 #[test]
940 fn test_compression_encoding() {
941 assert_eq!(Compression::Plaintext.encoding(), "identity");
942 assert_eq!(Compression::Gzip.encoding(), "gzip");
943 assert_eq!(Compression::Zstd.encoding(), "zstd");
944 assert_eq!(Compression::Lzma.encoding(), "x-tor-lzma");
945 }
946
947 #[test]
948 fn test_dirport() {
949 let addr: IpAddr = "127.0.0.1".parse().unwrap();
950 let dirport = DirPort::new(addr, 9030);
951 assert_eq!(dirport.socket_addr(), SocketAddr::new(addr, 9030));
952 }
953
954 #[test]
955 fn test_extract_http_body() {
956 let response = b"HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\nHello, World!";
957 let body = extract_http_body(response).unwrap();
958 assert_eq!(body, b"Hello, World!");
959 }
960
961 #[test]
962 fn test_extract_http_body_error() {
963 let response = b"HTTP/1.0 404 Not Found\r\n\r\nNot Found";
964 let result = extract_http_body(response);
965 assert!(result.is_err());
966 }
967
968 #[test]
969 fn test_max_fingerprints() {
970 assert_eq!(MAX_FINGERPRINTS, 96);
971 assert_eq!(MAX_MICRODESCRIPTOR_HASHES, 90);
972 }
973}