#![allow(clippy::manual_let_else)]
//! Request smuggling detection.
//! Detects CL.TE, TE.CL, and TE.TE vulnerabilities over HTTP and HTTPS.

use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use url::Url;

/// The type of request smuggling vulnerability.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SmugglingType {
    /// Content-Length vs Transfer-Encoding mismatch.
    ClTe,
    /// Transfer-Encoding vs Content-Length mismatch.
    TeCl,
    /// Duplicate Transfer-Encoding with obfuscation.
    TeTe,
}

/// The result of a request smuggling detection probe.
#[derive(Debug, Clone)]
pub struct SmugglingResult {
    /// Whether the target is vulnerable.
    pub vulnerable: bool,
    /// The type of vulnerability detected.
    pub smuggling_type: SmugglingType,
    /// Evidence of the vulnerability.
    pub evidence: String,
}

/// Detects request smuggling vulnerabilities on the given target.
#[must_use]
pub async fn detect_smuggling(target: &str, timeout_dur: Duration) -> Vec<SmugglingResult> {
    let mut results = Vec::new();

    let url = match Url::parse(target) {
        Ok(u) => u,
        Err(error) => {
            tracing::warn!(target = %target, error = %error, "failed to parse smuggling target URL");
            return results;
        }
    };

    let host = match url.host_str() {
        Some(h) => h,
        None => return results,
    };
    let is_https = url.scheme() == "https";
    let port = url
        .port_or_known_default()
        .unwrap_or(if is_https { 443 } else { 80 });

    // SSRF protection: do not connect to internal addresses
    if let Ok(ip) = host.parse::<std::net::IpAddr>() {
        if crate::http_transport::execute::is_internal_ip(ip) {
            tracing::warn!(target = %target, host = %host, "smuggling detection blocked  -  internal IP");
            return results;
        }
    } else if let Ok(addrs) = tokio::net::lookup_host(format!("{host}:{port}")).await {
        for addr in addrs {
            if crate::http_transport::execute::is_internal_ip(addr.ip()) {
                tracing::warn!(target = %target, host = %host, ip = %addr.ip(), "smuggling detection blocked  -  resolves to internal IP");
                return results;
            }
        }
    }

    if let Some(res) = check_cl_te(host, port, is_https, timeout_dur).await {
        results.push(res);
    }

    if let Some(res) = check_te_cl(host, port, is_https, timeout_dur).await {
        results.push(res);
    }

    if let Some(res) = check_te_te(host, port, is_https, timeout_dur).await {
        results.push(res);
    }

    results
}

async fn check_cl_te(
    host: &str,
    port: u16,
    is_https: bool,
    timeout_dur: Duration,
) -> Option<SmugglingResult> {
    let payload = format!(
        "POST / HTTP/1.1\r\n\
         Host: {host}\r\n\
         Content-Length: 4\r\n\
         Transfer-Encoding: chunked\r\n\
         \r\n\
         1\r\n\
         Z\r\n\
         Q\r\n"
    );

    send_payload(host, port, is_https, &payload, timeout_dur)
        .await
        .map(|()| SmugglingResult {
            vulnerable: true,
            smuggling_type: SmugglingType::ClTe,
            evidence: "CL.TE vulnerability detected via timeout".to_string(),
        })
}

async fn check_te_cl(
    host: &str,
    port: u16,
    is_https: bool,
    timeout_dur: Duration,
) -> Option<SmugglingResult> {
    let payload = format!(
        "POST / HTTP/1.1\r\n\
         Host: {host}\r\n\
         Content-Length: 6\r\n\
         Transfer-Encoding: chunked\r\n\
         \r\n\
         0\r\n\
         \r\n\
         X"
    );

    send_payload(host, port, is_https, &payload, timeout_dur)
        .await
        .map(|()| SmugglingResult {
            vulnerable: true,
            smuggling_type: SmugglingType::TeCl,
            evidence: "TE.CL vulnerability detected via timeout".to_string(),
        })
}

