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 leyline_bssl::ssl::SslRef;

use crate::profile::TlsProfile;
use crate::tls::error::TlsError;

#[derive(Debug, Clone)]
pub(crate) struct HelloOptions {
    ech_grease: bool,
    alps_proto: Option<Vec<u8>>,
    alps_new_codepoint: bool,
    request_trust_anchors: bool,
    key_shares: Option<Vec<u16>>,
    tls12_extensions: bool,
}

impl HelloOptions {
    pub(crate) fn from_tls(tls: &TlsProfile) -> Result<Self, TlsError> {
        Ok(Self {
            ech_grease: tls.ech_grease,
            alps_proto: tls.alps.as_ref().map(|s| s.as_bytes().to_vec()),
            alps_new_codepoint: tls.alps_new_codepoint,
            request_trust_anchors: tls.request_trust_anchors,
            key_shares: tls.key_share_ids()?,
            tls12_extensions: tls.tls12_extensions,
        })
    }

    pub(crate) fn ech_grease(&self) -> bool {
        self.ech_grease
    }

    pub(crate) fn apply(&self, ssl: &mut SslRef, include_alps: bool) -> Result<(), TlsError> {
        if include_alps && let Some(ref alps) = self.alps_proto {
            ssl.add_application_settings(alps)
                .map_err(TlsError::from_stack)?;
            if self.alps_new_codepoint {
                ssl.set_alps_use_new_codepoint(true);
            }
        }

        if self.ech_grease {
            ssl.set_enable_ech_grease(true);
        }

        if let Some(ids) = &self.key_shares {
            ssl.set_client_key_shares(ids)
                .map_err(TlsError::from_stack)?;
        }

        if self.request_trust_anchors {
            ssl.set_requested_trust_anchors(&[]).map_err(|e| {
                TlsError::SslConfig(format!(
                    "profile requires the trust_anchors extension (0xCA34), which BoringSSL \
                     rejected ({e}); the ClientHello JA4 would not match the captured browser"
                ))
            })?;
        }

        if self.tls12_extensions {
            ssl.set_tls12_extensions(true);
        }

        Ok(())
    }
}

impl TlsProfile {
    fn key_share_ids(&self) -> Result<Option<Vec<u16>>, TlsError> {
        let Some(names) = self.key_shares.as_deref() else {
            return Ok(None);
        };
        let ids = names
            .iter()
            .map(|name| {
                crate::iana::curve_id(name)
                    .ok_or_else(|| TlsError::Profile(format!("unknown key share group: {name}")))
            })
            .collect::<Result<Vec<u16>, _>>()?;
        let mut curves = self
            .curves
            .iter()
            .filter_map(|name| crate::iana::curve_id(name));
        if !ids.iter().all(|id| curves.any(|curve| curve == *id)) {
            return Err(TlsError::Profile(format!(
                "key_shares {names:?} must be an ordered subsequence of curves {:?}",
                self.curves
            )));
        }
        Ok(Some(ids))
    }
}