/**
 * SSRF 防护：拒绝指向私有 / loopback / link-local / 云元数据 IP 的 host。
 * 任何用户可控的 fetch（web_fetch、未来 webhook 等）都要先过这一关。
 *
 * 局限：fetch 内部 connect 时仍会再 lookup 一次 host（DNS rebinding 窗口）。
 * 完整防护需走自定义 agent 强制 IP 直连，本工具是 90% 拦截 + redirect=manual 兜底。
 */

import { isIP } from 'net';

type DnsLookup = (hostname: string) => Promise<{ address: string; family: number }>;

export async function assertPublicHost(hostname: string, lookup: DnsLookup): Promise<void> {
  if (!hostname) throw new Error('SSRF guard: empty hostname');

  // 字面 IP 直接判
  if (isIP(hostname)) {
    if (isPrivateIp(hostname)) {
      throw new Error(`SSRF guard: literal private IP rejected: ${hostname}`);
    }
    return;
  }

  // 主机名解析后判 IP
  const { address } = await lookup(hostname);
  if (isPrivateIp(address)) {
    throw new Error(`SSRF guard: host ${hostname} resolves to private IP ${address}`);
  }
}

/**
 * 私有 / loopback / link-local / 云元数据范围。
 * 任何命中即拒。
 */
export function isPrivateIp(ip: string): boolean {
  const v = isIP(ip);
  if (v === 4) return isPrivateIpv4(ip);
  if (v === 6) return isPrivateIpv6(ip);
  return true; // 解析失败保守拒
}

function isPrivateIpv4(ip: string): boolean {
  const parts = ip.split('.').map((n) => Number(n));
  if (parts.length !== 4 || parts.some((n) => Number.isNaN(n))) return true;
  const [a, b] = parts;
  // 0.0.0.0/8 — wildcard / "this network"
  if (a === 0) return true;
  // 10.0.0.0/8 — RFC1918 private
  if (a === 10) return true;
  // 127.0.0.0/8 — loopback
  if (a === 127) return true;
  // 169.254.0.0/16 — link-local + 云元数据 169.254.169.254 (AWS/GCP/Azure)
  if (a === 169 && b === 254) return true;
  // 172.16.0.0/12 — RFC1918
  if (a === 172 && b >= 16 && b <= 31) return true;
  // 192.168.0.0/16 — RFC1918
  if (a === 192 && b === 168) return true;
  // 224.0.0.0/4 — multicast
  if (a >= 224 && a <= 239) return true;
  // 240.0.0.0/4 — reserved
  if (a >= 240) return true;
  return false;
}

function isPrivateIpv6(ip: string): boolean {
  const lower = ip.toLowerCase();
  if (lower === '::1' || lower === '::') return true;
  if (lower.startsWith('fc') || lower.startsWith('fd')) return true; // fc00::/7 ULA
  if (lower.startsWith('fe8') || lower.startsWith('fe9') || lower.startsWith('fea') || lower.startsWith('feb')) {
    return true; // fe80::/10 link-local
  }
  if (lower.startsWith('ff')) return true; // ff00::/8 multicast
  // IPv4-mapped: ::ffff:x.x.x.x → 走 v4 判定
  const mapped = lower.match(/^::ffff:([\d.]+)$/);
  if (mapped) return isPrivateIpv4(mapped[1]);
  return false;
}
