diff --git a/platform/src/components/aws/router.ts b/platform/src/components/aws/router.ts index bfd5805f01..082d0aae7d 100644 --- a/platform/src/components/aws/router.ts +++ b/platform/src/components/aws/router.ts @@ -2755,7 +2755,7 @@ async function routeSite(kvNamespace, metadata) { : ["", ".html", "/index.html"]; const v = await Promise.any(postfixes.map(p => cf.kvs().get(kvNamespace + ":" + u + p).then(v => p))); // files are stored in a subdirectory, add it to the request uri - event.request.uri = metadata.s3.dir + event.request.uri + v; + event.request.uri = metadata.s3.dir + baselessUri + v; setS3Origin(metadata.s3.domain); return; } catch (e) {} @@ -2765,7 +2765,7 @@ async function routeSite(kvNamespace, metadata) { for (var i=0, l=metadata.s3.routes.length; i /usage/index.html if (event.request.uri.endsWith("/")) { event.request.uri += "index.html"; diff --git a/platform/test/components/cloudfront.test.ts b/platform/test/components/cloudfront.test.ts index 61a9b2402e..5804f91a45 100644 --- a/platform/test/components/cloudfront.test.ts +++ b/platform/test/components/cloudfront.test.ts @@ -127,12 +127,7 @@ function createContext(input: CreateContextInput) { return { context, event }; } -function loadRouteSite(input: { - uri: string; - headers: Record; - cookies?: Record; - querystring?: Record; -}) { +function loadRouteSite(input: CreateContextInput) { const { context, event } = createContext(input); new vm.Script( @@ -465,4 +460,115 @@ describe("CloudFront router", () => { expect(response.body.data).toContain("Reduce cookie size"); }); }); + + describe("base path", () => { + const S3_DOMAIN = "assets.s3.us-east-1.amazonaws.com"; + + function siteMetadata(overrides: Record = {}) { + return { + base: "/admin/board", + s3: { domain: S3_DOMAIN, dir: "/_assets", routes: ["/_next/static"] }, + servers: [["server.lambda-url.us-east-1.on.aws", 0, 0]], + ...overrides, + }; + } + + it("keys an S3 route without the base", async () => { + // The regression. The route is matched against the baseless uri, so the + // key has to be built from it too. Building it from the full uri asks S3 + // for /_assets/admin/board/_next/... while the deploy uploaded + // /_assets/_next/..., so every asset 404s. S3 reports that as 403 when + // the caller cannot list, which sends people hunting for a permissions + // problem that is not there. + const { event, routeSite } = loadRouteSite({ + uri: "/admin/board/_next/static/css/abc123.css", + headers: { host: { value: "example.com" } }, + }); + + await routeSite("test", siteMetadata()); + + expect(event.request.uri).toBe("/_assets/_next/static/css/abc123.css"); + }); + + it("does not leave the base anywhere in the key", async () => { + const { event, routeSite } = loadRouteSite({ + uri: "/admin/board/_next/static/chunk.js", + headers: { host: { value: "example.com" } }, + }); + + await routeSite("test", siteMetadata()); + + expect(event.request.uri).not.toContain("/admin/board"); + }); + + it("sends it to the S3 origin", async () => { + let origin: any; + const { routeSite } = loadRouteSite({ + uri: "/admin/board/_next/static/chunk.js", + headers: { host: { value: "example.com" } }, + updateRequestOrigin: (o) => (origin = o), + }); + + await routeSite("test", siteMetadata()); + + expect(origin.domainName).toBe(S3_DOMAIN); + }); + + it("is unchanged for a site with no base", async () => { + const { event, routeSite } = loadRouteSite({ + uri: "/_next/static/css/abc123.css", + headers: { host: { value: "example.com" } }, + }); + + await routeSite("test", siteMetadata({ base: undefined })); + + expect(event.request.uri).toBe("/_assets/_next/static/css/abc123.css"); + }); + + it("keys a file found in the KV store without the base", async () => { + // The other branch that matches baseless and then keyed with the full + // uri. Its own comment says files are stored in the root. + const { event, routeSite } = loadRouteSite({ + uri: "/admin/board/about", + headers: { host: { value: "example.com" } }, + kvGet: async (key: string) => { + if (key === "test:/about.html") return ""; + throw new Error("missing"); + }, + }); + + await routeSite("test", siteMetadata()); + + expect(event.request.uri).toBe("/_assets/about.html"); + }); + + it("still appends index.html under a base", async () => { + const { event, routeSite } = loadRouteSite({ + uri: "/admin/board/docs/guide", + headers: { host: { value: "example.com" } }, + }); + + await routeSite( + "test", + siteMetadata({ + s3: { domain: S3_DOMAIN, dir: "/_assets", routes: ["/docs"] }, + }), + ); + + expect(event.request.uri).toBe("/_assets/docs/guide/index.html"); + }); + + it("leaves a path the S3 routes do not match to the server", async () => { + let origin: any; + const { routeSite } = loadRouteSite({ + uri: "/admin/board/b/123", + headers: { host: { value: "example.com" } }, + updateRequestOrigin: (o) => (origin = o), + }); + + await routeSite("test", siteMetadata()); + + expect(origin.domainName).toBe("server.lambda-url.us-east-1.on.aws"); + }); + }); });