Skip to content

Commit f69b638

Browse files
refactor: code
1 parent c64748e commit f69b638

File tree

5 files changed

+286
-327
lines changed

5 files changed

+286
-327
lines changed

src/middleware.js

Lines changed: 214 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@ const path = require("path");
22

33
const mime = require("mime-types");
44

5+
const onFinishedStream = require("on-finished");
6+
57
const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
6-
const { setStatusCode, send, sendError } = require("./utils/compatibleAPI");
8+
const { setStatusCode, send, pipe } = require("./utils/compatibleAPI");
79
const ready = require("./utils/ready");
10+
const escapeHtml = require("./utils/escapeHtml");
811

912
/** @typedef {import("./index.js").NextFunction} NextFunction */
1013
/** @typedef {import("./index.js").IncomingMessage} IncomingMessage */
1114
/** @typedef {import("./index.js").ServerResponse} ServerResponse */
1215
/** @typedef {import("./index.js").NormalizedHeaders} NormalizedHeaders */
16+
/** @typedef {import("fs").ReadStream} ReadStream */
17+
18+
const BYTES_RANGE_REGEXP = /^ *bytes/i;
1319

1420
/**
1521
* @param {string} type
@@ -21,7 +27,55 @@ function getValueContentRangeHeader(type, size, range) {
2127
return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`;
2228
}
2329

24-
const BYTES_RANGE_REGEXP = /^ *bytes/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+
*/
2579

2680
/**
2781
* @template {IncomingMessage} Request
@@ -63,7 +117,65 @@ function wrapper(context) {
63117
return;
64118
}
65119

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+
66177
async function processRequest() {
178+
// Pipe and SendFile
67179
/** @type {import("./utils/getFilenameFromUrl").Extra} */
68180
const extra = {};
69181
const filename = getFilenameFromUrl(
@@ -77,7 +189,7 @@ function wrapper(context) {
77189
context.logger.error(`Malicious path "${filename}".`);
78190
}
79191

80-
sendError(req, res, extra.errorCode, {
192+
sendError(extra.errorCode, {
81193
modifyResponseData: context.options.modifyResponseData,
82194
});
83195

@@ -90,6 +202,7 @@ function wrapper(context) {
90202
return;
91203
}
92204

205+
// Send logic
93206
let { headers } = context.options;
94207

95208
if (typeof headers === "function") {
@@ -152,7 +265,7 @@ function wrapper(context) {
152265
getValueContentRangeHeader("bytes", len),
153266
);
154267

155-
sendError(req, res, 416, {
268+
sendError(416, {
156269
headers: {
157270
"Content-Range": res.getHeader("Content-Range"),
158271
},
@@ -190,10 +303,104 @@ function wrapper(context) {
190303
const start = offset;
191304
const end = Math.max(offset, offset + len - 1);
192305

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+
}
196398
});
399+
400+
pipe(res, /** @type {ReadStream} */ (bufferOrStream));
401+
402+
// Response finished, cleanup
403+
onFinishedStream(res, cleanup);
197404
}
198405

199406
ready(context, processRequest, req);

0 commit comments

Comments
 (0)