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 tokio::net::TcpStream;

use super::{H1Io, H1PooledError};
use crate::Error;
use crate::h2::client::H2Client;
use crate::h2::config::H2Config;
use crate::pool::connect::tls_info;
use crate::pool::types::PoolKey;
use crate::pool::{Pool, TlsInfo};
use crate::tls::proxy;
use crate::tls::{FingerprintConnector, TlsError, TlsStream};

#[derive(Clone, Copy)]
pub(crate) enum H1Dial<'a> {
    Browser(&'a H2Config),
    Http1Only,
}

pub(super) fn tls_for_scheme(scheme: &str, tls: &TlsInfo) -> Option<TlsInfo> {
    if scheme == "https" {
        Some(tls.clone())
    } else {
        None
    }
}

pub(super) enum Dialed {
    H1(Box<dyn H1Io>, TlsInfo),
    H2(H2Client, TlsInfo),
}

pub(super) async fn open_new(
    pool: &Pool,
    key: &PoolKey,
    connector: &FingerprintConnector,
    dial: H1Dial<'_>,
) -> Result<Dialed, H1PooledError> {
    let (host, port, proxy) = (key.host.as_str(), key.port, key.proxy.as_deref());
    match key.scheme.as_str() {
        "https" => open_tls(pool, key, connector, dial).await,
        "http" => Ok(Dialed::H1(
            dial_plain(connector, host, port, proxy).await?,
            TlsInfo::default(),
        )),
        other => Err(H1PooledError::Config(format!(
            "unsupported URL scheme for HTTP/1.1: {other}"
        ))),
    }
}

async fn open_tls(
    pool: &Pool,
    key: &PoolKey,
    connector: &FingerprintConnector,
    dial: H1Dial<'_>,
) -> Result<Dialed, H1PooledError> {
    let (host, port, proxy) = (key.host.as_str(), key.port, key.proxy.as_deref());
    let tls_stream = match dial {
        H1Dial::Browser(_) => connector.connect(host, port, proxy).await?,
        H1Dial::Http1Only => connector.connect_h1(host, port, proxy).await?,
    };
    let tls = tls_info(&tls_stream);
    if let H1Dial::Browser(h2_config) = dial
        && tls_stream.alpn.as_deref() == Some(crate::tls::alpn::H2)
    {
        return hand_to_h2(pool, key, tls_stream, tls, h2_config).await;
    }
    Ok(Dialed::H1(Box::new(tls_stream.stream), tls))
}

async fn hand_to_h2(
    pool: &Pool,
    key: &PoolKey,
    tls_stream: TlsStream,
    tls: TlsInfo,
    h2_config: &H2Config,
) -> Result<Dialed, H1PooledError> {
    match crate::h2::start(tls_stream.stream, h2_config.clone()).await {
        Ok(handle) => {
            let (handle, tls) = pool.install_h2(key.clone(), handle, tls);
            Ok(Dialed::H2(handle, tls))
        }
        Err(e) => Err(H1PooledError::NotResendable(Error::from(e))),
    }
}

pub(super) async fn dial_plain(
    connector: &FingerprintConnector,
    host: &str,
    port: u16,
    proxy_url: Option<&str>,
) -> Result<Box<dyn H1Io>, H1PooledError> {
    let Some(proxy_url) = proxy_url else {
        let stream = connector
            .with_timeout(connector.dial_tcp(host, port))
            .await?;
        return Ok(Box::new(stream));
    };
    let parsed = url::Url::parse(proxy_url)
        .map_err(|e| H1PooledError::Config(format!("invalid proxy URL: {e}")))?;
    let io: Box<dyn H1Io> = match parsed.scheme() {
        "http" => Box::new(
            connector
                .with_timeout(proxy::connect_to_proxy(connector, &parsed))
                .await
                .map_err(TlsError::into_proxy)?,
        ),
        "https" => Box::new(
            connector
                .with_timeout(proxy::http::open_tls_to_proxy(connector, &parsed))
                .await
                .map_err(TlsError::into_proxy)?
                .stream,
        ),
        "socks5" | "socks5h" => Box::new(socks_tunnel(connector, host, port, &parsed).await?),
        other => {
            return Err(H1PooledError::Config(format!(
                "unsupported proxy scheme `{other}` for http:// URLs"
            )));
        }
    };
    Ok(io)
}

#[cfg(feature = "socks")]
async fn socks_tunnel(
    connector: &FingerprintConnector,
    host: &str,
    port: u16,
    proxy: &url::Url,
) -> Result<TcpStream, H1PooledError> {
    Ok(connector
        .with_timeout(proxy::socks5::tunnel(connector, host, port, proxy))
        .await
        .map_err(TlsError::into_proxy)?)
}

#[cfg(not(feature = "socks"))]
async fn socks_tunnel(
    _connector: &FingerprintConnector,
    _host: &str,
    _port: u16,
    _proxy: &url::Url,
) -> Result<TcpStream, H1PooledError> {
    Err(TlsError::proxy("SOCKS proxy support requires the `socks` feature").into())
}