You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
6.9 KiB
237 lines
6.9 KiB
"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 |
|
} = 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() { |
|
const filename = getFilenameFromUrl(context, |
|
/** @type {string} */ |
|
req.url); |
|
|
|
if (!filename) { |
|
await goNext(); |
|
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; |