//! SSRF deny-list — the load-bearing security mechanism behind the
//! v1.1.4 `http::*` SDK.
//!
//! The policy is applied to the **resolved IP address**, not the
//! hostname. That is the DNS-rebinding defense: a hostname that
//! resolves to a public IP at lookup time and a private IP at connect
//! time is not exploitable, because reqwest re-runs every connection
//! (including post-redirect hops) through [`SsrfResolver`], which
//! filters the address list before the socket is opened.
//!
//! [`SsrfPolicy::check`] returns a CIDR-*category* reason on denial
//! (e.g. `"loopback"`, `"private"`) — never the IP itself, so the
//! script-visible error can't be used to map the internal network.
//!
//! `PICLOUD_HTTP_ALLOW_PRIVATE=true` flips `allow_private`, which
//! short-circuits every check to allow. That is dev/test-only and the
//! binary logs a startup warning when it's set.

use std::future::Future;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::pin::Pin;
use std::sync::Arc;

use reqwest::dns::{Addrs, Name, Resolve, Resolving};

/// Decision policy for a single resolved IP. Cheap to clone (one bool).
#[derive(Debug, Clone, Copy)]
pub struct SsrfPolicy {
    /// When true, every address is allowed — the entire deny-list is
    /// disabled. Set from `PICLOUD_HTTP_ALLOW_PRIVATE`. Dev/test only.
    pub allow_private: bool,
}

impl SsrfPolicy {
    #[must_use]
    pub const fn new(allow_private: bool) -> Self {
        Self { allow_private }
    }

    /// `Ok(())` if the IP may be connected to; `Err(reason)` with a
    /// CIDR-category label otherwise. The reason is safe to surface to
    /// a script — it never contains the address.
    ///
    /// # Errors
    ///
    /// Returns the deny reason when `ip` falls in a blocked range and
    /// `allow_private` is false.
    pub fn check(&self, ip: IpAddr) -> Result<(), &'static str> {
        if self.allow_private {
            return Ok(());
        }
        match ip {
            IpAddr::V4(v4) => check_v4(v4),
            IpAddr::V6(v6) => check_v6(v6),
        }
    }

    #[must_use]
    pub fn is_allowed(&self, ip: IpAddr) -> bool {
        self.check(ip).is_ok()
    }
}

/// IPv4 deny-list. Order doesn't matter (ranges are disjoint by
/// construction); first match wins for the reason label.
// Several arms share a reason ("private") for distinct CIDRs — keeping
// them separate documents each blocked range explicitly.
#[allow(clippy::match_same_arms)]
fn check_v4(ip: Ipv4Addr) -> Result<(), &'static str> {
    let o = ip.octets();
    match o {
        [127, ..] => Err("loopback"),
        [0, ..] => Err("unspecified"), // 0.0.0.0/8 "this network"
        [10, ..] => Err("private"),
        [172, b, ..] if (16..=31).contains(&b) => Err("private"),
        [192, 168, ..] => Err("private"),
        [169, 254, ..] => Err("link-local"), // includes cloud metadata 169.254.169.254
        [100, b, ..] if (64..=127).contains(&b) => Err("carrier-grade-nat"),
        [224..=239, ..] => Err("multicast"),
        [240..=255, ..] => Err("reserved"),
        _ => Ok(()),
    }
}

/// IPv6 deny-list. IPv4-mapped addresses (`::ffff:0:0/96`) re-run the
/// v4 deny-list against the embedded address.
fn check_v6(ip: Ipv6Addr) -> Result<(), &'static str> {
    // IPv4-mapped (::ffff:a.b.c.d) — re-check the embedded v4 address
    // so a mapped private/loopback address can't sneak through.
    if let Some(v4) = ip.to_ipv4_mapped() {
        return check_v4(v4);
    }
    if ip == Ipv6Addr::LOCALHOST {
        return Err("loopback");
    }
    if ip == Ipv6Addr::UNSPECIFIED {
        return Err("unspecified");
    }
    let seg0 = ip.segments()[0];
    if seg0 & 0xffc0 == 0xfe80 {
        return Err("link-local"); // fe80::/10
    }
    if seg0 & 0xfe00 == 0xfc00 {
        return Err("unique-local"); // fc00::/7
    }
    if seg0 & 0xff00 == 0xff00 {
        return Err("multicast"); // ff00::/8
    }
    Ok(())
}

