Skip to main content

saluki_io/net/client/http/
client.rs

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