clone

push a directory of html from the terminal and get a four-character url back. a deno cli on cloudflare

feat/site-feedback
nzip / worker / src / index.ts
index.ts 4.6K
1import { api } from "./api.ts";2import { checkBearer } from "./auth.ts";3import type { Env } from "./env.ts";4import { err, isControlOrigin, siteAddressFromUrl } from "./env.ts";5import { runGc } from "./gc.ts";6import { logSecurityRequest, recordEnumerationProbe } from "./observability.ts";7import {8 evaluateEnumerationWindow,9 pruneSecurityTelemetry,10 sendDailySecurityDigest,11} from "./security_alerts.ts";12import { serve } from "./serve.ts";13import { handleNotifyPublic } from "./notify_public.ts";14import { drainNotifications, pruneNotifications } from "./notify.ts";1516function tooManyRequests(): Response {17 return new Response("rate limited — slow down\n", {18 status: 429,19 headers: {20 "content-type": "text/plain; charset=utf-8",21 "retry-after": "60",22 },23 });24}2526export default {27 async fetch(28 req: Request,29 env: Env,30 ctx: ExecutionContext,31 ): Promise<Response> {32 const url = new URL(req.url);33 const controlOrigin = isControlOrigin(env, url);34 const siteAddress = controlOrigin ? null : siteAddressFromUrl(env, url);35 const finish = (response: Response): Response => {36 // Cloudflare currently keys Worker response cache entries for the exact37 // custom domain and its wildcard route without preserving the request38 // hostname. Never let a public artifact response be replayed on a sibling39 // site's origin. Browser caching remains controlled by Cache-Control.40 response.headers.set("cloudflare-cdn-cache-control", "no-store");41 ctx.waitUntil(logSecurityRequest(req, env, url, response, siteAddress));42 ctx.waitUntil(43 recordEnumerationProbe(req, env, url, response, siteAddress),44 );45 return response;46 };4748 // The wildcard route must never widen the management surface. Only the49 // configured control origin may dispatch API or notification requests.50 if (!controlOrigin && siteAddress === null) {51 return finish(err("not found", 404));52 }5354 if (55 controlOrigin &&56 (url.pathname === "/api" || url.pathname.startsWith("/api/"))57 ) {58 if (!checkBearer(req, env)) return finish(err("unauthorized", 401));59 return finish(await api(req, env, url, ctx));60 }6162 if (63 controlOrigin &&64 (url.pathname === "/_notify" || url.pathname.startsWith("/_notify/"))65 ) {66 return finish(await handleNotifyPublic(req, env, url));67 }6869 const legacyUnlock = controlOrigin && req.method === "POST" &&70 /^\/[0-9a-f]{4}\/__unlock$/.test(url.pathname);71 const siteUnlock = siteAddress !== null && req.method === "POST" &&72 url.pathname === "/__unlock";73 const isUnlock = legacyUnlock || siteUnlock;74 const feedbackWrite = siteAddress !== null && req.method === "POST" &&75 url.pathname === "/__feedback";76 if (req.method !== "GET" && req.method !== "HEAD" && !isUnlock && !feedbackWrite) {77 return finish(err("method not allowed", 405));78 }7980 // Guessing defenses. The address space is only 16 bits, so throttle the two81 // enumeration surfaces per client IP. Asset subpaths (/{addr}/...) are left82 // alone — reaching them already requires knowing a live address.83 const ip = req.headers.get("cf-connecting-ip") ?? "local";84 if (isUnlock) {85 const address = siteAddress ?? url.pathname.slice(1, 5);86 const { success } = await env.RL_UNLOCK.limit({87 key: `${ip}:${address}`,88 });89 if (!success) return finish(tooManyRequests());90 } else if (feedbackWrite) {91 const { success } = await env.RL_FEEDBACK.limit({92 key: `${ip}:${siteAddress}`,93 });94 if (!success) return finish(tooManyRequests());95 } else if (96 (controlOrigin && /^\/[0-9a-f]{4}\/?$/.test(url.pathname)) ||97 (siteAddress !== null && url.pathname === "/")98 ) {99 const { success } = await env.RL_ENUM.limit({ key: ip });100 if (!success) return finish(tooManyRequests());101 }102103 return finish(await serve(req, env, url, siteAddress ?? undefined, ctx));104 },105106 async scheduled(107 controller: ScheduledController,108 env: Env,109 ctx: ExecutionContext,110 ): Promise<void> {111 if (controller.cron === "*/5 * * * *") {112 ctx.waitUntil(evaluateEnumerationWindow(env));113 ctx.waitUntil(drainNotifications(env));114 return;115 }116 ctx.waitUntil(117 Promise.all([118 runGc(env),119 pruneSecurityTelemetry(env),120 pruneNotifications(env),121 sendDailySecurityDigest(env),122 ]).then(([r]) =>123 console.log(124 `gc: ${r.expiredSites} expired sites, ${r.deletedObjects} objects deleted`,125 )126 ),127 );128 },129} satisfies ExportedHandler<Env>;