97 lines · 4.1 KB
import { env } from "cloudflare:workers";
import { userContentOriginFor } from "./usercontent-origin";
import { bytesToHex, hexToBytes, hkdfSha256, hmacSha256, utf8 } from "./sync-hash";

export const CAMO_PREFIX = "/camo/";
const CAMO_SALT = "gitfor-camo";
const CAMO_INFO = "image-proxy-v1";
const MAX_URL_LENGTH = 2048;
/** One canonical spelling per URL, so case changes can't mint fresh cache keys. */
const LOWER_HEX = /^[0-9a-f]+$/;

let cachedKey: { secret: string; key: Uint8Array } | null = null;

/** MAC key derived from SECRETS_KEY with HKDF so the proxy never sees the master key directly. */
function camoKey(secret: string | undefined): Uint8Array | null {
  if (!secret) return null;
  if (!cachedKey || cachedKey.secret !== secret)
    cachedKey = { secret, key: hkdfSha256(utf8(secret), CAMO_SALT, CAMO_INFO) };
  return cachedKey.key;
}

/** Only absolute http(s) URLs without credentials are proxied; anything else is dropped by the caller. */
export function proxiableImageUrl(raw: string): string | null {
  if (raw.length > MAX_URL_LENGTH) return null;
  let url: URL;
  try {
    url = new URL(raw);
  } catch {
    return null;
  }
  if (url.protocol !== "http:" && url.protocol !== "https:") return null;
  if (url.username || url.password || !url.hostname) return null;
  if (isOwnHost(url.hostname)) return null;
  url.hash = "";
  return url.href;
}

function hostOf(value: string | undefined | null): string | null {
  if (!value) return null;
  try {