leyline-http 0.1.1

HTTP client that mimics browsers on the wire
Documentation
#![forbid(unsafe_code)]
use tokio::net::TcpStream;

use crate::tls::error::TlsError;
use crate::tls::{SessionCache, TlsIo, TlsStream};

pub(crate) mod http;
#[cfg(feature = "socks")]
pub(crate) mod socks5;

const SOCKS5_DEFAULT_PORT: u16 = 1080;

pub(crate) async fn connect_to_proxy<C: crate::tls::TlsHandshake>(
    connector: &C,
    proxy: &url::Url,
) -> Result<TcpStream, TlsError> {
    let host = proxy
        .host_str()
        .ok_or_else(|| TlsError::proxy(format!("{} proxy has no host", proxy.scheme())))?;
    let port = proxy.port_or_known_default().unwrap_or(SOCKS5_DEFAULT_PORT);
    connector
        .dial_tcp(host, port)
        .await
        .map_err(TlsError::into_proxy)
}

pub(crate) enum Tunnel {
    Plain(TcpStream),
    Tls(TlsIo),
}

pub(crate) fn parse(proxy_url: &str) -> Result<url::Url, TlsError> {
    url::Url::parse(proxy_url).map_err(|e| TlsError::proxy(format!("invalid proxy URL: {e}")))
}

pub(crate) async fn open_tunnel<C: crate::tls::TlsHandshake>(
    connector: &C,
    host: &str,
    port: u16,
    proxy: &url::Url,
) -> Result<Tunnel, TlsError> {
    match proxy.scheme() {
        "socks5" | "socks5h" => {
            #[cfg(feature = "socks")]
            return socks5::tunnel(connector, host, port, proxy)
                .await
                .map(Tunnel::Plain);
            #[cfg(not(feature = "socks"))]
            return Err(TlsError::proxy(
                "SOCKS proxy support requires the `socks` feature",
            ));
        }
        "http" => http::tunnel(connector, host, port, proxy)
            .await
            .map(Tunnel::Plain),
        "https" => http::tunnel_via_tls(connector, host, port, proxy)
            .await
            .map(Tunnel::Tls),
        other => Err(TlsError::proxy(format!(
            "unsupported proxy scheme `{other}`: use an http, https, socks5, or socks5h proxy URL"
        ))),
    }
}

pub(crate) async fn handshake<C: crate::tls::TlsHandshake>(
    connector: &C,
    tunnel: Tunnel,
    host: &str,
    port: u16,
    proxy: &url::Url,
    include_alps: bool,
) -> Result<TlsStream, TlsError> {
    let session_key = SessionCache::key(host, port, Some(proxy));
    match tunnel {
        Tunnel::Plain(tcp) => {
            connector
                .do_tls_handshake(tcp, host, &session_key, include_alps)
                .await
        }
        Tunnel::Tls(io) => {
            connector
                .do_tls_handshake_nested(io, host, &session_key, include_alps)
                .await
        }
    }
}

#[cfg(test)]
mod tests;