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::io;
use std::net::SocketAddr;

use tokio::net::UdpSocket;

use crate::quic::connection::resolve_peer;
use crate::tls::FingerprintConnector;
#[cfg(feature = "socks")]
use crate::tls::proxy::socks5::udp::Socks5Udp;

pub(crate) enum DatagramTransport {
    Direct(UdpSocket),
    #[cfg(feature = "socks")]
    Socks5(Socks5Udp),
}

impl DatagramTransport {
    pub(crate) async fn open(
        connector: &FingerprintConnector,
        host: &str,
        port: u16,
        proxy: Option<&str>,
    ) -> Result<(Self, SocketAddr), String> {
        match proxy {
            None => Self::open_direct(connector, host, port).await,
            Some(proxy) => {
                let proxy =
                    url::Url::parse(proxy).map_err(|e| format!("invalid proxy URL: {e}"))?;
                if !carries_udp(proxy.scheme()) {
                    return Err(format!(
                        "HTTP/3 cannot use a `{}` proxy: QUIC needs a socks5:// or socks5h:// \
                         proxy with UDP ASSOCIATE",
                        proxy.scheme()
                    ));
                }
                Self::open_socks5(connector, host, port, &proxy).await
            }
        }
    }

    async fn open_direct(
        connector: &FingerprintConnector,
        host: &str,
        port: u16,
    ) -> Result<(Self, SocketAddr), String> {
        let peer_addr = resolve_peer(connector.resolver().as_ref(), host, port).await?;
        let socket = UdpSocket::bind(crate::util::unspecified_for(peer_addr))
            .await
            .map_err(|e| format!("udp bind: {e}"))?;
        socket
            .connect(peer_addr)
            .await
            .map_err(|e| format!("udp connect: {e}"))?;
        Ok((Self::Direct(socket), peer_addr))
    }

    #[cfg(feature = "socks")]
    async fn open_socks5(
        connector: &FingerprintConnector,
        host: &str,
        port: u16,
        proxy: &url::Url,
    ) -> Result<(Self, SocketAddr), String> {
        let (relay, relay_addr) = Socks5Udp::open(connector, proxy, host, port)
            .await
            .map_err(|e| format!("h3 proxy: {e}"))?;
        Ok((Self::Socks5(relay), relay_addr))
    }

    #[cfg(not(feature = "socks"))]
    async fn open_socks5(
        _: &FingerprintConnector,
        _: &str,
        _: u16,
        _: &url::Url,
    ) -> Result<(Self, SocketAddr), String> {
        Err(SOCKS_FEATURE_OFF.to_string())
    }

    pub(crate) fn local_addr(&self) -> io::Result<SocketAddr> {
        match self {
            Self::Direct(socket) => socket.local_addr(),
            #[cfg(feature = "socks")]
            Self::Socks5(relay) => relay.local_addr(),
        }
    }

    pub(crate) async fn send(&self, datagram: &[u8]) -> io::Result<usize> {
        match self {
            Self::Direct(socket) => socket.send(datagram).await,
            #[cfg(feature = "socks")]
            Self::Socks5(relay) => relay.send(datagram).await,
        }
    }

    pub(crate) async fn recv(&self, buf: &mut [u8]) -> io::Result<usize> {
        match self {
            Self::Direct(socket) => socket.recv(buf).await,
            #[cfg(feature = "socks")]
            Self::Socks5(relay) => relay.recv(buf).await,
        }
    }
}

fn carries_udp(proxy_scheme: &str) -> bool {
    matches!(proxy_scheme, "socks5" | "socks5h")
}

const PROXY_NO_UDP: &str = "HTTP/3 needs a socks5:// or socks5h:// proxy with UDP ASSOCIATE; \
                            http and https proxies cannot carry QUIC, use Auto or Http2";

const SOCKS_FEATURE_OFF: &str = "HTTP/3 through a SOCKS5 proxy needs the `socks` feature of \
                                 leyline-http; enable it, or use Auto or Http2";

pub(crate) fn h3_proxy_blocker(proxy: Option<&str>) -> Option<&'static str> {
    let udp = url::Url::parse(proxy?).is_ok_and(|url| carries_udp(url.scheme()));
    match (udp, cfg!(feature = "socks")) {
        (true, true) => None,
        (true, false) => Some(SOCKS_FEATURE_OFF),
        (false, _) => Some(PROXY_NO_UDP),
    }
}

pub(crate) fn proxy_carries_h3(proxy: Option<&str>) -> bool {
    h3_proxy_blocker(proxy).is_none()
}