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