Skip to content
File

Blob: src/worker/dav/subject.ts

typescript82 lines
1import { authenticateDavPat } from "@/worker/auth/basic";
2import { DAV_AREAS } from "@/worker/dav/areas";
3import { isDavWellKnownPath } from "@/worker/dav/discovery";
4import { davError, unauthorizedDav } from "@/worker/dav/http";
5import { dispatchDavArea, expensiveDavRequestArea, findDavArea, isExpensiveDavRequest } from "@/worker/dav/runtime";
6import { createControlPlaneDb } from "@/worker/db/d1/client";
7import { getSubjectByHostLabel } from "@/worker/db/d1/repository";
8import { enforceRateLimit } from "@/worker/middleware/rate-limit";
9import type { AppContext } from "@/worker/types";
10import { clientIp } from "@/worker/util/request-context";
11import { jsonError } from "@/worker/util/response";
12 
13function isControlPlaneOnlyPath(pathname: string): boolean {
14 return (
15 pathname === "/healthz" ||
16 pathname.startsWith("/api/v1/") ||
17 pathname === "/api/v1" ||
18 pathname === "/api/v1/openapi.json" ||
19 pathname === "/api/v1/openapi.yaml"
20 );
21}
22 
23function isLoopbackHost(host: string): boolean {
24 return (
25 host === "localhost" ||
26 host.endsWith(".localhost") ||
27 host === "127.0.0.1" ||
28 host.endsWith(".127.0.0.1") ||
29 host === "[::1]"
30 );
31}
32 
33function requiresHttps(c: AppContext): boolean {
34 const url = new URL(c.req.url);
35 const hostInfo = c.get("hostInfo");
36 return url.protocol === "http:" && !isLoopbackHost(hostInfo.normalizedHost);
37}
38 
39export async function handleSubjectDavRequest(c: AppContext): Promise<Response> {
40 const hostInfo = c.get("hostInfo");
41 if (hostInfo.kind !== "subject") return c.notFound();
42 
43 const pathname = new URL(c.req.url).pathname;
44 if (isControlPlaneOnlyPath(pathname)) return c.notFound();
45 if (!hostInfo.hostLabel) return c.notFound();
46 
47 const subject = await getSubjectByHostLabel(createControlPlaneDb(c.env.DAV_CONTROL_PLANE), hostInfo.hostLabel);
48 if (!subject) return c.notFound();
49 if (requiresHttps(c)) return davError(403, "HTTPS is required for DAV requests");
50 if (isDavWellKnownPath(pathname)) return c.redirect("/", 302);
51 
52 const auth = await authenticateDavPat(c.env, c.req.raw, subject);
53 if (!auth) {
54 const limited = await enforceRateLimit(c, "RL_DAV_AUTH", [
55 "dav_auth_failure",
56 subject.id,
57 hostInfo.hostLabel,
58 clientIp(c.req.raw),
59 ]);
60 if (limited) return limited;
61 return unauthorizedDav();
62 }
63 if (auth.subjectId !== subject.id) return jsonError("forbidden", "PAT does not match the subject host.", 403);
64 
65 const area = findDavArea(pathname, DAV_AREAS);
66 if (c.req.method === "OPTIONS")
67 return area ? await dispatchDavArea(area, { c, subject, auth, pathname }) : c.notFound();
68 
69 if (isExpensiveDavRequest(c.req.raw)) {
70 const limited = await enforceRateLimit(c, "RL_REPORT", [
71 "dav_expensive",
72 auth.subjectId,
73 auth.patId,
74 c.req.method,
75 expensiveDavRequestArea(pathname),
76 ]);
77 if (limited) return limited;
78 }
79 
80 return area ? await dispatchDavArea(area, { c, subject, auth, pathname }) : c.notFound();
81}