leyline-http 0.1.0

HTTP client that mimics browsers on the wire
Documentation
use std::time::Instant;

use leyline_bssl::ssl::SslVerifyMode;
use leyline_bssl::x509::X509VerifyError;
use tokio::net::TcpStream;

use super::FingerprintConnector;
use crate::tls::error::TlsError;
use crate::tls::trust::{
    TrustFailure, VerificationFailure, install_verifier, take_verification_failure,
};
use crate::tls::{TlsIo, TlsStream};
use crate::trace;

impl FingerprintConnector {
    pub(crate) async fn tls_handshake(
        &self,
        tcp_stream: TcpStream,
        host: &str,
        session_key: &str,
        include_alps: bool,
    ) -> Result<TlsStream, TlsError> {
        let (stream, meta) = self
            .handshake_over(tcp_stream, host, session_key, include_alps)
            .await?;
        Ok(meta.into_tls_stream(TlsIo::Boring(stream)))
    }

    pub(crate) async fn tls_handshake_nested(
        &self,
        inner: TlsIo,
        host: &str,
        session_key: &str,
        include_alps: bool,
    ) -> Result<TlsStream, TlsError> {
        let (stream, meta) = self
            .handshake_over(inner, host, session_key, include_alps)
            .await?;
        Ok(meta.into_tls_stream(TlsIo::Nested(Box::new(stream))))
    }

    async fn handshake_over<S>(
        &self,
        io: S,
        host: &str,
        session_key: &str,
        include_alps: bool,
    ) -> Result<(leyline_bssl_tokio::SslStream<S>, TlsMeta), TlsError>
    where
        S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
    {
        let started = Instant::now();
        let mut config = self
            .ssl_connector
            .configure()
            .map_err(TlsError::from_stack)?;

        if !include_alps {
            config
                .set_alpn_protos(b"\x08http/1.1")
                .map_err(TlsError::from_stack)?;
        }

        let host = crate::util::bare_host(host);
        let mut ssl = config.into_ssl(host).map_err(TlsError::from_stack)?;

        let insecure = self.insecure_mode();
        if insecure {
            ssl.set_verify(SslVerifyMode::NONE);
        }
        let verification_failure = (!insecure
            && (!self.pins.is_empty() || cfg!(target_os = "macos") && self.system_roots))
            .then(|| install_verifier(&mut ssl, &self.pins, host, self.system_roots));

        if !insecure {
            self.session_cache.attach(&mut ssl, session_key)?;
        }

        self.hello.apply(&mut ssl, include_alps)?;

        let stream = match leyline_bssl_tokio::SslStreamBuilder::new(ssl, io)
            .connect()
            .await
        {
            Ok(stream) => stream,
            Err(e) => {
                let verify_error = e.ssl().and_then(|ssl| ssl.verify_result().err());
                return Err(classify_handshake(
                    verification_failure.as_ref(),
                    verify_error,
                    e,
                ));
            }
        };

        let alpn = stream.ssl().selected_alpn_protocol().map(|p| p.to_vec());
        let peer_cert_der = stream
            .ssl()
            .peer_certificate()
            .and_then(|cert| cert.to_der().ok());
        let tls_version = Some(stream.ssl().version_str().to_string());
        let tls_cipher = stream.ssl().current_cipher().map(|c| c.name().to_string());

        if trace::on() {
            let proto = alpn
                .as_deref()
                .map(|p| String::from_utf8_lossy(p).into_owned());
            trace::tls(
                host,
                tls_version.as_deref(),
                tls_cipher.as_deref(),
                proto.as_deref(),
                started.elapsed(),
            );
        }

        Ok((
            stream,
            TlsMeta {
                alpn,
                peer_cert_der,
                tls_version,
                tls_cipher,
            },
        ))
    }
}

fn classify_handshake<S>(
    failure: Option<&VerificationFailure>,
    verify_error: Option<X509VerifyError>,
    error: leyline_bssl_tokio::HandshakeError<S>,
) -> TlsError {
    let verify_error = verify_error.filter(|error| *error != X509VerifyError::INVALID_CALL);
    match failure.and_then(take_verification_failure) {
        Some(TrustFailure::Certificate) => certificate(verify_error, &error),
        Some(TrustFailure::Hostname) => TlsError::Hostname(error.to_string()),
        Some(TrustFailure::Pinning) => TlsError::Pinning(error.to_string()),
        None if matches!(
            verify_error,
            Some(X509VerifyError::HOSTNAME_MISMATCH | X509VerifyError::IP_ADDRESS_MISMATCH)
        ) =>
        {
            TlsError::Hostname(error.to_string())
        }
        None if verify_error.is_some() => certificate(verify_error, &error),
        None => TlsError::from_handshake(&error),
    }
}

fn certificate<S>(
    verify_error: Option<X509VerifyError>,
    error: &leyline_bssl_tokio::HandshakeError<S>,
) -> TlsError {
    TlsError::Certificate {
        verify_code: verify_error.map(|e| e.as_raw()),
        reason: verify_error.map(|e| e.error_string()),
        detail: error.to_string(),
    }
}

struct TlsMeta {
    alpn: Option<Vec<u8>>,
    peer_cert_der: Option<Vec<u8>>,
    tls_version: Option<String>,
    tls_cipher: Option<String>,
}

impl TlsMeta {
    fn into_tls_stream(self, stream: TlsIo) -> TlsStream {
        TlsStream {
            stream,
            alpn: self.alpn,
            peer_cert_der: self.peer_cert_der,
            tls_version: self.tls_version,
            tls_cipher: self.tls_cipher,
        }
    }
}