Skip to main content

zcash_client_backend/tor/
http.rs

1//! HTTP requests over Tor.
2
3use std::{fmt, future::Future, io, sync::Arc, time::Duration};
4
5use arti_client::TorClient;
6use futures_util::task::SpawnExt;
7use http_body_util::{BodyExt, Empty};
8use hyper::{
9    Request, Response, StatusCode, Uri,
10    body::{Body, Buf, Bytes, Incoming},
11    client::conn,
12    http::{request::Builder, uri::Scheme},
13};
14use hyper_util::rt::TokioIo;
15use serde::de::DeserializeOwned;
16use tokio::io::{AsyncRead, AsyncWrite};
17use tokio_rustls::{
18    TlsConnector,
19    rustls::{ClientConfig, RootCertStore, pki_types::ServerName},
20};
21use tor_rtcompat::PreferredRuntime;
22use tracing::{debug, error};
23
24use super::{Client, Error, Timeouts};
25
26pub mod cryptex;
27
28/// How a particular connection failure should be retried.
29pub enum Retry {
30    /// Retry using the same Tor circuits that resulted in this error.
31    Same,
32    /// Retry using separate Tor circuits isolated from any other Tor usage.
33    Isolated,
34}
35
36/// The stage of an HTTP request that exceeded its deadline.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38#[non_exhaustive]
39pub enum TimeoutPhase {
40    /// Opening the Tor stream to the server, or completing the TLS handshake.
41    Connect,
42    /// Sending the request and receiving its response headers.
43    Request,
44    /// Receiving and parsing the response body.
45    ResponseBody,
46}
47
48impl fmt::Display for TimeoutPhase {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            TimeoutPhase::Connect => write!(f, "connecting to the server"),
52            TimeoutPhase::Request => write!(f, "awaiting the response headers"),
53            TimeoutPhase::ResponseBody => write!(f, "reading the response body"),
54        }
55    }
56}
57
58/// Runs `f`, failing with [`HttpError::Timeout`] if it does not finish within `limit`.
59async fn with_timeout<T>(
60    limit: Duration,
61    phase: TimeoutPhase,
62    f: impl Future<Output = Result<T, Error>>,
63) -> Result<T, Error> {
64    tokio::time::timeout(limit, f)
65        .await
66        .unwrap_or_else(|_| Err(HttpError::Timeout(phase).into()))
67}
68
69pub(super) fn url_is_https(url: &Uri) -> Result<bool, HttpError> {
70    // Only HTTP and HTTPS are supported. A missing scheme, or any other scheme (`ftp`,
71    // `ws`, `file`, ...), is rejected rather than silently treated as plaintext HTTP.
72    match url.scheme() {
73        Some(scheme) if scheme == &Scheme::HTTPS => Ok(true),
74        Some(scheme) if scheme == &Scheme::HTTP => Ok(false),
75        _ => Err(HttpError::NonHttpUrl),
76    }
77}
78
79pub(super) fn parse_url(url: &Uri) -> Result<(bool, String, u16), Error> {
80    let is_https = url_is_https(url)?;
81
82    let host = url.host().ok_or_else(|| HttpError::NonHttpUrl)?.to_string();
83
84    let port = match url.port_u16() {
85        Some(port) => port,
86        None if is_https => 443,
87        None => 80,
88    };
89
90    Ok((is_https, host, port))
91}
92
93/// Applies the caller's `request` closure, then defaults `Accept` to `application/json`
94/// if the closure did not already set it.
95///
96/// The default is applied *after* the closure (rather than before) because
97/// [`Builder::header`] appends rather than replaces: setting `Accept` before running the
98/// closure would leave the default value in place (as the first of two `Accept` values)
99/// even if the closure also set `Accept`, silently defeating any attempt by the caller to
100/// override it.
101fn with_default_json_accept(request: impl Fn(Builder) -> Builder) -> impl Fn(Builder) -> Builder {
102    move |builder| {
103        let builder = request(builder);
104        if builder
105            .headers_ref()
106            .is_some_and(|headers| headers.contains_key(hyper::header::ACCEPT))
107        {
108            builder
109        } else {
110            builder.header(hyper::header::ACCEPT, "application/json")
111        }
112    }
113}
114
115impl Client {
116    /// Makes an HTTP GET request over Tor.
117    ///
118    /// The `request` closure can be used to modify or append HTTP request headers. You
119    /// must not call the following [`Builder`] methods within it:
120    /// - [`Builder::method`] (this is internally set to `GET`).
121    /// - [`Builder::uri`] (this is internally set to `url`).
122    /// - [`Builder::header`] with header name `"Host"` (this is internally set based on
123    ///   `url`).
124    ///
125    /// Returns `Ok(response)` if an HTTP response is received, even if the HTTP status
126    /// code is not in the 200-299 success range (i.e. [`HttpError::Unsuccessful`] is
127    /// never returned).
128    ///
129    /// There are two arguments for controlling retry behaviour:
130    /// - `retry_limit` is the maximum number of times that a failed request should be
131    ///   retried. You can disable retries by setting this to 0.
132    /// - `retry_filter` can be used to only retry requests that fail in specific ways,
133    ///   and control how the retry is performed. You can disable retries by setting this
134    ///   to `|_| None`, and you can ensure the same circuit is reused by setting this to
135    ///   `|res| res.is_err().then_some(Retry::Same)` (e.g. if you require a persistent
136    ///   Tor client identity across queries).
137    ///
138    /// The [`Timeouts`] this client was created with are applied to each attempt
139    /// individually, so the total time this method can take is bounded by `retry_limit + 1`
140    /// times those deadlines. A request that exceeds one of them fails with
141    /// [`HttpError::Timeout`], which `retry_filter` sees like any other error.
142    #[tracing::instrument(skip(self, request, parse_response, retry_filter))]
143    pub async fn http_get<T, F: Future<Output = Result<T, Error>>>(
144        &self,
145        url: Uri,
146        request: impl Fn(Builder) -> Builder,
147        parse_response: impl FnOnce(Incoming) -> F,
148        retry_limit: u8,
149        retry_filter: impl Fn(Result<StatusCode, &Error>) -> Option<Retry>,
150    ) -> Result<Response<T>, Error> {
151        self.http_request(
152            url,
153            |builder| request(builder).method("GET"),
154            Empty::<Bytes>::new(),
155            parse_response,
156            retry_limit,
157            retry_filter,
158        )
159        .await
160    }
161
162    /// Makes an HTTP POST request over Tor.
163    ///
164    /// The `request` closure can be used to modify or append HTTP request headers. You
165    /// must not call the following [`Builder`] methods within it:
166    /// - [`Builder::method`] (this is internally set to `POST`).
167    /// - [`Builder::uri`] (this is internally set to `url`).
168    /// - [`Builder::header`] with header name `"Host"` (this is internally set based on
169    ///   `url`).
170    ///
171    /// Returns `Ok(response)` if an HTTP response is received, even if the HTTP status
172    /// code is not in the 200-299 success range (i.e. [`HttpError::Unsuccessful`] is
173    /// never returned).
174    ///
175    /// There are two arguments for controlling retry behaviour:
176    /// - `retry_limit` is the maximum number of times that a failed request should be
177    ///   retried. You can disable retries by setting this to 0.
178    /// - `retry_filter` can be used to only retry requests that fail in specific ways,
179    ///   and control how the retry is performed. You can disable retries by setting this
180    ///   to `|_| None`, and you can ensure the same circuit is reused by setting this to
181    ///   `|res| res.is_err().then_some(Retry::Same)` (e.g. if you require a persistent
182    ///   Tor client identity across queries).
183    ///
184    /// The [`Timeouts`] this client was created with are applied to each attempt
185    /// individually, so the total time this method can take is bounded by `retry_limit + 1`
186    /// times those deadlines. A request that exceeds one of them fails with
187    /// [`HttpError::Timeout`], which `retry_filter` sees like any other error.
188    #[tracing::instrument(skip(self, request, body, parse_response, retry_filter))]
189    pub async fn http_post<B, T, F>(
190        &self,
191        url: Uri,
192        request: impl Fn(Builder) -> Builder,
193        body: B,
194        parse_response: impl FnOnce(Incoming) -> F,
195        retry_limit: u8,
196        retry_filter: impl Fn(Result<StatusCode, &Error>) -> Option<Retry>,
197    ) -> Result<Response<T>, Error>
198    where
199        B: Body + Clone + Send + 'static,
200        B::Data: Send,
201        B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
202        F: Future<Output = Result<T, Error>>,
203    {
204        self.http_request(
205            url,
206            |builder| request(builder).method("POST"),
207            body,
208            parse_response,
209            retry_limit,
210            retry_filter,
211        )
212        .await
213    }
214
215    /// Makes an HTTP request over Tor.
216    ///
217    /// There are two arguments for controlling retry behaviour:
218    /// - `retry_limit` is the maximum number of times that a failed request should be
219    ///   retried. You can disable retries by setting this to 0.
220    /// - `retry_filter` can be used to only retry requests that fail in specific ways,
221    ///   and control how the retry is performed. You can disable retries by setting this
222    ///   to `|_| None`, and you can ensure the same circuit is reused by setting this to
223    ///   `|res| res.is_err().then_some(Retry::Same)` (e.g. if you require a persistent
224    ///   Tor client identity across queries).
225    ///
226    /// The [`Timeouts`] this client was created with are applied to each attempt
227    /// individually, so the total time this method can take is bounded by `retry_limit + 1`
228    /// times those deadlines. A request that exceeds one of them fails with
229    /// [`HttpError::Timeout`], which `retry_filter` sees like any other error.
230    async fn http_request<B, T, F>(
231        &self,
232        url: Uri,
233        request: impl Fn(Builder) -> Builder,
234        body: B,
235        parse_response: impl FnOnce(Incoming) -> F,
236        retry_limit: u8,
237        retry_filter: impl Fn(Result<StatusCode, &Error>) -> Option<Retry>,
238    ) -> Result<Response<T>, Error>
239    where
240        B: Body + Clone + Send + 'static,
241        B::Data: Send,
242        B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
243        F: Future<Output = Result<T, Error>>,
244    {
245        let mut retries_remaining = retry_limit;
246        let mut client = None;
247
248        let (parts, body) = loop {
249            let active = client.as_ref().unwrap_or(self);
250            let response = one_http_request(
251                &active.inner,
252                &active.timeouts,
253                url.clone(),
254                &request,
255                body.clone(),
256            )
257            .await;
258
259            match (
260                retries_remaining.checked_sub(1),
261                retry_filter(response.as_ref().map(|response| response.status())),
262            ) {
263                (Some(retries), Some(retry)) => {
264                    debug!("Retrying due to filter match");
265                    retries_remaining = retries;
266
267                    match retry {
268                        Retry::Same => (),
269                        Retry::Isolated => {
270                            debug!("Switching to isolated Tor circuit for retry");
271                            client = Some(self.isolated_client());
272                        }
273                    }
274                }
275                (None, _) | (_, None) => break response,
276            }
277        }?
278        .into_parts();
279
280        Ok(Response::from_parts(
281            parts,
282            with_timeout(
283                self.timeouts.response_body,
284                TimeoutPhase::ResponseBody,
285                parse_response(body),
286            )
287            .await?,
288        ))
289    }
290
291    /// Makes an HTTP GET request over Tor, parsing the response as JSON.
292    ///
293    /// This is a simple wrapper around [`Self::http_get`]. Use that method if you need
294    /// more control over the response parsing.
295    ///
296    /// The `request` closure runs first; this method then defaults `Accept` to
297    /// `application/json` only if the closure did not already set it, so a caller can
298    /// override the default `Accept` (or add other headers, such as `User-Agent`) by
299    /// setting them in the closure. See [`Self::http_get`] for the [`Builder`] methods you
300    /// must not call within it.
301    ///
302    /// Returns `Ok(response)` if an HTTP response is received, even if the HTTP status
303    /// code is not in the 200-299 success range (i.e. [`HttpError::Unsuccessful`] is
304    /// never returned).
305    ///
306    /// There are two arguments for controlling retry behaviour:
307    /// - `retry_limit` is the maximum number of times that a failed request should be
308    ///   retried. You can disable retries by setting this to 0.
309    /// - `retry_filter` can be used to only retry requests that fail in specific ways,
310    ///   and control how the retry is performed. You can disable retries by setting this
311    ///   to `|_| None`, and you can ensure the same circuit is reused by setting this to
312    ///   `|res| res.is_err().then_some(Retry::Same)` (e.g. if you require a persistent
313    ///   Tor client identity across queries).
314    ///
315    /// The [`Timeouts`] this client was created with are applied to each attempt
316    /// individually, so the total time this method can take is bounded by `retry_limit + 1`
317    /// times those deadlines. A request that exceeds one of them fails with
318    /// [`HttpError::Timeout`], which `retry_filter` sees like any other error.
319    pub async fn http_get_json<T: DeserializeOwned>(
320        &self,
321        url: Uri,
322        request: impl Fn(Builder) -> Builder,
323        retry_limit: u8,
324        retry_filter: impl Fn(Result<StatusCode, &Error>) -> Option<Retry>,
325    ) -> Result<Response<T>, Error> {
326        self.http_get(
327            url,
328            with_default_json_accept(request),
329            |body| async {
330                Ok(serde_json::from_reader(
331                    body.collect()
332                        .await
333                        .map_err(HttpError::from)?
334                        .aggregate()
335                        .reader(),
336                )
337                .map_err(HttpError::from)?)
338            },
339            retry_limit,
340            retry_filter,
341        )
342        .await
343    }
344}
345
346async fn one_http_request<B>(
347    tor_client: &TorClient<PreferredRuntime>,
348    timeouts: &Timeouts,
349    url: Uri,
350    request: impl FnOnce(Builder) -> Builder,
351    body: B,
352) -> Result<Response<Incoming>, Error>
353where
354    B: Body + Send + 'static,
355    B::Data: Send,
356    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
357{
358    let (is_https, host, port) = parse_url(&url)?;
359
360    debug!("Connecting through Tor to {}:{}", host, port);
361
362    // The Tor stream and the TLS handshake are bounded together, so that
363    // `Timeouts::connect` means the same thing here as it does for the gRPC transport
364    // (where `tonic` applies it to the connector as a whole).
365    if is_https {
366        let stream = with_timeout(timeouts.connect, TimeoutPhase::Connect, async {
367            // Box the Tor connection future. Its nested state is otherwise too deep
368            // for the compiler to lay out the futures of the callers.
369            let stream = Box::pin(tor_client.connect((host.as_str(), port))).await?;
370
371            // On apple-darwin targets there's an issue with the native TLS implementation
372            // when used over Tor circuits. We use Rustls instead.
373            //
374            // https://gitlab.torproject.org/tpo/core/arti/-/issues/715
375            let root_store = RootCertStore {
376                roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
377            };
378            let config = ClientConfig::builder()
379                .with_root_certificates(root_store)
380                .with_no_client_auth();
381            let connector = TlsConnector::from(Arc::new(config));
382            let dnsname = ServerName::try_from(host).expect("Already checked");
383            Ok(connector
384                .connect(dnsname, stream)
385                .await
386                .map_err(HttpError::Tls)?)
387        })
388        .await?;
389
390        with_timeout(
391            timeouts.request,
392            TimeoutPhase::Request,
393            make_http_request(stream, url, request, body),
394        )
395        .await
396    } else {
397        let stream = with_timeout(timeouts.connect, TimeoutPhase::Connect, async {
398            // Box the Tor connection future. Its nested state is otherwise too deep
399            // for the compiler to lay out the futures of the callers.
400            Ok(Box::pin(tor_client.connect((host.as_str(), port))).await?)
401        })
402        .await?;
403
404        with_timeout(
405            timeouts.request,
406            TimeoutPhase::Request,
407            make_http_request(stream, url, request, body),
408        )
409        .await
410    }
411}
412
413/// Overwrites `headers`'s `Host` value with `host`.
414///
415/// Uses [`hyper::HeaderMap::insert`], which replaces any existing values, rather than
416/// [`Builder::header`] (used for every other header on the request), which only appends.
417/// This is what lets [`make_http_request`] enforce that `Host` matches the circuit and TLS
418/// connection even if a caller's request-construction closure also set it.
419fn set_host_header(headers: &mut hyper::HeaderMap, host: &str) {
420    headers.insert(
421        hyper::header::HOST,
422        hyper::header::HeaderValue::from_str(host)
423            .expect("a URI authority is always a valid header value"),
424    );
425}
426
427async fn make_http_request<B>(
428    stream: impl AsyncRead + AsyncWrite + Unpin + Send + 'static,
429    url: Uri,
430    request: impl FnOnce(Builder) -> Builder,
431    body: B,
432) -> Result<Response<Incoming>, Error>
433where
434    B: Body + Send + 'static,
435    B::Data: Send,
436    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
437{
438    debug!("Making request");
439    let (mut sender, connection) = conn::http1::handshake(TokioIo::new(stream))
440        .await
441        .map_err(HttpError::from)?;
442
443    // Spawn a task to poll the connection and drive the HTTP state.
444    PreferredRuntime::current()?
445        .spawn(async move {
446            if let Err(e) = connection.await {
447                error!("Connection failed: {}", e);
448            }
449        })
450        .map_err(HttpError::from)?;
451
452    // Build the request. We let the caller make whatever request modifications they need,
453    // then set the URI and forcibly overwrite the Host header (see `set_host_header`) so
454    // that they are guaranteed to match the circuit and TLS connection, regardless of
455    // whether the caller's closure also set Host. Host is `host[":"port]` per RFC 9110:
456    // never userinfo, even if `url` carries any, and the port only when it's set.
457    let authority = url.authority().expect("Already checked");
458    let host = match authority.port() {
459        Some(port) => format!("{}:{}", authority.host(), port),
460        None => authority.host().to_string(),
461    };
462    let mut req = request(Request::builder())
463        .uri(url)
464        .body(body)
465        .map_err(HttpError::from)?;
466    set_host_header(req.headers_mut(), &host);
467    let response = sender.send_request(req).await.map_err(HttpError::from)?;
468    debug!("Response status code: {}", response.status());
469
470    Ok(response)
471}
472
473/// Errors that can occurr while using HTTP-over-Tor.
474#[derive(Debug)]
475#[non_exhaustive]
476pub enum HttpError {
477    /// A non-HTTP URL was encountered.
478    NonHttpUrl,
479    /// An HTTP error.
480    Http(hyper::http::Error),
481    /// A [`hyper`] error.
482    Hyper(hyper::Error),
483    /// A JSON parsing error.
484    Json(serde_json::Error),
485    /// An error occurred while spawning a background worker task for driving the HTTP
486    /// connection.
487    Spawn(futures_util::task::SpawnError),
488    /// A TLS-specific IO error.
489    Tls(io::Error),
490    /// The request did not complete within the corresponding [`Timeouts`] deadline.
491    ///
492    /// [`Timeouts`]: super::Timeouts
493    Timeout(TimeoutPhase),
494    /// The status code indicated that the request was unsuccessful.
495    ///
496    /// This is only returned by APIs that make specific queries, such as
497    /// [`Client::get_latest_zec_to_usd_rate`]. Generic APIs like [`Client::http_get`]
498    /// will not return this error variant.
499    Unsuccessful(hyper::http::StatusCode),
500}
501
502impl fmt::Display for HttpError {
503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504        match self {
505            HttpError::NonHttpUrl => write!(f, "Only HTTP or HTTPS URLs are supported"),
506            HttpError::Http(e) => write!(f, "HTTP error: {e}"),
507            HttpError::Hyper(e) => write!(f, "Hyper error: {e}"),
508            HttpError::Json(e) => write!(f, "Failed to parse JSON: {e}"),
509            HttpError::Spawn(e) => write!(f, "Failed to spawn task: {e}"),
510            HttpError::Tls(e) => write!(f, "TLS error: {e}"),
511            HttpError::Timeout(phase) => write!(f, "Timed out while {phase}"),
512            HttpError::Unsuccessful(status) => write!(f, "Request was unsuccessful ({status:?})"),
513        }
514    }
515}
516
517impl std::error::Error for HttpError {
518    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
519        match self {
520            HttpError::NonHttpUrl => None,
521            HttpError::Http(e) => Some(e),
522            HttpError::Hyper(e) => Some(e),
523            HttpError::Json(e) => Some(e),
524            HttpError::Spawn(e) => Some(e),
525            HttpError::Tls(e) => Some(e),
526            HttpError::Timeout(_) => None,
527            HttpError::Unsuccessful(_) => None,
528        }
529    }
530}
531
532impl From<hyper::http::Error> for HttpError {
533    fn from(e: hyper::http::Error) -> Self {
534        HttpError::Http(e)
535    }
536}
537
538impl From<hyper::Error> for HttpError {
539    fn from(e: hyper::Error) -> Self {
540        HttpError::Hyper(e)
541    }
542}
543
544impl From<serde_json::Error> for HttpError {
545    fn from(e: serde_json::Error) -> Self {
546        HttpError::Json(e)
547    }
548}
549
550impl From<futures_util::task::SpawnError> for HttpError {
551    fn from(e: futures_util::task::SpawnError) -> Self {
552        HttpError::Spawn(e)
553    }
554}
555
556#[cfg(test)]
557mod tests {
558    use std::time::Duration;
559
560    use http_body_util::Empty;
561    use hyper::{Uri, body::Bytes};
562    use tokio::io::AsyncReadExt;
563
564    use super::{
565        HttpError, TimeoutPhase, make_http_request, parse_url, set_host_header, url_is_https,
566        with_default_json_accept, with_timeout,
567    };
568    use crate::tor::Error;
569
570    /// A server that accepts the connection and then never speaks must not be able to
571    /// hold a request pending forever.
572    #[tokio::test]
573    async fn silent_server_times_out() {
574        // The far end of this duplex stream is held open for the duration of the test but
575        // never read from or written to, modelling a peer that stalls after connecting.
576        let (stream, _silent_peer) = tokio::io::duplex(1024);
577
578        let res = with_timeout(
579            Duration::from_millis(100),
580            TimeoutPhase::Request,
581            make_http_request(
582                stream,
583                "http://example.com/".parse().unwrap(),
584                |builder| builder.method("GET"),
585                Empty::<Bytes>::new(),
586            ),
587        )
588        .await;
589
590        assert!(
591            matches!(
592                res,
593                Err(Error::Http(HttpError::Timeout(TimeoutPhase::Request)))
594            ),
595            "expected a request timeout, got {res:?}",
596        );
597    }
598
599    #[test]
600    fn parse_url_infers_default_ports() {
601        assert_eq!(
602            parse_url(&"https://example.com/".parse::<Uri>().unwrap()).unwrap(),
603            (true, "example.com".to_string(), 443),
604        );
605        assert_eq!(
606            parse_url(&"http://example.com/".parse::<Uri>().unwrap()).unwrap(),
607            (false, "example.com".to_string(), 80),
608        );
609    }
610
611    #[test]
612    fn parse_url_respects_explicit_port() {
613        assert_eq!(
614            parse_url(&"http://example.com:8080/".parse::<Uri>().unwrap()).unwrap(),
615            (false, "example.com".to_string(), 8080),
616        );
617    }
618
619    #[test]
620    fn non_http_schemes_are_rejected() {
621        // The bug this guards against: a URL with an explicit non-HTTP(S) scheme used to
622        // be accepted and treated as plaintext HTTP, contradicting `NonHttpUrl`'s "only
623        // HTTP or HTTPS" contract. A non-HTTP(S) URL must never yield a usable request,
624        // by either of two paths: it fails to parse as a URI at all (`file:///...` has an
625        // empty authority, which hyper rejects), or it parses and `url_is_https` /
626        // `parse_url` reject its scheme.
627        for url in [
628            "ftp://example.com/",
629            "ws://example.com/",
630            "wss://example.com/",
631            "file:///etc/passwd",
632        ] {
633            let Ok(uri) = url.parse::<Uri>() else {
634                continue; // rejected at the URI-parse boundary; nothing reaches the transport
635            };
636            assert!(
637                matches!(url_is_https(&uri), Err(HttpError::NonHttpUrl)),
638                "{url} should be rejected by url_is_https",
639            );
640            assert!(
641                matches!(parse_url(&uri), Err(Error::Http(HttpError::NonHttpUrl))),
642                "{url} should be rejected by parse_url",
643            );
644        }
645    }
646
647    #[test]
648    fn missing_scheme_is_rejected() {
649        let uri = "//example.com/".parse::<Uri>().unwrap();
650        assert!(matches!(url_is_https(&uri), Err(HttpError::NonHttpUrl)));
651    }
652
653    /// A caller that doesn't set `Accept` itself gets the default `application/json`.
654    #[test]
655    fn with_default_json_accept_sets_accept_by_default() {
656        let builder = with_default_json_accept(|b| b)(hyper::Request::builder());
657        assert_eq!(
658            builder
659                .headers_ref()
660                .and_then(|h| h.get(hyper::header::ACCEPT))
661                .and_then(|v| v.to_str().ok()),
662            Some("application/json"),
663        );
664    }
665
666    /// The caller's closure runs before the default is applied, so a caller that sets
667    /// `Accept` itself gets that value instead of the default. (`Builder::header` appends
668    /// rather than replaces, so applying the default first would not achieve this: the
669    /// caller's value would just become a second, effectively-ignored `Accept` header.)
670    #[test]
671    fn with_default_json_accept_lets_caller_override_accept() {
672        let builder = with_default_json_accept(|b| b.header(hyper::header::ACCEPT, "text/plain"))(
673            hyper::Request::builder(),
674        );
675        assert_eq!(
676            builder
677                .headers_ref()
678                .and_then(|h| h.get(hyper::header::ACCEPT))
679                .and_then(|v| v.to_str().ok()),
680            Some("text/plain"),
681        );
682    }
683
684    /// The caller's closure can also set additional headers, such as `User-Agent`, that
685    /// `http_get_json` does not set itself.
686    #[test]
687    fn with_default_json_accept_allows_additional_headers() {
688        let builder =
689            with_default_json_accept(|b| b.header(hyper::header::USER_AGENT, "test-agent"))(
690                hyper::Request::builder(),
691            );
692        let headers = builder.headers_ref().unwrap();
693        assert_eq!(
694            headers
695                .get(hyper::header::ACCEPT)
696                .and_then(|v| v.to_str().ok()),
697            Some("application/json"),
698        );
699        assert_eq!(
700            headers
701                .get(hyper::header::USER_AGENT)
702                .and_then(|v| v.to_str().ok()),
703            Some("test-agent"),
704        );
705    }
706
707    /// With no prior `Host` value, `set_host_header` simply sets it.
708    #[test]
709    fn set_host_header_sets_host() {
710        let mut headers = hyper::HeaderMap::new();
711        set_host_header(&mut headers, "example.com");
712        assert_eq!(
713            headers
714                .get(hyper::header::HOST)
715                .and_then(|v| v.to_str().ok()),
716            Some("example.com"),
717        );
718    }
719
720    /// A bogus `Host` set by a request-construction closure must not survive into the
721    /// final request: `set_host_header` replaces it (via `HeaderMap::insert`) rather than
722    /// appending a second, easily-overlooked value alongside it (as `Builder::header`
723    /// would), so that `Host` cannot end up disagreeing with the circuit and TLS
724    /// connection that were established using the real host.
725    #[test]
726    fn set_host_header_overwrites_a_prior_value() {
727        let mut headers = hyper::HeaderMap::new();
728        headers.insert(hyper::header::HOST, "evil.example".parse().unwrap());
729        set_host_header(&mut headers, "example.com");
730
731        let values: Vec<&str> = headers
732            .get_all(hyper::header::HOST)
733            .iter()
734            .map(|v| v.to_str().unwrap())
735            .collect();
736        assert_eq!(values, vec!["example.com"]);
737    }
738
739    /// `make_http_request` must send exactly the `Host` header derived from the
740    /// connection's URL, discarding any `Host` a request-construction closure tries to
741    /// set. This pins the actual wiring end-to-end (through `Builder::header`'s append and
742    /// `set_host_header`'s insert) rather than just `set_host_header` in isolation:
743    /// deleting the `set_host_header` call in `make_http_request` turns this test red. The
744    /// URL's userinfo (`user:pass@`) must not appear in `Host` either.
745    #[tokio::test]
746    async fn make_http_request_forces_host_to_match_the_connection() {
747        let (stream, mut peer) = tokio::io::duplex(4096);
748
749        // Nothing ever responds, so `make_http_request` itself would hang forever
750        // awaiting a response; drive it in the background and inspect only what it wrote
751        // to the wire before that point.
752        let _handle = tokio::spawn(make_http_request(
753            stream,
754            "http://user:pass@example.com:1234/path".parse().unwrap(),
755            |builder| builder.header(hyper::header::HOST, "evil.example"),
756            Empty::<Bytes>::new(),
757        ));
758
759        // Read until the full request head has arrived.
760        let mut received = Vec::new();
761        let mut buf = [0u8; 1024];
762        loop {
763            let n = peer.read(&mut buf).await.unwrap();
764            assert!(
765                n > 0,
766                "connection closed before a full request head arrived"
767            );
768            received.extend_from_slice(&buf[..n]);
769            if received.windows(4).any(|w| w == b"\r\n\r\n") {
770                break;
771            }
772        }
773        let head = String::from_utf8(received).unwrap();
774
775        let host_values: Vec<&str> = head
776            .lines()
777            .filter_map(|line| {
778                let (name, value) = line.split_once(':')?;
779                name.eq_ignore_ascii_case("host").then(|| value.trim())
780            })
781            .collect();
782        assert_eq!(
783            host_values,
784            vec!["example.com:1234"],
785            "expected exactly one Host header, carrying the connection's own \
786             userinfo-stripped host:port, in request head:\n{head}",
787        );
788    }
789}
790
791#[cfg(all(test, live_network_tests))]
792mod live_network_tests {
793    use http_body_util::BodyExt;
794    use hyper::body::Buf;
795
796    use crate::tor::{
797        Client,
798        http::{HttpError, Retry},
799    };
800
801    #[test]
802    fn httpbin() {
803        let tor_dir = tempfile::tempdir().unwrap();
804
805        tokio::runtime::Runtime::new().unwrap().block_on(async {
806            // Start a new Tor client.
807            let client = Client::create(tor_dir.path(), |_| ()).await.unwrap();
808
809            // Test HTTP GET
810            let get_response = client
811                .http_get_json::<serde_json::Value>(
812                    "https://httpbin.org/get".parse().unwrap(),
813                    |b| b,
814                    3,
815                    |res| res.is_err().then_some(Retry::Same),
816                )
817                .await
818                .unwrap();
819            assert_eq!(
820                get_response.body().get("url").and_then(|v| v.as_str()),
821                Some("https://httpbin.org/get"),
822            );
823            assert_eq!(
824                get_response
825                    .body()
826                    .get("headers")
827                    .and_then(|v| v.as_object())
828                    .and_then(|h| h.get("Host"))
829                    .and_then(|v| v.as_str()),
830                Some("httpbin.org"),
831            );
832            assert!(
833                get_response
834                    .body()
835                    .get("args")
836                    .unwrap()
837                    .as_object()
838                    .unwrap()
839                    .is_empty()
840            );
841
842            // Test HTTP POST
843            let post_body = "Some body";
844            let post_response = client
845                .http_post(
846                    "https://httpbin.org/post".parse().unwrap(),
847                    |builder| builder.header(hyper::header::ACCEPT, "application/json"),
848                    http_body_util::Full::new(post_body.as_bytes()),
849                    |body| async {
850                        Ok(serde_json::from_reader::<_, serde_json::Value>(
851                            body.collect()
852                                .await
853                                .map_err(HttpError::from)?
854                                .aggregate()
855                                .reader(),
856                        )
857                        .map_err(HttpError::from)?)
858                    },
859                    3,
860                    |res| res.is_err().then_some(Retry::Same),
861                )
862                .await
863                .unwrap();
864            assert!(
865                post_response
866                    .body()
867                    .get("args")
868                    .unwrap()
869                    .as_object()
870                    .unwrap()
871                    .is_empty()
872            );
873            assert_eq!(
874                post_response.body().get("data").and_then(|v| v.as_str()),
875                Some(post_body),
876            );
877        })
878    }
879}