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::core::error::Result;
use crate::core::{Session, SessionIdentity};

use super::secret::ProxyKey;
use super::{Device, config};

impl Device {
    pub fn check(&self, session: &Session) -> Result<()> {
        let id = session.identity();
        let mut differences = match self.profile_id.as_deref() {
            Some(expected) => profile_difference(expected, &id),
            None => self.identity_differences(&id),
        };
        let profile_changed = !differences.is_empty();
        differences.extend(self.setting_differences(session, &id));
        differences.extend(self.strict_gaps());
        if differences.is_empty() {
            return Ok(());
        }
        let err = config(format!(
            "the session differs from the device: {}",
            differences.join("; ")
        ));
        Err(if profile_changed {
            err.with_source(crate::core::error::ProfileChanged)
        } else {
            err
        })
    }

    fn identity_differences(&self, id: &SessionIdentity) -> Vec<String> {
        let mut differences = Vec::new();
        if self.identity != id.to_identity() {
            differences.push(format!(
                "identity {:?} (device {:?})",
                id.to_identity(),
                self.identity
            ));
        }
        if self.platform != id.platform() {
            differences.push(format!(
                "platform {:?} (device {:?})",
                id.platform(),
                self.platform
            ));
        }
        if self.brand != id.brand() {
            differences.push(format!("brand {:?} (device {:?})", id.brand(), self.brand));
        }
        if let Some(expected) = self.user_agent.as_deref()
            && expected != id.user_agent()
        {
            differences.push(format!(
                "user_agent {:?} (device {expected:?})",
                id.user_agent()
            ));
        }
        differences
    }

    fn strict_gaps(&self) -> Vec<String> {
        let mut gaps = Vec::new();
        if !self.strict {
            return gaps;
        }
        if self.profile_id.is_none() {
            gaps.push("strict device has no profile_id".to_owned());
        }
        if self.proxy.is_none() && !self.env_proxy {
            gaps.push("strict device has no proxy and env_proxy is false".to_owned());
        }
        gaps
    }

    fn setting_differences(&self, session: &Session, id: &SessionIdentity) -> Vec<String> {
        let mut differences = Vec::new();
        if self.languages.as_deref() != id.languages() {
            differences.push(format!(
                "languages {:?} (device {:?})",
                id.languages(),
                self.languages
            ));
        }
        let proxy = session.proxy_url();
        if !self.env_proxy
            && self.proxy.as_ref().map(ProxyKey::of) != proxy.as_ref().map(ProxyKey::of)
        {
            differences.push(format!("proxy {proxy:?} (device {:?})", self.proxy));
        }
        differences
    }
}

fn profile_difference(expected: &str, id: &SessionIdentity) -> Vec<String> {
    match id.profile_id() {
        Some(actual) if actual == expected => Vec::new(),
        actual => vec![format!("profile_id {actual:?} (device {expected:?})")],
    }
}