middleware.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. "use strict";
  2. const path = require("path");
  3. const mime = require("mime-types");
  4. const parseRange = require("range-parser");
  5. const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
  6. const {
  7. getHeaderNames,
  8. getHeaderFromRequest,
  9. getHeaderFromResponse,
  10. setHeaderForResponse,
  11. setStatusCode,
  12. send
  13. } = require("./utils/compatibleAPI");
  14. const ready = require("./utils/ready");
  15. /** @typedef {import("./index.js").NextFunction} NextFunction */
  16. /** @typedef {import("./index.js").IncomingMessage} IncomingMessage */
  17. /** @typedef {import("./index.js").ServerResponse} ServerResponse */
  18. /**
  19. * @param {string} type
  20. * @param {number} size
  21. * @param {import("range-parser").Range} [range]
  22. * @returns {string}
  23. */
  24. function getValueContentRangeHeader(type, size, range) {
  25. return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`;
  26. }
  27. /**
  28. * @param {string | number} title
  29. * @param {string} body
  30. * @returns {string}
  31. */
  32. function createHtmlDocument(title, body) {
  33. 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`;
  34. }
  35. const BYTES_RANGE_REGEXP = /^ *bytes/i;
  36. /**
  37. * @template {IncomingMessage} Request
  38. * @template {ServerResponse} Response
  39. * @param {import("./index.js").Context<Request, Response>} context
  40. * @return {import("./index.js").Middleware<Request, Response>}
  41. */
  42. function wrapper(context) {
  43. return async function middleware(req, res, next) {
  44. const acceptedMethods = context.options.methods || ["GET", "HEAD"];
  45. // fixes #282. credit @cexoso. in certain edge situations res.locals is undefined.
  46. // eslint-disable-next-line no-param-reassign
  47. res.locals = res.locals || {};
  48. if (req.method && !acceptedMethods.includes(req.method)) {
  49. await goNext();
  50. return;
  51. }
  52. ready(context, processRequest, req);
  53. async function goNext() {
  54. if (!context.options.serverSideRender) {
  55. return next();
  56. }
  57. return new Promise(resolve => {
  58. ready(context, () => {
  59. /** @type {any} */
  60. // eslint-disable-next-line no-param-reassign
  61. res.locals.webpack = {
  62. devMiddleware: context
  63. };
  64. resolve(next());
  65. }, req);
  66. });
  67. }
  68. async function processRequest() {
  69. const filename = getFilenameFromUrl(context, /** @type {string} */req.url);
  70. if (!filename) {
  71. await goNext();
  72. return;
  73. }
  74. let {
  75. headers
  76. } = context.options;
  77. if (typeof headers === "function") {
  78. // @ts-ignore
  79. headers = headers(req, res, context);
  80. }
  81. /**
  82. * @type {{key: string, value: string | number}[]}
  83. */
  84. const allHeaders = [];
  85. if (typeof headers !== "undefined") {
  86. if (!Array.isArray(headers)) {
  87. // eslint-disable-next-line guard-for-in
  88. for (const name in headers) {
  89. // @ts-ignore
  90. allHeaders.push({
  91. key: name,
  92. value: headers[name]
  93. });
  94. }
  95. headers = allHeaders;
  96. }
  97. headers.forEach(
  98. /**
  99. * @param {{key: string, value: any}} header
  100. */
  101. header => {
  102. setHeaderForResponse(res, header.key, header.value);
  103. });
  104. }
  105. if (!getHeaderFromResponse(res, "Content-Type")) {
  106. // content-type name(like application/javascript; charset=utf-8) or false
  107. const contentType = mime.contentType(path.extname(filename));
  108. // Only set content-type header if media type is known
  109. // https://tools.ietf.org/html/rfc7231#section-3.1.1.5
  110. if (contentType) {
  111. setHeaderForResponse(res, "Content-Type", contentType);
  112. }
  113. }
  114. if (!getHeaderFromResponse(res, "Accept-Ranges")) {
  115. setHeaderForResponse(res, "Accept-Ranges", "bytes");
  116. }
  117. const rangeHeader = getHeaderFromRequest(req, "range");
  118. let start;
  119. let end;
  120. if (rangeHeader && BYTES_RANGE_REGEXP.test(rangeHeader)) {
  121. const size = await new Promise(resolve => {
  122. /** @type {import("fs").lstat} */
  123. context.outputFileSystem.lstat(filename, (error, stats) => {
  124. if (error) {
  125. context.logger.error(error);
  126. return;
  127. }
  128. resolve(stats.size);
  129. });
  130. });
  131. const parsedRanges = parseRange(size, rangeHeader, {
  132. combine: true
  133. });
  134. if (parsedRanges === -1) {
  135. const message = "Unsatisfiable range for 'Range' header.";
  136. context.logger.error(message);
  137. const existingHeaders = getHeaderNames(res);
  138. for (let i = 0; i < existingHeaders.length; i++) {
  139. res.removeHeader(existingHeaders[i]);
  140. }
  141. setStatusCode(res, 416);
  142. setHeaderForResponse(res, "Content-Range", getValueContentRangeHeader("bytes", size));
  143. setHeaderForResponse(res, "Content-Type", "text/html; charset=utf-8");
  144. const document = createHtmlDocument(416, `Error: ${message}`);
  145. const byteLength = Buffer.byteLength(document);
  146. setHeaderForResponse(res, "Content-Length", Buffer.byteLength(document));
  147. send(req, res, document, byteLength);
  148. return;
  149. } else if (parsedRanges === -2) {
  150. context.logger.error("A malformed 'Range' header was provided. A regular response will be sent for this request.");
  151. } else if (parsedRanges.length > 1) {
  152. 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.");
  153. }
  154. if (parsedRanges !== -2 && parsedRanges.length === 1) {
  155. // Content-Range
  156. setStatusCode(res, 206);
  157. setHeaderForResponse(res, "Content-Range", getValueContentRangeHeader("bytes", size, /** @type {import("range-parser").Ranges} */parsedRanges[0]));
  158. [{
  159. start,
  160. end
  161. }] = parsedRanges;
  162. }
  163. }
  164. const isFsSupportsStream = typeof context.outputFileSystem.createReadStream === "function";
  165. let bufferOtStream;
  166. let byteLength;
  167. try {
  168. if (typeof start !== "undefined" && typeof end !== "undefined" && isFsSupportsStream) {
  169. bufferOtStream = /** @type {import("fs").createReadStream} */
  170. context.outputFileSystem.createReadStream(filename, {
  171. start,
  172. end
  173. });
  174. byteLength = end - start + 1;
  175. } else {
  176. bufferOtStream = /** @type {import("fs").readFileSync} */context.outputFileSystem.readFileSync(filename);
  177. ({
  178. byteLength
  179. } = bufferOtStream);
  180. }
  181. } catch (_ignoreError) {
  182. await goNext();
  183. return;
  184. }
  185. send(req, res, bufferOtStream, byteLength);
  186. }
  187. };
  188. }
  189. module.exports = wrapper;