async fn check_te_te(
    host: &str,
    port: u16,
    is_https: bool,
    timeout_dur: Duration,
) -> Option<SmugglingResult> {
    let payload = format!(
        "POST / HTTP/1.1\r\n\
         Host: {host}\r\n\
         Content-Length: 4\r\n\
         Transfer-Encoding: chunked\r\n\
         Transfer-encoding: cow\r\n\
         \r\n\
         1\r\n\
         Z\r\n\
         Q\r\n"
    );

    send_payload(host, port, is_https, &payload, timeout_dur)
        .await
        .map(|()| SmugglingResult {
            vulnerable: true,
            smuggling_type: SmugglingType::TeTe,
            evidence: "TE.TE vulnerability detected via timeout".to_string(),
        })
}

/// Returns whether the smuggling module supports HTTPS targets.
#[must_use]
pub fn supports_https() -> bool {
    true
}

#[derive(Debug)]
struct NoCertificateVerification;

impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
    fn verify_server_cert(
        &self,
        _end_entity: &rustls::pki_types::CertificateDer<'_>,
        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
        _server_name: &rustls::pki_types::ServerName<'_>,
        _ocsp_response: &[u8],
        _now: rustls::pki_types::UnixTime,
    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
        Ok(rustls::client::danger::ServerCertVerified::assertion())
    }

    fn verify_tls12_signature(
        &self,
        _message: &[u8],
        _cert: &rustls::pki_types::CertificateDer<'_>,
        _dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
    }

    fn verify_tls13_signature(
        &self,
        _message: &[u8],
        _cert: &rustls::pki_types::CertificateDer<'_>,
        _dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
    }

    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        vec![
            rustls::SignatureScheme::RSA_PKCS1_SHA1,
            rustls::SignatureScheme::ECDSA_SHA1_Legacy,
            rustls::SignatureScheme::RSA_PKCS1_SHA256,
            rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
            rustls::SignatureScheme::RSA_PKCS1_SHA384,
            rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
            rustls::SignatureScheme::RSA_PKCS1_SHA512,
            rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
            rustls::SignatureScheme::RSA_PSS_SHA256,
            rustls::SignatureScheme::RSA_PSS_SHA384,
            rustls::SignatureScheme::RSA_PSS_SHA512,
            rustls::SignatureScheme::ED25519,
            rustls::SignatureScheme::ED448,
        ]
    }
}

async fn send_payload(
    host: &str,
    port: u16,
    is_https: bool,
    payload: &str,
    timeout_dur: Duration,
) -> Option<()> {
    if let Ok(addrs) = tokio::net::lookup_host((host, port)).await {
        for addr in addrs {
            if crate::http_transport::execute::is_internal_ip(addr.ip()) {
                return None;
            }
        }
    } else {
        return None;
    }

    let addr = format!("{host}:{port}");

    let stream = match tokio::time::timeout(timeout_dur, TcpStream::connect(&addr)).await {
        Ok(Ok(s)) => s,
        _ => return None,
    };

    if is_https {
        let mut root_store = rustls::RootCertStore::empty();
        root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
        let mut config = rustls::ClientConfig::builder()
            .with_root_certificates(root_store)
            .with_no_client_auth();
        config
            .dangerous()
            .set_certificate_verifier(Arc::new(NoCertificateVerification));

        let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
        let domain = match rustls::pki_types::ServerName::try_from(host) {
            Ok(d) => d.to_owned(),
            Err(_) => return None,
        };

        let mut tls_stream =
            match tokio::time::timeout(timeout_dur, connector.connect(domain, stream)).await {
                Ok(Ok(s)) => s,
                _ => return None,
            };

        if tls_stream.write_all(payload.as_bytes()).await.is_err() {
            return None;
        }

        let mut buffer = [0; 1024];
        match tokio::time::timeout(timeout_dur, tls_stream.read(&mut buffer)).await {
            Ok(Ok(n)) if n > 0 => None,
            Ok(_) => None,
            Err(error) => {
                tracing::warn!(host = %host, port = port, error = %error, "smuggling probe timed out over TLS");
                Some(())
            }
        }
    } else {
        let mut stream = stream;
        if stream.write_all(payload.as_bytes()).await.is_err() {
            return None;
        }

        let mut buffer = [0; 1024];
        match tokio::time::timeout(timeout_dur, stream.read(&mut buffer)).await {
            Ok(Ok(n)) if n > 0 => None,
            Ok(_) => None,
            Err(error) => {
                tracing::warn!(host = %host, port = port, error = %error, "smuggling probe timed out over TCP");
                Some(())
            }
        }
    }
}