/// Marker error returned by the resolver when *every* resolved address
/// for a host was denied. reqwest wraps this into a connect error; the
/// `http_service` impl walks the source chain for the
/// `"blocked by SSRF policy:"` prefix to surface a clean
/// [`crate::http_service::HttpError::Ssrf`] instead of a generic DNS
/// failure. Keeping the reason a category label means no IP leaks.
#[derive(Debug)]
struct SsrfBlocked {
    reason: &'static str,
}

impl std::fmt::Display for SsrfBlocked {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "blocked by SSRF policy: {}", self.reason)
    }
}

impl std::error::Error for SsrfBlocked {}

/// Prefix the resolver embeds in its error and the impl scans for.
pub const SSRF_BLOCK_PREFIX: &str = "blocked by SSRF policy: ";

/// Pluggable host→addresses lookup. Production uses the system
/// resolver; tests inject a closure (e.g. to simulate DNS rebinding —
/// a different address on a later call).
pub type LookupFn = Arc<
    dyn Fn(String) -> Pin<Box<dyn Future<Output = std::io::Result<Vec<SocketAddr>>> + Send>>
        + Send
        + Sync,
>;

fn system_lookup(
    host: String,
) -> Pin<Box<dyn Future<Output = std::io::Result<Vec<SocketAddr>>> + Send>> {
    Box::pin(async move {
        // Port 0 — reqwest overrides it with the real target port.
        Ok(tokio::net::lookup_host((host.as_str(), 0u16))
            .await?
            .collect())
    })
}

/// reqwest DNS resolver that delegates to the system resolver, then
/// filters the address list through [`SsrfPolicy`]. Plugged in via
/// `ClientBuilder::dns_resolver`, so it runs at the actual connection
/// point — including for every redirect hop. This is the DNS-rebinding
/// defense: filtering happens at connect time, not at URL-parse time.
#[derive(Clone)]
pub struct SsrfResolver {
    policy: SsrfPolicy,
    lookup: LookupFn,
}

impl SsrfResolver {
    #[must_use]
    pub fn new(policy: SsrfPolicy) -> Self {
        Self {
            policy,
            lookup: Arc::new(system_lookup),
        }
    }

    /// Construct with an injected lookup (tests only).
    #[must_use]
    pub fn with_lookup(policy: SsrfPolicy, lookup: LookupFn) -> Self {
        Self { policy, lookup }
    }
}

impl Resolve for SsrfResolver {
    fn resolve(&self, name: Name) -> Resolving {
        let policy = self.policy;
        let lookup = self.lookup.clone();
        let host = name.as_str().to_string();
        Box::pin(async move {
            let resolved: Vec<SocketAddr> = lookup(host)
                .await
                .map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) })?;

            // Empty resolution → genuine DNS miss; let reqwest surface
            // it as a normal "no addresses" error.
            if resolved.is_empty() {
                let addrs: Addrs = Box::new(std::iter::empty());
                return Ok(addrs);
            }

            let mut allowed: Vec<SocketAddr> = Vec::with_capacity(resolved.len());
            let mut last_reason: &'static str = "denied";
            for sa in resolved {
                match policy.check(sa.ip()) {
                    Ok(()) => allowed.push(sa),
                    Err(reason) => last_reason = reason,
                }
            }

            // Resolution returned addresses but the policy denied them
            // all → fail with the SSRF marker so the impl can report a
            // policy block (not a generic DNS error).
            if allowed.is_empty() {
                let err: Box<dyn std::error::Error + Send + Sync> = Box::new(SsrfBlocked {
                    reason: last_reason,
                });
                return Err(err);
            }

            let addrs: Addrs = Box::new(allowed.into_iter());
            Ok(addrs)
        })
    }
}

