import { env } from "cloudflare:workers"; import { enforceRateLimit, rawRequestIp } from "../limits"; import { HttpError } from "../errors"; import { customHostKey, isForgeHostname, metaKey, sitesDomain, slotKey } from "./config"; import { hostLabel } from "./naming"; import { accessSubject, challenge, handleAccessRoute } from "./access"; import { runSiteWorker } from "./runtime"; import { loadManifest, plain } from "./static"; import { handleAbuseReport, handleScratchUpload } from "./scratch"; import { meteredResponse } from "./usage"; import { ownerWithinSiteQuota, scratchWithinQuota } from "./metering"; import type { SiteMeta, SlotPointer } from "./config"; const POINTER_CACHE_SECONDS = 30; const REPORT_PATH = "/__gitfor/report"; const UNMETERED = new WeakSet(); type Resolved = { label: string; pointer: SlotPointer; meta: SiteMeta }; function pointer(label: string): Promise { return env.KV.get(slotKey(label), { type: "json", cacheTtl: POINTER_CACHE_SECONDS }); } function meta(siteId: string): Promise { return env.KV.get(metaKey(siteId), { type: "json", cacheTtl: POINTER_CACHE_SECONDS }); } /** Maps a request host to the site and deployment it serves; custom domains only after verification wrote KV. */ export async function resolveSiteHost(hostname: string): Promise { const domain = sitesDomain(); if (!domain) return null; let label = hostLabel(hostname, domain); if (!label) { const custom = await env.KV.get<{ siteId: string; label: string }>(customHostKey(hostname), { type: "json", cacheTtl: POINTER_CACHE_SECONDS, }); if (!custom) return null; label = custom.label; } const slot = await pointer(label); if (!slot) return null; const site = await meta(slot.siteId); return site ? { label, pointer: slot, meta: site } : null; } function isSitesHost(hostname: string, domain: string): boolean { return hostname === domain || hostname.endsWith(`.${domain}`); } async function serve(request: Request, ctx: ExecutionContext, resolved: Resolved): Promise { const { meta: site } = resolved; if (site.disabled) return plain(451, "This site has been disabled"); if (site.scratch && site.expiresAt !== null && site.expiresAt < Date.now()) return plain(410, "This scratch site expired"); if (site.scratch) await enforceRateLimit("GIT_LIMITER", `scratch-site:${site.id}`); const url = new URL(request.url); if (url.pathname === REPORT_PATH) return handleAbuseReport(request, site); if (site.scratch && !(await scratchWithinQuota(site))) { const refused = plain(429, "This scratch site used up today's free tier; claim it to lift the limits"); UNMETERED.add(refused); return refused; } if (!(await ownerWithinSiteQuota(site))) { const refused = plain(429, "This site's owner has used this month's site allowance"); UNMETERED.add(refused); return refused; } const route = await handleAccessRoute(request, site); if (route) return route; let subject: string | null = null; if (site.access !== "public" && !site.scratch) { subject = await accessSubject(request, site); if (!subject) return challenge(request); } const manifest = await loadManifest(resolved.pointer.deploymentId); if (!manifest) return plain(503, "Deployment unavailable"); return runSiteWorker(request, ctx, site, resolved.pointer.deploymentId, manifest, subject); } /** * Entry point for every request: returns null unless the host is the sites apex, a site label under it, or a * verified custom domain. The forge host is never treated as a site. */ export async function handleSitesRequest(request: Request, ctx: ExecutionContext): Promise { const domain = sitesDomain(); if (!domain) return null; const url = new URL(request.url); const hostname = url.hostname.toLowerCase(); if (isForgeHostname(hostname)) return null; try { if (hostname === domain) { if (url.pathname === "/__scratch") return await handleScratchUpload(request); return plain(404, "Nothing here. POST a .tar.gz to /__scratch to deploy a scratch site."); } const resolved = await resolveSiteHost(hostname); if (!resolved) return isSitesHost(hostname, domain) ? plain(404, "Site not found") : null; const response = await serve(request, ctx, resolved); // Refusals over the free tier are not metered, so an exhausted site stays cheap to hit. if (UNMETERED.has(response)) return response; const rayId = request.headers.get("cf-ray") ?? crypto.randomUUID(); return meteredResponse(response, resolved.meta, ctx, rayId); } catch (error) { if (error instanceof HttpError) return plain(error.status, error.message); console.error("Site dispatch failed", hostname, rawRequestIp(request), error); return plain(500, "Internal error"); } }