Skip to content

Commit

Permalink
refactor: code
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait authored Mar 27, 2024
1 parent c64748e commit f69b638
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 327 deletions.
221 changes: 214 additions & 7 deletions src/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ const path = require("path");

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

const onFinishedStream = require("on-finished");

const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
const { setStatusCode, send, sendError } = require("./utils/compatibleAPI");
const { setStatusCode, send, pipe } = require("./utils/compatibleAPI");
const ready = require("./utils/ready");
const escapeHtml = require("./utils/escapeHtml");

/** @typedef {import("./index.js").NextFunction} NextFunction */
/** @typedef {import("./index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("./index.js").ServerResponse} ServerResponse */
/** @typedef {import("./index.js").NormalizedHeaders} NormalizedHeaders */
/** @typedef {import("fs").ReadStream} ReadStream */

const BYTES_RANGE_REGEXP = /^ *bytes/i;

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

const BYTES_RANGE_REGEXP = /^ *bytes/i;
/**
* @param {import("fs").ReadStream} stream stream
* @param {boolean} suppress do need suppress?
* @returns {void}
*/
function destroyStream(stream, suppress) {
if (typeof stream.destroy === "function") {
stream.destroy();
}

if (typeof stream.close === "function") {
// Node.js core bug workaround
stream.on(
"open",
/**
* @this {import("fs").ReadStream}
*/
function onOpenClose() {
// @ts-ignore
if (typeof this.fd === "number") {
// actually close down the fd
this.close();
}
},
);
}

if (typeof stream.addListener === "function" && suppress) {
stream.removeAllListeners("error");
stream.addListener("error", () => {});
}
}

/** @type {Record<number, string>} */
const statuses = {
400: "Bad Request",
403: "Forbidden",
404: "Not Found",
416: "Range Not Satisfiable",
500: "Internal Server Error",
};

/**
* @template {IncomingMessage} Request
* @template {ServerResponse} Response
* @typedef {Object} SendErrorOptions send error options
* @property {Record<string, number | string | string[] | undefined>=} headers headers
* @property {import("./index").ModifyResponseData<Request, Response>=} modifyResponseData modify response data callback
*/

/**
* @template {IncomingMessage} Request
Expand Down Expand Up @@ -63,7 +117,65 @@ function wrapper(context) {
return;
}

/**
* @param {number} status status
* @param {Partial<SendErrorOptions<Request, Response>>=} options options
* @returns {void}
*/
function sendError(status, options) {
const content = statuses[status] || String(status);
let document = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>${escapeHtml(content)}</pre>
</body>
</html>`;

// Clear existing headers
const headers = res.getHeaderNames();

for (let i = 0; i < headers.length; i++) {
res.removeHeader(headers[i]);
}

if (options && options.headers) {
const keys = Object.keys(options.headers);

for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = options.headers[key];

if (typeof value !== "undefined") {
res.setHeader(key, value);
}
}
}

// Send basic response
setStatusCode(res, status);
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.setHeader("Content-Security-Policy", "default-src 'none'");
res.setHeader("X-Content-Type-Options", "nosniff");

let byteLength = Buffer.byteLength(document);

if (options && options.modifyResponseData) {
({ data: document, byteLength } =
/** @type {{data: string, byteLength: number }} */
(options.modifyResponseData(req, res, document, byteLength)));
}

res.setHeader("Content-Length", byteLength);

res.end(document);
}

async function processRequest() {
// Pipe and SendFile
/** @type {import("./utils/getFilenameFromUrl").Extra} */
const extra = {};
const filename = getFilenameFromUrl(
Expand All @@ -77,7 +189,7 @@ function wrapper(context) {
context.logger.error(`Malicious path "${filename}".`);
}

sendError(req, res, extra.errorCode, {
sendError(extra.errorCode, {
modifyResponseData: context.options.modifyResponseData,
});

Expand All @@ -90,6 +202,7 @@ function wrapper(context) {
return;
}

// Send logic
let { headers } = context.options;

if (typeof headers === "function") {
Expand Down Expand Up @@ -152,7 +265,7 @@ function wrapper(context) {
getValueContentRangeHeader("bytes", len),
);

sendError(req, res, 416, {
sendError(416, {
headers: {
"Content-Range": res.getHeader("Content-Range"),
},
Expand Down Expand Up @@ -190,10 +303,104 @@ function wrapper(context) {
const start = offset;
const end = Math.max(offset, offset + len - 1);

send(req, res, filename, start, end, goNext, {
modifyResponseData: context.options.modifyResponseData,
outputFileSystem: context.outputFileSystem,
// Stream logic
const isFsSupportsStream =
typeof context.outputFileSystem.createReadStream === "function";

/** @type {string | Buffer | ReadStream} */
let bufferOrStream;
let byteLength;

try {
if (isFsSupportsStream) {
bufferOrStream =
/** @type {import("fs").createReadStream} */
(context.outputFileSystem.createReadStream)(filename, {
start,
end,
});

// Handle files with zero bytes
byteLength = end === 0 ? 0 : end - start + 1;
} else {
bufferOrStream = /** @type {import("fs").readFileSync} */ (
context.outputFileSystem.readFileSync
)(filename);
({ byteLength } = bufferOrStream);
}
} catch (_ignoreError) {
await goNext();

return;
}

if (context.options.modifyResponseData) {
({ data: bufferOrStream, byteLength } =
context.options.modifyResponseData(
req,
res,
bufferOrStream,
byteLength,
));
}

res.setHeader("Content-Length", byteLength);

if (req.method === "HEAD") {
// For Koa
if (res.statusCode === 404) {
setStatusCode(res, 200);
}

res.end();
return;
}

const isPipeSupports =
typeof (
/** @type {import("fs").ReadStream} */ (bufferOrStream).pipe
) === "function";

if (!isPipeSupports) {
send(res, /** @type {Buffer} */ (bufferOrStream));
return;
}

// Cleanup
const cleanup = () => {
destroyStream(
/** @type {import("fs").ReadStream} */ (bufferOrStream),
true,
);
};

// Error handling
/** @type {import("fs").ReadStream} */
(bufferOrStream).on("error", (error) => {
// clean up stream early
cleanup();

// Handle Error
switch (/** @type {NodeJS.ErrnoException} */ (error).code) {
case "ENAMETOOLONG":
case "ENOENT":
case "ENOTDIR":
sendError(404, {
modifyResponseData: context.options.modifyResponseData,
});
break;
default:
sendError(500, {
modifyResponseData: context.options.modifyResponseData,
});
break;
}
});

pipe(res, /** @type {ReadStream} */ (bufferOrStream));

// Response finished, cleanup
onFinishedStream(res, cleanup);
}

ready(context, processRequest, req);
Expand Down
Loading

0 comments on commit f69b638

Please sign in to comment.