diff --git a/.changeset/shaky-kings-stare.md b/.changeset/shaky-kings-stare.md new file mode 100644 index 00000000..45b7ea8c --- /dev/null +++ b/.changeset/shaky-kings-stare.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/cloudflare": patch +--- + +feat(images): implement localPatterns for images diff --git a/examples/playground14/e2e/cloudflare.spec.ts b/examples/playground14/e2e/cloudflare.spec.ts index 93585ba1..19762e0c 100644 --- a/examples/playground14/e2e/cloudflare.spec.ts +++ b/examples/playground14/e2e/cloudflare.spec.ts @@ -19,14 +19,38 @@ test.describe("playground/cloudflare", () => { await expect(res.json()).resolves.toEqual(expect.objectContaining({ PROCESS_ENV_VAR: "process.env" })); }); - test("fetch an image allowed by remotePatterns", async ({ page }) => { - const res = await page.request.get("/_next/image?url=https://avatars.githubusercontent.com/u/248818"); - expect(res.status()).toBe(200); - expect(res.headers()).toMatchObject({ "content-type": "image/jpeg" }); + test.describe("remotePatterns", () => { + test("fetch an image allowed by remotePatterns", async ({ page }) => { + const res = await page.request.get("/_next/image?url=https://avatars.githubusercontent.com/u/248818"); + expect(res.status()).toBe(200); + expect(res.headers()).toMatchObject({ "content-type": "image/jpeg" }); + }); + + test("400 when fetching an image disallowed by remotePatterns", async ({ page }) => { + const res = await page.request.get("/_next/image?url=https://avatars.githubusercontent.com/u/248817"); + expect(res.status()).toBe(400); + }); }); - test("404 when fetching an image disallowed by remotePatterns", async ({ page }) => { - const res = await page.request.get("/_next/image?url=https://avatars.githubusercontent.com/u/248817"); - expect(res.status()).toBe(400); + test.describe("localPatterns", () => { + test("fetch an image allowed by localPatterns", async ({ page }) => { + const res = await page.request.get("/_next/image?url=/snipp/snipp.webp?iscute=yes"); + expect(res.status()).toBe(200); + expect(res.headers()).toMatchObject({ "content-type": "image/webp" }); + }); + + test("400 when fetching an image disallowed by localPatterns with wrong query parameter", async ({ + page, + }) => { + const res = await page.request.get("/_next/image?url=/snipp/snipp?iscute=no"); + expect(res.status()).toBe(400); + }); + + test("400 when fetching an image disallowed by localPatterns without query parameter", async ({ + page, + }) => { + const res = await page.request.get("/_next/image?url=/snipp/snipp"); + expect(res.status()).toBe(400); + }); }); }); diff --git a/examples/playground14/next.config.mjs b/examples/playground14/next.config.mjs index e3c9c7e1..eb9c2ef3 100644 --- a/examples/playground14/next.config.mjs +++ b/examples/playground14/next.config.mjs @@ -19,6 +19,12 @@ const nextConfig = { pathname: "/u/248818", }, ], + localPatterns: [ + { + pathname: "/snipp/**", + search: "?iscute=yes", + }, + ], }, }; diff --git a/examples/playground14/public/snipp/snipp.webp b/examples/playground14/public/snipp/snipp.webp new file mode 100644 index 00000000..366de09d Binary files /dev/null and b/examples/playground14/public/snipp/snipp.webp differ diff --git a/packages/cloudflare/src/cli/templates/images.spec.ts b/packages/cloudflare/src/cli/templates/images.spec.ts index 03c9e839..62375f1a 100644 --- a/packages/cloudflare/src/cli/templates/images.spec.ts +++ b/packages/cloudflare/src/cli/templates/images.spec.ts @@ -1,56 +1,56 @@ -/** - * See https://github.com/vercel/next.js/blob/64702a9/test/unit/image-optimizer/match-remote-pattern.test.ts - */ - import pm from "picomatch"; import { describe, expect, it } from "vitest"; -import { matchRemotePattern as m } from "./images.js"; +import type { LocalPattern } from "./images.js"; +import { matchLocalPattern, matchRemotePattern as mRP } from "./images.js"; +/** + * See https://github.com/vercel/next.js/blob/64702a9/test/unit/image-optimizer/match-remote-pattern.test.ts + */ describe("matchRemotePattern", () => { it("should match literal hostname", () => { const p = { hostname: pm.makeRe("example.com") } as const; - expect(m(p, new URL("https://example.com"))).toBe(true); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://example.net"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com/path"))).toBe(true); - expect(m(p, new URL("https://example.com/path/to"))).toBe(true); - expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true); - expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(true); - expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(true); - expect(m(p, new URL("http://example.com:81/path/to/file"))).toBe(true); + expect(mRP(p, new URL("https://example.com"))).toBe(true); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://example.net"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path"))).toBe(true); + expect(mRP(p, new URL("https://example.com/path/to"))).toBe(true); + expect(mRP(p, new URL("https://example.com/path/to/file"))).toBe(true); + expect(mRP(p, new URL("https://example.com:81/path/to/file"))).toBe(true); + expect(mRP(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(true); + expect(mRP(p, new URL("http://example.com:81/path/to/file"))).toBe(true); }); it("should match literal protocol and hostname", () => { const p = { protocol: "https", hostname: pm.makeRe("example.com") } as const; - expect(m(p, new URL("https://example.com"))).toBe(true); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com/path/to"))).toBe(true); - expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true); - expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true); - expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(true); - expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(true); - expect(m(p, new URL("http://example.com:81/path/to/file"))).toBe(false); - expect(m(p, new URL("ftp://example.com:81/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(true); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path/to"))).toBe(true); + expect(mRP(p, new URL("https://example.com/path/to/file"))).toBe(true); + expect(mRP(p, new URL("https://example.com/path/to/file"))).toBe(true); + expect(mRP(p, new URL("https://example.com:81/path/to/file"))).toBe(true); + expect(mRP(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(true); + expect(mRP(p, new URL("http://example.com:81/path/to/file"))).toBe(false); + expect(mRP(p, new URL("ftp://example.com:81/path/to/file"))).toBe(false); }); it("should match literal protocol, hostname, no port", () => { const p = { protocol: "https", hostname: pm.makeRe("example.com"), port: "" } as const; - expect(m(p, new URL("https://example.com"))).toBe(true); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true); - expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(true); - expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false); - expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); - expect(m(p, new URL("http://example.com:81/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(true); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path/to/file"))).toBe(true); + expect(mRP(p, new URL("https://example.com/path/to/file?q=1"))).toBe(true); + expect(mRP(p, new URL("http://example.com/path/to/file"))).toBe(false); + expect(mRP(p, new URL("ftp://example.com/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com:81/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); + expect(mRP(p, new URL("http://example.com:81/path/to/file"))).toBe(false); }); it("should match literal protocol, hostname, no port, no search", () => { @@ -60,17 +60,17 @@ describe("matchRemotePattern", () => { port: "", search: "", } as const; - expect(m(p, new URL("https://example.com"))).toBe(true); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true); - expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false); - expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false); - expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); - expect(m(p, new URL("http://example.com:81/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(true); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path/to/file"))).toBe(true); + expect(mRP(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false); + expect(mRP(p, new URL("http://example.com/path/to/file"))).toBe(false); + expect(mRP(p, new URL("ftp://example.com/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com:81/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); + expect(mRP(p, new URL("http://example.com:81/path/to/file"))).toBe(false); }); it("should match literal protocol, hostname, port 42", () => { @@ -79,25 +79,25 @@ describe("matchRemotePattern", () => { hostname: pm.makeRe("example.com"), port: "42", } as const; - expect(m(p, new URL("https://example.com:42"))).toBe(true); - expect(m(p, new URL("https://example.com.uk:42"))).toBe(false); - expect(m(p, new URL("https://sub.example.com:42"))).toBe(false); - expect(m(p, new URL("https://com:42"))).toBe(false); - expect(m(p, new URL("https://example.com:42/path/to/file"))).toBe(true); - expect(m(p, new URL("https://example.com:42/path/to/file?q=1"))).toBe(true); - expect(m(p, new URL("http://example.com:42/path/to/file"))).toBe(false); - expect(m(p, new URL("ftp://example.com:42/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com"))).toBe(false); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false); - expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false); - expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com:81"))).toBe(false); - expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42"))).toBe(true); + expect(mRP(p, new URL("https://example.com.uk:42"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com:42"))).toBe(false); + expect(mRP(p, new URL("https://com:42"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path/to/file"))).toBe(true); + expect(mRP(p, new URL("https://example.com:42/path/to/file?q=1"))).toBe(true); + expect(mRP(p, new URL("http://example.com:42/path/to/file"))).toBe(false); + expect(mRP(p, new URL("ftp://example.com:42/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false); + expect(mRP(p, new URL("http://example.com/path/to/file"))).toBe(false); + expect(mRP(p, new URL("ftp://example.com/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com:81"))).toBe(false); + expect(mRP(p, new URL("https://example.com:81/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); }); it("should match literal protocol, hostname, port, pathname", () => { @@ -107,27 +107,27 @@ describe("matchRemotePattern", () => { port: "42", pathname: pm.makeRe("/path/to/file", { dot: true }), } as const; - expect(m(p, new URL("https://example.com:42"))).toBe(false); - expect(m(p, new URL("https://example.com.uk:42"))).toBe(false); - expect(m(p, new URL("https://sub.example.com:42"))).toBe(false); - expect(m(p, new URL("https://example.com:42/path"))).toBe(false); - expect(m(p, new URL("https://example.com:42/path/to"))).toBe(false); - expect(m(p, new URL("https://example.com:42/file"))).toBe(false); - expect(m(p, new URL("https://example.com:42/path/to/file"))).toBe(true); - expect(m(p, new URL("https://example.com:42/path/to/file?q=1"))).toBe(true); - expect(m(p, new URL("http://example.com:42/path/to/file"))).toBe(false); - expect(m(p, new URL("ftp://example.com:42/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com"))).toBe(false); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://example.com/path"))).toBe(false); - expect(m(p, new URL("https://example.com/path/to"))).toBe(false); - expect(m(p, new URL("https://example.com/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false); - expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false); - expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk:42"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com:42"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path/to"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path/to/file"))).toBe(true); + expect(mRP(p, new URL("https://example.com:42/path/to/file?q=1"))).toBe(true); + expect(mRP(p, new URL("http://example.com:42/path/to/file"))).toBe(false); + expect(mRP(p, new URL("ftp://example.com:42/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path/to"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false); + expect(mRP(p, new URL("http://example.com/path/to/file"))).toBe(false); + expect(mRP(p, new URL("ftp://example.com/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com:81/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); }); it("should match literal protocol, hostname, port, pathname, search", () => { @@ -138,109 +138,109 @@ describe("matchRemotePattern", () => { pathname: pm.makeRe("/path/to/file", { dot: true }), search: "?q=1&a=two&s=!@$^&-_+/()[]{};:~", } as const; - expect(m(p, new URL("https://example.com:42"))).toBe(false); - expect(m(p, new URL("https://example.com.uk:42"))).toBe(false); - expect(m(p, new URL("https://sub.example.com:42"))).toBe(false); - expect(m(p, new URL("https://example.com:42/path"))).toBe(false); - expect(m(p, new URL("https://example.com:42/path/to"))).toBe(false); - expect(m(p, new URL("https://example.com:42/file"))).toBe(false); - expect(m(p, new URL("https://example.com:42/path/to/file"))).toBe(false); - expect(m(p, new URL("http://example.com:42/path/to/file"))).toBe(false); - expect(m(p, new URL("ftp://example.com:42/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com"))).toBe(false); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://example.com/path"))).toBe(false); - expect(m(p, new URL("https://example.com/path/to"))).toBe(false); - expect(m(p, new URL("https://example.com/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false); - expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false); - expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false); - expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); - expect(m(p, new URL("https://example.com:42/path/to/file?q=1"))).toBe(false); - expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two"))).toBe(false); - expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s"))).toBe(false); - expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s="))).toBe(false); - expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s=!@"))).toBe(false); - expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s=!@$^&-_+/()[]{};:~"))).toBe(true); - expect(m(p, new URL("https://example.com:42/path/to/file?q=1&s=!@$^&-_+/()[]{};:~&a=two"))).toBe(false); - expect(m(p, new URL("https://example.com:42/path/to/file?a=two&q=1&s=!@$^&-_+/()[]{};:~"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk:42"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com:42"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path/to"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path/to/file"))).toBe(false); + expect(mRP(p, new URL("http://example.com:42/path/to/file"))).toBe(false); + expect(mRP(p, new URL("ftp://example.com:42/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path/to"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false); + expect(mRP(p, new URL("http://example.com/path/to/file"))).toBe(false); + expect(mRP(p, new URL("ftp://example.com/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com:81/path/to/file"))).toBe(false); + expect(mRP(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path/to/file?q=1"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path/to/file?q=1&a=two"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s="))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s=!@"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s=!@$^&-_+/()[]{};:~"))).toBe(true); + expect(mRP(p, new URL("https://example.com:42/path/to/file?q=1&s=!@$^&-_+/()[]{};:~&a=two"))).toBe(false); + expect(mRP(p, new URL("https://example.com:42/path/to/file?a=two&q=1&s=!@$^&-_+/()[]{};:~"))).toBe(false); }); it("should match hostname pattern with single asterisk by itself", () => { const p = { hostname: pm.makeRe("avatars.*.example.com") } as const; - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false); - expect(m(p, new URL("https://avatars.example.com"))).toBe(false); - expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(true); - expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true); - expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://avatars.example.com"))).toBe(false); + expect(mRP(p, new URL("https://avatars.sfo1.example.com"))).toBe(true); + expect(mRP(p, new URL("https://avatars.iad1.example.com"))).toBe(true); + expect(mRP(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false); }); it("should match hostname pattern with single asterisk at beginning", () => { const p = { hostname: pm.makeRe("avatars.*1.example.com") } as const; - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false); - expect(m(p, new URL("https://avatars.example.com"))).toBe(false); - expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(true); - expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true); - expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false); - expect(m(p, new URL("https://avatars.sfo2.example.com"))).toBe(false); - expect(m(p, new URL("https://avatars.iad2.example.com"))).toBe(false); - expect(m(p, new URL("https://avatars.1.example.com"))).toBe(true); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://avatars.example.com"))).toBe(false); + expect(mRP(p, new URL("https://avatars.sfo1.example.com"))).toBe(true); + expect(mRP(p, new URL("https://avatars.iad1.example.com"))).toBe(true); + expect(mRP(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false); + expect(mRP(p, new URL("https://avatars.sfo2.example.com"))).toBe(false); + expect(mRP(p, new URL("https://avatars.iad2.example.com"))).toBe(false); + expect(mRP(p, new URL("https://avatars.1.example.com"))).toBe(true); }); it("should match hostname pattern with single asterisk in middle", () => { const p = { hostname: pm.makeRe("avatars.*a*.example.com") } as const; - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false); - expect(m(p, new URL("https://avatars.example.com"))).toBe(false); - expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(false); - expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true); - expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false); - expect(m(p, new URL("https://avatars.sfo2.example.com"))).toBe(false); - expect(m(p, new URL("https://avatars.iad2.example.com"))).toBe(true); - expect(m(p, new URL("https://avatars.a.example.com"))).toBe(true); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://avatars.example.com"))).toBe(false); + expect(mRP(p, new URL("https://avatars.sfo1.example.com"))).toBe(false); + expect(mRP(p, new URL("https://avatars.iad1.example.com"))).toBe(true); + expect(mRP(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false); + expect(mRP(p, new URL("https://avatars.sfo2.example.com"))).toBe(false); + expect(mRP(p, new URL("https://avatars.iad2.example.com"))).toBe(true); + expect(mRP(p, new URL("https://avatars.a.example.com"))).toBe(true); }); it("should match hostname pattern with single asterisk at end", () => { const p = { hostname: pm.makeRe("avatars.ia*.example.com") } as const; - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false); - expect(m(p, new URL("https://avatars.example.com"))).toBe(false); - expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(false); - expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true); - expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false); - expect(m(p, new URL("https://avatars.sfo2.example.com"))).toBe(false); - expect(m(p, new URL("https://avatars.iad2.example.com"))).toBe(true); - expect(m(p, new URL("https://avatars.ia.example.com"))).toBe(true); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://avatars.example.com"))).toBe(false); + expect(mRP(p, new URL("https://avatars.sfo1.example.com"))).toBe(false); + expect(mRP(p, new URL("https://avatars.iad1.example.com"))).toBe(true); + expect(mRP(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false); + expect(mRP(p, new URL("https://avatars.sfo2.example.com"))).toBe(false); + expect(mRP(p, new URL("https://avatars.iad2.example.com"))).toBe(true); + expect(mRP(p, new URL("https://avatars.ia.example.com"))).toBe(true); }); it("should match hostname pattern with double asterisk", () => { const p = { hostname: pm.makeRe("**.example.com") } as const; - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(true); - expect(m(p, new URL("https://deep.sub.example.com"))).toBe(true); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false); - expect(m(p, new URL("https://avatars.example.com"))).toBe(true); - expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(true); - expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true); - expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(true); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(true); + expect(mRP(p, new URL("https://deep.sub.example.com"))).toBe(true); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://avatars.example.com"))).toBe(true); + expect(mRP(p, new URL("https://avatars.sfo1.example.com"))).toBe(true); + expect(mRP(p, new URL("https://avatars.iad1.example.com"))).toBe(true); + expect(mRP(p, new URL("https://more.avatars.iad1.example.com"))).toBe(true); }); it("should match pathname pattern with single asterisk by itself", () => { @@ -248,22 +248,22 @@ describe("matchRemotePattern", () => { hostname: pm.makeRe("example.com"), pathname: pm.makeRe("/act123/*/pic.jpg", { dot: true }), } as const; - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://example.com/act123"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/usr6/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/team/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act456/team/pic.jpg"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/.a/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/team/usr4/pic.jpg"))).toBe(false); - expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/usr6/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/team/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act456/team/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/.a/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/team/usr4/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); }); it("should match pathname pattern with single asterisk at the beginning", () => { @@ -271,20 +271,20 @@ describe("matchRemotePattern", () => { hostname: pm.makeRe("example.com"), pathname: pm.makeRe("/act123/*4/pic.jpg", { dot: true }), } as const; - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://example.com/act123"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/team4/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act456/team5/pic.jpg"))).toBe(false); - expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/4/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/team4/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act456/team5/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/4/pic.jpg"))).toBe(true); }); it("should match pathname pattern with single asterisk in the middle", () => { @@ -292,21 +292,21 @@ describe("matchRemotePattern", () => { hostname: pm.makeRe("example.com"), pathname: pm.makeRe("/act123/*sr*/pic.jpg", { dot: true }), } as const; - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://example.com/act123"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/.sr6/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/team4/pic.jpg"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/team5/pic.jpg"))).toBe(false); - expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/sr/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/.sr6/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/team4/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/team5/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/sr/pic.jpg"))).toBe(true); }); it("should match pathname pattern with single asterisk at the end", () => { @@ -314,21 +314,21 @@ describe("matchRemotePattern", () => { hostname: pm.makeRe("example.com"), pathname: pm.makeRe("/act123/usr*/pic.jpg", { dot: true }), } as const; - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://example.com/act123"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false); - expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/usr/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/team4/pic.jpg"))).toBe(false); - expect(m(p, new URL("https://example.com/act456/team5/pic.jpg"))).toBe(false); - expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); - expect(m(p, new URL("https://sub.example.com/act123/usr6/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/usr/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/team4/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act456/team5/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com/act123/usr6/pic.jpg"))).toBe(false); }); it("should match pathname pattern with double asterisk", () => { @@ -336,28 +336,93 @@ describe("matchRemotePattern", () => { hostname: pm.makeRe("example.com"), pathname: pm.makeRe("/act123/**", { dot: true }), } as const; - expect(m(p, new URL("https://com"))).toBe(false); - expect(m(p, new URL("https://example.com"))).toBe(false); - expect(m(p, new URL("https://sub.example.com"))).toBe(false); - expect(m(p, new URL("https://example.com.uk"))).toBe(false); - expect(m(p, new URL("https://example.com/act123"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/usr6/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/team/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/.a/pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act123/team/.pic.jpg"))).toBe(true); - expect(m(p, new URL("https://example.com/act456/team/pic.jpg"))).toBe(false); - expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); - expect(m(p, new URL("https://sub.example.com/act123/team/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://com"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com.uk"))).toBe(false); + expect(mRP(p, new URL("https://example.com/act123"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/usr4"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/usr4/pic"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/usr6/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/team/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/.a/pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act123/team/.pic.jpg"))).toBe(true); + expect(mRP(p, new URL("https://example.com/act456/team/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); + expect(mRP(p, new URL("https://sub.example.com/act123/team/pic.jpg"))).toBe(false); }); it("should throw when hostname is missing", () => { const p = { protocol: "https" } as const; // @ts-ignore testing invalid input - expect(m(p, new URL("https://example.com"))).toBe(false); + expect(mRP(p, new URL("https://example.com"))).toBe(false); + }); +}); + +/** + * See https://github.com/vercel/next.js/blob/64702a9/test/unit/image-optimizer/match-local-pattern.test.ts + */ +describe("matchLocalPattern", () => { + const mLP = (p: LocalPattern, urlPathAndQuery: string) => + matchLocalPattern(p, new URL(urlPathAndQuery, "http://n")); + + it("should match anything when no pattern is defined", () => { + const p = {} as const; + expect(mLP(p, "/")).toBe(true); + expect(mLP(p, "/path")).toBe(true); + expect(mLP(p, "/path/to")).toBe(true); + expect(mLP(p, "/path/to/file")).toBe(true); + expect(mLP(p, "/path/to/file.txt")).toBe(true); + expect(mLP(p, "/path/to/file?q=1")).toBe(true); + expect(mLP(p, "/path/to/file?q=1&a=two")).toBe(true); + }); + + it("should match any path without a search query string", () => { + const p = { + search: "", + } as const; + expect(mLP(p, "/")).toBe(true); + expect(mLP(p, "/path")).toBe(true); + expect(mLP(p, "/path/to")).toBe(true); + expect(mLP(p, "/path/to/file")).toBe(true); + expect(mLP(p, "/path/to/file.txt")).toBe(true); + expect(mLP(p, "/path/to/file?q=1")).toBe(false); + expect(mLP(p, "/path/to/file?q=1&a=two")).toBe(false); + expect(mLP(p, "/path/to/file.txt?q=1&a=two")).toBe(false); + }); + + it("should match literal pathname and any search query string", () => { + const p = { + pathname: pm.makeRe("/path/to/file", { + dot: true, + }), + } as const; + expect(mLP(p, "/")).toBe(false); + expect(mLP(p, "/path")).toBe(false); + expect(mLP(p, "/path/to")).toBe(false); + expect(mLP(p, "/path/to/file")).toBe(true); + expect(mLP(p, "/path/to/file.txt")).toBe(false); + expect(mLP(p, "/path/to/file?q=1")).toBe(true); + expect(mLP(p, "/path/to/file?q=1&a=two")).toBe(true); + expect(mLP(p, "/path/to/file.txt?q=1&a=two")).toBe(false); + }); + + it("should match pathname with double asterisk", () => { + const p = { + pathname: pm.makeRe("/path/to/**", { + dot: true, + }), + } as const; + expect(mLP(p, "/")).toBe(false); + expect(mLP(p, "/path")).toBe(false); + expect(mLP(p, "/path/to")).toBe(true); + expect(mLP(p, "/path/to/file")).toBe(true); + expect(mLP(p, "/path/to/file.txt")).toBe(true); + expect(mLP(p, "/path/to/file?q=1")).toBe(true); + expect(mLP(p, "/path/to/file?q=1&a=two")).toBe(true); + expect(mLP(p, "/path/to/file.txt?q=1&a=two")).toBe(true); }); }); diff --git a/packages/cloudflare/src/cli/templates/images.ts b/packages/cloudflare/src/cli/templates/images.ts index 280eb781..7c9f4f58 100644 --- a/packages/cloudflare/src/cli/templates/images.ts +++ b/packages/cloudflare/src/cli/templates/images.ts @@ -7,6 +7,12 @@ export type RemotePattern = { search?: string; }; +export type LocalPattern = { + // pathname is always set in the manifest + pathname: string; + search?: string; +}; + /** * Fetches an images. * @@ -22,16 +28,27 @@ export function fetchImage(fetcher: Fetcher | undefined, imageUrl: string) { // Local if (imageUrl.startsWith("/")) { let pathname: string; + let url: URL; try { - const url = new URL(imageUrl, "http://n"); + // We only need pathname and search + url = new URL(imageUrl, "http://n"); pathname = decodeURIComponent(url.pathname); } catch { return getUrlErrorResponse(); } + if (/\/_next\/image($|\/)/.test(pathname)) { return getUrlErrorResponse(); } + // If localPatterns are not defined all local images are allowed. + if ( + __IMAGES_LOCAL_PATTERNS__.length > 0 && + !__IMAGES_LOCAL_PATTERNS__.some((p: LocalPattern) => matchLocalPattern(p, url)) + ) { + return getUrlErrorResponse(); + } + return fetcher?.fetch(`http://assets.local${imageUrl}`); } @@ -47,6 +64,7 @@ export function fetchImage(fetcher: Fetcher | undefined, imageUrl: string) { return getUrlErrorResponse(); } + // The remotePatterns is used to allow images from specific remote external paths and block all others. if (!__IMAGES_REMOTE_PATTERNS__.some((p: RemotePattern) => matchRemotePattern(p, url))) { return getUrlErrorResponse(); } @@ -83,6 +101,15 @@ export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean { return true; } +export function matchLocalPattern(pattern: LocalPattern, url: URL): boolean { + // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/shared/lib/match-local-pattern.ts + if (pattern.search !== undefined && pattern.search !== url.search) { + return false; + } + + return new RegExp(pattern.pathname).test(url.pathname); +} + /** * @returns same error as Next.js when the url query parameter is not accepted. */ @@ -93,6 +120,6 @@ function getUrlErrorResponse() { /* eslint-disable no-var */ declare global { var __IMAGES_REMOTE_PATTERNS__: RemotePattern[]; - var __IMAGES_LOCAL_PATTERNS__: unknown[]; + var __IMAGES_LOCAL_PATTERNS__: LocalPattern[]; } /* eslint-enable no-var */