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 crate::iana::is_grease;
use crate::profile::TlsProfile;

pub(crate) const PADDING: u16 = 0x0015;

pub(crate) const PRE_SHARED_KEY: u16 = 0x0029;

const ALPS_NEW: u16 = 0x44cd;

const ALPS_OLD: u16 = 0x4469;

#[derive(Clone, Copy)]
enum Switch {
    Always,
    Tls12,
    TcpOnly,
    QuicOnly,
    SessionTickets,
    SupportedGroups,
    SignatureAlgorithms,
    StatusRequest,
    SignedCertTimestamps,
    DelegatedCredentials,
    RecordSizeLimit,
    CompressCertificate,
    ApplicationSettings { new_codepoint: bool },
    TrustAnchors,
    EncryptedClientHello,
}

struct Extension {
    id: u16,
    name: &'static str,
    switch: Switch,
}

const fn ext(id: u16, name: &'static str, switch: Switch) -> Extension {
    Extension { id, name, switch }
}

const EXTENSIONS: &[Extension] = &[
    ext(0x0000, "server_name", Switch::Always),
    ext(0x0017, "extended_master_secret", Switch::Tls12),
    ext(0xff01, "renegotiation_info", Switch::Tls12),
    ext(0x000b, "ec_point_formats", Switch::TcpOnly),
    ext(0x0023, "session_ticket", Switch::SessionTickets),
    ext(0x0010, "alpn", Switch::Always),
    ext(0x0033, "key_share", Switch::Always),
    ext(0x002b, "supported_versions", Switch::Always),
    ext(0x002d, "psk_key_exchange_modes", Switch::Always),
    ext(0x000a, "supported_groups", Switch::SupportedGroups),
    ext(0x000d, "signature_algorithms", Switch::SignatureAlgorithms),
    ext(0x0005, "status_request", Switch::StatusRequest),
    ext(
        0x0012,
        "signed_certificate_timestamp",
        Switch::SignedCertTimestamps,
    ),
    ext(
        0x0022,
        "delegated_credentials",
        Switch::DelegatedCredentials,
    ),
    ext(0x001c, "record_size_limit", Switch::RecordSizeLimit),
    ext(0x001b, "compress_certificate", Switch::CompressCertificate),
    ext(
        ALPS_OLD,
        "application_settings",
        Switch::ApplicationSettings {
            new_codepoint: false,
        },
    ),
    ext(
        ALPS_NEW,
        "application_settings",
        Switch::ApplicationSettings {
            new_codepoint: true,
        },
    ),
    ext(0xca34, "trust_anchors", Switch::TrustAnchors),
    ext(0x0039, "quic_transport_parameters", Switch::QuicOnly),
    ext(
        0xfe0d,
        "encrypted_client_hello",
        Switch::EncryptedClientHello,
    ),
];

const ALWAYS_SENT: &str = "leyline always sends it";

const QUIC_ONLY: &str = "leyline sends it only in the HTTP/3 ClientHello";

const NO_BASE_VALUE: &str =
    "the base profile sets no value for it; pass a base profile whose [tls] table sets one";

impl Switch {
    fn advertised(self, tls: &TlsProfile, quic: bool) -> bool {
        match self {
            Self::Always => true,
            Self::Tls12 => !quic || tls.tls12_extensions,
            Self::TcpOnly => !quic,
            Self::QuicOnly => quic,
            Self::SessionTickets => !quic && tls.session_tickets,
            Self::SupportedGroups => !tls.curves.is_empty(),
            Self::SignatureAlgorithms => !tls.sigalgs.is_empty(),
            Self::StatusRequest => tls.ocsp_stapling,
            Self::SignedCertTimestamps => tls.signed_cert_timestamps,
            Self::DelegatedCredentials => tls.delegated_credentials.is_some(),
            Self::RecordSizeLimit => tls.record_size_limit.is_some(),
            Self::CompressCertificate => !tls.cert_compression.is_empty(),
            Self::ApplicationSettings { new_codepoint } => {
                tls.alps.is_some() && tls.alps_new_codepoint == new_codepoint
            }
            Self::TrustAnchors => tls.request_trust_anchors,
            Self::EncryptedClientHello => tls.ech_grease,
        }
    }

    fn set(self, tls: &mut TlsProfile, base: &TlsProfile, on: bool) -> Result<(), &'static str> {
        match self {
            Self::Always
            | Self::Tls12
            | Self::TcpOnly
            | Self::SupportedGroups
            | Self::SignatureAlgorithms => {
                if !on {
                    return Err(ALWAYS_SENT);
                }
            }
            Self::QuicOnly => {
                if on {
                    return Err(QUIC_ONLY);
                }
            }
            Self::SessionTickets => tls.session_tickets = on,
            Self::StatusRequest => tls.ocsp_stapling = on,
            Self::SignedCertTimestamps => tls.signed_cert_timestamps = on,
            Self::TrustAnchors => tls.request_trust_anchors = on,
            Self::EncryptedClientHello => tls.ech_grease = on,
            Self::DelegatedCredentials => {
                tls.delegated_credentials = carried(on, base.delegated_credentials.clone())?;
            }
            Self::RecordSizeLimit => {
                tls.record_size_limit = carried(on, base.record_size_limit)?;
            }
            Self::CompressCertificate => {
                let list = Some(base.cert_compression.clone()).filter(|list| !list.is_empty());
                tls.cert_compression = carried(on, list)?.unwrap_or_default();
            }
            Self::ApplicationSettings { new_codepoint } => {
                if on {
                    tls.alps = carried(on, base.alps.clone())?;
                    tls.alps_new_codepoint = new_codepoint;
                } else if tls.alps_new_codepoint == new_codepoint {
                    tls.alps = None;
                }
            }
        }
        Ok(())
    }
}

fn carried<T>(on: bool, base: Option<T>) -> Result<Option<T>, &'static str> {
    match (on, base) {
        (false, _) => Ok(None),
        (true, Some(value)) => Ok(Some(value)),
        (true, None) => Err(NO_BASE_VALUE),
    }
}

impl TlsProfile {
    pub(crate) fn advertised_extensions(&self, quic: bool) -> Vec<(u16, &'static str)> {
        EXTENSIONS
            .iter()
            .filter(|ext| ext.switch.advertised(self, quic))
            .map(|ext| (ext.id, ext.name))
            .collect()
    }

    pub(crate) fn apply_extensions(&mut self, ids: &[u16]) -> Result<Vec<u16>, String> {
        let base = self.clone();
        let mut order = Vec::new();
        for (index, &id) in ids.iter().enumerate() {
            if is_grease(id) {
                self.grease = true;
            } else if id == PADDING {
                if index + 1 != ids.len() {
                    return Err(format!(
                        "padding (0x{PADDING:04x}) is at entry {index}; BoringSSL always sends it last"
                    ));
                }
            } else if id == PRE_SHARED_KEY {
                self.pre_shared_key = true;
            } else if EXTENSIONS.iter().any(|ext| ext.id == id) {
                order.push(id);
            } else {
                return Err(format!(
                    "extension 0x{id:04x} ({id}) is not one leyline can send"
                ));
            }
        }
        self.padding = ids.contains(&PADDING);
        for ext in EXTENSIONS {
            ext.switch
                .set(self, &base, order.contains(&ext.id))
                .map_err(|why| format!("{} (0x{:04x}) is missing: {why}", ext.name, ext.id))?;
        }
        Ok(order)
    }
}