@@ -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,43 @@ 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+ ctx . waitUntil ( reader . cancel ( ) ) ;
88+
89+ if ( value ) {
90+ contentType = detectContentType ( value ) ;
91+ }
92+
93+ if ( contentType && ! ( contentType === SVG && ! __IMAGES_ALLOW_SVG__ ) ) {
94+ const headers = new Headers ( imgResponse . headers ) ;
95+ headers . set ( "content-type" , contentType ) ;
96+ headers . set ( "content-disposition" , __IMAGES_CONTENT_DISPOSITION__ ) ;
97+ headers . set ( "content-security-policy" , __IMAGES_CONTENT_SECURITY_POLICY__ ) ;
98+ return new Response ( body1 , { ...imgResponse , headers } ) ;
99+ }
100+
101+ return new Response ( '"url" parameter is valid but image type is not allowed' , {
102+ status : 400 ,
103+ } ) ;
104+ } catch {
105+ return new Response ( '"url" parameter is valid but upstream response is invalid' , {
106+ status : 400 ,
107+ } ) ;
108+ }
73109}
74110
75111export function matchRemotePattern ( pattern : RemotePattern , url : URL ) : boolean {
@@ -113,9 +149,67 @@ function getUrlErrorResponse() {
113149 return new Response ( `"url" parameter is not allowed` , { status : 400 } ) ;
114150}
115151
152+ const AVIF = "image/avif" ;
153+ const WEBP = "image/webp" ;
154+ const PNG = "image/png" ;
155+ const JPEG = "image/jpeg" ;
156+ const GIF = "image/gif" ;
157+ const SVG = "image/svg+xml" ;
158+ const ICO = "image/x-icon" ;
159+ const ICNS = "image/x-icns" ;
160+ const TIFF = "image/tiff" ;
161+ const BMP = "image/bmp" ;
162+
163+ /**
164+ * Detects the content type by looking at the first few bytes of a file
165+ *
166+ * Based on https://github.com/vercel/next.js/blob/72c9635/packages/next/src/server/image-optimizer.ts#L155
167+ *
168+ * @param buffer The image bytes
169+ * @returns a content type of undefined for unsupported content
170+ */
171+ export function detectContentType ( buffer : Uint8Array ) {
172+ if ( [ 0xff , 0xd8 , 0xff ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
173+ return JPEG ;
174+ }
175+ if ( [ 0x89 , 0x50 , 0x4e , 0x47 , 0x0d , 0x0a , 0x1a , 0x0a ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
176+ return PNG ;
177+ }
178+ if ( [ 0x47 , 0x49 , 0x46 , 0x38 ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
179+ return GIF ;
180+ }
181+ if ( [ 0x52 , 0x49 , 0x46 , 0x46 , 0 , 0 , 0 , 0 , 0x57 , 0x45 , 0x42 , 0x50 ] . every ( ( b , i ) => ! b || buffer [ i ] === b ) ) {
182+ return WEBP ;
183+ }
184+ if ( [ 0x3c , 0x3f , 0x78 , 0x6d , 0x6c ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
185+ return SVG ;
186+ }
187+ if ( [ 0x3c , 0x73 , 0x76 , 0x67 ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
188+ return SVG ;
189+ }
190+ if ( [ 0 , 0 , 0 , 0 , 0x66 , 0x74 , 0x79 , 0x70 , 0x61 , 0x76 , 0x69 , 0x66 ] . every ( ( b , i ) => ! b || buffer [ i ] === b ) ) {
191+ return AVIF ;
192+ }
193+ if ( [ 0x00 , 0x00 , 0x01 , 0x00 ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
194+ return ICO ;
195+ }
196+ if ( [ 0x69 , 0x63 , 0x6e , 0x73 ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
197+ return ICNS ;
198+ }
199+ if ( [ 0x49 , 0x49 , 0x2a , 0x00 ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
200+ return TIFF ;
201+ }
202+ if ( [ 0x42 , 0x4d ] . every ( ( b , i ) => buffer [ i ] === b ) ) {
203+ return BMP ;
204+ }
205+ }
206+
116207/* eslint-disable no-var */
117208declare global {
118209 var __IMAGES_REMOTE_PATTERNS__ : RemotePattern [ ] ;
119210 var __IMAGES_LOCAL_PATTERNS__ : LocalPattern [ ] ;
211+ var __IMAGES_ALLOW_SVG__ : boolean ;
212+ var __IMAGES_CONTENT_SECURITY_POLICY__ : string ;
213+ var __IMAGES_CONTENT_DISPOSITION__ : string ;
120214}
121215/* eslint-enable no-var */
0 commit comments