"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 `${"\n" + '\n' + "\n" + '\n' + ""}${title}\n` + `\n` + `\n` + `
${body}
\n` + `\n` + `\n`; } const BYTES_RANGE_REGEXP = /^ *bytes/i; /** * @template {IncomingMessage} Request * @template {ServerResponse} Response * @param {import("./index.js").Context} context * @return {import("./index.js").Middleware} */ 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;