apollo-http-client 0.3.0

HTTP client for Apollo platform
Documentation
use std::sync::Arc;

use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
use hyper_util::client::legacy::connect::HttpConnector;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::{ClientConfig, RootCertStore, crypto::aws_lc_rs as aws_lc_rs_crypto};
use rustls_pki_types::pem::{Error as PemError, PemObject};

use crate::config::{ClientAuthentication, Protocol, TlsConfig};
use crate::error::HttpClientError;

/// Returns a clone of `config` with ALPN protocols overwritten to match `protocol`.
///
/// Call this when injecting a pre-built [`ClientConfig`] via [`HttpClientBuilder`]
/// so ALPN reflects the configured HTTP version.
pub(crate) fn with_protocol_alpn(config: &ClientConfig, protocol: Protocol) -> ClientConfig {
    let mut out = config.clone();
    out.alpn_protocols = match protocol {
        Protocol::Http1 => vec![b"http/1.1".to_vec()],
        Protocol::Http2 => vec![b"h2".to_vec()],
        Protocol::Alpn => vec![b"h2".to_vec(), b"http/1.1".to_vec()],
    };
    out
}

/// Builds a TLS [`ClientConfig`] for the given protocol and TLS settings.
///
/// The ALPN protocols are set according to `protocol`:
/// - [`Protocol::Http1`] — `["http/1.1"]`
/// - [`Protocol::Http2`] — `["h2"]`
/// - [`Protocol::Alpn`] — `["h2", "http/1.1"]`
///
/// The returned `Arc<ClientConfig>` can be shared with a proxy connector to
/// perform TLS inside a CONNECT tunnel.
pub(crate) fn build_tls_config(
    protocol: Protocol,
    tls: &TlsConfig,
) -> Result<Arc<ClientConfig>, HttpClientError> {
    let provider = Arc::new(aws_lc_rs_crypto::default_provider());

    let builder =
        ClientConfig::builder_with_provider(provider).with_safe_default_protocol_versions()?;

    let server_auth = if tls.danger_accept_invalid_certs {
        builder
            .dangerous()
            .with_custom_certificate_verifier(Arc::new(SkipCertVerification))
    } else {
        let root_store = build_root_cert_store(tls)?;
        builder.with_root_certificates(root_store)
    };

    let base = match &tls.client_authentication {
        None => server_auth.with_no_client_auth(),
        Some(auth) => {
            let (chain, key) = parse_client_identity(auth)?;
            server_auth
                .with_client_auth_cert(chain, key)
                .map_err(|source| HttpClientError::TlsClientAuth { source })?
        }
    };

    let mut config = base;
    config.alpn_protocols = match protocol {
        Protocol::Http1 => vec![b"http/1.1".to_vec()],
        Protocol::Http2 => vec![b"h2".to_vec()],
        Protocol::Alpn => vec![b"h2".to_vec(), b"http/1.1".to_vec()],
    };

    Ok(Arc::new(config))
}

/// Builds the root certificate store from the OS native store (if enabled) and
/// any user-supplied CA bundle. Errors when verification is enabled but no
/// trust anchors are configured.
fn build_root_cert_store(tls: &TlsConfig) -> Result<RootCertStore, HttpClientError> {
    let mut root_store = RootCertStore::empty();

    if tls.use_native_certificate_store {
        for cert in rustls_native_certs::load_native_certs().certs {
            root_store.add(cert)?;
        }
    }

    if let Some(pem) = &tls.certificate_authorities {
        let certs = parse_certificate_authorities(pem.unredact().as_bytes())?;
        for cert in certs {
            root_store.add(cert)?;
        }
    }

    if root_store.is_empty() {
        return Err(HttpClientError::TlsNoTrustAnchors);
    }

    Ok(root_store)
}

/// Parses a PEM bundle into certificate DER bytes. Non-certificate PEM blocks
/// in the bundle are silently skipped. Returns `invalid_err(e)` if the bundle
/// contains malformed PEM and `empty_err` if it contains no certificate
/// blocks — these vary per field so callers can attribute the error to the
/// originating config key.
fn parse_pem_chain<F>(
    pem: &[u8],
    invalid_err: F,
    empty_err: HttpClientError,
) -> Result<Vec<CertificateDer<'static>>, HttpClientError>
where
    F: FnOnce(PemError) -> HttpClientError,
{
    let certs: Vec<_> = CertificateDer::pem_slice_iter(pem)
        .collect::<Result<_, _>>()
        .map_err(invalid_err)?;

    if certs.is_empty() {
        return Err(empty_err);
    }

    Ok(certs)
}

