138 lines · 5.4 KB
import { HttpError } from "./errors";
import { CAMO_PREFIX, verifyCamoPath } from "./camo-sign";
import { enforceRateLimit, requestIp } from "./limits";
import { assertResolvesPublic } from "./net/resolve-guard";
import { isOwnHost } from "./camo-sign";
import { limitStream } from "./git/ref-sync-protocol";

const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
const MAX_REDIRECTS = 3;
const UPSTREAM_TIMEOUT_MS = 10_000;
const CACHE_TTL_SECONDS = 24 * 60 * 60;
const NEGATIVE_TTL_SECONDS = 5 * 60;
const USER_AGENT = "gitfor-camo/1.0 (+image proxy)";
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
const HTTP_OK = 200;
const HTTP_NOT_FOUND = 404;
const HTTP_UNSUPPORTED = 415;
const HTTP_TOO_LARGE = 413;
const HTTP_BAD_GATEWAY = 502;
const HTTP_METHOD_NOT_ALLOWED = 405;
/** Raster formats browsers render in `<img>`; SVG is allowed only because the sandbox CSP makes its scripts inert. */
const IMAGE_TYPE = /^image\/(?:png|jpeg|gif|webp|avif|bmp|x-icon|vnd\.microsoft\.icon|apng|svg\+xml)$/i;

const RESPONSE_HEADERS: Record<string, string> = {
  "x-content-type-options": "nosniff",
  "content-disposition": "inline",
  "content-security-policy": "default-src 'none'; sandbox",
  "referrer-policy": "no-referrer",
  "cross-origin-resource-policy": "same-origin",
  "x-frame-options": "DENY",
};

function reply(status: number, message: string): Response {
  return new Response(`${message}\n`, {
    status,
    headers: {
      ...RESPONSE_HEADERS,
      "content-type": "text/plain; charset=utf-8",
      "cache-control": `public, max-age=${NEGATIVE_TTL_SECONDS}`,
    },