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