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
#![forbid(unsafe_code)]
mod compress;
mod context;
mod debug;
mod encode;
mod route;
mod send;

pub use compress::ContentEncoding;

use http::{HeaderName, HeaderValue, Method};

use crate::profile::{HeaderAnchor, Preset};

use crate::core::body::Body;
use crate::core::config::{ProxyConfig, RedirectPolicy, TimeoutConfig};
use crate::core::digest::DigestAuth;
use crate::core::error::{Error, Kind};
use crate::core::headers::HeaderList;
use crate::core::into_url::IntoUrl;
#[cfg(feature = "multipart")]
use crate::core::multipart::Form;
use crate::core::retry::RetryPolicy;
use crate::core::session::Session;

pub trait IntoParamPair {
    fn into_param_pair(self) -> (String, String);
}

impl<K, V> IntoParamPair for (K, V)
where
    K: AsRef<str>,
    V: AsRef<str>,
{
    fn into_param_pair(self) -> (String, String) {
        (self.0.as_ref().to_string(), self.1.as_ref().to_string())
    }
}

impl<K, V> IntoParamPair for &(K, V)
where
    K: AsRef<str>,
    V: AsRef<str>,
{
    fn into_param_pair(self) -> (String, String) {
        (self.0.as_ref().to_string(), self.1.as_ref().to_string())
    }
}

#[must_use = "builders are lazy: nothing happens until `.send()` / `.build()`"]
pub struct RequestBuilder {
    pub(super) session: Session,
    pub(super) method: Method,
    pub(super) url: String,
    pub(super) preset: Option<Preset>,
    pub(super) body: Body,
    pub(super) headers: HeaderList,
    pub(super) query_params: Vec<(String, String)>,
    pub(super) timeouts: Option<TimeoutConfig>,
    pub(super) builder_error: Option<Error>,
    pub(super) stream_response: bool,
    pub(super) compress: Option<ContentEncoding>,
    pub(super) retry_policy: RetryPolicy,
    pub(super) digest_auth: Option<DigestAuth>,
    pub(super) proxy: Option<ProxyConfig>,
    pub(super) header_order: Option<Vec<String>>,
    pub(super) preset_user: bool,
    pub(super) redirect: Option<RedirectPolicy>,
    pub(super) initiator: Option<url::Url>,
    pub(crate) trusted_origin: Option<url::Url>,
    pub(super) tag: Option<String>,
    pub(super) status_errors: bool,
}

fn default_preset(session: &Session, method: &Method) -> Option<Preset> {
    if !session.impersonates() {
        return None;
    }
    match *method {
        Method::GET | Method::HEAD => Some(Preset::Navigate),
        _ => None,
    }
}

impl RequestBuilder {
    pub(crate) fn new(session: &Session, method: Method, url: &str) -> Self {
        let preset = default_preset(session, &method);
        Self {
            session: session.clone(),
            method,
            url: url.to_string(),
            preset,
            body: Body::default(),
            headers: HeaderList::new(),
            query_params: Vec::new(),
            timeouts: None,
            builder_error: None,
            stream_response: false,
            compress: None,
            retry_policy: session.default_retry().clone(),
            digest_auth: None,
            proxy: None,
            header_order: None,
            preset_user: false,
            redirect: None,
            initiator: None,
            trusted_origin: None,
            tag: None,
            status_errors: false,
        }
    }

    pub(crate) fn from_url(session: &Session, method: Method, url: impl IntoUrl) -> Self {
        match url.into_url_with_base(session.base_url()) {
            Ok(url) => Self::new(session, method, url.as_str()),
            Err(err) => {
                let mut builder = Self::new(session, method, "");
                builder.builder_error = Some(err);
                builder
            }
        }
    }

    pub(crate) fn fail(&mut self, err: Error) {
        if self.builder_error.is_none() {
            self.builder_error = Some(err);
        }
    }

    fn infer_from_content_type(&mut self) {
        if self.preset_user || !self.session.impersonates() {
            return;
        }
        if !matches!(self.method, Method::POST | Method::PUT | Method::PATCH) {
            return;
        }
        let Some(ct) = self.headers.get("content-type") else {
            return;
        };
        let ct = String::from_utf8_lossy(ct.as_bytes()).to_ascii_lowercase();
        if ct.starts_with("application/json") {
            self.preset = Some(Preset::Xhr);
        } else if ct.starts_with("application/x-www-form-urlencoded") {
            self.preset = Some(Preset::Form);
        }
    }

