@@ -19,7 +19,7 @@ export type LocalPattern = {
1919 * Local images (starting with a '/' as fetched using the passed fetcher).
2020 * Remote images should match the configured remote patterns or a 404 response is returned.
2121 */
22- export function fetchImage ( fetcher : Fetcher | undefined , imageUrl : string ) {
22+ export async function fetchImage ( fetcher : Fetcher | undefined , imageUrl : string , ctx : ExecutionContext ) {
2323 // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/server/image-optimizer.ts#L208
2424 if ( ! imageUrl || imageUrl . length > 3072 || imageUrl . startsWith ( "//" ) ) {
2525 return getUrlErrorResponse ( ) ;
@@ -69,7 +69,45 @@ export function fetchImage(fetcher: Fetcher | undefined, imageUrl: string) {
6969 return getUrlErrorResponse ( ) ;
7070 }
7171
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+ }
73111}
74112
75113export function matchRemotePattern ( pattern : RemotePattern , url : URL ) : boolean {
@@ -113,9 +151,67 @@ function getUrlErrorResponse() {
113151 return new Response ( `"url" parameter is not allowed` , { status : 400 } ) ;
114152}
115153
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+
116209/* eslint-disable no-var */
117210declare global {
118211 var __IMAGES_REMOTE_PATTERNS__ : RemotePattern [ ] ;
119212 var __IMAGES_LOCAL_PATTERNS__ : LocalPattern [ ] ;
213+ var __IMAGES_ALLOW_SVG__ : boolean ;
214+ var __IMAGES_CONTENT_SECURITY_POLICY__ : string ;
215+ var __IMAGES_CONTENT_DISPOSITION__ : string ;
120216}
121217/* eslint-enable no-var */
0 commit comments