226 lines
7.8 KiB
JavaScript
226 lines
7.8 KiB
JavaScript
"use strict";
|
|
|
|
const path = require("path");
|
|
const mime = require("mime-types");
|
|
const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
|
|
const {
|
|
getHeaderNames,
|
|
getHeaderFromRequest,
|
|
getHeaderFromResponse,
|
|
setHeaderForResponse,
|
|
setStatusCode,
|
|
send,
|
|
sendError
|
|
} = require("./utils/compatibleAPI");
|
|
const ready = require("./utils/ready");
|
|
|
|
/** @typedef {import("./index.js").NextFunction} NextFunction */
|
|
/** @typedef {import("./index.js").IncomingMessage} IncomingMessage */
|
|
/** @typedef {import("./index.js").ServerResponse} ServerResponse */
|
|
|
|
/**
|
|
* @param {string} type
|
|
* @param {number} size
|
|
* @param {import("range-parser").Range} [range]
|
|
* @returns {string}
|
|
*/
|
|
function getValueContentRangeHeader(type, size, range) {
|
|
return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`;
|
|
}
|
|
|
|
/**
|
|
* @param {string | number} title
|
|
* @param {string} body
|
|
* @returns {string}
|
|
*/
|
|
function createHtmlDocument(title, body) {
|
|
return `${"<!DOCTYPE html>\n" + '<html lang="en">\n' + "<head>\n" + '<meta charset="utf-8">\n' + "<title>"}${title}</title>\n` + `</head>\n` + `<body>\n` + `<pre>${body}</pre>\n` + `</body>\n` + `</html>\n`;
|
|
}
|
|
const BYTES_RANGE_REGEXP = /^ *bytes/i;
|
|
|
|
/**
|
|
* @template {IncomingMessage} Request
|
|
* @template {ServerResponse} Response
|
|
* @param {import("./index.js").Context<Request, Response>} context
|
|
* @return {import("./index.js").Middleware<Request, Response>}
|
|
*/
|
|
function wrapper(context) {
|
|
return async function middleware(req, res, next) {
|
|
const acceptedMethods = context.options.methods || ["GET", "HEAD"];
|
|
|
|
// fixes #282. credit @cexoso. in certain edge situations res.locals is undefined.
|
|
// eslint-disable-next-line no-param-reassign
|
|
res.locals = res.locals || {};
|
|
if (req.method && !acceptedMethods.includes(req.method)) {
|
|
await goNext();
|
|
return;
|
|
}
|
|
ready(context, processRequest, req);
|
|
async function goNext() {
|
|
if (!context.options.serverSideRender) {
|
|
return next();
|
|
}
|
|
return new Promise(resolve => {
|
|
ready(context, () => {
|
|
/** @type {any} */
|
|
// eslint-disable-next-line no-param-reassign
|
|
res.locals.webpack = {
|
|
devMiddleware: context
|
|
};
|
|
resolve(next());
|
|
}, req);
|
|
});
|
|
}
|
|
async function processRequest() {
|
|
/** @type {import("./utils/getFilenameFromUrl").Extra} */
|
|
const extra = {};
|
|
const filename = getFilenameFromUrl(context, /** @type {string} */req.url);
|
|
if (!filename) {
|
|
await goNext();
|
|
return;
|
|
}
|
|
if (extra.errorCode) {
|
|
if (extra.errorCode === 403) {
|
|
context.logger.error(`Malicious path "${filename}".`);
|
|
}
|
|
sendError(req, res, extra.errorCode, {
|
|
modifyResponseData: context.options.modifyResponseData
|
|
});
|
|
return;
|
|
}
|
|
let {
|
|
headers
|
|
} = context.options;
|
|
if (typeof headers === "function") {
|
|
// @ts-ignore
|
|
headers = headers(req, res, context);
|
|
}
|
|
|
|
/**
|
|
* @type {{key: string, value: string | number}[]}
|
|
*/
|
|
const allHeaders = [];
|
|
if (typeof headers !== "undefined") {
|
|
if (!Array.isArray(headers)) {
|
|
// eslint-disable-next-line guard-for-in
|
|
for (const name in headers) {
|
|
// @ts-ignore
|
|
allHeaders.push({
|
|
key: name,
|
|
value: headers[name]
|
|
});
|
|
}
|
|
headers = allHeaders;
|
|
}
|
|
headers.forEach(
|
|
/**
|
|
* @param {{key: string, value: any}} header
|
|
*/
|
|
header => {
|
|
setHeaderForResponse(res, header.key, header.value);
|
|
});
|
|
}
|
|
if (!getHeaderFromResponse(res, "Content-Type")) {
|
|
// content-type name(like application/javascript; charset=utf-8) or false
|
|
const contentType = mime.contentType(path.extname(filename));
|
|
|
|
// Only set content-type header if media type is known
|
|
// https://tools.ietf.org/html/rfc7231#section-3.1.1.5
|
|
if (contentType) {
|
|
setHeaderForResponse(res, "Content-Type", contentType);
|
|
} else if (context.options.mimeTypeDefault) {
|
|
setHeaderForResponse(res, "Content-Type", context.options.mimeTypeDefault);
|
|
}
|
|
}
|
|
if (!getHeaderFromResponse(res, "Accept-Ranges")) {
|
|
setHeaderForResponse(res, "Accept-Ranges", "bytes");
|
|
}
|
|
const rangeHeader = getHeaderFromRequest(req, "range");
|
|
let start;
|
|
let end;
|
|
if (rangeHeader && BYTES_RANGE_REGEXP.test(rangeHeader)) {
|
|
const size = await new Promise(resolve => {
|
|
/** @type {import("fs").lstat} */
|
|
context.outputFileSystem.lstat(filename, (error, stats) => {
|
|
if (error) {
|
|
context.logger.error(error);
|
|
return;
|
|
}
|
|
resolve(stats.size);
|
|
});
|
|
});
|
|
|
|
// eslint-disable-next-line global-require
|
|
const parsedRanges = require("range-parser")(size, rangeHeader, {
|
|
combine: true
|
|
});
|
|
if (parsedRanges === -1) {
|
|
const message = "Unsatisfiable range for 'Range' header.";
|
|
context.logger.error(message);
|
|
const existingHeaders = getHeaderNames(res);
|
|
for (let i = 0; i < existingHeaders.length; i++) {
|
|
res.removeHeader(existingHeaders[i]);
|
|
}
|
|
setStatusCode(res, 416);
|
|
setHeaderForResponse(res, "Content-Range", getValueContentRangeHeader("bytes", size));
|
|
setHeaderForResponse(res, "Content-Type", "text/html; charset=utf-8");
|
|
|
|
/** @type {string | Buffer | import("fs").ReadStream} */
|
|
let document = createHtmlDocument(416, `Error: ${message}`);
|
|
let byteLength = Buffer.byteLength(document);
|
|
setHeaderForResponse(res, "Content-Length", Buffer.byteLength(document));
|
|
if (context.options.modifyResponseData) {
|
|
({
|
|
data: document,
|
|
byteLength
|
|
} = context.options.modifyResponseData(req, res, document, byteLength));
|
|
}
|
|
send(req, res, document, byteLength);
|
|
return;
|
|
} else if (parsedRanges === -2) {
|
|
context.logger.error("A malformed 'Range' header was provided. A regular response will be sent for this request.");
|
|
} else if (parsedRanges.length > 1) {
|
|
context.logger.error("A 'Range' header with multiple ranges was provided. Multiple ranges are not supported, so a regular response will be sent for this request.");
|
|
}
|
|
if (parsedRanges !== -2 && parsedRanges.length === 1) {
|
|
// Content-Range
|
|
setStatusCode(res, 206);
|
|
setHeaderForResponse(res, "Content-Range", getValueContentRangeHeader("bytes", size, /** @type {import("range-parser").Ranges} */parsedRanges[0]));
|
|
[{
|
|
start,
|
|
end
|
|
}] = parsedRanges;
|
|
}
|
|
}
|
|
const isFsSupportsStream = typeof context.outputFileSystem.createReadStream === "function";
|
|
let bufferOrStream;
|
|
let byteLength;
|
|
try {
|
|
if (typeof start !== "undefined" && typeof end !== "undefined" && isFsSupportsStream) {
|
|
bufferOrStream = /** @type {import("fs").createReadStream} */
|
|
context.outputFileSystem.createReadStream(filename, {
|
|
start,
|
|
end
|
|
});
|
|
byteLength = 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));
|
|
}
|
|
send(req, res, bufferOrStream, byteLength);
|
|
}
|
|
};
|
|
}
|
|
module.exports = wrapper; |