hyper_rustls/connector/
builder.rs

1use std::sync::Arc;
2
3use hyper_util::client::legacy::connect::HttpConnector;
4#[cfg(any(
5    feature = "rustls-native-certs",
6    feature = "rustls-platform-verifier",
7    feature = "webpki-roots"
8))]
9use rustls::crypto::CryptoProvider;
10use rustls::ClientConfig;
11
12use super::{DefaultServerNameResolver, HttpsConnector, ResolveServerName};
13#[cfg(any(
14    feature = "rustls-native-certs",
15    feature = "webpki-roots",
16    feature = "rustls-platform-verifier"
17))]
18use crate::config::ConfigBuilderExt;
19use pki_types::ServerName;
20
21/// A builder for an [`HttpsConnector`]
22///
23/// This makes configuration flexible and explicit and ensures connector
24/// features match crate features
25///
26/// # Examples
27///
28/// ```
29/// use hyper_rustls::HttpsConnectorBuilder;
30///
31/// # #[cfg(all(feature = "webpki-roots", feature = "http1", feature="aws-lc-rs"))]
32/// # {
33/// # let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
34///     let https = HttpsConnectorBuilder::new()
35///     .with_webpki_roots()
36///     .https_only()
37///     .enable_http1()
38///     .build();
39/// # }
40/// ```
41pub struct ConnectorBuilder<State>(State);
42
43/// State of a builder that needs a TLS client config next
44pub struct WantsTlsConfig(());
45
46impl ConnectorBuilder<WantsTlsConfig> {
47    /// Creates a new [`ConnectorBuilder`]
48    pub fn new() -> Self {
49        Self(WantsTlsConfig(()))
50    }
51
52    /// Passes a rustls [`ClientConfig`] to configure the TLS connection
53    ///
54    /// The [`alpn_protocols`](ClientConfig::alpn_protocols) field is
55    /// required to be empty (or the function will panic) and will be
56    /// rewritten to match the enabled schemes (see
57    /// [`enable_http1`](ConnectorBuilder::enable_http1),
58    /// [`enable_http2`](ConnectorBuilder::enable_http2)) before the
59    /// connector is built.
60    pub fn with_tls_config(self, config: ClientConfig) -> ConnectorBuilder<WantsSchemes> {
61        assert!(
62            config.alpn_protocols.is_empty(),
63            "ALPN protocols should not be pre-defined"
64        );
65        ConnectorBuilder(WantsSchemes { tls_config: config })
66    }
67
68    /// Shorthand for using rustls' default crypto provider and other defaults, and
69    /// the platform verifier.
70    ///
71    /// See [`ConfigBuilderExt::with_platform_verifier()`].
72    #[cfg(all(
73        any(feature = "ring", feature = "aws-lc-rs"),
74        feature = "rustls-platform-verifier"
75    ))]
76    pub fn with_platform_verifier(self) -> ConnectorBuilder<WantsSchemes> {
77        self.with_tls_config(
78            ClientConfig::builder()
79                .with_platform_verifier()
80                .with_no_client_auth(),
81        )
82    }
83
84    /// Shorthand for using a custom [`CryptoProvider`] and the platform verifier.
85    ///
86    /// See [`ConfigBuilderExt::with_platform_verifier()`].
87    #[cfg(feature = "rustls-platform-verifier")]
88    pub fn with_provider_and_platform_verifier(
89        self,
90        provider: impl Into<Arc<CryptoProvider>>,
91    ) -> std::io::Result<ConnectorBuilder<WantsSchemes>> {
92        Ok(self.with_tls_config(
93            ClientConfig::builder_with_provider(provider.into())
94                .with_safe_default_protocol_versions()
95                .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
96                .with_platform_verifier()
97                .with_no_client_auth(),
98        ))
99    }
100
101    /// Shorthand for using rustls' default crypto provider and safe defaults, with
102    /// native roots.
103    ///
104    /// See [`ConfigBuilderExt::with_native_roots`]
105    #[cfg(all(
106        any(feature = "ring", feature = "aws-lc-rs"),
107        feature = "rustls-native-certs"
108    ))]
109    pub fn with_native_roots(self) -> std::io::Result<ConnectorBuilder<WantsSchemes>> {
110        Ok(self.with_tls_config(
111            ClientConfig::builder()
112                .with_native_roots()?
113                .with_no_client_auth(),
114        ))
115    }
116
117    /// Shorthand for using a custom [`CryptoProvider`] and native roots
118    ///
119    /// See [`ConfigBuilderExt::with_native_roots`]
120    #[cfg(feature = "rustls-native-certs")]
121    pub fn with_provider_and_native_roots(
122        self,
123        provider: impl Into<Arc<CryptoProvider>>,
124    ) -> std::io::Result<ConnectorBuilder<WantsSchemes>> {
125        Ok(self.with_tls_config(
126            ClientConfig::builder_with_provider(provider.into())
127                .with_safe_default_protocol_versions()
128                .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
129                .with_native_roots()?
130                .with_no_client_auth(),
131        ))
132    }
133
134    /// Shorthand for using rustls' default crypto provider and its
135    /// safe defaults.
136    ///
137    /// See [`ConfigBuilderExt::with_webpki_roots`]
138    #[cfg(all(any(feature = "ring", feature = "aws-lc-rs"), feature = "webpki-roots"))]
139    pub fn with_webpki_roots(self) -> ConnectorBuilder<WantsSchemes> {
140        self.with_tls_config(
141            ClientConfig::builder()
142                .with_webpki_roots()
143                .with_no_client_auth(),
144        )
145    }
146
147    /// Shorthand for using a custom [`CryptoProvider`], Rustls' safe default
148    /// protocol versions and Mozilla roots
149    ///
150    /// See [`ConfigBuilderExt::with_webpki_roots`]
151    #[cfg(feature = "webpki-roots")]
152    pub fn with_provider_and_webpki_roots(
153        self,
154        provider: impl Into<Arc<CryptoProvider>>,
155    ) -> Result<ConnectorBuilder<WantsSchemes>, rustls::Error> {
156        Ok(self.with_tls_config(
157            ClientConfig::builder_with_provider(provider.into())
158                .with_safe_default_protocol_versions()?
159                .with_webpki_roots()
160                .with_no_client_auth(),
161        ))
162    }
163}
164
165impl Default for ConnectorBuilder<WantsTlsConfig> {
166    fn default() -> Self {
167        Self::new()
168    }
169}
170
171/// State of a builder that needs schemes (https:// and http://) to be
172/// configured next
173pub struct WantsSchemes {
174    tls_config: ClientConfig,
175}
176
177impl ConnectorBuilder<WantsSchemes> {
178    /// Enforce the use of HTTPS when connecting
179    ///
180    /// Only URLs using the HTTPS scheme will be connectable.
181    pub fn https_only(self) -> ConnectorBuilder<WantsProtocols1> {
182        ConnectorBuilder(WantsProtocols1 {
183            tls_config: self.0.tls_config,
184            https_only: true,
185            server_name_resolver: None,
186        })
187    }
188
189    /// Allow both HTTPS and HTTP when connecting
190    ///
191    /// HTTPS URLs will be handled through rustls,
192    /// HTTP URLs will be handled by the lower-level connector.
193    pub fn https_or_http(self) -> ConnectorBuilder<WantsProtocols1> {
194        ConnectorBuilder(WantsProtocols1 {
195            tls_config: self.0.tls_config,
196            https_only: false,
197            server_name_resolver: None,
198        })
199    }
200}
201
202/// State of a builder that needs to have some protocols (HTTP1 or later)
203/// enabled next
204///
205/// No protocol has been enabled at this point.
206pub struct WantsProtocols1 {
207    tls_config: ClientConfig,
208    https_only: bool,
209    server_name_resolver: Option<Arc<dyn ResolveServerName + Sync + Send>>,
210}
211
212impl WantsProtocols1 {
213    fn wrap_connector<H>(self, conn: H) -> HttpsConnector<H> {
214        HttpsConnector {
215            force_https: self.https_only,
216            http: conn,
217            tls_config: std::sync::Arc::new(self.tls_config),
218            server_name_resolver: self
219                .server_name_resolver
220                .unwrap_or_else(|| Arc::new(DefaultServerNameResolver::default())),
221        }
222    }
223
224    fn build(self) -> HttpsConnector<HttpConnector> {
225        let mut http = HttpConnector::new();
226        // HttpConnector won't enforce scheme, but HttpsConnector will
227        http.enforce_http(false);
228        self.wrap_connector(http)
229    }
230}
231
232impl ConnectorBuilder<WantsProtocols1> {
233    /// Enable HTTP1
234    ///
235    /// This needs to be called explicitly, no protocol is enabled by default
236    #[cfg(feature = "http1")]
237    pub fn enable_http1(self) -> ConnectorBuilder<WantsProtocols2> {
238        ConnectorBuilder(WantsProtocols2 { inner: self.0 })
239    }
240
241    /// Enable HTTP2
242    ///
243    /// This needs to be called explicitly, no protocol is enabled by default
244    #[cfg(feature = "http2")]
245    pub fn enable_http2(mut self) -> ConnectorBuilder<WantsProtocols3> {
246        self.0.tls_config.alpn_protocols = vec![b"h2".to_vec()];
247        ConnectorBuilder(WantsProtocols3 {
248            inner: self.0,
249            enable_http1: false,
250        })
251    }
252
253    /// Enable all HTTP versions built into this library (enabled with Cargo features)
254    ///
255    /// For now, this could enable both HTTP 1 and 2, depending on active features.
256    /// In the future, other supported versions will be enabled as well.
257    #[cfg(feature = "http2")]
258    pub fn enable_all_versions(mut self) -> ConnectorBuilder<WantsProtocols3> {
259        #[cfg(feature = "http1")]
260        let alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
261        #[cfg(not(feature = "http1"))]
262        let alpn_protocols = vec![b"h2".to_vec()];
263
264        self.0.tls_config.alpn_protocols = alpn_protocols;
265        ConnectorBuilder(WantsProtocols3 {
266            inner: self.0,
267            enable_http1: cfg!(feature = "http1"),
268        })
269    }
270
271    /// Override server name for the TLS stack
272    ///
273    /// By default, for each connection hyper-rustls will extract host portion
274    /// of the destination URL and verify that server certificate contains
275    /// this value.
276    ///
277    /// If this method is called, hyper-rustls will instead use this resolver
278    /// to compute the value used to verify the server certificate.
279    pub fn with_server_name_resolver(
280        mut self,
281        resolver: impl ResolveServerName + 'static + Sync + Send,
282    ) -> Self {
283        self.0.server_name_resolver = Some(Arc::new(resolver));
284        self
285    }
286
287    /// Override server name for the TLS stack
288    ///
289    /// By default, for each connection hyper-rustls will extract host portion
290    /// of the destination URL and verify that server certificate contains
291    /// this value.
292    ///
293    /// If this method is called, hyper-rustls will instead verify that server
294    /// certificate contains `override_server_name`. Domain name included in
295    /// the URL will not affect certificate validation.
296    #[deprecated(
297        since = "0.27.1",
298        note = "use Self::with_server_name_resolver with FixedServerNameResolver instead"
299    )]
300    pub fn with_server_name(self, mut override_server_name: String) -> Self {
301        // remove square brackets around IPv6 address.
302        if let Some(trimmed) = override_server_name
303            .strip_prefix('[')
304            .and_then(|s| s.strip_suffix(']'))
305        {
306            override_server_name = trimmed.to_string();
307        }
308
309        self.with_server_name_resolver(move |_: &_| {
310            ServerName::try_from(override_server_name.clone())
311        })
312    }
313}
314
315/// State of a builder with HTTP1 enabled, that may have some other
316/// protocols (HTTP2 or later) enabled next
317///
318/// At this point a connector can be built, see
319/// [`build`](ConnectorBuilder<WantsProtocols2>::build) and
320/// [`wrap_connector`](ConnectorBuilder<WantsProtocols2>::wrap_connector).
321pub struct WantsProtocols2 {
322    inner: WantsProtocols1,
323}
324
325impl ConnectorBuilder<WantsProtocols2> {
326    /// Enable HTTP2
327    ///
328    /// This needs to be called explicitly, no protocol is enabled by default
329    #[cfg(feature = "http2")]
330    pub fn enable_http2(mut self) -> ConnectorBuilder<WantsProtocols3> {
331        self.0.inner.tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
332        ConnectorBuilder(WantsProtocols3 {
333            inner: self.0.inner,
334            enable_http1: true,
335        })
336    }
337
338    /// This builds an [`HttpsConnector`] built on hyper's default [`HttpConnector`]
339    pub fn build(self) -> HttpsConnector<HttpConnector> {
340        self.0.inner.build()
341    }
342
343    /// This wraps an arbitrary low-level connector into an [`HttpsConnector`]
344    pub fn wrap_connector<H>(self, conn: H) -> HttpsConnector<H> {
345        // HTTP1-only, alpn_protocols stays empty
346        // HttpConnector doesn't have a way to say http1-only;
347        // its connection pool may still support HTTP2
348        // though it won't be used
349        self.0.inner.wrap_connector(conn)
350    }
351}
352
353/// State of a builder with HTTP2 (and possibly HTTP1) enabled
354///
355/// At this point a connector can be built, see
356/// [`build`](ConnectorBuilder<WantsProtocols3>::build) and
357/// [`wrap_connector`](ConnectorBuilder<WantsProtocols3>::wrap_connector).
358#[cfg(feature = "http2")]
359pub struct WantsProtocols3 {
360    inner: WantsProtocols1,
361    // ALPN is built piecemeal without the need to read back this field
362    #[allow(dead_code)]
363    enable_http1: bool,
364}
365
366#[cfg(feature = "http2")]
367impl ConnectorBuilder<WantsProtocols3> {
368    /// This builds an [`HttpsConnector`] built on hyper's default [`HttpConnector`]
369    pub fn build(self) -> HttpsConnector<HttpConnector> {
370        self.0.inner.build()
371    }
372
373    /// This wraps an arbitrary low-level connector into an [`HttpsConnector`]
374    pub fn wrap_connector<H>(self, conn: H) -> HttpsConnector<H> {
375        // If HTTP1 is disabled, we can set http2_only
376        // on the Client (a higher-level object that uses the connector)
377        // client.http2_only(!self.0.enable_http1);
378        self.0.inner.wrap_connector(conn)
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    // Typical usage
385    #[test]
386    #[cfg(all(feature = "webpki-roots", feature = "http1"))]
387    fn test_builder() {
388        ensure_global_state();
389        let _connector = super::ConnectorBuilder::new()
390            .with_webpki_roots()
391            .https_only()
392            .enable_http1()
393            .build();
394    }
395
396    #[test]
397    #[cfg(feature = "http1")]
398    #[should_panic(expected = "ALPN protocols should not be pre-defined")]
399    fn test_reject_predefined_alpn() {
400        ensure_global_state();
401        let roots = rustls::RootCertStore::empty();
402        let mut config_with_alpn = rustls::ClientConfig::builder()
403            .with_root_certificates(roots)
404            .with_no_client_auth();
405        config_with_alpn.alpn_protocols = vec![b"fancyprotocol".to_vec()];
406        let _connector = super::ConnectorBuilder::new()
407            .with_tls_config(config_with_alpn)
408            .https_only()
409            .enable_http1()
410            .build();
411    }
412
413    #[test]
414    #[cfg(all(feature = "http1", feature = "http2"))]
415    fn test_alpn() {
416        ensure_global_state();
417        let roots = rustls::RootCertStore::empty();
418        let tls_config = rustls::ClientConfig::builder()
419            .with_root_certificates(roots)
420            .with_no_client_auth();
421        let connector = super::ConnectorBuilder::new()
422            .with_tls_config(tls_config.clone())
423            .https_only()
424            .enable_http1()
425            .build();
426        assert!(connector
427            .tls_config
428            .alpn_protocols
429            .is_empty());
430        let connector = super::ConnectorBuilder::new()
431            .with_tls_config(tls_config.clone())
432            .https_only()
433            .enable_http2()
434            .build();
435        assert_eq!(&connector.tls_config.alpn_protocols, &[b"h2".to_vec()]);
436        let connector = super::ConnectorBuilder::new()
437            .with_tls_config(tls_config.clone())
438            .https_only()
439            .enable_http1()
440            .enable_http2()
441            .build();
442        assert_eq!(
443            &connector.tls_config.alpn_protocols,
444            &[b"h2".to_vec(), b"http/1.1".to_vec()]
445        );
446        let connector = super::ConnectorBuilder::new()
447            .with_tls_config(tls_config)
448            .https_only()
449            .enable_all_versions()
450            .build();
451        assert_eq!(
452            &connector.tls_config.alpn_protocols,
453            &[b"h2".to_vec(), b"http/1.1".to_vec()]
454        );
455    }
456
457    #[test]
458    #[cfg(all(not(feature = "http1"), feature = "http2"))]
459    fn test_alpn_http2() {
460        let roots = rustls::RootCertStore::empty();
461        let tls_config = rustls::ClientConfig::builder()
462            .with_safe_defaults()
463            .with_root_certificates(roots)
464            .with_no_client_auth();
465        let connector = super::ConnectorBuilder::new()
466            .with_tls_config(tls_config.clone())
467            .https_only()
468            .enable_http2()
469            .build();
470        assert_eq!(&connector.tls_config.alpn_protocols, &[b"h2".to_vec()]);
471        let connector = super::ConnectorBuilder::new()
472            .with_tls_config(tls_config)
473            .https_only()
474            .enable_all_versions()
475            .build();
476        assert_eq!(&connector.tls_config.alpn_protocols, &[b"h2".to_vec()]);
477    }
478
479    fn ensure_global_state() {
480        #[cfg(feature = "ring")]
481        let _ = rustls::crypto::ring::default_provider().install_default();
482        #[cfg(feature = "aws-lc-rs")]
483        let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
484    }
485}