this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add support for images CSP, disposition, and allow SVG (#776)

Co-authored-by: Magnus Dahl Eide <magnus@dahleide.com>

authored by

Victor Berchet
Magnus Dahl Eide
and committed by
GitHub
bd448a83 38fb247d

+119 -5
+5
.changeset/mean-sloths-sit.md
··· 1 + --- 2 + "@opennextjs/cloudflare": patch 3 + --- 4 + 5 + Add support for images CSP, disposition, and allow SVG
+15 -2
packages/cloudflare/src/cli/build/open-next/compile-images.ts
··· 18 18 ? JSON.parse(fs.readFileSync(imagesManifestPath, { encoding: "utf-8" })) 19 19 : {}; 20 20 21 + const __IMAGES_REMOTE_PATTERNS__ = JSON.stringify(imagesManifest?.images?.remotePatterns ?? []); 22 + const __IMAGES_LOCAL_PATTERNS__ = JSON.stringify(imagesManifest?.images?.localPatterns ?? []); 23 + const __IMAGES_ALLOW_SVG__ = JSON.stringify(Boolean(imagesManifest?.images?.dangerouslyAllowSVG)); 24 + const __IMAGES_CONTENT_SECURITY_POLICY__ = JSON.stringify( 25 + imagesManifest?.images?.contentSecurityPolicy ?? "script-src 'none'; frame-src 'none'; sandbox;" 26 + ); 27 + const __IMAGES_CONTENT_DISPOSITION__ = JSON.stringify( 28 + imagesManifest?.images?.contentDispositionType ?? "attachment" 29 + ); 30 + 21 31 await build({ 22 32 entryPoints: [imagesPath], 23 33 outdir: path.join(options.outputDir, "cloudflare"), ··· 27 37 target: "esnext", 28 38 platform: "node", 29 39 define: { 30 - __IMAGES_REMOTE_PATTERNS__: JSON.stringify(imagesManifest?.images?.remotePatterns ?? []), 31 - __IMAGES_LOCAL_PATTERNS__: JSON.stringify(imagesManifest?.images?.localPatterns ?? []), 40 + __IMAGES_REMOTE_PATTERNS__, 41 + __IMAGES_LOCAL_PATTERNS__, 42 + __IMAGES_ALLOW_SVG__, 43 + __IMAGES_CONTENT_SECURITY_POLICY__, 44 + __IMAGES_CONTENT_DISPOSITION__, 32 45 }, 33 46 }); 34 47 }
+98 -2
packages/cloudflare/src/cli/templates/images.ts
··· 19 19 * Local images (starting with a '/' as fetched using the passed fetcher). 20 20 * Remote images should match the configured remote patterns or a 404 response is returned. 21 21 */ 22 - export function fetchImage(fetcher: Fetcher | undefined, imageUrl: string) { 22 + export async function fetchImage(fetcher: Fetcher | undefined, imageUrl: string, ctx: ExecutionContext) { 23 23 // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/server/image-optimizer.ts#L208 24 24 if (!imageUrl || imageUrl.length > 3072 || imageUrl.startsWith("//")) { 25 25 return getUrlErrorResponse(); ··· 69 69 return getUrlErrorResponse(); 70 70 } 71 71 72 - return fetch(imageUrl, { cf: { cacheEverything: true } }); 72 + const imgResponse = await fetch(imageUrl, { cf: { cacheEverything: true } }); 73 + 74 + if (!imgResponse.body) { 75 + return imgResponse; 76 + } 77 + 78 + const buffer = new ArrayBuffer(32); 79 + 80 + try { 81 + let contentType: string | undefined; 82 + // body1 is eventually used for the response 83 + // body2 is used to detect the content type 84 + const [body1, body2] = imgResponse.body.tee(); 85 + const reader = body2.getReader({ mode: "byob" }); 86 + const { value } = await reader.read(new Uint8Array(buffer)); 87 + // Release resources by calling `reader.cancel()` 88 + // `ctx.waitUntil` keeps the runtime running until the promise settles without having to wait here. 89 + ctx.waitUntil(reader.cancel()); 90 + 91 + if (value) { 92 + contentType = detectContentType(value); 93 + } 94 + 95 + if (contentType && !(contentType === SVG && !__IMAGES_ALLOW_SVG__)) { 96 + const headers = new Headers(imgResponse.headers); 97 + headers.set("content-type", contentType); 98 + headers.set("content-disposition", __IMAGES_CONTENT_DISPOSITION__); 99 + headers.set("content-security-policy", __IMAGES_CONTENT_SECURITY_POLICY__); 100 + return new Response(body1, { ...imgResponse, headers }); 101 + } 102 + 103 + return new Response('"url" parameter is valid but image type is not allowed', { 104 + status: 400, 105 + }); 106 + } catch { 107 + return new Response('"url" parameter is valid but upstream response is invalid', { 108 + status: 400, 109 + }); 110 + } 73 111 } 74 112 75 113 export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean { ··· 113 151 return new Response(`"url" parameter is not allowed`, { status: 400 }); 114 152 } 115 153 154 + const AVIF = "image/avif"; 155 + const WEBP = "image/webp"; 156 + const PNG = "image/png"; 157 + const JPEG = "image/jpeg"; 158 + const GIF = "image/gif"; 159 + const SVG = "image/svg+xml"; 160 + const ICO = "image/x-icon"; 161 + const ICNS = "image/x-icns"; 162 + const TIFF = "image/tiff"; 163 + const BMP = "image/bmp"; 164 + 165 + /** 166 + * Detects the content type by looking at the first few bytes of a file 167 + * 168 + * Based on https://github.com/vercel/next.js/blob/72c9635/packages/next/src/server/image-optimizer.ts#L155 169 + * 170 + * @param buffer The image bytes 171 + * @returns a content type of undefined for unsupported content 172 + */ 173 + export function detectContentType(buffer: Uint8Array) { 174 + if ([0xff, 0xd8, 0xff].every((b, i) => buffer[i] === b)) { 175 + return JPEG; 176 + } 177 + if ([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((b, i) => buffer[i] === b)) { 178 + return PNG; 179 + } 180 + if ([0x47, 0x49, 0x46, 0x38].every((b, i) => buffer[i] === b)) { 181 + return GIF; 182 + } 183 + if ([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50].every((b, i) => !b || buffer[i] === b)) { 184 + return WEBP; 185 + } 186 + if ([0x3c, 0x3f, 0x78, 0x6d, 0x6c].every((b, i) => buffer[i] === b)) { 187 + return SVG; 188 + } 189 + if ([0x3c, 0x73, 0x76, 0x67].every((b, i) => buffer[i] === b)) { 190 + return SVG; 191 + } 192 + if ([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66].every((b, i) => !b || buffer[i] === b)) { 193 + return AVIF; 194 + } 195 + if ([0x00, 0x00, 0x01, 0x00].every((b, i) => buffer[i] === b)) { 196 + return ICO; 197 + } 198 + if ([0x69, 0x63, 0x6e, 0x73].every((b, i) => buffer[i] === b)) { 199 + return ICNS; 200 + } 201 + if ([0x49, 0x49, 0x2a, 0x00].every((b, i) => buffer[i] === b)) { 202 + return TIFF; 203 + } 204 + if ([0x42, 0x4d].every((b, i) => buffer[i] === b)) { 205 + return BMP; 206 + } 207 + } 208 + 116 209 /* eslint-disable no-var */ 117 210 declare global { 118 211 var __IMAGES_REMOTE_PATTERNS__: RemotePattern[]; 119 212 var __IMAGES_LOCAL_PATTERNS__: LocalPattern[]; 213 + var __IMAGES_ALLOW_SVG__: boolean; 214 + var __IMAGES_CONTENT_SECURITY_POLICY__: string; 215 + var __IMAGES_CONTENT_DISPOSITION__: string; 120 216 } 121 217 /* eslint-enable no-var */
+1 -1
packages/cloudflare/src/cli/templates/worker.ts
··· 41 41 // Fallback for the Next default image loader. 42 42 if (url.pathname === `${globalThis.__NEXT_BASE_PATH__}/_next/image`) { 43 43 const imageUrl = url.searchParams.get("url") ?? ""; 44 - return fetchImage(env.ASSETS, imageUrl); 44 + return await fetchImage(env.ASSETS, imageUrl, ctx); 45 45 } 46 46 47 47 // - `Request`s are handled by the Next server