use std::sync::Arc; use hyper_util::client::legacy::connect::HttpConnector; #[cfg(any( feature = "rustls-native-certs", feature = "rustls-platform-verifier", feature = "webpki-roots" ))] use rustls::crypto::CryptoProvider; use rustls::ClientConfig; use super::{DefaultServerNameResolver, HttpsConnector, ResolveServerName}; #[cfg(any( feature = "rustls-native-certs", feature = "webpki-roots", feature = "rustls-platform-verifier" ))] use crate::config::ConfigBuilderExt; use pki_types::ServerName; /// A builder for an [`HttpsConnector`] /// /// This makes configuration flexible and explicit and ensures connector /// features match crate features /// /// # Examples /// /// ``` /// use hyper_rustls::HttpsConnectorBuilder; /// /// # #[cfg(all(feature = "webpki-roots", feature = "http1", feature="aws-lc-rs"))] /// # { /// # let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); /// let https = HttpsConnectorBuilder::new() /// .with_webpki_roots() /// .https_only() /// .enable_http1() /// .build(); /// # } /// ``` pub struct ConnectorBuilder(State); /// State of a builder that needs a TLS client config next pub struct WantsTlsConfig(()); impl ConnectorBuilder { /// Creates a new [`ConnectorBuilder`] pub fn new() -> Self { Self(WantsTlsConfig(())) } /// Passes a rustls [`ClientConfig`] to configure the TLS connection /// /// The [`alpn_protocols`](ClientConfig::alpn_protocols) field is /// required to be empty (or the function will panic) and will be /// rewritten to match the enabled schemes (see /// [`enable_http1`](ConnectorBuilder::enable_http1), /// [`enable_http2`](ConnectorBuilder::enable_http2)) before the /// connector is built. pub fn with_tls_config(self, config: ClientConfig) -> ConnectorBuilder { assert!( config.alpn_protocols.is_empty(), "ALPN protocols should not be pre-defined" ); ConnectorBuilder(WantsSchemes { tls_config: config }) } /// Shorthand for using rustls' default crypto provider and other defaults, and /// the platform verifier. /// /// See [`ConfigBuilderExt::with_platform_verifier()`]. #[cfg(all( any(feature = "ring", feature = "aws-lc-rs"), feature = "rustls-platform-verifier" ))] pub fn with_platform_verifier(self) -> ConnectorBuilder { self.try_with_platform_verifier() .expect("failure to initialize platform verifier") } /// Shorthand for using rustls' default crypto provider and other defaults, and /// the platform verifier. /// /// See [`ConfigBuilderExt::with_platform_verifier()`]. #[cfg(all( any(feature = "ring", feature = "aws-lc-rs"), feature = "rustls-platform-verifier" ))] pub fn try_with_platform_verifier( self, ) -> Result, rustls::Error> { Ok(self.with_tls_config( ClientConfig::builder() .try_with_platform_verifier()? .with_no_client_auth(), )) } /// Shorthand for using a custom [`CryptoProvider`] and the platform verifier. /// /// See [`ConfigBuilderExt::with_platform_verifier()`]. #[cfg(feature = "rustls-platform-verifier")] pub fn with_provider_and_platform_verifier( self, provider: impl Into>, ) -> std::io::Result> { Ok(self.with_tls_config( ClientConfig::builder_with_provider(provider.into()) .with_safe_default_protocol_versions() .and_then(|builder| builder.try_with_platform_verifier()) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? .with_no_client_auth(), )) } /// Shorthand for using rustls' default crypto provider and safe defaults, with /// native roots. /// /// See [`ConfigBuilderExt::with_native_roots`] #[cfg(all( any(feature = "ring", feature = "aws-lc-rs"), feature = "rustls-native-certs" ))] pub fn with_native_roots(self) -> std::io::Result> { Ok(self.with_tls_config( ClientConfig::builder() .with_native_roots()? .with_no_client_auth(), )) } /// Shorthand for using a custom [`CryptoProvider`] and native roots /// /// See [`ConfigBuilderExt::with_native_roots`] #[cfg(feature = "rustls-native-certs")] pub fn with_provider_and_native_roots( self, provider: impl Into>, ) -> std::io::Result> { Ok(self.with_tls_config( ClientConfig::builder_with_provider(provider.into()) .with_safe_default_protocol_versions() .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))? .with_native_roots()? .with_no_client_auth(), )) } /// Shorthand for using rustls' default crypto provider and its /// safe defaults. /// /// See [`ConfigBuilderExt::with_webpki_roots`] #[cfg(all(any(feature = "ring", feature = "aws-lc-rs"), feature = "webpki-roots"))] pub fn with_webpki_roots(self) -> ConnectorBuilder { self.with_tls_config( ClientConfig::builder() .with_webpki_roots() .with_no_client_auth(), ) } /// Shorthand for using a custom [`CryptoProvider`], Rustls' safe default /// protocol versions and Mozilla roots /// /// See [`ConfigBuilderExt::with_webpki_roots`] #[cfg(feature = "webpki-roots")] pub fn with_provider_and_webpki_roots( self, provider: impl Into>, ) -> Result, rustls::Error> { Ok(self.with_tls_config( ClientConfig::builder_with_provider(provider.into()) .with_safe_default_protocol_versions()? .with_webpki_roots() .with_no_client_auth(), )) } } impl Default for ConnectorBuilder { fn default() -> Self { Self::new() } } /// State of a builder that needs schemes (https:// and http://) to be /// configured next pub struct WantsSchemes { tls_config: ClientConfig, } impl ConnectorBuilder { /// Enforce the use of HTTPS when connecting /// /// Only URLs using the HTTPS scheme will be connectable. pub fn https_only(self) -> ConnectorBuilder { ConnectorBuilder(WantsProtocols1 { tls_config: self.0.tls_config, https_only: true, server_name_resolver: None, }) } /// Allow both HTTPS and HTTP when connecting /// /// HTTPS URLs will be handled through rustls, /// HTTP URLs will be handled by the lower-level connector. pub fn https_or_http(self) -> ConnectorBuilder { ConnectorBuilder(WantsProtocols1 { tls_config: self.0.tls_config, https_only: false, server_name_resolver: None, }) } } /// State of a builder that needs to have some protocols (HTTP1 or later) /// enabled next /// /// No protocol has been enabled at this point. pub struct WantsProtocols1 { tls_config: ClientConfig, https_only: bool, server_name_resolver: Option>, } impl WantsProtocols1 { fn wrap_connector(self, conn: H) -> HttpsConnector { HttpsConnector { force_https: self.https_only, http: conn, tls_config: std::sync::Arc::new(self.tls_config), server_name_resolver: self .server_name_resolver .unwrap_or_else(|| Arc::new(DefaultServerNameResolver::default())), } } fn build(self) -> HttpsConnector { let mut http = HttpConnector::new(); // HttpConnector won't enforce scheme, but HttpsConnector will http.enforce_http(false); self.wrap_connector(http) } } impl ConnectorBuilder { /// Enable HTTP1 /// /// This needs to be called explicitly, no protocol is enabled by default #[cfg(feature = "http1")] pub fn enable_http1(self) -> ConnectorBuilder { ConnectorBuilder(WantsProtocols2 { inner: self.0 }) } /// Enable HTTP2 /// /// This needs to be called explicitly, no protocol is enabled by default #[cfg(feature = "http2")] pub fn enable_http2(mut self) -> ConnectorBuilder { self.0.tls_config.alpn_protocols = vec![b"h2".to_vec()]; ConnectorBuilder(WantsProtocols3 { inner: self.0, enable_http1: false, }) } /// Enable all HTTP versions built into this library (enabled with Cargo features) /// /// For now, this could enable both HTTP 1 and 2, depending on active features. /// In the future, other supported versions will be enabled as well. #[cfg(feature = "http2")] pub fn enable_all_versions(mut self) -> ConnectorBuilder { #[cfg(feature = "http1")] let alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; #[cfg(not(feature = "http1"))] let alpn_protocols = vec![b"h2".to_vec()]; self.0.tls_config.alpn_protocols = alpn_protocols; ConnectorBuilder(WantsProtocols3 { inner: self.0, enable_http1: cfg!(feature = "http1"), }) } /// Override server name for the TLS stack /// /// By default, for each connection hyper-rustls will extract host portion /// of the destination URL and verify that server certificate contains /// this value. /// /// If this method is called, hyper-rustls will instead use this resolver /// to compute the value used to verify the server certificate. pub fn with_server_name_resolver( mut self, resolver: impl ResolveServerName + 'static + Sync + Send, ) -> Self { self.0.server_name_resolver = Some(Arc::new(resolver)); self } /// Override server name for the TLS stack /// /// By default, for each connection hyper-rustls will extract host portion /// of the destination URL and verify that server certificate contains /// this value. /// /// If this method is called, hyper-rustls will instead verify that server /// certificate contains `override_server_name`. Domain name included in /// the URL will not affect certificate validation. #[deprecated( since = "0.27.1", note = "use Self::with_server_name_resolver with FixedServerNameResolver instead" )] pub fn with_server_name(self, mut override_server_name: String) -> Self { // remove square brackets around IPv6 address. if let Some(trimmed) = override_server_name .strip_prefix('[') .and_then(|s| s.strip_suffix(']')) { override_server_name = trimmed.to_string(); } self.with_server_name_resolver(move |_: &_| { ServerName::try_from(override_server_name.clone()) }) } } /// State of a builder with HTTP1 enabled, that may have some other /// protocols (HTTP2 or later) enabled next /// /// At this point a connector can be built, see /// [`build`](ConnectorBuilder::build) and /// [`wrap_connector`](ConnectorBuilder::wrap_connector). pub struct WantsProtocols2 { inner: WantsProtocols1, } impl ConnectorBuilder { /// Enable HTTP2 /// /// This needs to be called explicitly, no protocol is enabled by default #[cfg(feature = "http2")] pub fn enable_http2(mut self) -> ConnectorBuilder { self.0.inner.tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; ConnectorBuilder(WantsProtocols3 { inner: self.0.inner, enable_http1: true, }) } /// This builds an [`HttpsConnector`] built on hyper's default [`HttpConnector`] pub fn build(self) -> HttpsConnector { self.0.inner.build() } /// This wraps an arbitrary low-level connector into an [`HttpsConnector`] pub fn wrap_connector(self, conn: H) -> HttpsConnector { // HTTP1-only, alpn_protocols stays empty // HttpConnector doesn't have a way to say http1-only; // its connection pool may still support HTTP2 // though it won't be used self.0.inner.wrap_connector(conn) } } /// State of a builder with HTTP2 (and possibly HTTP1) enabled /// /// At this point a connector can be built, see /// [`build`](ConnectorBuilder::build) and /// [`wrap_connector`](ConnectorBuilder::wrap_connector). #[cfg(feature = "http2")] pub struct WantsProtocols3 { inner: WantsProtocols1, // ALPN is built piecemeal without the need to read back this field #[allow(dead_code)] enable_http1: bool, } #[cfg(feature = "http2")] impl ConnectorBuilder { /// This builds an [`HttpsConnector`] built on hyper's default [`HttpConnector`] pub fn build(self) -> HttpsConnector { self.0.inner.build() } /// This wraps an arbitrary low-level connector into an [`HttpsConnector`] pub fn wrap_connector(self, conn: H) -> HttpsConnector { // If HTTP1 is disabled, we can set http2_only // on the Client (a higher-level object that uses the connector) // client.http2_only(!self.0.enable_http1); self.0.inner.wrap_connector(conn) } } #[cfg(test)] mod tests { // Typical usage #[test] #[cfg(all(feature = "webpki-roots", feature = "http1"))] fn test_builder() { ensure_global_state(); let _connector = super::ConnectorBuilder::new() .with_webpki_roots() .https_only() .enable_http1() .build(); } #[test] #[cfg(feature = "http1")] #[should_panic(expected = "ALPN protocols should not be pre-defined")] fn test_reject_predefined_alpn() { ensure_global_state(); let roots = rustls::RootCertStore::empty(); let mut config_with_alpn = rustls::ClientConfig::builder() .with_root_certificates(roots) .with_no_client_auth(); config_with_alpn.alpn_protocols = vec![b"fancyprotocol".to_vec()]; let _connector = super::ConnectorBuilder::new() .with_tls_config(config_with_alpn) .https_only() .enable_http1() .build(); } #[test] #[cfg(all(feature = "http1", feature = "http2"))] fn test_alpn() { ensure_global_state(); let roots = rustls::RootCertStore::empty(); let tls_config = rustls::ClientConfig::builder() .with_root_certificates(roots) .with_no_client_auth(); let connector = super::ConnectorBuilder::new() .with_tls_config(tls_config.clone()) .https_only() .enable_http1() .build(); assert!(connector .tls_config .alpn_protocols .is_empty()); let connector = super::ConnectorBuilder::new() .with_tls_config(tls_config.clone()) .https_only() .enable_http2() .build(); assert_eq!(&connector.tls_config.alpn_protocols, &[b"h2".to_vec()]); let connector = super::ConnectorBuilder::new() .with_tls_config(tls_config.clone()) .https_only() .enable_http1() .enable_http2() .build(); assert_eq!( &connector.tls_config.alpn_protocols, &[b"h2".to_vec(), b"http/1.1".to_vec()] ); let connector = super::ConnectorBuilder::new() .with_tls_config(tls_config) .https_only() .enable_all_versions() .build(); assert_eq!( &connector.tls_config.alpn_protocols, &[b"h2".to_vec(), b"http/1.1".to_vec()] ); } #[test] #[cfg(all(not(feature = "http1"), feature = "http2"))] fn test_alpn_http2() { let roots = rustls::RootCertStore::empty(); let tls_config = rustls::ClientConfig::builder() .with_safe_defaults() .with_root_certificates(roots) .with_no_client_auth(); let connector = super::ConnectorBuilder::new() .with_tls_config(tls_config.clone()) .https_only() .enable_http2() .build(); assert_eq!(&connector.tls_config.alpn_protocols, &[b"h2".to_vec()]); let connector = super::ConnectorBuilder::new() .with_tls_config(tls_config) .https_only() .enable_all_versions() .build(); assert_eq!(&connector.tls_config.alpn_protocols, &[b"h2".to_vec()]); } fn ensure_global_state() { #[cfg(feature = "ring")] let _ = rustls::crypto::ring::default_provider().install_default(); #[cfg(feature = "aws-lc-rs")] let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); } }