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 leyline_bssl::ssl::ErrorCode;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProxyReply {
    HttpStatus(u16),
    Socks5(u8),
}

#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TlsError {
    #[error("ssl config: {0}")]
    SslConfig(String),

    #[error("tls profile: {0}")]
    Profile(String),

    #[error("trust store: {0}")]
    TrustStore(String),

    #[error("tls handshake: {0}")]
    Handshake(String),

    #[error("tls handshake closed or reset by peer")]
    Rejected(#[source] io::Error),

    #[error("tls handshake io")]
    HandshakeIo(#[source] io::Error),

    #[error("certificate verification failed: {detail}")]
    Certificate {
        verify_code: Option<i32>,
        reason: Option<&'static str>,
        detail: String,
    },

    #[error("hostname: {0}")]
    Hostname(String),

    #[error("certificate pin: {0}")]
    Pinning(String),

    #[error("tcp connect")]
    TcpConnect(#[source] io::Error),

    #[error("dns resolution")]
    Dns(#[source] io::Error),

    #[error("proxy: {detail}")]
    Proxy {
        status: Option<u16>,
        detail: String,
        #[source]
        source: Option<io::Error>,
    },

    #[error("proxy could not reach the target: {detail}")]
    ProxyTargetUnreachable { reply: ProxyReply, detail: String },
}

fn peer_closed(kind: io::ErrorKind) -> bool {
    matches!(
        kind,
        io::ErrorKind::UnexpectedEof
            | io::ErrorKind::ConnectionReset
            | io::ErrorKind::ConnectionAborted
            | io::ErrorKind::BrokenPipe
    )
}

impl TlsError {
    pub(crate) fn from_handshake<S>(e: &leyline_bssl_tokio::HandshakeError<S>) -> Self {
        if let Some(io) = e.as_io_error() {
            let copy = io::Error::new(io.kind(), io.to_string());
            if peer_closed(io.kind()) {
                return Self::Rejected(copy);
            }
            return Self::HandshakeIo(copy);
        }
        let msg = e.to_string();
        if e.code() == Some(ErrorCode::SYSCALL) {
            Self::Rejected(io::Error::new(io::ErrorKind::UnexpectedEof, msg))
        } else {
            Self::Handshake(msg)
        }
    }

    pub(crate) fn from_stack(e: leyline_bssl::error::ErrorStack) -> Self {
        Self::SslConfig(e.to_string())
    }

    pub(crate) fn proxy(detail: impl Into<String>) -> Self {
        Self::Proxy {
            status: None,
            detail: detail.into(),
            source: None,
        }
    }

    pub(crate) fn proxy_io(source: io::Error) -> Self {
        Self::Proxy {
            status: None,
            detail: "proxy connection failed".into(),
            source: Some(source),
        }
    }

    pub(crate) fn proxy_target(reply: ProxyReply, detail: impl Into<String>) -> Self {
        Self::ProxyTargetUnreachable {
            reply,
            detail: detail.into(),
        }
    }

    pub(crate) fn into_proxy(self) -> Self {
        match self {
            Self::Proxy { .. } | Self::ProxyTargetUnreachable { .. } => self,
            Self::Dns(e) | Self::TcpConnect(e) | Self::HandshakeIo(e) | Self::Rejected(e) => {
                Self::proxy_io(e)
            }
            other => Self::proxy(other.to_string()),
        }
    }

    pub(crate) fn is_handshake(&self) -> bool {
        matches!(
            self,
            Self::Handshake(_) | Self::HandshakeIo(_) | Self::Rejected(_)
        )
    }

    pub(crate) fn is_connect_phase(&self) -> bool {
        matches!(self, Self::Dns(_) | Self::TcpConnect(_)) || self.is_handshake()
    }

    pub(crate) fn io_source(&self) -> Option<&io::Error> {
        match self {
            Self::TcpConnect(e) | Self::Dns(e) | Self::HandshakeIo(e) | Self::Rejected(e) => {
                Some(e)
            }
            Self::Proxy { source, .. } => source.as_ref(),
            _ => None,
        }
    }

    pub(crate) fn duplicate(&self) -> Self {
        let io = |e: &io::Error| io::Error::new(e.kind(), e.to_string());
        match self {
            Self::SslConfig(m) => Self::SslConfig(m.clone()),
            Self::Profile(m) => Self::Profile(m.clone()),
            Self::TrustStore(m) => Self::TrustStore(m.clone()),
            Self::Handshake(m) => Self::Handshake(m.clone()),
            Self::Rejected(e) => Self::Rejected(io(e)),
            Self::HandshakeIo(e) => Self::HandshakeIo(io(e)),
            Self::Certificate {
                verify_code,
                reason,
                detail,
            } => Self::Certificate {
                verify_code: *verify_code,
                reason: *reason,
                detail: detail.clone(),
            },
            Self::Hostname(m) => Self::Hostname(m.clone()),
            Self::Pinning(m) => Self::Pinning(m.clone()),
            Self::TcpConnect(e) => Self::TcpConnect(io(e)),
            Self::Dns(e) => Self::Dns(io(e)),
            Self::Proxy {
                status,
                detail,
                source,
            } => Self::Proxy {
                status: *status,
                detail: detail.clone(),
                source: source.as_ref().map(io),
            },
            Self::ProxyTargetUnreachable { reply, detail } => Self::ProxyTargetUnreachable {
                reply: *reply,
                detail: detail.clone(),
            },
        }
    }
}