Skip to main content

tor_dirclient/
request.rs

1//! Descriptions objects for different kinds of directory requests
2//! that we can make.
3
4use tor_circmgr::ClientDirTunnel;
5use tor_llcrypto::pk::rsa::RsaIdentity;
6use tor_netdoc::doc::authcert::AuthCertKeyIds;
7use tor_netdoc::doc::microdesc::MdDigest;
8use tor_netdoc::doc::netstatus::ConsensusFlavor;
9#[cfg(feature = "routerdesc")]
10use tor_netdoc::doc::routerdesc::{ExtraInfoDigest, RdDigest};
11
12#[cfg(feature = "hs-client")]
13use tor_hscrypto::pk::HsBlindId;
14
15/// Alias for a result with a `RequestError`.
16type Result<T> = std::result::Result<T, crate::err::RequestError>;
17
18use base64ct::{Base64Unpadded, Encoding as _};
19use std::borrow::Cow;
20use std::future::Future;
21use std::iter::FromIterator;
22use std::pin::Pin;
23use std::sync::Arc;
24use std::time::{Duration, SystemTime};
25
26use itertools::Itertools;
27
28use crate::AnonymizedRequest;
29use crate::body::RequestBody;
30use crate::err::RequestError;
31
32/// Declare an inaccessible public type.
33pub(crate) mod sealed {
34    use tor_circmgr::ClientDirTunnel;
35
36    use crate::body::RequestBody;
37
38    use super::{AnonymizedRequest, Result};
39
40    use std::future::Future;
41    use std::pin::Pin;
42
43    /// Sealed trait to help implement [`Requestable`](super::Requestable): not
44    /// visible outside this crate, so we can change its methods however we like.
45    pub trait RequestableInner: Send + Sync {
46        /// Build an [`http::Request`] from this Requestable, if
47        /// it is well-formed.
48        fn make_request(&self) -> Result<http::Request<RequestBody>>;
49
50        /// Return true if partial response bodies are potentially useful.
51        ///
52        /// This is true for request types where we're going to be downloading
53        /// multiple documents, and we know how to parse out the ones we wanted
54        /// if the answer is truncated.
55        fn partial_response_body_ok(&self) -> bool;
56
57        /// Return the maximum allowable response length we'll accept for this
58        /// request.
59        fn max_response_len(&self) -> usize;
60
61        /// Optimization: return an error if there is some problem with the provided circuit that
62        /// makes it unlikely to be useful for this request.
63        ///
64        /// *Do not* use this method to check for security properties.
65        fn check_circuit_skip_optimization<'a>(
66            &self,
67            tunnel: &'a ClientDirTunnel,
68        ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
69            let _ = tunnel;
70            Box::pin(async { Ok(()) })
71        }
72
73        /// Return a value to say whether this request must be anonymized.
74        fn anonymized(&self) -> AnonymizedRequest;
75    }
76}
77
78/// A request for an object that can be served over the Tor directory system.
79pub trait Requestable: sealed::RequestableInner {
80    /// Return a wrapper around this [`Requestable`] that implements `Debug`,
81    /// and whose output shows the actual HTTP request that will be generated.
82    ///
83    /// The format is not guaranteed to  be stable.
84    fn debug_request(&self) -> DisplayRequestable<'_, Self>
85    where
86        Self: Sized,
87    {
88        DisplayRequestable(self)
89    }
90}
91impl<T: sealed::RequestableInner> Requestable for T {}
92
93/// A wrapper to implement [`Requestable::debug_request`].
94pub struct DisplayRequestable<'a, R: Requestable>(&'a R);
95
96impl<'a, R: Requestable> std::fmt::Debug for DisplayRequestable<'a, R> {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        write!(f, "{:?}", self.0.make_request())
99    }
100}
101
102impl sealed::RequestableInner for Arc<dyn Requestable> {
103    fn make_request(&self) -> Result<http::Request<RequestBody>> {
104        let r: &dyn Requestable = self.as_ref();
105        r.make_request()
106    }
107
108    fn partial_response_body_ok(&self) -> bool {
109        let r: &dyn Requestable = self.as_ref();
110        r.partial_response_body_ok()
111    }
112
113    fn anonymized(&self) -> AnonymizedRequest {
114        let r: &dyn Requestable = self.as_ref();
115        r.anonymized()
116    }
117
118    fn max_response_len(&self) -> usize {
119        let r: &dyn Requestable = self.as_ref();
120        r.max_response_len()
121    }
122
123    fn check_circuit_skip_optimization<'a>(
124        &self,
125        tunnel: &'a ClientDirTunnel,
126    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
127        let r: &dyn Requestable = self.as_ref();
128        r.check_circuit_skip_optimization(tunnel)
129    }
130}
131
132/// How much clock skew do we allow in the distance between the directory
133/// cache's clock and our own?
134///
135///  If we find more skew than this, we end the
136/// request early, on the theory that the directory will not tell us any
137/// information we'd accept.
138#[derive(Clone, Debug)]
139struct SkewLimit {
140    /// We refuse to proceed if the directory says we are more fast than this.
141    ///
142    /// (This is equivalent to deciding that, from our perspective, the
143    /// directory is at least this slow.)
144    max_fast: Duration,
145
146    /// We refuse to proceed if the directory says that we are more slow than
147    /// this.
148    ///
149    /// (This is equivalent to deciding that, from our perspective, the
150    /// directory is at least this fast.)
151    max_slow: Duration,
152}
153
154/// A Request for a consensus directory.
155#[derive(Debug, Clone)]
156pub struct ConsensusRequest {
157    /// What flavor of consensus are we asking for?  Right now, only
158    /// "microdesc" and "ns" are supported.
159    flavor: ConsensusFlavor,
160    /// A list of the authority identities that we believe in.  We tell the
161    /// directory cache only to give us a consensus if it is signed by enough
162    /// of these authorities.
163    authority_ids: Vec<RsaIdentity>,
164    /// The publication time of the most recent consensus we have.  Used to
165    /// generate an If-Modified-Since header so that we don't get a document
166    /// we already have.
167    last_consensus_published: Option<SystemTime>,
168    /// A set of SHA3-256 digests of the _signed portion_ of consensuses we have.
169    /// Used to declare what diffs we would accept.
170    last_consensus_sha3_256: Vec<[u8; 32]>,
171    /// If present, the largest amount of clock skew to allow between ourself and a directory cache.
172    skew_limit: Option<SkewLimit>,
173}
174
175impl ConsensusRequest {
176    /// Create a new request for a consensus directory document.
177    pub fn new(flavor: ConsensusFlavor) -> Self {
178        ConsensusRequest {
179            flavor,
180            authority_ids: Vec::new(),
181            last_consensus_published: None,
182            last_consensus_sha3_256: Vec::new(),
183            skew_limit: None,
184        }
185    }
186
187    /// Add `id` to the list of authorities that this request should
188    /// say we believe in.
189    pub fn push_authority_id(&mut self, id: RsaIdentity) {
190        self.authority_ids.push(id);
191    }
192
193    /// Add `d` to the list of consensus digests this request should
194    /// say we already have.
195    pub fn push_old_consensus_digest(&mut self, d: [u8; 32]) {
196        self.last_consensus_sha3_256.push(d);
197    }
198
199    /// Set the publication time we should say we have for our last
200    /// consensus to `when`.
201    pub fn set_last_consensus_date(&mut self, when: SystemTime) {
202        self.last_consensus_published = Some(when);
203    }
204
205    /// Return a slice of the consensus digests that we're saying we
206    /// already have.
207    pub fn old_consensus_digests(&self) -> impl Iterator<Item = &[u8; 32]> {
208        self.last_consensus_sha3_256.iter()
209    }
210
211    /// Return an iterator of the authority identities that this request
212    /// is saying we believe in.
213    pub fn authority_ids(&self) -> impl Iterator<Item = &RsaIdentity> {
214        self.authority_ids.iter()
215    }
216
217    /// Return the date we're reporting for our most recent consensus.
218    pub fn last_consensus_date(&self) -> Option<SystemTime> {
219        self.last_consensus_published
220    }
221
222    /// Tell the directory client that we should abort the request early if the
223    /// directory's clock skew exceeds certain limits.
224    ///
225    /// The `max_fast` parameter is the most fast that we're willing to be with
226    /// respect to the directory (or in other words, the most slow that we're
227    /// willing to let the directory be with respect to us).
228    ///
229    /// The `max_slow` parameter is the most _slow_ that we're willing to be with
230    /// respect to the directory ((or in other words, the most slow that we're
231    /// willing to let the directory be with respect to us).
232    pub fn set_skew_limit(&mut self, max_fast: Duration, max_slow: Duration) {
233        self.skew_limit = Some(SkewLimit { max_fast, max_slow });
234    }
235}
236
237/// Convert a list of digests in some format to a string, for use in a request
238///
239/// The digests `DL` will be sorted, converted to strings with `EF`,
240/// separated with `sep`, and returned as an fresh `String`.
241///
242/// If the digests list is empty, returns None instead.
243//
244// In principle this ought to be doable with much less allocating,
245// starting with hex::encode etc.
246fn digest_list_stringify<'d, D, DL, EF>(digests: DL, encode: EF, sep: &str) -> Option<String>
247where
248    DL: IntoIterator<Item = &'d D> + 'd,
249    D: PartialOrd + Ord + 'd,
250    EF: Fn(&'d D) -> String,
251{
252    let mut digests = digests.into_iter().collect_vec();
253    if digests.is_empty() {
254        return None;
255    }
256    digests.sort_unstable();
257    let ids = digests.into_iter().map(encode).map(Cow::Owned);
258    // name collision with unstable Iterator::intersperse
259    // https://github.com/rust-lang/rust/issues/48919
260    let ids = Itertools::intersperse(ids, Cow::Borrowed(sep)).collect::<String>();
261    Some(ids)
262}
263
264impl Default for ConsensusRequest {
265    fn default() -> Self {
266        Self::new(ConsensusFlavor::Microdesc)
267    }
268}
269
270impl sealed::RequestableInner for ConsensusRequest {
271    fn make_request(&self) -> Result<http::Request<RequestBody>> {
272        // Build the URL.
273        let mut uri = "/tor/status-vote/current/consensus".to_string();
274        match self.flavor {
275            ConsensusFlavor::Plain => {}
276            flav => {
277                uri.push('-');
278                uri.push_str(flav.name());
279            }
280        }
281        let d_encode_hex = |id: &RsaIdentity| hex::encode(id.as_bytes());
282        if let Some(ids) = digest_list_stringify(&self.authority_ids, d_encode_hex, "+") {
283            // With authorities, "../consensus/<F1>+<F2>+<F3>"
284            uri.push('/');
285            uri.push_str(&ids);
286        }
287        // Without authorities, "../consensus-microdesc"
288
289        let mut req = http::Request::builder().method("GET").uri(uri);
290        req = add_common_headers(req, self.anonymized());
291
292        // Possibly, add an if-modified-since header.
293        if let Some(when) = self.last_consensus_date() {
294            req = req.header(
295                http::header::IF_MODIFIED_SINCE,
296                httpdate::fmt_http_date(when),
297            );
298        }
299
300        // Possibly, add an X-Or-Diff-From-Consensus header.
301        if let Some(ids) = digest_list_stringify(&self.last_consensus_sha3_256, hex::encode, ", ") {
302            req = req.header("X-Or-Diff-From-Consensus", &ids);
303        }
304
305        Ok(req.body(RequestBody::default())?)
306    }
307
308    fn partial_response_body_ok(&self) -> bool {
309        false
310    }
311
312    fn max_response_len(&self) -> usize {
313        (16 * 1024 * 1024) - 1
314    }
315
316    fn check_circuit_skip_optimization<'a>(
317        &self,
318        tunnel: &'a ClientDirTunnel,
319    ) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
320        let skew_limit = self.skew_limit.clone();
321        Box::pin(async move {
322            use tor_proto::ClockSkew::*;
323            // This is the clock skew _according to the directory_.
324            let skew = tunnel.first_hop_clock_skew().await?;
325            match (&skew_limit, &skew) {
326                (Some(SkewLimit { max_slow, .. }), Slow(slow)) if slow > max_slow => {
327                    Err(RequestError::TooMuchClockSkew)
328                }
329                (Some(SkewLimit { max_fast, .. }), Fast(fast)) if fast > max_fast => {
330                    Err(RequestError::TooMuchClockSkew)
331                }
332                (_, _) => Ok(()),
333            }
334        })
335    }
336
337    fn anonymized(&self) -> AnonymizedRequest {
338        AnonymizedRequest::Direct
339    }
340}
341
342/// A request for one or more authority certificates.
343#[derive(Debug, Clone, Default)]
344pub struct AuthCertRequest {
345    /// The identity/signing keys of the certificates we want.
346    ids: Vec<AuthCertKeyIds>,
347}
348
349impl AuthCertRequest {
350    /// Create a new request, asking for no authority certificates.
351    pub fn new() -> Self {
352        AuthCertRequest::default()
353    }
354
355    /// Add `ids` to the list of certificates we're asking for.
356    pub fn push(&mut self, ids: AuthCertKeyIds) {
357        self.ids.push(ids);
358    }
359
360    /// Return a list of the keys that we're asking for.
361    pub fn keys(&self) -> impl Iterator<Item = &AuthCertKeyIds> {
362        self.ids.iter()
363    }
364}
365
366impl sealed::RequestableInner for AuthCertRequest {
367    fn make_request(&self) -> Result<http::Request<RequestBody>> {
368        if self.ids.is_empty() {
369            return Err(RequestError::EmptyRequest);
370        }
371        let mut ids = self.ids.clone();
372        ids.sort_unstable();
373
374        let ids: Vec<String> = ids
375            .iter()
376            .map(|id| {
377                format!(
378                    "{}-{}",
379                    hex::encode(id.id_fingerprint.as_bytes()),
380                    hex::encode(id.sk_fingerprint.as_bytes())
381                )
382            })
383            .collect();
384
385        let uri = format!("/tor/keys/fp-sk/{}", ids.join("+"));
386
387        let req = http::Request::builder().method("GET").uri(uri);
388        let req = add_common_headers(req, self.anonymized());
389
390        Ok(req.body(RequestBody::default())?)
391    }
392
393    fn partial_response_body_ok(&self) -> bool {
394        self.ids.len() > 1
395    }
396
397    fn max_response_len(&self) -> usize {
398        // TODO: Pick a more principled number; I just made this one up.
399        self.ids.len().saturating_mul(16 * 1024)
400    }
401
402    fn anonymized(&self) -> AnonymizedRequest {
403        AnonymizedRequest::Direct
404    }
405}
406
407impl FromIterator<AuthCertKeyIds> for AuthCertRequest {
408    fn from_iter<I: IntoIterator<Item = AuthCertKeyIds>>(iter: I) -> Self {
409        let mut req = Self::new();
410        for i in iter {
411            req.push(i);
412        }
413        req
414    }
415}
416
417/// A request for one or more microdescriptors
418#[derive(Debug, Clone, Default)]
419pub struct MicrodescRequest {
420    /// The SHA256 digests of the microdescriptors we want.
421    digests: Vec<MdDigest>,
422}
423
424impl MicrodescRequest {
425    /// Construct a request for no microdescriptors.
426    pub fn new() -> Self {
427        MicrodescRequest::default()
428    }
429    /// Add `d` to the list of microdescriptors we want to request.
430    pub fn push(&mut self, d: MdDigest) {
431        self.digests.push(d);
432    }
433
434    /// Return a list of the microdescriptor digests that we're asking for.
435    pub fn digests(&self) -> impl Iterator<Item = &MdDigest> {
436        self.digests.iter()
437    }
438}
439
440impl sealed::RequestableInner for MicrodescRequest {
441    fn make_request(&self) -> Result<http::Request<RequestBody>> {
442        let d_encode_b64 = |d: &[u8; 32]| Base64Unpadded::encode_string(&d[..]);
443        let ids = digest_list_stringify(&self.digests, d_encode_b64, "-")
444            .ok_or(RequestError::EmptyRequest)?;
445        let uri = format!("/tor/micro/d/{}", ids);
446        let req = http::Request::builder().method("GET").uri(uri);
447
448        let req = add_common_headers(req, self.anonymized());
449
450        Ok(req.body(RequestBody::default())?)
451    }
452
453    fn partial_response_body_ok(&self) -> bool {
454        self.digests.len() > 1
455    }
456
457    fn max_response_len(&self) -> usize {
458        // TODO: Pick a more principled number; I just made this one up.
459        self.digests.len().saturating_mul(8 * 1024)
460    }
461
462    fn anonymized(&self) -> AnonymizedRequest {
463        AnonymizedRequest::Direct
464    }
465}
466
467impl FromIterator<MdDigest> for MicrodescRequest {
468    fn from_iter<I: IntoIterator<Item = MdDigest>>(iter: I) -> Self {
469        let mut req = Self::new();
470        for i in iter {
471            req.push(i);
472        }
473        req
474    }
475}
476
477/// A request for one, many or all router descriptors.
478#[derive(Debug, Clone)]
479#[cfg(feature = "routerdesc")]
480pub struct RouterDescRequest {
481    /// The descriptors to request.
482    requested_descriptors: RequestedDescs,
483}
484
485/// Tracks the different router descriptor types.
486#[derive(Debug, Clone)]
487#[cfg(feature = "routerdesc")]
488enum RequestedDescs {
489    /// If this is set, we just ask for all the descriptors.
490    All,
491    /// A list of digests to download.
492    Digests(Vec<RdDigest>),
493}
494
495#[cfg(feature = "routerdesc")]
496// TODO: This is probably not a reasonable default.
497impl Default for RouterDescRequest {
498    fn default() -> Self {
499        RouterDescRequest {
500            requested_descriptors: RequestedDescs::Digests(Vec::new()),
501        }
502    }
503}
504
505#[cfg(feature = "routerdesc")]
506impl RouterDescRequest {
507    /// Construct a request for all router descriptors.
508    pub fn all() -> Self {
509        RouterDescRequest {
510            requested_descriptors: RequestedDescs::All,
511        }
512    }
513    /// Construct a new empty request.
514    pub fn new() -> Self {
515        RouterDescRequest::default()
516    }
517}
518
519#[cfg(feature = "routerdesc")]
520impl sealed::RequestableInner for RouterDescRequest {
521    fn make_request(&self) -> Result<http::Request<RequestBody>> {
522        let mut uri = "/tor/server/".to_string();
523
524        match self.requested_descriptors {
525            RequestedDescs::Digests(ref digests) => {
526                uri.push_str("d/");
527                let ids = digest_list_stringify(digests, hex::encode, "+")
528                    .ok_or(RequestError::EmptyRequest)?;
529                uri.push_str(&ids);
530            }
531            RequestedDescs::All => {
532                uri.push_str("all");
533            }
534        }
535
536        let req = http::Request::builder().method("GET").uri(uri);
537        let req = add_common_headers(req, self.anonymized());
538
539        Ok(req.body(RequestBody::default())?)
540    }
541
542    fn partial_response_body_ok(&self) -> bool {
543        match self.requested_descriptors {
544            RequestedDescs::Digests(ref digests) => digests.len() > 1,
545            RequestedDescs::All => true,
546        }
547    }
548
549    fn max_response_len(&self) -> usize {
550        // TODO: Pick a more principled number; I just made these up.
551        match self.requested_descriptors {
552            RequestedDescs::Digests(ref digests) => digests.len().saturating_mul(8 * 1024),
553            RequestedDescs::All => 64 * 1024 * 1024, // big but not impossible
554        }
555    }
556
557    fn anonymized(&self) -> AnonymizedRequest {
558        AnonymizedRequest::Direct
559    }
560}
561
562#[cfg(feature = "routerdesc")]
563impl FromIterator<RdDigest> for RouterDescRequest {
564    fn from_iter<I: IntoIterator<Item = RdDigest>>(iter: I) -> Self {
565        let digests = iter.into_iter().collect();
566
567        RouterDescRequest {
568            requested_descriptors: RequestedDescs::Digests(digests),
569        }
570    }
571}
572
573/// A request for the descriptor of whatever relay we are making the request to
574#[derive(Debug, Clone, Default)]
575#[cfg(feature = "routerdesc")]
576#[non_exhaustive]
577pub struct RoutersOwnDescRequest {}
578
579#[cfg(feature = "routerdesc")]
580impl RoutersOwnDescRequest {
581    /// Construct a new request.
582    pub fn new() -> Self {
583        RoutersOwnDescRequest::default()
584    }
585}
586
587#[cfg(feature = "routerdesc")]
588impl sealed::RequestableInner for RoutersOwnDescRequest {
589    fn make_request(&self) -> Result<http::Request<RequestBody>> {
590        let uri = "/tor/server/authority";
591        let req = http::Request::builder().method("GET").uri(uri);
592        let req = add_common_headers(req, self.anonymized());
593
594        Ok(req.body(RequestBody::default())?)
595    }
596
597    fn partial_response_body_ok(&self) -> bool {
598        false
599    }
600
601    fn max_response_len(&self) -> usize {
602        // As of 2026, directory authorities don't accept a routerdesc longer than
603        // 20000 bytes.  This should be plenty for the foreseeable future.
604        128 * 1024
605    }
606
607    fn anonymized(&self) -> AnonymizedRequest {
608        AnonymizedRequest::Direct
609    }
610}
611
612/// A request for one or many extra-infos.
613///
614/// <https://spec.torproject.org/dir-spec/general-use-http-urls.html>
615/// (search in page for "extra-info")
616#[derive(Debug, Clone)]
617#[cfg(feature = "routerdesc")]
618pub struct ExtraInfoRequest {
619    /// The extra-infos to request.
620    requested_extra_infos: RequestedExtraInfos,
621}
622
623/// Which extra-info documents to download.
624///
625/// Currently only a subset of the available URLs are supported.
626#[derive(Debug, Clone)]
627#[cfg(feature = "routerdesc")]
628#[non_exhaustive]
629enum RequestedExtraInfos {
630    /// Just ask for all the extra-infos.
631    ///
632    /// `http://<hostname>/tor/extra/all`
633    All,
634    /// Download extra-infos with these SHA-1 digests.
635    ///
636    /// `http://<hostname>/tor/extra/d/...`
637    Digests(Vec<ExtraInfoDigest>),
638}
639
640#[cfg(feature = "routerdesc")]
641impl Default for ExtraInfoRequest {
642    // TODO: This is probably not a reasonable default.
643    fn default() -> Self {
644        Self {
645            requested_extra_infos: RequestedExtraInfos::Digests(Vec::new()),
646        }
647    }
648}
649
650#[cfg(feature = "routerdesc")]
651impl ExtraInfoRequest {
652    /// Construct a request for all extra-infos.
653    pub fn all() -> Self {
654        Self {
655            requested_extra_infos: RequestedExtraInfos::All,
656        }
657    }
658    /// Construct a new empty request.
659    pub fn new() -> Self {
660        Self::default()
661    }
662}
663
664#[cfg(feature = "routerdesc")]
665impl sealed::RequestableInner for ExtraInfoRequest {
666    fn make_request(&self) -> Result<http::Request<RequestBody>> {
667        let mut uri = "/tor/extra/".to_string();
668
669        match &self.requested_extra_infos {
670            RequestedExtraInfos::All => uri.push_str("all"),
671            RequestedExtraInfos::Digests(digests) => {
672                uri.push_str("d/");
673                let ids = digest_list_stringify(digests, hex::encode_upper, "+")
674                    .ok_or(RequestError::EmptyRequest)?;
675                uri.push_str(&ids);
676            }
677        }
678
679        let req = http::Request::builder().method("GET").uri(uri);
680        let req = add_common_headers(req, self.anonymized());
681        Ok(req.body(RequestBody::default())?)
682    }
683
684    fn partial_response_body_ok(&self) -> bool {
685        match &self.requested_extra_infos {
686            RequestedExtraInfos::Digests(digests) => digests.len() > 1,
687            RequestedExtraInfos::All => true,
688        }
689    }
690
691    fn max_response_len(&self) -> usize {
692        // TODO torspec#392: Pick more principled size limits.
693        // These were copied from the RouterDescRequest impl and doubled.
694        match &self.requested_extra_infos {
695            RequestedExtraInfos::Digests(digests) => digests.len().saturating_mul(16 * 1024),
696            RequestedExtraInfos::All => 128 * 1024 * 1024,
697        }
698    }
699
700    fn anonymized(&self) -> AnonymizedRequest {
701        AnonymizedRequest::Direct
702    }
703}
704
705#[cfg(feature = "routerdesc")]
706impl FromIterator<ExtraInfoDigest> for ExtraInfoRequest {
707    fn from_iter<T: IntoIterator<Item = ExtraInfoDigest>>(iter: T) -> Self {
708        Self {
709            requested_extra_infos: RequestedExtraInfos::Digests(iter.into_iter().collect()),
710        }
711    }
712}
713
714/// A request to download a hidden service descriptor
715///
716/// rend-spec-v3 2.2.6
717#[derive(Debug, Clone)]
718#[cfg(feature = "hs-client")]
719pub struct HsDescDownloadRequest {
720    /// What hidden service?
721    hsid: HsBlindId,
722    /// What's the largest acceptable response length?
723    max_len: usize,
724}
725
726#[cfg(feature = "hs-client")]
727impl HsDescDownloadRequest {
728    /// Construct a request for a single onion service descriptor by its
729    /// blinded ID.
730    pub fn new(hsid: HsBlindId) -> Self {
731        /// Default maximum length to use when we have no other information.
732        const DEFAULT_HSDESC_MAX_LEN: usize = 50_000;
733        HsDescDownloadRequest {
734            hsid,
735            max_len: DEFAULT_HSDESC_MAX_LEN,
736        }
737    }
738
739    /// Set the maximum acceptable response length.
740    pub fn set_max_len(&mut self, max_len: usize) {
741        self.max_len = max_len;
742    }
743}
744
745#[cfg(feature = "hs-client")]
746impl sealed::RequestableInner for HsDescDownloadRequest {
747    fn make_request(&self) -> Result<http::Request<RequestBody>> {
748        let hsid = Base64Unpadded::encode_string(self.hsid.as_ref());
749        // We hardcode version 3 here; if we ever have a v4 onion service
750        // descriptor, it will need a different kind of Request.
751        let uri = format!("/tor/hs/3/{}", hsid);
752        let req = http::Request::builder().method("GET").uri(uri);
753        let req = add_common_headers(req, self.anonymized());
754        Ok(req.body(RequestBody::default())?)
755    }
756
757    fn partial_response_body_ok(&self) -> bool {
758        false
759    }
760
761    fn max_response_len(&self) -> usize {
762        self.max_len
763    }
764
765    fn anonymized(&self) -> AnonymizedRequest {
766        AnonymizedRequest::Anonymized
767    }
768}
769
770/// Define a request type for uploading a document.
771#[allow(unused)]
772macro_rules! upload_request {
773    {
774        $(
775            $(#[$m:meta])*
776            pub struct $t:ident (
777                // Does this request require anonymity?
778                // This should be a variant of AnonymizedRequest.
779                $anonymity:ident,
780                // To what URI at the server should the document be posted?
781                // This should begin with "/tor">
782                $uri:expr,
783                // Total maximum length of the _response_ that we'll accept.
784                // If the response is larger than this, we'll abort the request.
785                //
786                // Note that expected response body for a POST is typically _empty_,
787                // but this needs to be nonzero in order to account for
788                // the status line and headers.
789                $max_response_len:expr
790            )
791        );*
792        $(;)?
793    } => {
794        $(
795            $(#[$m])*
796            #[derive(Clone, Debug)]
797            pub struct $t(Arc<str>);
798
799            impl $t {
800                /// Create a new upload request
801                pub fn new(document: Arc<str>) -> Self {
802                    Self(document)
803                }
804            }
805
806            impl sealed::RequestableInner for $t {
807                fn make_request(&self) -> Result<http::Request<RequestBody>> {
808                    /// The upload URI.
809                    const URI: &str = $uri;
810
811                    let req = http::Request::builder().method("POST").uri(URI);
812                    let req = add_common_headers(req, self.anonymized());
813                    Ok(req.body(RequestBody::from(Arc::clone(&self.0)))?)
814                }
815
816                fn partial_response_body_ok(&self) -> bool {
817                    false
818                }
819
820                fn max_response_len(&self) -> usize {
821                    $max_response_len
822                }
823
824                fn anonymized(&self) -> AnonymizedRequest {
825                    AnonymizedRequest::$anonymity
826                }
827            }
828         )*
829    }
830}
831
832#[cfg(feature = "hs-service")]
833upload_request! {
834
835    /// A request to upload a hidden service descriptor
836    ///
837    /// rend-spec-v3 2.2.6
838    pub struct HsDescUploadRequest(
839        Anonymized,
840        "/tor/hs/3/publish",
841        // A real Tor POST _response_ will always be less than this length, which
842        // will fit into 3 DATA messages at most. (The reply will be a single
843        // HTTP line, followed by a Date header.)
844        // Do not increase this limit without thinking about side channels!
845        //
846        // (Note that the body will be empty, but we need to allow some space
847        // to account for the status line and headers.)
848        1024
849    )
850}
851
852#[cfg(feature = "relay")]
853upload_request! {
854    /// A request to upload a router descriptor and optional extra-info document.
855    pub struct UploadRouterDesc(Direct, "/tor/", 4096);
856
857}
858
859/// Encodings that all Tor clients support.
860const UNIVERSAL_ENCODINGS: &str = "deflate, identity";
861
862/// List all the encodings we accept
863fn all_encodings() -> String {
864    #[allow(unused_mut)]
865    let mut encodings = UNIVERSAL_ENCODINGS.to_string();
866    #[cfg(feature = "xz")]
867    {
868        encodings += ", x-tor-lzma";
869    }
870    #[cfg(feature = "zstd")]
871    {
872        encodings += ", x-zstd";
873    }
874
875    encodings
876}
877
878/// Add commonly used headers to the HTTP request.
879///
880/// (Right now, this is only Accept-Encoding.)
881fn add_common_headers(
882    req: http::request::Builder,
883    anon: AnonymizedRequest,
884) -> http::request::Builder {
885    // TODO: gzip, brotli
886    match anon {
887        AnonymizedRequest::Anonymized => {
888            // In an anonymized request, we do not admit to supporting any
889            // encoding besides those that are always available.
890            req.header(http::header::ACCEPT_ENCODING, UNIVERSAL_ENCODINGS)
891        }
892        AnonymizedRequest::Direct => req.header(http::header::ACCEPT_ENCODING, all_encodings()),
893    }
894}
895
896#[cfg(test)]
897mod test {
898    // @@ begin test lint list maintained by maint/add_warning @@
899    #![allow(clippy::bool_assert_comparison)]
900    #![allow(clippy::clone_on_copy)]
901    #![allow(clippy::dbg_macro)]
902    #![allow(clippy::mixed_attributes_style)]
903    #![allow(clippy::print_stderr)]
904    #![allow(clippy::print_stdout)]
905    #![allow(clippy::single_char_pattern)]
906    #![allow(clippy::unwrap_used)]
907    #![allow(clippy::unchecked_time_subtraction)]
908    #![allow(clippy::useless_vec)]
909    #![allow(clippy::needless_pass_by_value)]
910    #![allow(clippy::string_slice)] // See arti#2571
911    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
912    use super::sealed::RequestableInner;
913    use super::*;
914    use web_time_compat::SystemTimeExt;
915
916    #[test]
917    fn test_md_request() -> Result<()> {
918        let d1 = b"This is a testing digest. it isn";
919        let d2 = b"'t actually SHA-256.............";
920
921        let mut req = MicrodescRequest::default();
922        req.push(*d1);
923        assert!(!req.partial_response_body_ok());
924        req.push(*d2);
925        assert!(req.partial_response_body_ok());
926        assert_eq!(req.max_response_len(), 16 << 10);
927
928        let req = crate::util::request_to_string(&req.make_request()?);
929
930        assert_eq!(
931            req,
932            format!(
933                "GET /tor/micro/d/J3QgYWN0dWFsbHkgU0hBLTI1Ni4uLi4uLi4uLi4uLi4-VGhpcyBpcyBhIHRlc3RpbmcgZGlnZXN0LiBpdCBpc24 HTTP/1.0\r\naccept-encoding: {}\r\n\r\n",
934                all_encodings()
935            )
936        );
937
938        // Try it with FromIterator, and use some accessors.
939        let req2: MicrodescRequest = vec![*d1, *d2].into_iter().collect();
940        let ds: Vec<_> = req2.digests().collect();
941        assert_eq!(ds, vec![d1, d2]);
942        let req2 = crate::util::request_to_string(&req2.make_request()?);
943        assert_eq!(req, req2);
944
945        Ok(())
946    }
947
948    #[test]
949    fn test_cert_request() -> Result<()> {
950        let d1 = b"This is a testing dn";
951        let d2 = b"'t actually SHA-256.";
952        let key1 = AuthCertKeyIds {
953            id_fingerprint: (*d1).into(),
954            sk_fingerprint: (*d2).into(),
955        };
956
957        let d3 = b"blah blah blah 1 2 3";
958        let d4 = b"I like pizza from Na";
959        let key2 = AuthCertKeyIds {
960            id_fingerprint: (*d3).into(),
961            sk_fingerprint: (*d4).into(),
962        };
963
964        let mut req = AuthCertRequest::default();
965        req.push(key1);
966        assert!(!req.partial_response_body_ok());
967        req.push(key2);
968        assert!(req.partial_response_body_ok());
969        assert_eq!(req.max_response_len(), 32 << 10);
970
971        let keys: Vec<_> = req.keys().collect();
972        assert_eq!(keys, vec![&key1, &key2]);
973
974        let req = crate::util::request_to_string(&req.make_request()?);
975
976        assert_eq!(
977            req,
978            format!(
979                "GET /tor/keys/fp-sk/5468697320697320612074657374696e6720646e-27742061637475616c6c79205348412d3235362e+626c616820626c616820626c6168203120322033-49206c696b652070697a7a612066726f6d204e61 HTTP/1.0\r\naccept-encoding: {}\r\n\r\n",
980                all_encodings()
981            )
982        );
983
984        let req2: AuthCertRequest = vec![key1, key2].into_iter().collect();
985        let req2 = crate::util::request_to_string(&req2.make_request()?);
986        assert_eq!(req, req2);
987
988        Ok(())
989    }
990
991    #[test]
992    fn test_consensus_request() -> Result<()> {
993        let d1 = RsaIdentity::from_bytes(
994            &hex::decode("03479E93EBF3FF2C58C1C9DBF2DE9DE9C2801B3E").unwrap(),
995        )
996        .unwrap();
997
998        let d2 = b"blah blah blah 12 blah blah blah";
999        let d3 = SystemTime::get();
1000        let mut req = ConsensusRequest::default();
1001
1002        let when = httpdate::fmt_http_date(d3);
1003
1004        req.push_authority_id(d1);
1005        req.push_old_consensus_digest(*d2);
1006        req.set_last_consensus_date(d3);
1007        assert!(!req.partial_response_body_ok());
1008        assert_eq!(req.max_response_len(), (16 << 20) - 1);
1009        assert_eq!(req.old_consensus_digests().next(), Some(d2));
1010        assert_eq!(req.authority_ids().next(), Some(&d1));
1011        assert_eq!(req.last_consensus_date(), Some(d3));
1012
1013        let req = crate::util::request_to_string(&req.make_request()?);
1014
1015        assert_eq!(
1016            req,
1017            format!(
1018                "GET /tor/status-vote/current/consensus-microdesc/03479e93ebf3ff2c58c1c9dbf2de9de9c2801b3e HTTP/1.0\r\naccept-encoding: {}\r\nif-modified-since: {}\r\nx-or-diff-from-consensus: 626c616820626c616820626c616820313220626c616820626c616820626c6168\r\n\r\n",
1019                all_encodings(),
1020                when
1021            )
1022        );
1023
1024        // Request without authorities
1025        let req = ConsensusRequest::default();
1026        let req = crate::util::request_to_string(&req.make_request()?);
1027        assert_eq!(
1028            req,
1029            format!(
1030                "GET /tor/status-vote/current/consensus-microdesc HTTP/1.0\r\naccept-encoding: {}\r\n\r\n",
1031                all_encodings()
1032            )
1033        );
1034
1035        Ok(())
1036    }
1037
1038    #[test]
1039    #[cfg(feature = "routerdesc")]
1040    fn test_rd_request_all() -> Result<()> {
1041        let req = RouterDescRequest::all();
1042        assert!(req.partial_response_body_ok());
1043        assert_eq!(req.max_response_len(), 1 << 26);
1044
1045        let req = crate::util::request_to_string(&req.make_request()?);
1046
1047        assert_eq!(
1048            req,
1049            format!(
1050                "GET /tor/server/all HTTP/1.0\r\naccept-encoding: {}\r\n\r\n",
1051                all_encodings()
1052            )
1053        );
1054
1055        Ok(())
1056    }
1057
1058    #[test]
1059    #[cfg(feature = "routerdesc")]
1060    fn test_rd_request() -> Result<()> {
1061        let d1 = b"at some point I got ";
1062        let d2 = b"of writing in hex...";
1063
1064        let mut req = RouterDescRequest::default();
1065
1066        if let RequestedDescs::Digests(ref mut digests) = req.requested_descriptors {
1067            digests.push(*d1);
1068        }
1069        assert!(!req.partial_response_body_ok());
1070        if let RequestedDescs::Digests(ref mut digests) = req.requested_descriptors {
1071            digests.push(*d2);
1072        }
1073        assert!(req.partial_response_body_ok());
1074        assert_eq!(req.max_response_len(), 16 << 10);
1075
1076        let req = crate::util::request_to_string(&req.make_request()?);
1077
1078        assert_eq!(
1079            req,
1080            format!(
1081                "GET /tor/server/d/617420736f6d6520706f696e74204920676f7420+6f662077726974696e6720696e206865782e2e2e HTTP/1.0\r\naccept-encoding: {}\r\n\r\n",
1082                all_encodings()
1083            )
1084        );
1085
1086        // Try it with FromIterator, and use some accessors.
1087        let req2: RouterDescRequest = vec![*d1, *d2].into_iter().collect();
1088        let ds: Vec<_> = match req2.requested_descriptors {
1089            RequestedDescs::Digests(ref digests) => digests.iter().collect(),
1090            RequestedDescs::All => Vec::new(),
1091        };
1092        assert_eq!(ds, vec![d1, d2]);
1093        let req2 = crate::util::request_to_string(&req2.make_request()?);
1094        assert_eq!(req, req2);
1095        Ok(())
1096    }
1097
1098    #[test]
1099    #[cfg(feature = "routerdesc")]
1100    fn test_extra_info_request() -> Result<()> {
1101        let req = ExtraInfoRequest::from_iter([[0; 20], [1; 20], [2; 20]]);
1102        assert_eq!(
1103            crate::util::request_to_string(&req.make_request()?),
1104            format!(
1105                "GET /tor/extra/d/{}+{}+{} HTTP/1.0\r\naccept-encoding: {}\r\n\r\n",
1106                hex::encode_upper([0; 20]),
1107                hex::encode_upper([1; 20]),
1108                hex::encode_upper([2; 20]),
1109                all_encodings()
1110            )
1111        );
1112
1113        let req = ExtraInfoRequest::all();
1114        assert_eq!(
1115            crate::util::request_to_string(&req.make_request()?),
1116            format!(
1117                "GET /tor/extra/all HTTP/1.0\r\naccept-encoding: {}\r\n\r\n",
1118                all_encodings()
1119            )
1120        );
1121        Ok(())
1122    }
1123
1124    #[test]
1125    #[cfg(feature = "hs-client")]
1126    fn test_hs_desc_download_request() -> Result<()> {
1127        use tor_llcrypto::pk::ed25519::Ed25519Identity;
1128        let hsid = [1, 2, 3, 4].iter().cycle().take(32).cloned().collect_vec();
1129        let hsid = Ed25519Identity::new(hsid[..].try_into().unwrap());
1130        let hsid = HsBlindId::from(hsid);
1131        let req = HsDescDownloadRequest::new(hsid);
1132        assert!(!req.partial_response_body_ok());
1133        assert_eq!(req.max_response_len(), 50 * 1000);
1134
1135        let req = crate::util::request_to_string(&req.make_request()?);
1136
1137        assert_eq!(
1138            req,
1139            format!(
1140                "GET /tor/hs/3/AQIDBAECAwQBAgMEAQIDBAECAwQBAgMEAQIDBAECAwQ HTTP/1.0\r\naccept-encoding: {}\r\n\r\n",
1141                UNIVERSAL_ENCODINGS
1142            )
1143        );
1144
1145        Ok(())
1146    }
1147}