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::h2::H2Config;
use crate::h2::config::{PseudoOrder, SettingId};
use crate::profile::H2Profile;

const DEFAULT_CONNECTION_WINDOW: u32 = 65_535;

const NO_PRIORITY_FRAMES: &str = "0";

impl H2Profile {
    fn clear_settings(&mut self) {
        self.header_table_size = None;
        self.enable_push = None;
        self.max_concurrent_streams = None;
        self.initial_stream_window_size = None;
        self.max_frame_size = None;
        self.max_header_list_size = None;
        self.unknown_setting8 = None;
        self.unknown_setting9 = None;
    }

    fn set_setting(&mut self, id: SettingId, value: u32) -> Result<(), String> {
        match id {
            SettingId::HeaderTableSize => self.header_table_size = Some(value),
            SettingId::EnablePush => {
                self.enable_push = Some(match value {
                    0 => false,
                    1 => true,
                    other => return Err(format!("ENABLE_PUSH value {other} is not 0 or 1")),
                });
            }
            SettingId::MaxConcurrentStreams => self.max_concurrent_streams = Some(value),
            SettingId::InitialWindowSize => self.initial_stream_window_size = Some(value),
            SettingId::MaxFrameSize => self.max_frame_size = Some(value),
            SettingId::MaxHeaderListSize => self.max_header_list_size = Some(value),
            SettingId::Unknown8 => self.unknown_setting8 = Some(value),
            SettingId::Unknown9 => self.unknown_setting9 = Some(value),
        }
        Ok(())
    }
}

fn number<T>(text: &str, what: &str) -> Result<T, String>
where
    T: std::str::FromStr,
    T::Err: std::fmt::Display,
{
    text.trim()
        .parse()
        .map_err(|e| format!("{what} {text:?} is not a number: {e}"))
}

impl H2Profile {
    pub(super) fn apply_akamai(&mut self, raw: &str) -> Result<(), String> {
        let parts: Vec<&str> = raw.trim().split('|').collect();
        let [settings, window, priority, pseudo] = parts.as_slice() else {
            return Err(format!(
                "expected 4 '|'-separated fields (SETTINGS|WINDOW_UPDATE|PRIORITY|pseudo-headers), \
                 got {}",
                parts.len()
            ));
        };
        self.clear_settings();
        self.settings_order.clear();
        for entry in settings.split([';', ',']).filter(|e| !e.is_empty()) {
            let (code, value) = entry
                .split_once(':')
                .ok_or_else(|| format!("SETTINGS entry {entry:?} is not id:value"))?;
            let code: u16 = number(code, "SETTINGS id")?;
            let id = SettingId::from_code(code)
                .ok_or_else(|| format!("SETTINGS id {code} is not one leyline can send"))?;
            if self.settings_order.iter().any(|key| key == id.key()) {
                return Err(format!("SETTINGS id {code} appears twice"));
            }
            self.set_setting(id, number(value, "SETTINGS value")?)?;
            self.settings_order.push(id.key().to_owned());
        }
        let increment: u32 = number(window, "WINDOW_UPDATE")?;
        self.initial_connection_window_size = Some(
            increment
                .checked_add(DEFAULT_CONNECTION_WINDOW)
                .ok_or_else(|| format!("WINDOW_UPDATE {increment} overflows the window"))?,
        );
        if *priority != NO_PRIORITY_FRAMES {
            return Err(format!(
                "PRIORITY field {priority:?} lists PRIORITY frames; leyline sends none, so the field \
                 must be {NO_PRIORITY_FRAMES}"
            ));
        }
        self.pseudo_order = pseudo
            .split(',')
            .map(|label| {
                PseudoOrder::key_for_label(label.trim())
                    .map(str::to_owned)
                    .ok_or_else(|| format!("pseudo-header {label:?} is not one of m, a, s, p"))
            })
            .collect::<Result<_, _>>()?;
        self.fingerprint = None;
        self.platforms.clear();
        H2Config::from_profile(self).map_err(|err| err.to_string())?;
        Ok(())
    }
}