File
Blob: src/worker/dav/runtime.ts
| 1 | import type { DavPatAuth } from "@/worker/auth/basic"; |
| 2 | import { davError, emptyResponse, methodNotAllowed } from "@/worker/dav/http"; |
| 3 | import type { SubjectRow } from "@/worker/db/d1/schema"; |
| 4 | import type { AppContext } from "@/worker/types"; |
| 5 | |
| 6 | export interface DavRequestContext { |
| 7 | c: AppContext; |
| 8 | subject: SubjectRow; |
| 9 | auth: DavPatAuth; |
| 10 | pathname: string; |
| 11 | } |
| 12 | |
| 13 | export interface DavArea { |
| 14 | name: "files" | "caldav" | "carddav" | "discovery"; |
| 15 | allow: string; |
| 16 | optionsHeaders: HeadersInit; |
| 17 | matches(pathname: string): boolean; |
| 18 | canUseMethod(auth: DavPatAuth, method: string): boolean; |
| 19 | handle(context: DavRequestContext): Promise<Response>; |
| 20 | } |
| 21 | |
| 22 | function allowedMethods(allow: string): Set<string> { |
| 23 | return new Set(allow.split(",").map((method) => method.trim())); |
| 24 | } |
| 25 | |
| 26 | export function findDavArea(pathname: string, areas: readonly DavArea[]): DavArea | null { |
| 27 | return areas.find((area) => area.matches(pathname)) ?? null; |
| 28 | } |
| 29 | |
| 30 | export function isMethodAllowed(area: DavArea, method: string): boolean { |
| 31 | return allowedMethods(area.allow).has(method); |
| 32 | } |
| 33 | |
| 34 | export function isExpensiveDavRequest(request: Request): boolean { |
| 35 | if (request.method === "REPORT") return true; |
| 36 | return request.method === "PROPFIND" && request.headers.get("depth")?.toLowerCase() === "infinity"; |
| 37 | } |
| 38 | |
| 39 | export function expensiveDavRequestArea(pathname: string): string { |
| 40 | return pathname === "/files" || pathname.startsWith("/files/") ? "files" : (pathname.split("/")[1] ?? "unknown"); |
| 41 | } |
| 42 | |
| 43 | export async function dispatchDavArea(area: DavArea, context: DavRequestContext): Promise<Response> { |
| 44 | const method = context.c.req.method; |
| 45 | if (!area.canUseMethod(context.auth, method)) return davError(403, "PAT scope does not allow this method"); |
| 46 | if (method === "OPTIONS") return emptyResponse(204, area.optionsHeaders); |
| 47 | if (!isMethodAllowed(area, method)) return methodNotAllowed(area.allow); |
| 48 | return await area.handle(context); |
| 49 | } |