/// Build the resolver. reqwest's `dns_resolver` is generic over a
/// concrete `R: Resolve` (it stores `Arc<R>`), so this returns the
/// concrete `Arc<SsrfResolver>` rather than a trait object.
#[must_use]
pub fn resolver(policy: SsrfPolicy) -> Arc<SsrfResolver> {
    Arc::new(SsrfResolver::new(policy))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::str::FromStr;

    fn denied(ip: &str) -> &'static str {
        let policy = SsrfPolicy::new(false);
        policy
            .check(IpAddr::from_str(ip).unwrap())
            .expect_err(&format!("{ip} should be denied"))
    }

    fn allowed(ip: &str) {
        let policy = SsrfPolicy::new(false);
        policy
            .check(IpAddr::from_str(ip).unwrap())
            .unwrap_or_else(|r| panic!("{ip} should be allowed, denied as {r}"));
    }

    #[test]
    fn denies_ipv4_loopback() {
        assert_eq!(denied("127.0.0.1"), "loopback");
        assert_eq!(denied("127.1.2.3"), "loopback");
    }

    #[test]
    fn denies_ipv4_unspecified() {
        assert_eq!(denied("0.0.0.0"), "unspecified");
    }

    #[test]
    fn denies_rfc1918_private() {
        assert_eq!(denied("10.0.0.1"), "private");
        assert_eq!(denied("10.255.255.255"), "private");
        assert_eq!(denied("172.16.0.1"), "private");
        assert_eq!(denied("172.31.255.255"), "private");
        assert_eq!(denied("192.168.0.1"), "private");
    }

    #[test]
    fn allows_172_outside_private_range() {
        // 172.15.x and 172.32.x are public — only 172.16.0.0/12 is private.
        allowed("172.15.0.1");
        allowed("172.32.0.1");
    }

    #[test]
    fn denies_link_local_and_cloud_metadata() {
        assert_eq!(denied("169.254.0.1"), "link-local");
        // The cloud metadata endpoint is the canonical SSRF target.
        assert_eq!(denied("169.254.169.254"), "link-local");
    }

    #[test]
    fn denies_carrier_grade_nat() {
        assert_eq!(denied("100.64.0.1"), "carrier-grade-nat");
        assert_eq!(denied("100.127.255.255"), "carrier-grade-nat");
        // 100.63.x and 100.128.x are outside 100.64.0.0/10.
        allowed("100.63.0.1");
        allowed("100.128.0.1");
    }

    #[test]
    fn denies_multicast_and_reserved() {
        assert_eq!(denied("224.0.0.1"), "multicast");
        assert_eq!(denied("239.255.255.255"), "multicast");
        assert_eq!(denied("240.0.0.1"), "reserved");
        assert_eq!(denied("255.255.255.255"), "reserved");
    }

    #[test]
    fn allows_public_ipv4() {
        allowed("1.1.1.1");
        allowed("8.8.8.8");
        allowed("93.184.216.34"); // example.com
    }

    #[test]
    fn denies_ipv6_loopback() {
        assert_eq!(denied("::1"), "loopback");
    }

    #[test]
    fn denies_ipv6_unspecified() {
        assert_eq!(denied("::"), "unspecified");
    }

    #[test]
    fn denies_ipv6_link_local() {
        assert_eq!(denied("fe80::1"), "link-local");
        assert_eq!(denied("febf:ffff::1"), "link-local");
    }

    #[test]
    fn denies_ipv6_unique_local() {
        assert_eq!(denied("fc00::1"), "unique-local");
        assert_eq!(denied("fd12:3456::1"), "unique-local");
    }

    #[test]
    fn denies_ipv6_multicast() {
        assert_eq!(denied("ff00::1"), "multicast");
        assert_eq!(denied("ff02::1"), "multicast");
    }

    #[test]
    fn allows_public_ipv6() {
        allowed("2606:4700:4700::1111"); // cloudflare
        allowed("2001:4860:4860::8888"); // google
    }

    #[test]
    fn ipv4_mapped_ipv6_rechecks_embedded_address() {
        // ::ffff:127.0.0.1 must be denied via the embedded-v4 re-check.
        assert_eq!(denied("::ffff:127.0.0.1"), "loopback");
        assert_eq!(denied("::ffff:10.0.0.1"), "private");
        assert_eq!(denied("::ffff:169.254.169.254"), "link-local");
        // A mapped *public* address stays allowed.
        allowed("::ffff:1.1.1.1");
    }

    #[test]
    fn allow_private_disables_all_denials() {
        let policy = SsrfPolicy::new(true);
        for ip in ["127.0.0.1", "10.0.0.1", "169.254.169.254", "::1", "fe80::1"] {
            assert!(policy.is_allowed(IpAddr::from_str(ip).unwrap()));
        }
    }

    // --- resolver-path tests (the connect-time filter) ---

    use std::sync::atomic::{AtomicUsize, Ordering};

    fn name(s: &str) -> Name {
        Name::from_str(s).unwrap()
    }

    fn fixed_lookup(addrs: Vec<SocketAddr>) -> LookupFn {
        Arc::new(move |_host| {
            let addrs = addrs.clone();
            Box::pin(async move { Ok(addrs) })
        })
    }

    #[tokio::test]
    async fn resolver_returns_only_allowed_addresses() {
        // A host resolving to one public + one private IP yields only
        // the public one to reqwest.
        let public: SocketAddr = "1.1.1.1:0".parse().unwrap();
        let private: SocketAddr = "10.0.0.1:0".parse().unwrap();
        let resolver =
            SsrfResolver::with_lookup(SsrfPolicy::new(false), fixed_lookup(vec![public, private]));
        let got: Vec<SocketAddr> = resolver
            .resolve(name("mixed.example"))
            .await
            .unwrap()
            .collect();
        assert_eq!(got, vec![public]);
    }

    #[tokio::test]
    async fn resolver_all_denied_fails_with_ssrf_marker() {
        // A host resolving to ONLY private IPs fails with the SSRF
        // marker (not a generic empty/DNS result).
        let resolver = SsrfResolver::with_lookup(
            SsrfPolicy::new(false),
            fixed_lookup(vec![
                "10.0.0.1:0".parse().unwrap(),
                "127.0.0.1:0".parse().unwrap(),
            ]),
        );
        let Err(err) = resolver.resolve(name("internal.example")).await else {
            panic!("all-denied resolution should error");
        };
        assert!(
            err.to_string().starts_with(SSRF_BLOCK_PREFIX),
            "expected SSRF marker, got: {err}"
        );
    }

    #[tokio::test]
    async fn resolver_dns_rebinding_second_resolution_denied() {
        // Simulate rebinding: public IP on the first lookup, private on
        // the second. The connect-time filter denies the second.
        let calls = Arc::new(AtomicUsize::new(0));
        let calls2 = calls.clone();
        let lookup: LookupFn = Arc::new(move |_host| {
            let n = calls2.fetch_add(1, Ordering::SeqCst);
            Box::pin(async move {
                let addr: SocketAddr = if n == 0 {
                    "1.1.1.1:0".parse().unwrap()
                } else {
                    "127.0.0.1:0".parse().unwrap()
                };
                Ok(vec![addr])
            })
        });
        let resolver = SsrfResolver::with_lookup(SsrfPolicy::new(false), lookup);

        // First resolution: public → allowed.
        let first: Vec<SocketAddr> = resolver
            .resolve(name("rebind.example"))
            .await
            .unwrap()
            .collect();
        assert_eq!(first, vec!["1.1.1.1:0".parse::<SocketAddr>().unwrap()]);

        // Second resolution: rebinding returns loopback → denied.
        let Err(err) = resolver.resolve(name("rebind.example")).await else {
            panic!("rebound private address must be denied");
        };
        assert!(err.to_string().contains("loopback"));
    }

    #[tokio::test]
    async fn resolver_empty_resolution_is_not_ssrf() {
        // Genuine DNS miss (no addresses) returns an empty iterator,
        // NOT the SSRF marker — reqwest surfaces a normal DNS error.
        let resolver = SsrfResolver::with_lookup(SsrfPolicy::new(false), fixed_lookup(vec![]));
        let got: Vec<SocketAddr> = resolver
            .resolve(name("nxdomain.example"))
            .await
            .unwrap()
            .collect();
        assert!(got.is_empty());
    }
}
