middleware.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  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"]; // fixes #282. credit @cexoso. in certain edge situations res.locals is undefined.
  45. // eslint-disable-next-line no-param-reassign
  46. res.locals = res.locals || {};
  47. if (req.method && !acceptedMethods.includes(req.method)) {
  48. await goNext();
  49. return;
  50. }
  51. ready(context, processRequest, req);
  52. async function goNext() {
  53. if (!context.options.serverSideRender) {
  54. return next();
  55. }
  56. return new Promise(resolve => {
  57. ready(context, () => {
  58. /** @type {any} */
  59. // eslint-disable-next-line no-param-reassign
  60. res.locals.webpack = {
  61. devMiddleware: context
  62. };
  63. resolve(next());
  64. }, req);
  65. });
  66. }
  67. async function processRequest() {
  68. const filename = getFilenameFromUrl(context,
  69. /** @type {string} */
  70. req.url);
  71. if (!filename) {
  72. await goNext();
  73. return;
  74. }
  75. let {
  76. headers
  77. } = context.options;
  78. if (typeof headers === "function") {
  79. // @ts-ignore
  80. headers = headers(req, res, context);
  81. }
  82. /**
  83. * @type {{key: string, value: string | number}[]}
  84. */
  85. const allHeaders = [];
  86. if (typeof headers !== "undefined") {
  87. if (!Array.isArray(headers)) {
  88. // eslint-disable-next-line guard-for-in
  89. for (const name in headers) {
  90. // @ts-ignore
  91. allHeaders.push({
  92. key: name,
  93. value: headers[name]
  94. });
  95. }
  96. headers = allHeaders;
  97. }
  98. headers.forEach(
  99. /**
  100. * @param {{key: string, value: any}} header
  101. */
  102. header => {
  103. setHeaderForResponse(res, header.key, header.value);
  104. });
  105. }
  106. if (!getHeaderFromResponse(res, "Content-Type")) {
  107. // content-type name(like application/javascript; charset=utf-8) or false
  108. const contentType = mime.contentType(path.extname(filename)); // 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,
  158. /** @type {import("range-parser").Ranges} */
  159. parsedRanges[0]));
  160. [{
  161. start,
  162. end
  163. }] = parsedRanges;
  164. }
  165. }
  166. const isFsSupportsStream = typeof context.outputFileSystem.createReadStream === "function";
  167. let bufferOtStream;
  168. let byteLength;
  169. try {
  170. if (typeof start !== "undefined" && typeof end !== "undefined" && isFsSupportsStream) {
  171. bufferOtStream =
  172. /** @type {import("fs").createReadStream} */
  173. context.outputFileSystem.createReadStream(filename, {
  174. start,
  175. end
  176. });
  177. byteLength = end - start + 1;
  178. } else {
  179. bufferOtStream =
  180. /** @type {import("fs").readFileSync} */
  181. context.outputFileSystem.readFileSync(filename);
  182. ({
  183. byteLength
  184. } = bufferOtStream);
  185. }
  186. } catch (_ignoreError) {
  187. await goNext();
  188. return;
  189. }
  190. send(req, res, bufferOtStream, byteLength);
  191. }
  192. };
  193. }
  194. module.exports = wrapper;