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::sync::Arc;

use http::header::USER_AGENT;

use crate::audit::AuditTlsCache;
use crate::core::error::{Error, Kind, Result};
use crate::core::{CompressionConfig, Identity};
use crate::h2::H2Config;
use crate::profile::{
    Browser, BrowserProfile, ChromiumBrand, HeaderStyle, Platform, PlatformIdentity,
};
use crate::tcp::TcpProfile;

pub(in crate::core::session) enum IdentitySource {
    Bare,
    Profile(Arc<BrowserProfile>),
    Browser { tls: Browser, http: Browser },
}

pub(in crate::core::session) struct IdentityInputs<'a> {
    pub source: IdentitySource,
    pub platform: Platform,
    pub brand: ChromiumBrand,
    pub compression: CompressionConfig,
    #[cfg(feature = "http3")]
    pub h3_required: bool,
    pub tcp: &'a TcpProfile,
    pub audit: bool,
    pub default_headers: &'a [(String, String)],
    pub languages: Option<&'a [String]>,
}

pub(in crate::core::session) struct DerivedIdentity {
    pub browser: Option<Browser>,
    pub identity: Option<Identity>,
    pub platform: Platform,
    pub profile: Arc<BrowserProfile>,
    pub header_style: HeaderStyle,
    pub header_order: Option<Vec<String>>,
    pub user_agent: String,
    pub sec_ch_ua: String,
    pub accept_language: String,
    pub h2_config: H2Config,
    #[cfg(feature = "http3")]
    pub h3_config: Option<crate::quic::H3Config>,
    pub audit_tls: Option<Arc<AuditTlsCache>>,
}

struct Selected {
    browser: Option<Browser>,
    identity: Option<Identity>,
    http_profile: Option<&'static BrowserProfile>,
    base: Arc<BrowserProfile>,
}

impl Selected {
    fn new(source: IdentitySource, platform: Platform) -> Result<Self> {
        match source {
            IdentitySource::Bare => Ok(Self::without_browser(BrowserProfile::bare_shared())),
            IdentitySource::Profile(profile) => Ok(Self::without_browser(profile)),
            IdentitySource::Browser { tls, http } => Self::for_browsers(tls, http, platform),
        }
    }

    fn without_browser(base: Arc<BrowserProfile>) -> Self {
        Self {
            browser: None,
            identity: None,
            http_profile: None,
            base,
        }
    }

    fn for_browsers(tls: Browser, http: Browser, platform: Platform) -> Result<Self> {
        let tls = tls.for_platform(platform);
        let http = http.for_platform(platform);
        Ok(Self {
            browser: Some(tls),
            identity: Some(Identity::locked(http, platform).rotate_tls(tls)?),
            http_profile: Some(http.profile()),
            base: tls.shared_profile(),
        })
    }
}

impl BrowserProfile {
    pub(in crate::core::session) fn resolve_presented(
        &self,
        platform: Platform,
        brand: ChromiumBrand,
    ) -> Result<PlatformIdentity> {
        self.resolve_identity(platform, brand)
            .map_err(|e| Error::new(Kind::Config).with_message(e.to_string()))
    }
}

fn session_user_agent(default_headers: &[(String, String)]) -> Option<&str> {
    default_headers
        .iter()
        .rev()
        .find(|(name, _)| name.eq_ignore_ascii_case(USER_AGENT.as_str()))
        .map(|(_, value)| value.as_str())
}

fn session_accept_language(
    languages: Option<&[String]>,
    style: HeaderStyle,
    bare: bool,
    profile_value: Option<String>,
) -> String {
    match languages {
        Some(tags) => crate::profile::languages::accept_language(tags, style),
        None if bare => String::new(),
        None => profile_value.unwrap_or_default(),
    }
}

impl BrowserProfile {
    fn derive_h2(&self, platform: Platform, max_body: usize) -> Result<H2Config> {
        let resolved = self.h2.resolve_for_platform(platform)?;
        let mut config = H2Config::from_profile(&resolved)?;
        config.max_response_body_bytes = max_body;
        Ok(config)
    }
}

#[cfg(feature = "http3")]
impl BrowserProfile {
    fn derive_h3(&self, max_body: usize, required: bool) -> Result<Option<crate::quic::H3Config>> {
        match crate::quic::H3Config::from_profile(self) {
            Ok(mut config) => {
                config.max_response_body_bytes = max_body as u64;
                Ok(Some(config))
            }
            Err(error) if required => Err(error),
            Err(_) => Ok(None),
        }
    }
}

pub(in crate::core::session) fn derive_identity(
    input: IdentityInputs<'_>,
) -> Result<DerivedIdentity> {
    let platform = input.platform.resolve();
    let brand = input.brand;
    let max_body = input.compression.max_body_size;
    let bare = matches!(input.source, IdentitySource::Bare);
    let selected = Selected::new(input.source, platform)?;
    let profile = brand.tls_profile(selected.base);
    let http_profile = selected.http_profile.unwrap_or(&*profile);
    let header_style = brand
        .header_style()
        .unwrap_or(http_profile.meta.header_style);
    let header_order = http_profile.meta.header_order.clone();
    let resolved = http_profile.resolve_presented(platform, brand)?;
    let h2_config = profile.derive_h2(platform, max_body)?;
    #[cfg(feature = "http3")]
    let h3_config = profile.derive_h3(max_body, input.h3_required)?;
    let audit_tls = input
        .audit
        .then(|| Arc::new(profile.audit_cache(&h2_config, input.tcp)));

    Ok(DerivedIdentity {
        browser: selected.browser,
        identity: selected.identity,
        platform,
        profile,
        header_style,
        header_order,
        sec_ch_ua: match session_user_agent(input.default_headers) {
            Some(_) => String::new(),
            None => resolved.sec_ch_ua,
        },
        user_agent: session_user_agent(input.default_headers)
            .map_or(resolved.user_agent, str::to_owned),
        accept_language: session_accept_language(
            input.languages,
            header_style,
            bare,
            resolved.accept_language,
        ),
        h2_config,
        #[cfg(feature = "http3")]
        h3_config,
        audit_tls,
    })
}