    pub fn timeout(mut self, config: impl Into<TimeoutConfig>) -> Self {
        self.timeouts = Some(config.into());
        self
    }

    pub fn preset(mut self, preset: Preset) -> Self {
        self.preset = Some(preset);
        self.preset_user = true;
        self
    }

    pub fn header_order(mut self, order: &[&str]) -> Self {
        self.header_order = Some(order.iter().map(|s| (*s).to_string()).collect());
        self
    }

    pub fn body(mut self, body: impl Into<Body>) -> Self {
        self.body = body.into();
        self
    }

    pub fn json(mut self, value: &impl serde::Serialize) -> Self {
        match serde_json::to_vec(value) {
            Ok(bytes) => {
                self.put("content-type", "application/json");
                self.body = Body::from(bytes);
            }
            Err(e) => {
                self.builder_error = Some(Error::new(Kind::Json).with_source(e));
            }
        }
        self
    }

    pub fn form<I, P>(mut self, params: I) -> Self
    where
        I: IntoIterator<Item = P>,
        P: IntoParamPair,
    {
        let pairs = collect_pairs(params);
        let encoded = encode::url_encode_pairs(&pairs);
        self.put("content-type", "application/x-www-form-urlencoded");
        self.body = Body::from(encoded.into_bytes());
        self
    }

    pub fn stream(mut self) -> Self {
        self.stream_response = true;
        self
    }

    pub fn compress(mut self, encoding: ContentEncoding) -> Self {
        self.compress = Some(encoding);
        self
    }

    pub fn query<I, P>(mut self, params: I) -> Self
    where
        I: IntoIterator<Item = P>,
        P: IntoParamPair,
    {
        for pair in params {
            self.query_params.push(pair.into_param_pair());
        }
        self
    }

    pub fn header(
        mut self,
        name: impl TryInto<HeaderName>,
        value: impl TryInto<HeaderValue>,
    ) -> Self {
        if let Err(err) = self.headers.append(name, value) {
            self.fail(err);
        }
        self
    }

    fn put(&mut self, name: impl TryInto<HeaderName>, value: impl TryInto<HeaderValue>) {
        if let Err(err) = self.headers.set(name, value) {
            self.fail(err);
        }
    }

    pub fn headers<I, P>(mut self, headers: I) -> Self
    where
        I: IntoIterator<Item = P>,
        P: IntoParamPair,
    {
        for pair in headers {
            let (k, v) = pair.into_param_pair();
            if let Err(err) = self.headers.append(k, v) {
                self.fail(err);
                return self;
            }
        }
        self
    }

    pub fn anchored(
        mut self,
        anchor: HeaderAnchor,
        name: impl TryInto<HeaderName>,
        value: impl TryInto<HeaderValue>,
    ) -> Self {
        if let Err(err) = self.headers.append_anchored(anchor, name, value) {
            self.fail(err);
        }
        self
    }

    pub fn bearer_auth(mut self, token: &str) -> Self {
        self.put("authorization", format!("Bearer {token}"));
        self
    }

    pub fn basic_auth(mut self, username: &str, password: &str) -> Self {
        let encoded = crate::util::base64_encode(&format!("{username}:{password}"));
        self.put("authorization", format!("Basic {encoded}"));
        self
    }

    pub fn retry(mut self, policy: RetryPolicy) -> Self {
        self.retry_policy = policy;
        self
    }

    pub fn redirect(mut self, policy: RedirectPolicy) -> Self {
        self.redirect = Some(policy);
        self
    }

    pub fn digest_auth(mut self, auth: DigestAuth) -> Self {
        self.digest_auth = Some(auth);
        self
    }

    #[cfg(feature = "multipart")]
    pub fn multipart(mut self, form: Form) -> Self {
        self.put("content-type", form.content_type());
        self.body = form.into_stream_body();
        self
    }

    pub fn proxy(mut self, config: impl Into<ProxyConfig>) -> Self {
        self.proxy = Some(config.into());
        self
    }
}

#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;

fn collect_pairs<I, P>(params: I) -> Vec<(String, String)>
where
    I: IntoIterator<Item = P>,
    P: IntoParamPair,
{
    params
        .into_iter()
        .map(IntoParamPair::into_param_pair)
        .collect()
}