hyper-rustls 0.25.0

Rustls+hyper integration for pure rust HTTPS
Documentation
#[cfg(feature = "tokio-runtime")]
use hyper::client::HttpConnector;
#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
use rustls::crypto::CryptoProvider;
use rustls::ClientConfig;

use super::HttpsConnector;
#[cfg(any(feature = "rustls-native-certs", feature = "webpki-roots"))]
use crate::config::ConfigBuilderExt;

/// 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 = "tokio-runtime", feature = "http1"))]
/// let https = HttpsConnectorBuilder::new()
///     .with_webpki_roots()
///     .https_only()
///     .enable_http1()
///     .build();
/// ```
pub struct ConnectorBuilder<State>(State);

/// State of a builder that needs a TLS client config next
pub struct WantsTlsConfig(());

impl ConnectorBuilder<WantsTlsConfig> {
    /// 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<WantsSchemes> {
        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 safe defaults, with
    /// native roots.
    ///
    /// See [`ConfigBuilderExt::with_native_roots`]
    #[cfg(all(feature = "ring", feature = "rustls-native-certs"))]
    pub fn with_native_roots(self) -> std::io::Result<ConnectorBuilder<WantsSchemes>> {
        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: CryptoProvider,
    ) -> std::io::Result<ConnectorBuilder<WantsSchemes>> {
        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(feature = "ring", feature = "webpki-roots"))]
    pub fn with_webpki_roots(self) -> ConnectorBuilder<WantsSchemes> {
        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: CryptoProvider,
    ) -> Result<ConnectorBuilder<WantsSchemes>, 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<WantsTlsConfig> {
    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<WantsSchemes> {
    /// Enforce the use of HTTPS when connecting
    ///
    /// Only URLs using the HTTPS scheme will be connectable.
    pub fn https_only(self) -> ConnectorBuilder<WantsProtocols1> {
        ConnectorBuilder(WantsProtocols1 {
            tls_config: self.0.tls_config,
            https_only: true,
            override_server_name: 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<WantsProtocols1> {
        ConnectorBuilder(WantsProtocols1 {
            tls_config: self.0.tls_config,
            https_only: false,
            override_server_name: 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,
    override_server_name: Option<String>,
}

impl WantsProtocols1 {
    fn wrap_connector<H>(self, conn: H) -> HttpsConnector<H> {
        HttpsConnector {
            force_https: self.https_only,
            http: conn,
            tls_config: std::sync::Arc::new(self.tls_config),
            override_server_name: self.override_server_name,
        }
    }

    #[cfg(feature = "tokio-runtime")]
    fn build(self) -> HttpsConnector<HttpConnector> {
        let mut http = HttpConnector::new();
        // HttpConnector won't enforce scheme, but HttpsConnector will
        http.enforce_http(false);
        self.wrap_connector(http)
    }
}

impl ConnectorBuilder<WantsProtocols1> {
    /// Enable HTTP1
    ///
    /// This needs to be called explicitly, no protocol is enabled by default
    #[cfg(feature = "http1")]
    pub fn enable_http1(self) -> ConnectorBuilder<WantsProtocols2> {
        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<WantsProtocols3> {
        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<WantsProtocols3> {
        #[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 verify that server
    /// certificate contains `override_server_name`. Domain name included in
    /// the URL will not affect certificate validation.
    pub fn with_server_name(mut self, override_server_name: String) -> Self {
        self.0.override_server_name = Some(override_server_name);
        self
    }
}

/// 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<WantsProtocols2>::build) and
/// [`wrap_connector`](ConnectorBuilder<WantsProtocols2>::wrap_connector).
pub struct WantsProtocols2 {
    inner: WantsProtocols1,
}

impl ConnectorBuilder<WantsProtocols2> {
    /// Enable HTTP2
    ///
    /// This needs to be called explicitly, no protocol is enabled by default
    #[cfg(feature = "http2")]
    pub fn enable_http2(mut self) -> ConnectorBuilder<WantsProtocols3> {
        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`]
    #[cfg(feature = "tokio-runtime")]
    pub fn build(self) -> HttpsConnector<HttpConnector> {
        self.0.inner.build()
    }

    /// This wraps an arbitrary low-level connector into an [`HttpsConnector`]
    pub fn wrap_connector<H>(self, conn: H) -> HttpsConnector<H> {
        // 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<WantsProtocols3>::build) and
/// [`wrap_connector`](ConnectorBuilder<WantsProtocols3>::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<WantsProtocols3> {
    /// This builds an [`HttpsConnector`] built on hyper's default [`HttpConnector`]
    #[cfg(feature = "tokio-runtime")]
    pub fn build(self) -> HttpsConnector<HttpConnector> {
        self.0.inner.build()
    }

    /// This wraps an arbitrary low-level connector into an [`HttpsConnector`]
    pub fn wrap_connector<H>(self, conn: H) -> HttpsConnector<H> {
        // 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() {
        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() {
        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() {
        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()]);
    }
}