@@ -19,7 +19,7 @@ export type LocalPattern = {
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,7 +69,45 @@ export function fetchImage(fetcher: Fetcher | undefined, imageUrl: string) {
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,9 +151,67 @@ function getUrlErrorResponse() {
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 */
0 commit comments