249 lines
7.2 KiB
JavaScript
249 lines
7.2 KiB
JavaScript
|
"use strict";
|
||
|
|
||
|
const path = require("path");
|
||
|
|
||
|
const mime = require("mime-types");
|
||
|
|
||
|
const parseRange = require("range-parser");
|
||
|
|
||
|
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, extra);
|
||
|
|
||
|
if (!filename) {
|
||
|
await goNext();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (extra.errorCode) {
|
||
|
if (extra.errorCode === 403) {
|
||
|
context.logger.error(`Malicious path "${filename}".`);
|
||
|
}
|
||
|
|
||
|
sendError(req, res, extra.errorCode);
|
||
|
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);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
});
|
||
|
});
|
||
|
const parsedRanges = parseRange(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");
|
||
|
const document = createHtmlDocument(416, `Error: ${message}`);
|
||
|
const byteLength = Buffer.byteLength(document);
|
||
|
setHeaderForResponse(res, "Content-Length", Buffer.byteLength(document));
|
||
|
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 bufferOtStream;
|
||
|
let byteLength;
|
||
|
|
||
|
try {
|
||
|
if (typeof start !== "undefined" && typeof end !== "undefined" && isFsSupportsStream) {
|
||
|
bufferOtStream =
|
||
|
/** @type {import("fs").createReadStream} */
|
||
|
context.outputFileSystem.createReadStream(filename, {
|
||
|
start,
|
||
|
end
|
||
|
});
|
||
|
byteLength = end - start + 1;
|
||
|
} else {
|
||
|
bufferOtStream =
|
||
|
/** @type {import("fs").readFileSync} */
|
||
|
context.outputFileSystem.readFileSync(filename);
|
||
|
({
|
||
|
byteLength
|
||
|
} = bufferOtStream);
|
||
|
}
|
||
|
} catch (_ignoreError) {
|
||
|
await goNext();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
send(req, res, bufferOtStream, byteLength);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
module.exports = wrapper;
|