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::profile::TlsProfile;
use crate::profile::extension::{PADDING, PRE_SHARED_KEY};

impl TlsProfile {
    pub(crate) fn extension_ids(&self) -> Vec<u16> {
        let mut ids = match self.extension_permutation.as_deref() {
            Some(order) => order.to_vec(),
            None => self
                .advertised_extensions(false)
                .into_iter()
                .map(|(id, _)| id)
                .collect(),
        };
        if self.padding {
            ids.push(PADDING);
        }
        ids
    }

    pub(super) fn validate(&self, quic: bool) -> Result<(), String> {
        self.validate_tail(quic)?;
        let Some(order) = self.extension_permutation.as_deref() else {
            return Ok(());
        };
        if order.is_empty() {
            return Err(
                "extension_permutation is empty; omit the key to accept BoringSSL's \
                        default extension order"
                    .to_string(),
            );
        }

        let advertised = self.advertised_extensions(quic);
        for (index, id) in order.iter().enumerate() {
            if *id == PRE_SHARED_KEY {
                return Err(format!(
                    "extension_permutation entry {index} is pre_shared_key (0x{PRE_SHARED_KEY:04x}), \
                     which TLS 1.3 fixes last and BoringSSL will not reposition; drop it from the list"
                ));
            }
            if order[..index].contains(id) {
                return Err(format!(
                    "extension_permutation repeats 0x{id:04x} at entry {index}; BoringSSL rejects a \
                     duplicated extension ID, leaving the ClientHello in its default order"
                ));
            }
            if !advertised.iter().any(|(known, _)| known == id) {
                return Err(format!(
                    "extension_permutation entry {index} is 0x{id:04x}, which this profile never \
                     advertises; order only the extensions the [tls] block turns on"
                ));
            }
        }

        let missing: Vec<String> = advertised
            .iter()
            .filter(|(id, _)| !order.contains(id))
            .map(|(id, name)| format!("{name} (0x{id:04x})"))
            .collect();
        if !missing.is_empty() {
            return Err(format!(
                "extension_permutation omits {}; BoringSSL appends whatever the list leaves out, in \
                 its own order, so the tail of the ClientHello would not match the captured browser. \
                 Add each one at its captured position",
                missing.join(", ")
            ));
        }

        Ok(())
    }

    fn validate_tail(&self, quic: bool) -> Result<(), String> {
        if self.extension_tail.is_empty() {
            return Ok(());
        }
        if !quic || self.grease || !self.permute_extensions || self.extension_permutation.is_some()
        {
            return Err(
                "extension_tail belongs in [h3.tls] with permute_extensions = true, grease = false \
                 and no extension_permutation; it pins the last extensions of a shuffled ClientHello"
                    .to_string(),
            );
        }
        let advertised = self.advertised_extensions(quic);
        for (index, id) in self.extension_tail.iter().enumerate() {
            if *id == PRE_SHARED_KEY || self.extension_tail[..index].contains(id) {
                return Err(format!(
                    "extension_tail entry {index} (0x{id:04x}) is pre_shared_key or a repeat"
                ));
            }
            if !advertised.iter().any(|(known, _)| known == id) {
                return Err(format!(
                    "extension_tail entry {index} is 0x{id:04x}, which this profile never advertises"
                ));
            }
        }
        Ok(())
    }
}