middleware.js 7.2 KB

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