leyline-http 0.1.5

HTTP client that impersonates Chrome, Firefox, and Safari on the wire: TLS (JA3/JA4), HTTP/2, and HTTP/3 fingerprints
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 host = crate::util::bare_host(host);
        let (ssl, verification_failure) = self.prepare_ssl(host, session_key, 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 meta = TlsMeta::of(stream.ssl());
        if trace::on() {
            meta.trace(host, started);
        }
        Ok((stream, meta))
    }

    fn prepare_ssl(
        &self,
        host: &str,
        session_key: &str,
        include_alps: bool,
    ) -> Result<(leyline_bssl::ssl::Ssl, Option<VerificationFailure>), TlsError> {
        let mut config = self
            .ssl_connector
            .configure()
            .map_err(TlsError::from_stack)?;
        if !include_alps {
            config
                .set_alpn_protos(crate::tls::alpn::HTTP11_WIRE)
                .map_err(TlsError::from_stack)?;
        }
        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)?;
        Ok((ssl, verification_failure))
    }
}

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 of(ssl: &leyline_bssl::ssl::SslRef) -> Self {
        Self {
            alpn: ssl.selected_alpn_protocol().map(<[u8]>::to_vec),
            peer_cert_der: ssl.peer_certificate().and_then(|cert| cert.to_der().ok()),
            tls_version: Some(ssl.version_str().to_string()),
            tls_cipher: ssl.current_cipher().map(|c| c.name().to_string()),
        }
    }

    fn trace(&self, host: &str, started: Instant) {
        let proto = self
            .alpn
            .as_deref()
            .map(|p| String::from_utf8_lossy(p).into_owned());
        trace::tls(
            host,
            self.tls_version.as_deref(),
            self.tls_cipher.as_deref(),
            proto.as_deref(),
            started.elapsed(),
        );
    }

    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,
        }
    }
}