// Host-header allowlist for DNS-rebinding protection.
//
// Background: the panel binds 0.0.0.0:8080 and ships default credentials
// (admin / wechat). Without Host-header validation, a malicious site the
// operator visits can use DNS rebinding to point a hostname at the panel's
// LAN/loopback IP and drive every authenticated API from the operator's own
// browser — including the docker.sock-backed admin endpoints. The
// `sameSite: 'lax'` cookie does not stop this: after rebinding, the browser
// treats the attacker hostname as same-origin with the panel and includes
// any cookie it issues. The fix is host-allowlisting at the request edge.
//
// Default allowlist (covers documented deploys without operator action):
//   - loopback: localhost / 127.0.0.1 / ::1
//   - RFC1918 private LAN: 10/8, 172.16-31/12, 192.168/16
//   - link-local IPv4: 169.254/16
// Public hostnames (the recommended reverse-proxy deployment) must be added
// via PANEL_ALLOWED_HOSTS=<comma-separated>.

export function parseHost(headerHost: string | undefined): string {
  if (!headerHost) return '';
  const trimmed = headerHost.trim();
  if (!trimmed) return '';
  if (trimmed.startsWith('[')) {
    const close = trimmed.indexOf(']');
    if (close <= 0) return '';
    return trimmed.slice(0, close + 1).toLowerCase();
  }
  const colon = trimmed.lastIndexOf(':');
  const host = colon > 0 ? trimmed.slice(0, colon) : trimmed;
  return host.toLowerCase();
}

export function isLoopbackHost(host: string): boolean {
  return (
    host === 'localhost' ||
    host === '127.0.0.1' ||
    host === '[::1]' ||
    host === '::1'
  );
}

export function isPrivateIpv4(host: string): boolean {
  const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
  if (!m) return false;
  const o = [m[1], m[2], m[3], m[4]].map((s) => Number(s));
  if (o.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return false;
  // 10.0.0.0/8
  if (o[0] === 10) return true;
  // 172.16.0.0/12
  if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return true;
  // 192.168.0.0/16
  if (o[0] === 192 && o[1] === 168) return true;
  // 169.254.0.0/16 (link-local)
  if (o[0] === 169 && o[1] === 254) return true;
  return false;
}

export function parseAllowedHosts(raw: string | undefined): string[] {
  if (!raw) return [];
  const out: string[] = [];
  for (const part of raw.split(',')) {
    const lower = part.trim().toLowerCase();
    if (lower) out.push(lower);
  }
  return [...new Set(out)];
}

export function isAllowedHost(host: string, allowlist: string[]): boolean {
  if (!host) return false;
  if (isLoopbackHost(host)) return true;
  if (isPrivateIpv4(host)) return true;
  for (const entry of allowlist) {
    if (entry === host) return true;
    // 通配子域：*.example.com 匹配任意子域（a.example.com），但不匹配裸 example.com。
    if (entry.startsWith('*.')) {
      const suffix = entry.slice(1); // ".example.com"
      if (host.length > suffix.length && host.endsWith(suffix)) return true;
    }
  }
  return false;
}

// 反代/CDN（Cloudflare、nginx、Caddy 等）部署时，真实对外域名可能在 X-Forwarded-Host 里，
// 而 Host 被改写成内部地址。综合判定：Host 或 X-Forwarded-Host 任一在白名单即放行。
// 安全性：DNS-rebinding 攻击者直连面板时，浏览器 fetch 无法设置 X-Forwarded-Host（禁止首部），
// 故该首部只会由可信反代设置，不会被攻击者利用。
export function isRequestHostAllowed(
  hostHeader: string | undefined,
  forwardedHostHeader: string | string[] | undefined,
  allowlist: string[],
): boolean {
  if (isAllowedHost(parseHost(hostHeader), allowlist)) return true;
  let xfh = Array.isArray(forwardedHostHeader) ? forwardedHostHeader[0] : forwardedHostHeader;
  if (xfh) {
    xfh = xfh.split(',')[0]; // 多级代理链取第一个（最初的客户端 Host）
    if (isAllowedHost(parseHost(xfh), allowlist)) return true;
  }
  return false;
}
