saluki_io/net/client/http/
client.rs

1#[cfg(unix)]
2use std::path::PathBuf;
3use std::{
4    future::Future,
5    pin::Pin,
6    task::{Context, Poll},
7    time::Duration,
8};
9
10use bytes::{Buf, Bytes};
11use http::{Request, Response, Uri};
12use http_body::{Body, Frame, SizeHint};
13use http_body_util::combinators::BoxBody;
14use hyper::body::Incoming;
15use hyper_http_proxy::Proxy;
16use hyper_util::{
17    client::legacy::{connect::capture_connection, Builder},
18    rt::{TokioExecutor, TokioTimer},
19};
20use metrics::Counter;
21use pin_project::pin_project;
22use rustls::ClientConfig;
23use saluki_error::GenericError;
24use saluki_metrics::MetricsBuilder;
25use saluki_tls::{ensure_client_config_fips_compliant, ClientTLSConfigBuilder, TlsMinimumVersion};
26use stringtheory::MetaString;
27use tower::{timeout::TimeoutLayer, util::BoxCloneService, BoxError, Service, ServiceBuilder, ServiceExt as _};
28
29use super::{
30    conn::{check_connection_state, HttpProtocol, HttpsCapableConnectorBuilder},
31    telemetry::HttpTransactionErrorTelemetry,
32    EndpointTelemetryLayer,
33};
34
35/// The type-erased body type used internally by [`HttpClient`].
36///
37/// All request bodies are converted to this type before being sent over the wire, which ensures a single
38/// monomorphization of the underlying HTTP/2 and TLS stacks regardless of the caller's body type.
39pub type ClientBody = BoxBody<Bytes, Box<dyn std::error::Error + Send + Sync>>;
40
41#[pin_project]
42struct ClientBodyAdapter<B> {
43    #[pin]
44    inner: B,
45    size_hint: SizeHint,
46}
47
48impl<B> Body for ClientBodyAdapter<B>
49where
50    B: Body,
51    B::Data: Buf,
52    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
53{
54    type Data = Bytes;
55    type Error = Box<dyn std::error::Error + Send + Sync>;
56
57    fn poll_frame(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
58        self.project().inner.poll_frame(cx).map(|maybe_frame| {
59            maybe_frame.map(|result| {
60                result
61                    .map(|frame| frame.map_data(|mut data| data.copy_to_bytes(data.remaining())))
62                    .map_err(Into::into)
63            })
64        })
65    }
66
67    fn size_hint(&self) -> SizeHint {
68        self.size_hint
69    }
70}
71
72/// An HTTP client.
73#[derive(Clone)]
74pub struct HttpClient {
75    inner: BoxCloneService<Request<ClientBody>, Response<Incoming>, BoxError>,
76}
77
78impl HttpClient {
79    /// Creates a new builder for configuring an HTTP client.
80    pub fn builder() -> HttpClientBuilder {
81        HttpClientBuilder::default()
82    }
83
84    /// Sends a request to the server, and waits for a response.
85    ///
86    /// The request body is type-erased internally, so callers can use any body type that implements
87    /// [`Body`] with `Data` types that implement [`Buf`].
88    ///
89    /// # Errors
90    ///
91    /// If there was an error sending the request, an error will be returned.
92    pub async fn send<B>(&mut self, req: Request<B>) -> Result<Response<Incoming>, GenericError>
93    where
94        B: Body + Send + Sync + 'static,
95        B::Data: Buf + Send,
96        B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
97    {
98        let mut req = req.map(into_client_body);
99        let captured_conn = capture_connection(&mut req);
100        let result = self
101            .inner
102            .ready()
103            .await
104            .map_err(GenericError::from_boxed)?
105            .call(req)
106            .await;
107
108        check_connection_state(captured_conn);
109
110        result.map_err(GenericError::from_boxed)
111    }
112}
113
114impl Service<Request<ClientBody>> for HttpClient {
115    type Response = Response<Incoming>;
116    type Error = BoxError;
117    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
118
119    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
120        self.inner.poll_ready(cx)
121    }
122
123    fn call(&mut self, mut req: Request<ClientBody>) -> Self::Future {
124        let captured_conn = capture_connection(&mut req);
125        let fut = self.inner.call(req);
126
127        Box::pin(async move {
128            let result = fut.await;
129
130            check_connection_state(captured_conn);
131
132            result
133        })
134    }
135}
136
137/// Converts an arbitrary body into the type-erased [`ClientBody`].
138///
139/// This uses `Buf::copy_to_bytes` for the data conversion, which is zero-copy when the underlying
140/// data is already `Bytes`.
141pub fn into_client_body<B>(body: B) -> ClientBody
142where
143    B: Body + Send + Sync + 'static,
144    B::Data: Buf + Send,
145    B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
146{
147    let size_hint = body.size_hint();
148    BoxBody::new(ClientBodyAdapter { inner: body, size_hint })
149}
150
151/// An HTTP client builder.
152///
153/// Provides an ergonomic builder API for configuring an HTTP client.
154///
155/// # Defaults
156///
157/// A number of sensible defaults are provided:
158///
159/// - support for both HTTP and HTTPS (uses platform's root certificates for server certificate validation)
160/// - support for both HTTP/1.1 and HTTP/2 (automatically negotiated via ALPN)
161/// - non-infinite timeouts for various stages of the request lifecycle (30 second connect timeout, 60 second per-request timeout)
162/// - connection pool for reusing connections (45 second idle connection timeout, and a maximum of 5 idle connections
163///   per host)
164/// - support for FIPS-compliant cryptography if the `fips` feature is enabled in the `saluki-tls` crate
165pub struct HttpClientBuilder {
166    connector_builder: HttpsCapableConnectorBuilder,
167    hyper_builder: Builder,
168    tls_builder: ClientTLSConfigBuilder,
169    client_tls_config: Option<ClientConfig>,
170    request_timeout: Option<Duration>,
171    endpoint_telemetry: Option<EndpointTelemetryLayer>,
172    proxies: Option<Vec<Proxy>>,
173}
174
175impl HttpClientBuilder {
176    /// Sets the timeout when connecting to the remote host.
177    ///
178    /// Defaults to 30 seconds.
179    pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
180        self.connector_builder = self.connector_builder.with_connect_timeout(timeout);
181        self
182    }
183
184    /// Sets the timeout for completing the TLS handshake after a connection is established.
185    ///
186    /// This bounds only the TLS handshake step, distinct from the connect timeout, which bounds the underlying TCP
187    /// (or other transport) connection setup that precedes it.
188    ///
189    /// Defaults to 10 seconds.
190    pub fn with_tls_handshake_timeout(mut self, timeout: Duration) -> Self {
191        self.connector_builder = self.connector_builder.with_tls_handshake_timeout(timeout);
192        self
193    }
194
195    /// Sets the per-request timeout.
196    ///
197    /// The request timeout applies to each individual request made to the remote host, including each request made when
198    /// retrying a failed request.
199    ///
200    /// Defaults to 20 seconds.
201    pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
202        self.request_timeout = Some(timeout);
203        self
204    }
205
206    /// Allow requests to run indefinitely.
207    ///
208    /// This means there will be no overall timeout for the request, but the request still may be subject to other
209    /// configuration settings, such as the connect timeout or retry policy.
210    pub fn without_request_timeout(mut self) -> Self {
211        self.request_timeout = None;
212        self
213    }
214
215    /// Sets the HTTP protocol selection for client connections.
216    ///
217    /// Defaults to [`HttpProtocol::Auto`], which automatically negotiates HTTP/2 with HTTP/1.1 fallback.
218    pub fn with_http_protocol(mut self, protocol: HttpProtocol) -> Self {
219        self.connector_builder = self.connector_builder.with_http_protocol(protocol);
220        self
221    }
222
223    /// Sets the maximum age of a connection before it's closed.
224    ///
225    /// This is distinct from the maximum idle time: if any connection's age exceeds `limit`, it will be closed rather
226    /// than being reused and added to the idle connection pool.
227    ///
228    /// Defaults to no limit.
229    pub fn with_connection_age_limit<L>(mut self, limit: L) -> Self
230    where
231        L: Into<Option<Duration>>,
232    {
233        self.connector_builder = self.connector_builder.with_connection_age_limit(limit);
234        self
235    }
236
237    /// Sets the maximum number of idle connections per host.
238    ///
239    /// Defaults to 5.
240    pub fn with_max_idle_conns_per_host(mut self, max: usize) -> Self {
241        self.hyper_builder.pool_max_idle_per_host(max);
242        self
243    }
244
245    /// Sets the idle connection timeout.
246    ///
247    /// Once a connection has been idle in the pool for longer than this duration, it will be closed and removed from
248    /// the pool.
249    ///
250    /// Defaults to 45 seconds.
251    pub fn with_idle_conn_timeout(mut self, timeout: Duration) -> Self {
252        self.hyper_builder.pool_idle_timeout(timeout);
253        self
254    }
255
256    /// Sets the proxies to be used for outgoing requests.
257    ///
258    /// Defaults to no proxies. (i.e requests will be sent directly without using a proxy).
259    pub fn with_proxies(mut self, proxies: Vec<Proxy>) -> Self {
260        self.proxies = Some(proxies);
261        self
262    }
263
264    /// Enables per-endpoint telemetry for HTTP transactions.
265    ///
266    /// See [`EndpointTelemetryLayer`] for more information.
267    pub fn with_endpoint_telemetry<F>(mut self, metrics_builder: MetricsBuilder, endpoint_name_fn: Option<F>) -> Self
268    where
269        F: Fn(&Uri) -> Option<MetaString> + Send + Sync + 'static,
270    {
271        let error_telemetry = HttpTransactionErrorTelemetry::from_builder(&metrics_builder);
272        self.connector_builder = self.connector_builder.with_error_telemetry(error_telemetry.clone());
273
274        let mut layer = EndpointTelemetryLayer::default()
275            .with_metrics_builder(metrics_builder)
276            .with_error_telemetry(error_telemetry);
277
278        if let Some(endpoint_name_fn) = endpoint_name_fn {
279            layer = layer.with_endpoint_name_fn(endpoint_name_fn);
280        }
281
282        self.endpoint_telemetry = Some(layer);
283        self
284    }
285
286    /// Sets a Unix domain socket path to route all connections through.
287    ///
288    /// When set, the client will connect to this Unix socket instead of performing DNS resolution
289    /// and TCP connection. The URI host is ignored—all requests are sent through the configured
290    /// socket.
291    ///
292    /// Defaults to unset (TCP connections via DNS).
293    #[cfg(unix)]
294    pub fn with_unix_socket_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
295        self.connector_builder = self.connector_builder.with_unix_socket_path(path);
296        self
297    }
298
299    /// Sets the TLS configuration.
300    ///
301    /// A TLS configuration builder is provided to allow for more advanced configuration of the TLS connection.
302    /// [`Self::with_client_tls_config`] overrides these settings regardless of call order.
303    pub fn with_tls_config<F>(mut self, f: F) -> Self
304    where
305        F: FnOnce(ClientTLSConfigBuilder) -> ClientTLSConfigBuilder,
306    {
307        self.tls_builder = f(self.tls_builder);
308        self
309    }
310
311    /// Sets a complete Rustls client TLS configuration.
312    ///
313    /// This configuration takes precedence over all option-based settings made through [`Self::with_tls_config`] or
314    /// [`Self::with_min_tls_version`], regardless of call order. The configuration is otherwise preserved, including
315    /// its certificate verifier, client identity, enabled TLS protocol versions, and other security settings.
316    ///
317    /// Any ALPN protocols already present in `config` are discarded before the configuration is passed to
318    /// `hyper-rustls`. [`Self::with_http_protocol`] remains the sole source of HTTP protocol selection:
319    /// [`HttpProtocol::Auto`] advertises HTTP/2 with HTTP/1.1 fallback, while [`HttpProtocol::Http1`] enables only
320    /// HTTP/1.1 and does not advertise ALPN.
321    ///
322    /// The supplied configuration is validated for FIPS compliance during [`Self::build`].
323    pub fn with_client_tls_config(mut self, config: ClientConfig) -> Self {
324        self.client_tls_config = Some(config);
325        self
326    }
327
328    /// Sets the minimum TLS protocol version for HTTPS connections.
329    ///
330    /// Defaults to TLS 1.2.
331    ///
332    /// This updates the same TLS builder configured by [`Self::with_tls_config`], so call order matters when both
333    /// methods change the minimum TLS version. [`Self::with_client_tls_config`] overrides this setting regardless of
334    /// call order.
335    pub fn with_min_tls_version(mut self, version: TlsMinimumVersion) -> Self {
336        self.tls_builder = self.tls_builder.with_min_tls_version(version);
337        self
338    }
339
340    /// Sets the underlying Hyper client configuration.
341    ///
342    /// This is provided to allow for more advanced configuration of the Hyper client itself, and should generally be
343    /// used sparingly.
344    pub fn with_hyper_config<F>(mut self, f: F) -> Self
345    where
346        F: FnOnce(&mut Builder),
347    {
348        f(&mut self.hyper_builder);
349        self
350    }
351
352    /// Sets a counter that gets incremented with the number of bytes sent over the connection.
353    ///
354    /// This tracks bytes sent at the HTTP client level, which includes headers and body but doesn't include underlying
355    /// transport overhead, such as TLS handshaking, and so on.
356    ///
357    /// Defaults to unset.
358    pub fn with_bytes_sent_counter(mut self, counter: Counter) -> Self {
359        self.connector_builder = self.connector_builder.with_bytes_sent_counter(counter);
360        self
361    }
362
363    /// Builds the `HttpClient`.
364    ///
365    /// # Errors
366    ///
367    /// If there was an error building the TLS configuration for the client, or if a supplied complete TLS
368    /// configuration fails FIPS validation in a FIPS build, an error will be returned.
369    pub fn build(self) -> Result<HttpClient, GenericError> {
370        let tls_config = match self.client_tls_config {
371            Some(mut config) => {
372                ensure_client_config_fips_compliant(&config)?;
373                config.alpn_protocols.clear();
374                config
375            }
376            None => self.tls_builder.build()?,
377        };
378        let connector = self.connector_builder.build(tls_config)?;
379        let tls_handshake_timeout = connector.tls_handshake_timeout();
380        // TODO(fips): Look into updating `hyper-http-proxy` to use the provided connector for establishing the
381        // connection to the proxy itself, even when the proxy is at an HTTPS URL, to ensure our desired TLS stack is
382        // being used.
383        let mut proxy_connector = hyper_http_proxy::ProxyConnector::new(connector)?;
384        // A zero timeout means the handshake deadline is disabled, matching `HttpsCapableConnector`'s own
385        // handling of `Duration::ZERO` for direct connections.
386        let proxy_tls_handshake_timeout = (!tls_handshake_timeout.is_zero()).then_some(tls_handshake_timeout);
387        proxy_connector.set_tls_handshake_timeout(proxy_tls_handshake_timeout);
388        if let Some(proxies) = &self.proxies {
389            for proxy in proxies {
390                proxy_connector.add_proxy(proxy.to_owned());
391            }
392        }
393        let client = self.hyper_builder.build(proxy_connector);
394
395        let inner = ServiceBuilder::new()
396            .option_layer(self.request_timeout.map(TimeoutLayer::new))
397            .option_layer(self.endpoint_telemetry)
398            .service(client.map_err(BoxError::from))
399            .boxed_clone();
400
401        Ok(HttpClient { inner })
402    }
403}
404
405impl Default for HttpClientBuilder {
406    fn default() -> Self {
407        let mut hyper_builder = Builder::new(TokioExecutor::new());
408        hyper_builder
409            .pool_timer(TokioTimer::new())
410            .pool_max_idle_per_host(5)
411            .pool_idle_timeout(Duration::from_secs(45));
412
413        Self {
414            connector_builder: HttpsCapableConnectorBuilder::default(),
415            hyper_builder,
416            tls_builder: ClientTLSConfigBuilder::new(),
417            client_tls_config: None,
418            request_timeout: Some(Duration::from_secs(20)),
419            endpoint_telemetry: None,
420            proxies: None,
421        }
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use std::{sync::Arc, time::Duration};
428
429    use http_body::Body as _;
430    use http_body_util::{Empty, Full};
431    use rustls::{server::WebPkiClientVerifier, ClientConfig, RootCertStore, ServerConfig};
432    use saluki_tls::test_util::SelfSignedCert;
433    use tokio::{
434        io::{AsyncReadExt as _, AsyncWriteExt as _},
435        net::TcpListener,
436        time::timeout,
437    };
438    use tokio_rustls::TlsAcceptor;
439
440    use super::*;
441
442    #[test]
443    fn into_client_body_preserves_exact_size_hint() {
444        let body = Full::new(Bytes::from_static(b"hello"));
445        let converted = into_client_body(body);
446
447        assert_eq!(Some(5), converted.size_hint().exact());
448    }
449
450    fn initialize_crypto_provider() {
451        let _ = saluki_tls::initialize_default_crypto_provider();
452        assert!(
453            rustls::crypto::CryptoProvider::get_default().is_some(),
454            "default crypto provider should be installed"
455        );
456    }
457
458    #[tokio::test]
459    async fn complete_tls_config_preserves_mtls_and_overrides_option_settings() {
460        initialize_crypto_provider();
461        let (server_config, client_config) = mutual_tls_configs();
462        let builder = HttpClient::builder()
463            .with_client_tls_config(client_config)
464            .with_tls_config(|builder| builder.with_root_cert_store(RootCertStore::empty()));
465
466        let _ = send_request_to_tls_server(builder, server_config)
467            .await
468            .expect("complete config should verify the server and present the client identity");
469    }
470
471    #[tokio::test]
472    async fn complete_tls_config_alpn_is_normalized_for_http1() {
473        initialize_crypto_provider();
474        let (mut server_config, mut client_config) = mutual_tls_configs();
475        server_config.alpn_protocols = vec![b"custom".to_vec(), b"http/1.1".to_vec()];
476        client_config.alpn_protocols = vec![b"custom".to_vec()];
477        let builder = HttpClient::builder().with_client_tls_config(client_config);
478
479        let negotiated_alpn = send_request_to_tls_server(builder, server_config)
480            .await
481            .expect("client should normalize ALPN and complete an HTTP/1.1 request");
482
483        assert_eq!(negotiated_alpn, None);
484    }
485
486    #[tokio::test]
487    async fn tls_handshake_timeout_fires_against_a_stalled_server() {
488        initialize_crypto_provider();
489        let server_cert = SelfSignedCert::localhost();
490
491        let listener = TcpListener::bind("127.0.0.1:0").await.expect("should bind listener");
492        let port = listener.local_addr().expect("should have local address").port();
493        let server_task = tokio::spawn(async move {
494            let (stream, _) = listener.accept().await.expect("server should accept a connection");
495            // Accept the TCP connection but never complete the TLS handshake, so the client's handshake
496            // deadline is the only thing that can end the connection attempt.
497            tokio::time::sleep(Duration::from_secs(30)).await;
498            drop(stream);
499        });
500
501        let mut client = HttpClient::builder()
502            .with_tls_config(|builder| builder.with_root_cert_store(root_store(&server_cert)))
503            .with_tls_handshake_timeout(Duration::from_millis(200))
504            .with_http_protocol(HttpProtocol::Http1)
505            .build()
506            .expect("client should build");
507        let request = Request::get(format!("https://localhost:{port}/"))
508            .body(Empty::<Bytes>::new())
509            .expect("request should build");
510
511        let error = timeout(Duration::from_secs(5), client.send(request))
512            .await
513            .expect("request should not hit the outer test timeout")
514            .expect_err("handshake should time out before completing");
515        assert!(
516            format!("{error:#}").contains("TLS handshake timed out"),
517            "expected a TLS handshake timeout, got: {error:#}"
518        );
519
520        server_task.abort();
521    }
522
523    #[tokio::test]
524    async fn tls_handshake_timeout_fires_against_a_stalled_proxy_tunnel() {
525        initialize_crypto_provider();
526
527        let listener = TcpListener::bind("127.0.0.1:0").await.expect("should bind listener");
528        let proxy_addr = listener.local_addr().expect("should have local address");
529        let proxy_task = tokio::spawn(async move {
530            let (mut stream, _) = listener.accept().await.expect("proxy should accept a connection");
531            let mut request = [0; 4096];
532            let bytes_read = stream
533                .read(&mut request)
534                .await
535                .expect("proxy should read the CONNECT request");
536            assert!(bytes_read > 0, "proxy should receive a CONNECT request");
537            stream
538                .write_all(b"HTTP/1.1 200 OK\r\n\r\n")
539                .await
540                .expect("proxy should write the CONNECT response");
541            // Complete the CONNECT tunnel but never speak TLS, so the client's handshake deadline is
542            // the only thing that can end the connection attempt.
543            tokio::time::sleep(Duration::from_secs(30)).await;
544            drop(stream);
545        });
546
547        let proxy_uri: Uri = format!("http://{proxy_addr}").parse().expect("proxy URI should parse");
548        let mut client = HttpClient::builder()
549            .with_proxies(vec![Proxy::new(hyper_http_proxy::Intercept::All, proxy_uri)])
550            .with_tls_config(|builder| builder.with_root_cert_store(RootCertStore::empty()))
551            .with_tls_handshake_timeout(Duration::from_millis(200))
552            .with_http_protocol(HttpProtocol::Http1)
553            .build()
554            .expect("client should build");
555        let request = Request::get("https://example.invalid/")
556            .body(Empty::<Bytes>::new())
557            .expect("request should build");
558
559        let error = timeout(Duration::from_secs(5), client.send(request))
560            .await
561            .expect("request should not hit the outer test timeout")
562            .expect_err("handshake should time out before completing");
563        assert!(
564            format!("{error:#}").contains("TLS handshake timed out"),
565            "expected a TLS handshake timeout, got: {error:#}"
566        );
567
568        proxy_task.abort();
569    }
570
571    #[tokio::test]
572    async fn zero_tls_handshake_timeout_disables_the_proxy_tunnel_deadline() {
573        initialize_crypto_provider();
574
575        let listener = TcpListener::bind("127.0.0.1:0").await.expect("should bind listener");
576        let proxy_addr = listener.local_addr().expect("should have local address");
577        let proxy_task = tokio::spawn(async move {
578            let (mut stream, _) = listener.accept().await.expect("proxy should accept a connection");
579            let mut request = [0; 4096];
580            let bytes_read = stream
581                .read(&mut request)
582                .await
583                .expect("proxy should read the CONNECT request");
584            assert!(bytes_read > 0, "proxy should receive a CONNECT request");
585            stream
586                .write_all(b"HTTP/1.1 200 OK\r\n\r\n")
587                .await
588                .expect("proxy should write the CONNECT response");
589            // Complete the CONNECT tunnel but never speak TLS, so the request only fails if some
590            // deadline (mistakenly) bounds the handshake.
591            tokio::time::sleep(Duration::from_secs(30)).await;
592            drop(stream);
593        });
594
595        let proxy_uri: Uri = format!("http://{proxy_addr}").parse().expect("proxy URI should parse");
596        let mut client = HttpClient::builder()
597            .with_proxies(vec![Proxy::new(hyper_http_proxy::Intercept::All, proxy_uri)])
598            .with_tls_config(|builder| builder.with_root_cert_store(RootCertStore::empty()))
599            .with_tls_handshake_timeout(Duration::ZERO)
600            .with_http_protocol(HttpProtocol::Http1)
601            .build()
602            .expect("client should build");
603        let request = Request::get("https://example.invalid/")
604            .body(Empty::<Bytes>::new())
605            .expect("request should build");
606
607        // A zero handshake timeout means "disabled", so the request should still be pending well past
608        // the point where a mistakenly active zero-length deadline would have already failed it.
609        let result = timeout(Duration::from_millis(300), client.send(request)).await;
610        assert!(
611            result.is_err(),
612            "a disabled timeout should not fail the proxy tunnel's TLS handshake, got: {result:?}"
613        );
614
615        proxy_task.abort();
616    }
617
618    fn mutual_tls_configs() -> (ServerConfig, ClientConfig) {
619        let server_cert = SelfSignedCert::localhost();
620        let client_cert = SelfSignedCert::new(["saluki-client"]);
621        let client_verifier = WebPkiClientVerifier::builder(Arc::new(root_store(&client_cert)))
622            .build()
623            .expect("client certificate verifier should build");
624        let server_config = ServerConfig::builder()
625            .with_client_cert_verifier(client_verifier)
626            .with_single_cert(server_cert.cert_chain(), server_cert.private_key())
627            .expect("server TLS config should build");
628        let client_config = ClientConfig::builder()
629            .with_root_certificates(root_store(&server_cert))
630            .with_client_auth_cert(client_cert.cert_chain(), client_cert.private_key())
631            .expect("client TLS config should build");
632
633        (server_config, client_config)
634    }
635
636    fn root_store(cert: &SelfSignedCert) -> RootCertStore {
637        let mut root_store = RootCertStore::empty();
638        root_store
639            .add(cert.cert_chain().pop().expect("certificate chain should not be empty"))
640            .expect("self-signed certificate should be a valid trust anchor");
641        root_store
642    }
643
644    async fn send_request_to_tls_server(
645        builder: HttpClientBuilder, server_config: ServerConfig,
646    ) -> Result<Option<Vec<u8>>, GenericError> {
647        let listener = TcpListener::bind("127.0.0.1:0").await?;
648        let port = listener.local_addr()?.port();
649        let server_task = tokio::spawn(async move {
650            let (stream, _) = listener.accept().await.expect("server should accept a connection");
651            let mut stream = TlsAcceptor::from(Arc::new(server_config))
652                .accept(stream)
653                .await
654                .expect("TLS handshake should succeed");
655            let negotiated_alpn = stream.get_ref().1.alpn_protocol().map(ToOwned::to_owned);
656            let mut request = [0; 4096];
657            let bytes_read = stream.read(&mut request).await.expect("server should read the request");
658            assert!(bytes_read > 0, "server should receive an HTTP request");
659            stream
660                .write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\nconnection: close\r\n\r\n")
661                .await
662                .expect("server should write the response");
663            negotiated_alpn
664        });
665
666        let mut client = builder.with_http_protocol(HttpProtocol::Http1).build()?;
667        let request = Request::get(format!("https://localhost:{port}/"))
668            .body(Empty::<Bytes>::new())
669            .expect("request should build");
670        let response = timeout(Duration::from_secs(5), client.send(request))
671            .await
672            .map_err(|_| GenericError::msg("HTTP request timed out"))??;
673        assert_eq!(response.status(), http::StatusCode::OK);
674        Ok(timeout(Duration::from_secs(5), server_task)
675            .await
676            .expect("TLS server should finish")
677            .expect("TLS server task should not panic"))
678    }
679}