diff --git a/src/lib/cloudflare/public-ssr-gateway.ts b/src/lib/cloudflare/public-ssr-gateway.ts index b826ed051..0b9dc4d9a 100644 --- a/src/lib/cloudflare/public-ssr-gateway.ts +++ b/src/lib/cloudflare/public-ssr-gateway.ts @@ -11,11 +11,19 @@ export type PublicSsrLocale = "en-us" | "zh-cn"; const STATIC_PUBLIC_PATHS = new Set([ "/catalog/bus/map", "/guides/markdown-support", + "/api-docs", "/mobile-app", "/privacy", "/terms", ]); const STATIC_PUBLIC_ROOTS = ["/api/docs"]; +const DIRECT_REQUEST_PATHS = new Set([ + "/", + "/error", + "/llms.txt", + "/robots.txt", + "/sitemap.xml", +]); const CATALOG_QUERY_KEYS: Record> = { "/catalog/courses": new Set([ @@ -50,6 +58,7 @@ const DYNAMIC_OR_PRIVATE_ROOTS = [ "/api", "/catalog", "/community", + "/e2e", "/oauth", "/workspace", ]; @@ -92,7 +101,7 @@ export function resolvePublicSsrMode(request: Request): PublicSsrMode | null { } if ( - url.pathname === "/" || + DIRECT_REQUEST_PATHS.has(url.pathname) || url.pathname.startsWith("/_app/") || url.pathname.startsWith("/.well-known/") || DYNAMIC_OR_PRIVATE_ROOTS.some((root) => matchesPathRoot(url.pathname, root)) diff --git a/src/worker.js b/src/worker.js index 3b8009b0a..a13732eb9 100644 --- a/src/worker.js +++ b/src/worker.js @@ -12,6 +12,8 @@ import { resolvePublicSsrLocale, resolvePublicSsrMode, } from "./lib/cloudflare/public-ssr-gateway"; +import { buildContentSecurityPolicy } from "./lib/security/csp"; +import { CONTENT_SIGNAL } from "./lib/seo/content-signal"; const app = svelteKitWorker; @@ -53,6 +55,27 @@ function createNonce() { return btoa(String.fromCharCode(...bytes)); } +function publicNotFoundResponse(locale, headRequest) { + const body = buildPublicNotFoundHtml(locale); + return new Response(headRequest ? null : body, { + status: 404, + headers: { + "Cache-Control": "private, no-store", + "Cloudflare-CDN-Cache-Control": "no-store", + "Content-Language": locale, + "Content-Security-Policy": buildContentSecurityPolicy(createNonce()), + "Content-Signal": CONTENT_SIGNAL, + "Content-Type": "text/html; charset=utf-8", + "Permissions-Policy": "camera=(), microphone=(), geolocation=()", + "Referrer-Policy": "strict-origin-when-cross-origin", + Vary: "Accept-Language, Cookie", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "SAMEORIGIN", + "x-request-id": crypto.randomUUID(), + }, + }); +} + class ScriptNonceRewriter { constructor(nonce) { this.nonce = nonce; @@ -164,6 +187,9 @@ export default { if (!mode) return app.fetch(directRequest(request), env, context); const locale = resolvePublicSsrLocale(request); + if (mode === "not-found") { + return publicNotFoundResponse(locale, request.method === "HEAD"); + } const cachedRequest = publicSsrRequest(request, mode, locale); const cacheUrl = new URL(cachedRequest.url); const response = await context.exports diff --git a/tests/unit/public-ssr-gateway.test.ts b/tests/unit/public-ssr-gateway.test.ts index 5609a9d9b..af6061da8 100644 --- a/tests/unit/public-ssr-gateway.test.ts +++ b/tests/unit/public-ssr-gateway.test.ts @@ -18,6 +18,7 @@ describe("public SSR gateway", () => { "/catalog/sections?semesterId=301&teacher=Li", "/catalog/teachers?departmentId=1", "/catalog/bus/map", + "/api-docs", "/api/docs/rest/catalog", "/mobile-app", "/privacy", @@ -35,6 +36,11 @@ describe("public SSR gateway", () => { "/catalog/courses?unknown=value", "/catalog/courses?__life_locale=en-us", "/community/users/example", + "/e2e/oauth/callback?code=example&state=test", + "/error?error=access_denied", + "/llms.txt", + "/robots.txt", + "/sitemap.xml", "/workspace/overview", ])("bypasses private or mixed route %s", (path) => { expect(resolvePublicSsrMode(request(path))).toBeNull();