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(data:DefaultServerNameResolver::default())),
221 }
222 }
223
224 fn build(self) -> HttpsConnector<HttpConnector> {
225 let mut http: HttpConnector = HttpConnector::new();
226 // HttpConnector won't enforce scheme, but HttpsConnector will
227 http.enforce_http(is_enforced:false);
228 self.wrap_connector(conn: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}
486