hyper_rustls/connector/
builder.rs

1use rustls::ClientConfig;
2
3use super::HttpsConnector;
4#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
5use crate::config::ConfigBuilderExt;
6
7#[cfg(feature = "tokio-runtime")]
8use hyper::client::HttpConnector;
9
10/// A builder for an [`HttpsConnector`]
11///
12/// This makes configuration flexible and explicit and ensures connector
13/// features match crate features
14///
15/// # Examples
16///
17/// ```
18/// use hyper_rustls::HttpsConnectorBuilder;
19///
20/// # #[cfg(all(feature = "webpki-roots", feature = "tokio-runtime", feature = "http1"))]
21/// let https = HttpsConnectorBuilder::new()
22///     .with_webpki_roots()
23///     .https_only()
24///     .enable_http1()
25///     .build();
26/// ```
27pub struct ConnectorBuilder<State>(State);
28
29/// State of a builder that needs a TLS client config next
30pub struct WantsTlsConfig(());
31
32impl ConnectorBuilder<WantsTlsConfig> {
33    /// Creates a new [`ConnectorBuilder`]
34    pub fn new() -> Self {
35        Self(WantsTlsConfig(()))
36    }
37
38    /// Passes a rustls [`ClientConfig`] to configure the TLS connection
39    ///
40    /// The [`alpn_protocols`](ClientConfig::alpn_protocols) field is
41    /// required to be empty (or the function will panic) and will be
42    /// rewritten to match the enabled schemes (see
43    /// [`enable_http1`](ConnectorBuilder::enable_http1),
44    /// [`enable_http2`](ConnectorBuilder::enable_http2)) before the
45    /// connector is built.
46    pub fn with_tls_config(self, config: ClientConfig) -> ConnectorBuilder<WantsSchemes> {
47        assert!(
48            config.alpn_protocols.is_empty(),
49            "ALPN protocols should not be pre-defined"
50        );
51        ConnectorBuilder(WantsSchemes { tls_config: config })
52    }
53
54    /// Shorthand for using rustls' [safe defaults][with_safe_defaults]
55    /// and native roots
56    ///
57    /// See [`ConfigBuilderExt::with_native_roots`]
58    ///
59    /// [with_safe_defaults]: rustls::ConfigBuilder::with_safe_defaults
60    #[cfg(feature = "rustls-native-certs")]
61    #[cfg_attr(docsrs, doc(cfg(feature = "rustls-native-certs")))]
62    pub fn with_native_roots(self) -> ConnectorBuilder<WantsSchemes> {
63        self.with_tls_config(
64            ClientConfig::builder()
65                .with_safe_defaults()
66                .with_native_roots()
67                .with_no_client_auth(),
68        )
69    }
70
71    /// Shorthand for using rustls' [safe defaults][with_safe_defaults]
72    /// and Mozilla roots
73    ///
74    /// See [`ConfigBuilderExt::with_webpki_roots`]
75    ///
76    /// [with_safe_defaults]: rustls::ConfigBuilder::with_safe_defaults
77    #[cfg(feature = "webpki-roots")]
78    #[cfg_attr(docsrs, doc(cfg(feature = "webpki-roots")))]
79    pub fn with_webpki_roots(self) -> ConnectorBuilder<WantsSchemes> {
80        self.with_tls_config(
81            ClientConfig::builder()
82                .with_safe_defaults()
83                .with_webpki_roots()
84                .with_no_client_auth(),
85        )
86    }
87}
88
89impl Default for ConnectorBuilder<WantsTlsConfig> {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95/// State of a builder that needs schemes (https:// and http://) to be
96/// configured next
97pub struct WantsSchemes {
98    tls_config: ClientConfig,
99}
100
101impl ConnectorBuilder<WantsSchemes> {
102    /// Enforce the use of HTTPS when connecting
103    ///
104    /// Only URLs using the HTTPS scheme will be connectable.
105    pub fn https_only(self) -> ConnectorBuilder<WantsProtocols1> {
106        ConnectorBuilder(WantsProtocols1 {
107            tls_config: self.0.tls_config,
108            https_only: true,
109            override_server_name: None,
110        })
111    }
112
113    /// Allow both HTTPS and HTTP when connecting
114    ///
115    /// HTTPS URLs will be handled through rustls,
116    /// HTTP URLs will be handled by the lower-level connector.
117    pub fn https_or_http(self) -> ConnectorBuilder<WantsProtocols1> {
118        ConnectorBuilder(WantsProtocols1 {
119            tls_config: self.0.tls_config,
120            https_only: false,
121            override_server_name: None,
122        })
123    }
124}
125
126/// State of a builder that needs to have some protocols (HTTP1 or later)
127/// enabled next
128///
129/// No protocol has been enabled at this point.
130pub struct WantsProtocols1 {
131    tls_config: ClientConfig,
132    https_only: bool,
133    override_server_name: Option<String>,
134}
135
136impl WantsProtocols1 {
137    fn wrap_connector<H>(self, conn: H) -> HttpsConnector<H> {
138        HttpsConnector {
139            force_https: self.https_only,
140            http: conn,
141            tls_config: std::sync::Arc::new(self.tls_config),
142            override_server_name: self.override_server_name,
143        }
144    }
145
146    #[cfg(feature = "tokio-runtime")]
147    fn build(self) -> HttpsConnector<HttpConnector> {
148        let mut http = HttpConnector::new();
149        // HttpConnector won't enforce scheme, but HttpsConnector will
150        http.enforce_http(false);
151        self.wrap_connector(http)
152    }
153}
154
155impl ConnectorBuilder<WantsProtocols1> {
156    /// Enable HTTP1
157    ///
158    /// This needs to be called explicitly, no protocol is enabled by default
159    #[cfg(feature = "http1")]
160    pub fn enable_http1(self) -> ConnectorBuilder<WantsProtocols2> {
161        ConnectorBuilder(WantsProtocols2 { inner: self.0 })
162    }
163
164    /// Enable HTTP2
165    ///
166    /// This needs to be called explicitly, no protocol is enabled by default
167    #[cfg(feature = "http2")]
168    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
169    pub fn enable_http2(mut self) -> ConnectorBuilder<WantsProtocols3> {
170        self.0.tls_config.alpn_protocols = vec![b"h2".to_vec()];
171        ConnectorBuilder(WantsProtocols3 {
172            inner: self.0,
173            enable_http1: false,
174        })
175    }
176
177    /// Enable all HTTP versions built into this library (enabled with Cargo features)
178    ///
179    /// For now, this could enable both HTTP 1 and 2, depending on active features.
180    /// In the future, other supported versions will be enabled as well.
181    #[cfg(feature = "http2")]
182    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
183    pub fn enable_all_versions(mut self) -> ConnectorBuilder<WantsProtocols3> {
184        #[cfg(feature = "http1")]
185        let alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
186        #[cfg(not(feature = "http1"))]
187        let alpn_protocols = vec![b"h2".to_vec()];
188
189        self.0.tls_config.alpn_protocols = alpn_protocols;
190        ConnectorBuilder(WantsProtocols3 {
191            inner: self.0,
192            enable_http1: cfg!(feature = "http1"),
193        })
194    }
195
196    /// Override server name for the TLS stack
197    ///
198    /// By default, for each connection hyper-rustls will extract host portion
199    /// of the destination URL and verify that server certificate contains
200    /// this value.
201    ///
202    /// If this method is called, hyper-rustls will instead verify that server
203    /// certificate contains `override_server_name`. Domain name included in
204    /// the URL will not affect certificate validation.
205    pub fn with_server_name(mut self, override_server_name: String) -> Self {
206        self.0.override_server_name = Some(override_server_name);
207        self
208    }
209}
210
211/// State of a builder with HTTP1 enabled, that may have some other
212/// protocols (HTTP2 or later) enabled next
213///
214/// At this point a connector can be built, see
215/// [`build`](ConnectorBuilder<WantsProtocols2>::build) and
216/// [`wrap_connector`](ConnectorBuilder<WantsProtocols2>::wrap_connector).
217pub struct WantsProtocols2 {
218    inner: WantsProtocols1,
219}
220
221impl ConnectorBuilder<WantsProtocols2> {
222    /// Enable HTTP2
223    ///
224    /// This needs to be called explicitly, no protocol is enabled by default
225    #[cfg(feature = "http2")]
226    #[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
227    pub fn enable_http2(mut self) -> ConnectorBuilder<WantsProtocols3> {
228        self.0.inner.tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
229        ConnectorBuilder(WantsProtocols3 {
230            inner: self.0.inner,
231            enable_http1: true,
232        })
233    }
234
235    /// This builds an [`HttpsConnector`] built on hyper's default [`HttpConnector`]
236    #[cfg(feature = "tokio-runtime")]
237    pub fn build(self) -> HttpsConnector<HttpConnector> {
238        self.0.inner.build()
239    }
240
241    /// This wraps an arbitrary low-level connector into an [`HttpsConnector`]
242    pub fn wrap_connector<H>(self, conn: H) -> HttpsConnector<H> {
243        // HTTP1-only, alpn_protocols stays empty
244        // HttpConnector doesn't have a way to say http1-only;
245        // its connection pool may still support HTTP2
246        // though it won't be used
247        self.0.inner.wrap_connector(conn)
248    }
249}
250
251/// State of a builder with HTTP2 (and possibly HTTP1) enabled
252///
253/// At this point a connector can be built, see
254/// [`build`](ConnectorBuilder<WantsProtocols3>::build) and
255/// [`wrap_connector`](ConnectorBuilder<WantsProtocols3>::wrap_connector).
256#[cfg(feature = "http2")]
257pub struct WantsProtocols3 {
258    inner: WantsProtocols1,
259    // ALPN is built piecemeal without the need to read back this field
260    #[allow(dead_code)]
261    enable_http1: bool,
262}
263
264#[cfg(feature = "http2")]
265impl ConnectorBuilder<WantsProtocols3> {
266    /// This builds an [`HttpsConnector`] built on hyper's default [`HttpConnector`]
267    #[cfg(feature = "tokio-runtime")]
268    pub fn build(self) -> HttpsConnector<HttpConnector> {
269        self.0.inner.build()
270    }
271
272    /// This wraps an arbitrary low-level connector into an [`HttpsConnector`]
273    pub fn wrap_connector<H>(self, conn: H) -> HttpsConnector<H> {
274        // If HTTP1 is disabled, we can set http2_only
275        // on the Client (a higher-level object that uses the connector)
276        // client.http2_only(!self.0.enable_http1);
277        self.0.inner.wrap_connector(conn)
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    // Typical usage
284    #[test]
285    #[cfg(all(feature = "webpki-roots", feature = "http1"))]
286    fn test_builder() {
287        let _connector = super::ConnectorBuilder::new()
288            .with_webpki_roots()
289            .https_only()
290            .enable_http1()
291            .build();
292    }
293
294    #[test]
295    #[cfg(feature = "http1")]
296    #[should_panic(expected = "ALPN protocols should not be pre-defined")]
297    fn test_reject_predefined_alpn() {
298        let roots = rustls::RootCertStore::empty();
299        let mut config_with_alpn = rustls::ClientConfig::builder()
300            .with_safe_defaults()
301            .with_root_certificates(roots)
302            .with_no_client_auth();
303        config_with_alpn.alpn_protocols = vec![b"fancyprotocol".to_vec()];
304        let _connector = super::ConnectorBuilder::new()
305            .with_tls_config(config_with_alpn)
306            .https_only()
307            .enable_http1()
308            .build();
309    }
310
311    #[test]
312    #[cfg(all(feature = "http1", feature = "http2"))]
313    fn test_alpn() {
314        let roots = rustls::RootCertStore::empty();
315        let tls_config = rustls::ClientConfig::builder()
316            .with_safe_defaults()
317            .with_root_certificates(roots)
318            .with_no_client_auth();
319        let connector = super::ConnectorBuilder::new()
320            .with_tls_config(tls_config.clone())
321            .https_only()
322            .enable_http1()
323            .build();
324        assert!(connector
325            .tls_config
326            .alpn_protocols
327            .is_empty());
328        let connector = super::ConnectorBuilder::new()
329            .with_tls_config(tls_config.clone())
330            .https_only()
331            .enable_http2()
332            .build();
333        assert_eq!(&connector.tls_config.alpn_protocols, &[b"h2".to_vec()]);
334        let connector = super::ConnectorBuilder::new()
335            .with_tls_config(tls_config.clone())
336            .https_only()
337            .enable_http1()
338            .enable_http2()
339            .build();
340        assert_eq!(
341            &connector.tls_config.alpn_protocols,
342            &[b"h2".to_vec(), b"http/1.1".to_vec()]
343        );
344        let connector = super::ConnectorBuilder::new()
345            .with_tls_config(tls_config)
346            .https_only()
347            .enable_all_versions()
348            .build();
349        assert_eq!(
350            &connector.tls_config.alpn_protocols,
351            &[b"h2".to_vec(), b"http/1.1".to_vec()]
352        );
353    }
354
355    #[test]
356    #[cfg(all(not(feature = "http1"), feature = "http2"))]
357    fn test_alpn_http2() {
358        let roots = rustls::RootCertStore::empty();
359        let tls_config = rustls::ClientConfig::builder()
360            .with_safe_defaults()
361            .with_root_certificates(roots)
362            .with_no_client_auth();
363        let connector = super::ConnectorBuilder::new()
364            .with_tls_config(tls_config.clone())
365            .https_only()
366            .enable_http2()
367            .build();
368        assert_eq!(&connector.tls_config.alpn_protocols, &[b"h2".to_vec()]);
369        let connector = super::ConnectorBuilder::new()
370            .with_tls_config(tls_config)
371            .https_only()
372            .enable_all_versions()
373            .build();
374        assert_eq!(&connector.tls_config.alpn_protocols, &[b"h2".to_vec()]);
375    }
376}