fn parse_certificate_authorities(
    pem: &[u8],
) -> Result<Vec<CertificateDer<'static>>, HttpClientError> {
    parse_pem_chain(
        pem,
        |source| HttpClientError::TlsCertificateAuthoritiesInvalid { source },
        HttpClientError::TlsCertificateAuthoritiesEmpty,
    )
}

fn parse_client_certificate_chain(
    pem: &[u8],
) -> Result<Vec<CertificateDer<'static>>, HttpClientError> {
    parse_pem_chain(
        pem,
        |source| HttpClientError::TlsClientCertificateChainInvalid { source },
        HttpClientError::TlsClientCertificateChainEmpty,
    )
}

/// Parses the client certificate chain and private key for mutual TLS.
fn parse_client_identity(
    auth: &ClientAuthentication,
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), HttpClientError> {
    let chain = parse_client_certificate_chain(auth.certificate_chain.unredact().as_bytes())?;
    let key = parse_client_key(auth.key.unredact().as_bytes())?;
    Ok((chain, key))
}

/// `rustls-pki-types` only recognises unencrypted PKCS#1, PKCS#8, and SEC1
/// blocks, so encrypted keys would surface as "no key found". Detect them up
/// front and emit a dedicated error pointing at the actual problem.
const ENCRYPTED_KEY_HEADER: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----";

fn parse_client_key(pem: &[u8]) -> Result<PrivateKeyDer<'static>, HttpClientError> {
    if std::str::from_utf8(pem).is_ok_and(|s| s.contains(ENCRYPTED_KEY_HEADER)) {
        return Err(HttpClientError::TlsClientKeyEncrypted);
    }

    PrivateKeyDer::from_pem_slice(pem).map_err(|e| match e {
        PemError::NoItemsFound => HttpClientError::TlsClientKeyMissing,
        e => HttpClientError::TlsClientKeyInvalid { source: e },
    })
}

/// Wraps a [`ClientConfig`] and an [`HttpConnector`] into an HTTPS connector
/// that handles both plain HTTP and HTTPS connections.
pub(crate) fn build_https_connector(
    http: HttpConnector,
    tls_config: Arc<ClientConfig>,
    protocol: Protocol,
) -> HttpsConnector<HttpConnector> {
    // hyper-rustls sets ALPN itself via enable_http1 / enable_http2 / enable_all_versions;
    // it panics if the config already has ALPN protocols. Strip them from the clone so the
    // original Arc<ClientConfig> (with ALPN) can still be used for proxy CONNECT tunnels.
    let mut base = (*tls_config).clone();
    base.alpn_protocols.clear();
    let builder = HttpsConnectorBuilder::new()
        .with_tls_config(base)
        .https_or_http();

    match protocol {
        Protocol::Http1 => builder.enable_http1().wrap_connector(http),
        Protocol::Http2 => builder.enable_http2().wrap_connector(http),
        Protocol::Alpn => builder.enable_all_versions().wrap_connector(http),
    }
}

/// A no-op TLS certificate verifier that accepts any certificate.
///
/// Only used when `tls.danger_accept_invalid_certs` is enabled in the client config.
#[derive(Debug)]
struct SkipCertVerification;

impl rustls::client::danger::ServerCertVerifier for SkipCertVerification {
    fn verify_server_cert(
        &self,
        _end_entity: &rustls::pki_types::CertificateDer<'_>,
        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
        _server_name: &rustls::pki_types::ServerName<'_>,
        _ocsp_response: &[u8],
        _now: rustls::pki_types::UnixTime,
    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
        Ok(rustls::client::danger::ServerCertVerified::assertion())
    }

    fn verify_tls12_signature(
        &self,
        _message: &[u8],
        _cert: &rustls::pki_types::CertificateDer<'_>,
        _dupe_value: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
    }

    fn verify_tls13_signature(
        &self,
        _message: &[u8],
        _cert: &rustls::pki_types::CertificateDer<'_>,
        _dupe_value: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
    }

    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        use rustls::SignatureScheme;
        vec![
            SignatureScheme::RSA_PKCS1_SHA1,
            SignatureScheme::ECDSA_SHA1_Legacy,
            SignatureScheme::RSA_PKCS1_SHA256,
            SignatureScheme::ECDSA_NISTP256_SHA256,
            SignatureScheme::RSA_PKCS1_SHA384,
            SignatureScheme::ECDSA_NISTP384_SHA384,
            SignatureScheme::RSA_PKCS1_SHA512,
            SignatureScheme::ECDSA_NISTP521_SHA512,
            SignatureScheme::RSA_PSS_SHA256,
            SignatureScheme::RSA_PSS_SHA384,
            SignatureScheme::RSA_PSS_SHA512,
            SignatureScheme::ED25519,
            SignatureScheme::ED448,
        ]
    }
}