// CORS proxy for fetching RSS / Atom feeds from the browser.
//
// This is a Netlify Edge Function (Deno runtime) — the deployed equivalent of a
// Cloudflare Worker. Browsers cannot fetch most feeds directly because the feed
// host does not send CORS headers. Syndicate Elsewhere tries a direct fetch
// first and falls back to this proxy (configured in Settings → CORS proxy).
//
// Usage:  /rss-proxy?url=https%3A%2F%2Fexample.com%2Ffeed.xml
//
// SSRF guard: only http(s) targets are allowed and requests to localhost /
// private network ranges are refused so this cannot be used to probe internal
// services. It is still effectively an open proxy for public URLs — fine for a
// personal site, but front it with auth or an allow-list if abuse is a concern.

const CORS: Record<string, string> = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type",
};

function isBlockedHost(host: string): boolean {
  const h = host.toLowerCase().replace(/^\[|\]$/g, ""); // strip IPv6 brackets
  if (h === "localhost" || h.endsWith(".localhost")) return true;
  if (h === "0.0.0.0" || h === "::1") return true;
  if (/^127\./.test(h)) return true;                       // loopback
  if (/^10\./.test(h)) return true;                        // private
  if (/^192\.168\./.test(h)) return true;                  // private
  if (/^169\.254\./.test(h)) return true;                  // link-local
  if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;   // private
  if (h.startsWith("fc") || h.startsWith("fd") || h.startsWith("fe80:")) return true; // IPv6 private/link-local
  return false;
}

export default async (request: Request): Promise<Response> => {
  if (request.method === "OPTIONS") {
    return new Response(null, { status: 204, headers: CORS });
  }
  if (request.method !== "GET") {
    return new Response("Method not allowed", { status: 405, headers: CORS });
  }

  const target = new URL(request.url).searchParams.get("url");
  if (!target) {
    return new Response("Missing ?url= parameter", { status: 400, headers: CORS });
  }

  let parsed: URL;
  try {
    parsed = new URL(target);
  } catch {
    return new Response("Invalid url", { status: 400, headers: CORS });
  }
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
    return new Response("Only http(s) URLs are allowed", { status: 400, headers: CORS });
  }
  if (isBlockedHost(parsed.hostname)) {
    return new Response("Host not allowed", { status: 403, headers: CORS });
  }

  try {
    const upstream = await fetch(parsed.toString(), {
      headers: {
        Accept: "application/rss+xml, application/atom+xml, application/xml, text/xml, */*",
        "User-Agent": "SyndicateElsewhere/1.0 (+https://tools.jamesking.io)",
      },
      redirect: "follow",
    });

    // Stream the raw bytes through unchanged. Using upstream.text() here would
    // UTF-8-decode the body and corrupt binary responses (e.g. cover images
    // fetched for Bluesky link cards) — so pass the body stream straight on.
    const contentType = upstream.headers.get("content-type") || "application/octet-stream";

    return new Response(upstream.body, {
      status: upstream.status,
      headers: {
        ...CORS,
        "Content-Type": contentType,
        "Cache-Control": "public, max-age=300",
      },
    });
  } catch (err) {
    return new Response("Upstream fetch failed: " + (err instanceof Error ? err.message : String(err)), {
      status: 502,
      headers: CORS,
    });
  }
};

// Inline route config — Netlify mounts this function at /rss-proxy.
export const config = { path: "/rss-proxy" };
