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::{Duration, Instant};

use leyline_bssl::ssl::SslConnector;
use tokio::net::TcpStream;

use crate::core::SocketConfig;
use crate::core::deadline::{Elapsed, within};
use crate::profile::BrowserProfile;
use crate::tcp::TcpProfile;

use crate::tls::TlsStream;
use crate::tls::builder::{TlsMinVersion, apply_profile_with_trust};
use crate::tls::error::TlsError;
use crate::tls::happy_eyeballs::{HappyEyeballsConfig, happy_eyeballs_connect};
use crate::tls::hello::HelloOptions;
use crate::tls::nonblocking::connect_one;
use crate::tls::resolver::{Resolver, SystemResolver};
use crate::tls::session_cache::SessionCache;
use crate::tls::trust::TlsTrustConfig;
use crate::trace;

mod handshake;

#[derive(Clone)]
pub struct FingerprintConnector {
    ssl_connector: SslConnector,
    tcp_profile: TcpProfile,
    trust: TlsTrustConfig,
    hello: HelloOptions,
    session_cache: SessionCache,
    accept_invalid_certs: std::sync::Arc<std::sync::atomic::AtomicBool>,
    resolver: Arc<dyn Resolver>,
    happy_eyeballs: HappyEyeballsConfig,
    connect_timeout: Option<Duration>,
    socket_config: SocketConfig,
    pins: Vec<[u8; 32]>,
    system_roots: bool,
    has_client_identity: bool,
}

impl FingerprintConnector {
    #[cfg(any(test, feature = "bench-internals"))]
    pub fn new(profile: &BrowserProfile, tcp: TcpProfile) -> Result<Self, TlsError> {
        Self::new_with_trust(profile, tcp, &TlsTrustConfig::default())
    }

