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 std::time::SystemTime;

use url::Url;

use super::super::decompress::{decompress_and_strip, drain_stream_into_vec};
use super::journey::Journey;
use crate::core::Session;
use crate::core::deadline::Deadline;
use crate::core::device::{SessionState, StateParts, unix_secs};
use crate::core::error::Result;
use crate::core::response::Response;
use crate::core::transport::{ResponseMode, TransportResponse};
use crate::util::lock;

impl Session {
    pub(super) fn store_cookies(
        &self,
        resp_headers: &[(http::HeaderName, http::HeaderValue)],
        url: &Url,
    ) {
        let set_cookies: Vec<&str> = resp_headers
            .iter()
            .filter(|(k, _)| *k == "set-cookie")
            .filter_map(|(_, v)| v.to_str().ok())
            .collect();
        if !set_cookies.is_empty() {
            self.inner
                .cookie_jar
                .store_response_cookies(set_cookies.as_slice(), url);
        }
    }

    #[must_use]
    pub fn state(&self) -> SessionState {
        let now = SystemTime::now();
        SessionState::from_parts(StateParts {
            tls_sessions: self.inner.connector.session_cache().export(unix_secs(now)),
            #[cfg(feature = "http3")]
            alt_svc: self.inner.pool.alt_svc_entries(),
            #[cfg(not(feature = "http3"))]
            alt_svc: Vec::new(),
            hsts: lock(&self.inner.pool.hsts).export(now),
        })
    }

    pub(crate) fn restore_state(&self, parts: &StateParts) {
        let now = SystemTime::now();
        self.inner
            .connector
            .session_cache()
            .import(&parts.tls_sessions, unix_secs(now));
        #[cfg(feature = "http3")]
        self.inner.pool.restore_alt_svc(&parts.alt_svc);
        lock(&self.inner.pool.hsts).import(&parts.hsts, now);
    }

    pub(super) fn note_hsts(
        &self,
        url: &Url,
        resp_headers: &[(http::HeaderName, http::HeaderValue)],
    ) {
        if self.inner.tls_trust.accepts_invalid_certs() {
            return;
        }
        let values: Vec<&str> = resp_headers
            .iter()
            .filter(|(k, _)| *k == "strict-transport-security")
            .filter_map(|(_, v)| v.to_str().ok())
            .collect();
        if !values.is_empty() {
            lock(&self.inner.pool.hsts).note(url, &values, SystemTime::now());
        }
    }

    pub(in crate::core::session) fn hsts_upgrade(&self, url: &Arc<Url>) -> Arc<Url> {
        lock(&self.inner.pool.hsts)
            .upgrade(url, SystemTime::now())
            .map_or_else(|| Arc::clone(url), Arc::new)
    }

    #[cfg(feature = "http3")]
    pub(super) fn note_alt_svc(
        &self,
        url: &Url,
        resp_headers: &[(http::HeaderName, http::HeaderValue)],
    ) {
        let owned: Vec<String> = resp_headers
            .iter()
            .filter(|(k, _)| *k == "alt-svc")
            .map(|(_, v)| String::from_utf8_lossy(v.as_bytes()).into_owned())
            .collect();
        let fields: Vec<&str> = owned.iter().map(String::as_str).collect();
        if fields.is_empty() {
            return;
        }
        let age = resp_headers
            .iter()
            .find(|(k, _)| *k == "age")
            .and_then(|(_, v)| v.to_str().ok())
            .and_then(|v| crate::util::delta_seconds(v.split(',').next().unwrap_or_default()))
            .map_or(std::time::Duration::ZERO, std::time::Duration::from_secs);
        if let Some(host) = url.host_str()
            && let Some(port) = url.port_or_known_default()
        {
            self.inner.pool.note_alt_svc(host, port, &fields, age);
        }
    }

    pub(super) async fn assemble_response(
        &self,
        leg: TransportResponse,
        journey: Journey,
        audit_headers: Vec<(String, String)>,
        response: ResponseMode,
        deadline: &Deadline,
    ) -> Result<Response> {
        let TransportResponse {
            status,
            headers,
            trailers,
            body,
            final_url,
            version,
            tls,
            ..
        } = leg;
        let (body, headers) = self
            .finalize_response_body(
                body,
                headers,
                response.keeps_stream(status.as_u16()),
                deadline,
            )
            .await?;
        let audited = self.inner.audit_tls.is_some();
        let mut audit_headers = audit_headers;
        audit_headers.retain(|(name, value)| crate::core::transport::sent_on(version, name, value));
        Ok(Response {
            status,
            headers: headers.into_iter().collect(),
            body,
            url: final_url,
            redirect_chain: journey.chain,
            version,
            trailers,
            request_headers: audit_headers,
            tls,
            request_method: if audited {
                journey.method
            } else {
                String::new()
            },
            audit_tls: self.inner.audit_tls.clone(),
            audit_cache: std::sync::OnceLock::new(),
            compression: self.inner.compression,
            timing: journey.timing,
            attempts: 1,
            proxy: None,
        })
    }

    pub(super) async fn finalize_response_body(
        &self,
        resp_body_shape: crate::core::transport::TransportBody,
        resp_headers: Vec<(http::HeaderName, http::HeaderValue)>,
        keep_stream: bool,
        deadline: &Deadline,
    ) -> Result<(
        crate::core::response::ResponseBody,
        Vec<(http::HeaderName, http::HeaderValue)>,
    )> {
        Ok(match resp_body_shape {
            crate::core::transport::TransportBody::Streaming(mut bs) if keep_stream => {
                bs.set_read_timeout(deadline.read());
                bs.set_body_timeout(deadline.body());
                bs.stop_on(&self.inner.shutdown);
                bs.watch_end();
                (
                    crate::core::response::ResponseBody::Streaming(bs),
                    resp_headers,
                )
            }
            crate::core::transport::TransportBody::Streaming(mut bs) => {
                bs.set_read_timeout(deadline.read());
                bs.set_body_timeout(deadline.body());
                let buf = drain_stream_into_vec(bs, self.inner.compression.max_body_size).await?;
                let (buf, resp_headers) =
                    decompress_and_strip(buf, resp_headers, &self.inner.compression)?;
                (
                    crate::core::response::ResponseBody::Buffered(buf),
                    resp_headers,
                )
            }
            crate::core::transport::TransportBody::Buffered(buf) => {
                if keep_stream {
                    let mut bs =
                        crate::core::body_stream::BodyStream::from_bytes(bytes::Bytes::from(buf));
                    bs.stop_on(&self.inner.shutdown);
                    bs.watch_end();
                    (
                        crate::core::response::ResponseBody::Streaming(bs),
                        resp_headers,
                    )
                } else {
                    let (buf, resp_headers) =
                        decompress_and_strip(buf, resp_headers, &self.inner.compression)?;
                    (
                        crate::core::response::ResponseBody::Buffered(buf),
                        resp_headers,
                    )
                }
            }
        })
    }
}