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}