119 lines · 4.0 KB
import { env } from "cloudflare:workers";
import { assertPublicHttpsUrl } from "../git/relay";
import { HttpError, badRequest } from "../errors";

const OUTBOUND_TIMEOUT_MS = 10_000;
const MAX_REQUEST_BYTES = 256 * 1024;
const MAX_RESPONSE_BYTES = 64 * 1024;
const HTTP_OK_MIN = 200;
const HTTP_OK_MAX = 300;
const HTTP_REDIRECT_MIN = 300;
const HTTP_REDIRECT_MAX = 400;
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);

/** `permanent` failures (bad config, rejected URL, most 4xx) are not worth retrying. */
export type OutboundResult = {
  status: number | null;
  body: string;
  error: string | null;
  durationMs: number;
  permanent?: boolean;
};

/**
 * Every integration URL must be public https (no IP literals, internal suffixes or our own zone). Local
 * development can opt into loopback sinks with INTEGRATIONS_ALLOW_LOOPBACK, which production never sets.
 */
export function assertOutboundUrl(raw: string, allowLoopback = env.INTEGRATIONS_ALLOW_LOOPBACK === "true"): URL {
  if (allowLoopback) {
    try {
      const url = new URL(raw);
      if ((url.protocol === "http:" || url.protocol === "https:") && LOOPBACK_HOSTS.has(url.hostname)) return url;
    } catch {
      throw badRequest("Invalid URL");
    }
  }
  return assertPublicHttpsUrl(raw);
}

async function readCapped(response: Response, maxBytes: number): Promise<string> {
  if (!response.body) return "";