    pub fn new_with_trust(
        profile: &BrowserProfile,
        tcp: TcpProfile,
        trust: &TlsTrustConfig,
    ) -> Result<Self, TlsError> {
        let trust = trust.clone();
        let mut builder = SslConnector::bare_builder(leyline_bssl::ssl::SslMethod::tls())
            .map_err(TlsError::from_stack)?;

        let identity =
            apply_profile_with_trust(&mut builder, profile, TlsMinVersion::Tls12, &trust)?;

        let hello = HelloOptions::from_tls(&profile.tls)?;

        builder
            .set_alpn_protos(crate::tls::alpn::BROWSER_WIRE)
            .map_err(TlsError::from_stack)?;

        SessionCache::register(&mut builder)?;
        let accept_invalid_certs = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));

        Ok(Self {
            ssl_connector: builder.build(),
            tcp_profile: tcp,
            hello,
            session_cache: SessionCache::new(identity),
            accept_invalid_certs,
            resolver: Arc::new(SystemResolver),
            happy_eyeballs: HappyEyeballsConfig::default(),
            connect_timeout: None,
            socket_config: SocketConfig::default(),
            pins: trust.pinned_leaf_sha256().to_vec(),
            system_roots: trust.uses_system_roots(),
            has_client_identity: trust.has_client_identity(),
            trust,
        })
    }

    pub(crate) fn with_fresh_session_cache(&self) -> Self {
        Self {
            session_cache: self.session_cache.cleared(),
            ..self.clone()
        }
    }

    pub(crate) fn with_profile(&self, profile: &BrowserProfile) -> Result<Self, TlsError> {
        use std::sync::atomic::Ordering;
        let mut next = Self::new_with_trust(profile, self.tcp_profile.clone(), &self.trust)?;
        next.resolver = self.resolver.clone();
        next.happy_eyeballs = self.happy_eyeballs;
        next.connect_timeout = self.connect_timeout;
        next.socket_config = self.socket_config.clone();
        if self.accept_invalid_certs.load(Ordering::Relaxed) {
            next.set_accept_invalid_certs(true);
        }
        Ok(next)
    }

    pub(crate) fn session_cache(&self) -> &SessionCache {
        &self.session_cache
    }

    #[cfg(feature = "http3")]
    pub(crate) fn resolver(&self) -> &Arc<dyn Resolver> {
        &self.resolver
    }

    pub(crate) fn has_origin_tls_identity(&self) -> bool {
        !self.pins.is_empty() || self.has_client_identity
    }

    pub fn set_accept_invalid_certs(&mut self, accept: bool) {
        use std::sync::atomic::Ordering;
        self.accept_invalid_certs.store(accept, Ordering::Relaxed);
    }

    fn insecure_mode(&self) -> bool {
        use std::sync::atomic::Ordering;
        self.accept_invalid_certs.load(Ordering::Relaxed)
    }

    pub fn with_resolver(mut self, resolver: Arc<dyn Resolver>) -> Self {
        self.resolver = resolver;
        self
    }

    pub fn with_happy_eyeballs_config(mut self, config: HappyEyeballsConfig) -> Self {
        self.happy_eyeballs = config;
        self
    }

    pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
        self.connect_timeout = Some(timeout);
        self
    }

    pub fn with_socket_config(mut self, config: SocketConfig) -> Self {
        self.socket_config = config;
        self
    }

    pub async fn connect(
        &self,
        host: &str,
        port: u16,
        proxy: Option<&str>,
    ) -> Result<TlsStream, TlsError> {
        match proxy {
            Some(proxy_url) => self.connect_via_proxy(host, port, proxy_url, true).await,
            None => {
                self.with_timeout(self.connect_direct_with_alpn(host, port, None))
                    .await
            }
        }
    }

    pub async fn connect_h1(
        &self,
        host: &str,
        port: u16,
        proxy: Option<&str>,
    ) -> Result<TlsStream, TlsError> {
        match proxy {
            Some(proxy_url) => self.connect_via_proxy(host, port, proxy_url, false).await,
            None => {
                self.with_timeout(self.connect_direct_with_alpn(
                    host,
                    port,
                    Some(crate::tls::alpn::HTTP11_WIRE),
                ))
                .await
            }
        }
    }

    async fn connect_via_proxy(
        &self,
        host: &str,
        port: u16,
        proxy_url: &str,
        include_alps: bool,
    ) -> Result<TlsStream, TlsError> {
        let proxy = crate::tls::proxy::parse(proxy_url)?;
        let started = Instant::now();
        let tunnel = self
            .with_timeout(crate::tls::proxy::open_tunnel(self, host, port, &proxy))
            .await
            .map_err(TlsError::into_proxy)?;
        let remaining = self
            .connect_timeout
            .map(|budget| budget.saturating_sub(started.elapsed()));
        let handshake =
            crate::tls::proxy::handshake(self, tunnel, host, port, &proxy, include_alps);
        within(remaining, handshake)
            .await
            .map_err(|Elapsed| connect_timeout())?
    }

    pub(crate) fn tcp_profile(&self) -> &TcpProfile {
        &self.tcp_profile
    }

    pub(crate) async fn with_timeout<T, F>(&self, fut: F) -> Result<T, TlsError>
    where
        F: std::future::Future<Output = Result<T, TlsError>>,
    {
        within(self.connect_timeout, fut)
            .await
            .map_err(|Elapsed| connect_timeout())?
    }

    async fn connect_direct_with_alpn(
        &self,
        host: &str,
        port: u16,
        alpn_override: Option<&[u8]>,
    ) -> Result<TlsStream, TlsError> {
        let tcp_stream = self.dial_tcp(host, port).await?;
        let session_key = SessionCache::key(host, port, None);
        self.tls_handshake(tcp_stream, host, &session_key, alpn_override.is_none())
            .await
    }

    pub(crate) async fn dial_tcp(&self, host: &str, port: u16) -> Result<TcpStream, TlsError> {
        let started = Instant::now();
        let addrs = self
            .resolver
            .resolve(host, port)
            .await
            .map_err(TlsError::Dns)?;
        trace::dns(host, port, addrs.len(), started.elapsed());

        if addrs.is_empty() {
            return Err(TlsError::Dns(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                "no addresses resolved",
            )));
        }

        let tcp_profile = self.tcp_profile.clone();
        let socket_config = self.socket_config.clone();
        let started = Instant::now();
        let (tcp_stream, _addr) =
            happy_eyeballs_connect(addrs, self.happy_eyeballs, move |sock_addr| {
                let socket_config = socket_config.clone();
                let tcp_profile = tcp_profile.clone();
                async move { connect_one(sock_addr, &tcp_profile, &socket_config).await }
            })
            .await
            .map_err(TlsError::TcpConnect)?;
        trace::connect(host, port, false, started.elapsed());
        Ok(tcp_stream)
    }
}

fn connect_timeout() -> TlsError {
    TlsError::TcpConnect(std::io::Error::new(
        std::io::ErrorKind::TimedOut,
        "connect timeout",
    ))
}

impl std::fmt::Debug for FingerprintConnector {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("FingerprintConnector")
            .field("tcp_profile", &self.tcp_profile)
            .field("ech_grease", &self.hello.ech_grease())
            .finish_non_exhaustive()
    }
}