@@ -2,14 +2,20 @@ const path = require("path");
2
2
3
3
const mime = require ( "mime-types" ) ;
4
4
5
+ const onFinishedStream = require ( "on-finished" ) ;
6
+
5
7
const getFilenameFromUrl = require ( "./utils/getFilenameFromUrl" ) ;
6
- const { setStatusCode, send, sendError } = require ( "./utils/compatibleAPI" ) ;
8
+ const { setStatusCode, send, pipe } = require ( "./utils/compatibleAPI" ) ;
7
9
const ready = require ( "./utils/ready" ) ;
10
+ const escapeHtml = require ( "./utils/escapeHtml" ) ;
8
11
9
12
/** @typedef {import("./index.js").NextFunction } NextFunction */
10
13
/** @typedef {import("./index.js").IncomingMessage } IncomingMessage */
11
14
/** @typedef {import("./index.js").ServerResponse } ServerResponse */
12
15
/** @typedef {import("./index.js").NormalizedHeaders } NormalizedHeaders */
16
+ /** @typedef {import("fs").ReadStream } ReadStream */
17
+
18
+ const BYTES_RANGE_REGEXP = / ^ * b y t e s / i;
13
19
14
20
/**
15
21
* @param {string } type
@@ -21,7 +27,55 @@ function getValueContentRangeHeader(type, size, range) {
21
27
return `${ type } ${ range ? `${ range . start } -${ range . end } ` : "*" } /${ size } ` ;
22
28
}
23
29
24
- const BYTES_RANGE_REGEXP = / ^ * b y t e s / i;
30
+ /**
31
+ * @param {import("fs").ReadStream } stream stream
32
+ * @param {boolean } suppress do need suppress?
33
+ * @returns {void }
34
+ */
35
+ function destroyStream ( stream , suppress ) {
36
+ if ( typeof stream . destroy === "function" ) {
37
+ stream . destroy ( ) ;
38
+ }
39
+
40
+ if ( typeof stream . close === "function" ) {
41
+ // Node.js core bug workaround
42
+ stream . on (
43
+ "open" ,
44
+ /**
45
+ * @this {import("fs").ReadStream}
46
+ */
47
+ function onOpenClose ( ) {
48
+ // @ts -ignore
49
+ if ( typeof this . fd === "number" ) {
50
+ // actually close down the fd
51
+ this . close ( ) ;
52
+ }
53
+ } ,
54
+ ) ;
55
+ }
56
+
57
+ if ( typeof stream . addListener === "function" && suppress ) {
58
+ stream . removeAllListeners ( "error" ) ;
59
+ stream . addListener ( "error" , ( ) => { } ) ;
60
+ }
61
+ }
62
+
63
+ /** @type {Record<number, string> } */
64
+ const statuses = {
65
+ 400 : "Bad Request" ,
66
+ 403 : "Forbidden" ,
67
+ 404 : "Not Found" ,
68
+ 416 : "Range Not Satisfiable" ,
69
+ 500 : "Internal Server Error" ,
70
+ } ;
71
+
72
+ /**
73
+ * @template {IncomingMessage} Request
74
+ * @template {ServerResponse} Response
75
+ * @typedef {Object } SendErrorOptions send error options
76
+ * @property {Record<string, number | string | string[] | undefined>= } headers headers
77
+ * @property {import("./index").ModifyResponseData<Request, Response>= } modifyResponseData modify response data callback
78
+ */
25
79
26
80
/**
27
81
* @template {IncomingMessage} Request
@@ -63,7 +117,65 @@ function wrapper(context) {
63
117
return ;
64
118
}
65
119
120
+ /**
121
+ * @param {number } status status
122
+ * @param {Partial<SendErrorOptions<Request, Response>>= } options options
123
+ * @returns {void }
124
+ */
125
+ function sendError ( status , options ) {
126
+ const content = statuses [ status ] || String ( status ) ;
127
+ let document = `<!DOCTYPE html>
128
+ <html lang="en">
129
+ <head>
130
+ <meta charset="utf-8">
131
+ <title>Error</title>
132
+ </head>
133
+ <body>
134
+ <pre>${ escapeHtml ( content ) } </pre>
135
+ </body>
136
+ </html>` ;
137
+
138
+ // Clear existing headers
139
+ const headers = res . getHeaderNames ( ) ;
140
+
141
+ for ( let i = 0 ; i < headers . length ; i ++ ) {
142
+ res . removeHeader ( headers [ i ] ) ;
143
+ }
144
+
145
+ if ( options && options . headers ) {
146
+ const keys = Object . keys ( options . headers ) ;
147
+
148
+ for ( let i = 0 ; i < keys . length ; i ++ ) {
149
+ const key = keys [ i ] ;
150
+ const value = options . headers [ key ] ;
151
+
152
+ if ( typeof value !== "undefined" ) {
153
+ res . setHeader ( key , value ) ;
154
+ }
155
+ }
156
+ }
157
+
158
+ // Send basic response
159
+ setStatusCode ( res , status ) ;
160
+ res . setHeader ( "Content-Type" , "text/html; charset=utf-8" ) ;
161
+ res . setHeader ( "Content-Security-Policy" , "default-src 'none'" ) ;
162
+ res . setHeader ( "X-Content-Type-Options" , "nosniff" ) ;
163
+
164
+ let byteLength = Buffer . byteLength ( document ) ;
165
+
166
+ if ( options && options . modifyResponseData ) {
167
+ ( { data : document , byteLength } =
168
+ /** @type {{data: string, byteLength: number } } */
169
+ ( options . modifyResponseData ( req , res , document , byteLength ) ) ) ;
170
+ }
171
+
172
+ res . setHeader ( "Content-Length" , byteLength ) ;
173
+
174
+ res . end ( document ) ;
175
+ }
176
+
66
177
async function processRequest ( ) {
178
+ // Pipe and SendFile
67
179
/** @type {import("./utils/getFilenameFromUrl").Extra } */
68
180
const extra = { } ;
69
181
const filename = getFilenameFromUrl (
@@ -77,7 +189,7 @@ function wrapper(context) {
77
189
context . logger . error ( `Malicious path "${ filename } ".` ) ;
78
190
}
79
191
80
- sendError ( req , res , extra . errorCode , {
192
+ sendError ( extra . errorCode , {
81
193
modifyResponseData : context . options . modifyResponseData ,
82
194
} ) ;
83
195
@@ -90,6 +202,7 @@ function wrapper(context) {
90
202
return ;
91
203
}
92
204
205
+ // Send logic
93
206
let { headers } = context . options ;
94
207
95
208
if ( typeof headers === "function" ) {
@@ -152,7 +265,7 @@ function wrapper(context) {
152
265
getValueContentRangeHeader ( "bytes" , len ) ,
153
266
) ;
154
267
155
- sendError ( req , res , 416 , {
268
+ sendError ( 416 , {
156
269
headers : {
157
270
"Content-Range" : res . getHeader ( "Content-Range" ) ,
158
271
} ,
@@ -190,10 +303,104 @@ function wrapper(context) {
190
303
const start = offset ;
191
304
const end = Math . max ( offset , offset + len - 1 ) ;
192
305
193
- send ( req , res , filename , start , end , goNext , {
194
- modifyResponseData : context . options . modifyResponseData ,
195
- outputFileSystem : context . outputFileSystem ,
306
+ // Stream logic
307
+ const isFsSupportsStream =
308
+ typeof context . outputFileSystem . createReadStream === "function" ;
309
+
310
+ /** @type {string | Buffer | ReadStream } */
311
+ let bufferOrStream ;
312
+ let byteLength ;
313
+
314
+ try {
315
+ if ( isFsSupportsStream ) {
316
+ bufferOrStream =
317
+ /** @type {import("fs").createReadStream } */
318
+ ( context . outputFileSystem . createReadStream ) ( filename , {
319
+ start,
320
+ end,
321
+ } ) ;
322
+
323
+ // Handle files with zero bytes
324
+ byteLength = end === 0 ? 0 : end - start + 1 ;
325
+ } else {
326
+ bufferOrStream = /** @type {import("fs").readFileSync } */ (
327
+ context . outputFileSystem . readFileSync
328
+ ) ( filename ) ;
329
+ ( { byteLength } = bufferOrStream ) ;
330
+ }
331
+ } catch ( _ignoreError ) {
332
+ await goNext ( ) ;
333
+
334
+ return ;
335
+ }
336
+
337
+ if ( context . options . modifyResponseData ) {
338
+ ( { data : bufferOrStream , byteLength } =
339
+ context . options . modifyResponseData (
340
+ req ,
341
+ res ,
342
+ bufferOrStream ,
343
+ byteLength ,
344
+ ) ) ;
345
+ }
346
+
347
+ res . setHeader ( "Content-Length" , byteLength ) ;
348
+
349
+ if ( req . method === "HEAD" ) {
350
+ // For Koa
351
+ if ( res . statusCode === 404 ) {
352
+ setStatusCode ( res , 200 ) ;
353
+ }
354
+
355
+ res . end ( ) ;
356
+ return ;
357
+ }
358
+
359
+ const isPipeSupports =
360
+ typeof (
361
+ /** @type {import("fs").ReadStream } */ ( bufferOrStream ) . pipe
362
+ ) === "function" ;
363
+
364
+ if ( ! isPipeSupports ) {
365
+ send ( res , /** @type {Buffer } */ ( bufferOrStream ) ) ;
366
+ return ;
367
+ }
368
+
369
+ // Cleanup
370
+ const cleanup = ( ) => {
371
+ destroyStream (
372
+ /** @type {import("fs").ReadStream } */ ( bufferOrStream ) ,
373
+ true ,
374
+ ) ;
375
+ } ;
376
+
377
+ // Error handling
378
+ /** @type {import("fs").ReadStream } */
379
+ ( bufferOrStream ) . on ( "error" , ( error ) => {
380
+ // clean up stream early
381
+ cleanup ( ) ;
382
+
383
+ // Handle Error
384
+ switch ( /** @type {NodeJS.ErrnoException } */ ( error ) . code ) {
385
+ case "ENAMETOOLONG" :
386
+ case "ENOENT" :
387
+ case "ENOTDIR" :
388
+ sendError ( 404 , {
389
+ modifyResponseData : context . options . modifyResponseData ,
390
+ } ) ;
391
+ break ;
392
+ default :
393
+ sendError ( 500 , {
394
+ modifyResponseData : context . options . modifyResponseData ,
395
+ } ) ;
396
+ break ;
397
+ }
196
398
} ) ;
399
+
400
+ pipe ( res , /** @type {ReadStream } */ ( bufferOrStream ) ) ;
401
+
402
+ // Response finished, cleanup
403
+ onFinishedStream ( res , cleanup ) ;
197
404
}
198
405
199
406
ready ( context , processRequest , req ) ;
0 commit comments