middleware.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. "use strict";
  2. const path = require("path");
  3. const mime = require("mime-types");
  4. const onFinishedStream = require("on-finished");
  5. const getFilenameFromUrl = require("./utils/getFilenameFromUrl");
  6. const {
  7. setStatusCode,
  8. getStatusCode,
  9. getRequestHeader,
  10. getRequestMethod,
  11. getRequestURL,
  12. getResponseHeader,
  13. setResponseHeader,
  14. removeResponseHeader,
  15. getResponseHeaders,
  16. send,
  17. finish,
  18. pipe,
  19. createReadStreamOrReadFileSync,
  20. getOutgoing,
  21. initState,
  22. setState,
  23. getReadyReadableStreamState
  24. } = require("./utils/compatibleAPI");
  25. const ready = require("./utils/ready");
  26. const parseTokenList = require("./utils/parseTokenList");
  27. const memorize = require("./utils/memorize");
  28. /** @typedef {import("./index.js").NextFunction} NextFunction */
  29. /** @typedef {import("./index.js").IncomingMessage} IncomingMessage */
  30. /** @typedef {import("./index.js").ServerResponse} ServerResponse */
  31. /** @typedef {import("./index.js").NormalizedHeaders} NormalizedHeaders */
  32. /** @typedef {import("fs").ReadStream} ReadStream */
  33. const BYTES_RANGE_REGEXP = /^ *bytes/i;
  34. /**
  35. * @param {string} type
  36. * @param {number} size
  37. * @param {import("range-parser").Range} [range]
  38. * @returns {string}
  39. */
  40. function getValueContentRangeHeader(type, size, range) {
  41. return `${type} ${range ? `${range.start}-${range.end}` : "*"}/${size}`;
  42. }
  43. /**
  44. * Parse an HTTP Date into a number.
  45. *
  46. * @param {string} date
  47. * @returns {number}
  48. */
  49. function parseHttpDate(date) {
  50. const timestamp = date && Date.parse(date);
  51. // istanbul ignore next: guard against date.js Date.parse patching
  52. return typeof timestamp === "number" ? timestamp : NaN;
  53. }
  54. const CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/;
  55. /**
  56. * @param {import("fs").ReadStream} stream stream
  57. * @param {boolean} suppress do need suppress?
  58. * @returns {void}
  59. */
  60. function destroyStream(stream, suppress) {
  61. if (typeof stream.destroy === "function") {
  62. stream.destroy();
  63. }
  64. if (typeof stream.close === "function") {
  65. // Node.js core bug workaround
  66. stream.on("open",
  67. /**
  68. * @this {import("fs").ReadStream}
  69. */
  70. function onOpenClose() {
  71. // @ts-ignore
  72. if (typeof this.fd === "number") {
  73. // actually close down the fd
  74. this.close();
  75. }
  76. });
  77. }
  78. if (typeof stream.addListener === "function" && suppress) {
  79. stream.removeAllListeners("error");
  80. stream.addListener("error", () => {});
  81. }
  82. }
  83. /** @type {Record<number, string>} */
  84. const statuses = {
  85. 400: "Bad Request",
  86. 403: "Forbidden",
  87. 404: "Not Found",
  88. 416: "Range Not Satisfiable",
  89. 500: "Internal Server Error"
  90. };
  91. const parseRangeHeaders = memorize(
  92. /**
  93. * @param {string} value
  94. * @returns {import("range-parser").Result | import("range-parser").Ranges}
  95. */
  96. value => {
  97. const [len, rangeHeader] = value.split("|");
  98. // eslint-disable-next-line global-require
  99. return require("range-parser")(Number(len), rangeHeader, {
  100. combine: true
  101. });
  102. });
  103. /**
  104. * @template {IncomingMessage} Request
  105. * @template {ServerResponse} Response
  106. * @typedef {Object} SendErrorOptions send error options
  107. * @property {Record<string, number | string | string[] | undefined>=} headers headers
  108. * @property {import("./index").ModifyResponseData<Request, Response>=} modifyResponseData modify response data callback
  109. */
  110. /**
  111. * @template {IncomingMessage} Request
  112. * @template {ServerResponse} Response
  113. * @param {import("./index.js").FilledContext<Request, Response>} context
  114. * @return {import("./index.js").Middleware<Request, Response>}
  115. */
  116. function wrapper(context) {
  117. return async function middleware(req, res, next) {
  118. const acceptedMethods = context.options.methods || ["GET", "HEAD"];
  119. initState(res);
  120. async function goNext() {
  121. if (!context.options.serverSideRender) {
  122. return next();
  123. }
  124. return new Promise(resolve => {
  125. ready(context, () => {
  126. setState(res, "webpack", {
  127. devMiddleware: context
  128. });
  129. resolve(next());
  130. }, req);
  131. });
  132. }
  133. const method = getRequestMethod(req);
  134. if (method && !acceptedMethods.includes(method)) {
  135. await goNext();
  136. return;
  137. }
  138. /**
  139. * @param {number} status status
  140. * @param {Partial<SendErrorOptions<Request, Response>>=} options options
  141. * @returns {void}
  142. */
  143. function sendError(status, options) {
  144. // eslint-disable-next-line global-require
  145. const escapeHtml = require("./utils/escapeHtml");
  146. const content = statuses[status] || String(status);
  147. let document = Buffer.from(`<!DOCTYPE html>
  148. <html lang="en">
  149. <head>
  150. <meta charset="utf-8">
  151. <title>Error</title>
  152. </head>
  153. <body>
  154. <pre>${escapeHtml(content)}</pre>
  155. </body>
  156. </html>`, "utf-8");
  157. // Clear existing headers
  158. const headers = getResponseHeaders(res);
  159. for (let i = 0; i < headers.length; i++) {
  160. removeResponseHeader(res, headers[i]);
  161. }
  162. if (options && options.headers) {
  163. const keys = Object.keys(options.headers);
  164. for (let i = 0; i < keys.length; i++) {
  165. const key = keys[i];
  166. const value = options.headers[key];
  167. if (typeof value !== "undefined") {
  168. setResponseHeader(res, key, value);
  169. }
  170. }
  171. }
  172. // Send basic response
  173. setStatusCode(res, status);
  174. setResponseHeader(res, "Content-Type", "text/html; charset=utf-8");
  175. setResponseHeader(res, "Content-Security-Policy", "default-src 'none'");
  176. setResponseHeader(res, "X-Content-Type-Options", "nosniff");
  177. let byteLength = Buffer.byteLength(document);
  178. if (options && options.modifyResponseData) {
  179. ({
  180. data: document,
  181. byteLength
  182. } = /** @type {{ data: Buffer, byteLength: number }} */
  183. options.modifyResponseData(req, res, document, byteLength));
  184. }
  185. setResponseHeader(res, "Content-Length", byteLength);
  186. finish(res, document);
  187. }
  188. /**
  189. * @param {NodeJS.ErrnoException} error
  190. */
  191. function errorHandler(error) {
  192. switch (error.code) {
  193. case "ENAMETOOLONG":
  194. case "ENOENT":
  195. case "ENOTDIR":
  196. sendError(404, {
  197. modifyResponseData: context.options.modifyResponseData
  198. });
  199. break;
  200. default:
  201. sendError(500, {
  202. modifyResponseData: context.options.modifyResponseData
  203. });
  204. break;
  205. }
  206. }
  207. function isConditionalGET() {
  208. return getRequestHeader(req, "if-match") || getRequestHeader(req, "if-unmodified-since") || getRequestHeader(req, "if-none-match") || getRequestHeader(req, "if-modified-since");
  209. }
  210. function isPreconditionFailure() {
  211. // if-match
  212. const ifMatch = /** @type {string} */getRequestHeader(req, "if-match");
  213. // A recipient MUST ignore If-Unmodified-Since if the request contains
  214. // an If-Match header field; the condition in If-Match is considered to
  215. // be a more accurate replacement for the condition in
  216. // If-Unmodified-Since, and the two are only combined for the sake of
  217. // interoperating with older intermediaries that might not implement If-Match.
  218. if (ifMatch) {
  219. const etag = getResponseHeader(res, "ETag");
  220. return !etag || ifMatch !== "*" && parseTokenList(ifMatch).every(match => match !== etag && match !== `W/${etag}` && `W/${match}` !== etag);
  221. }
  222. // if-unmodified-since
  223. const ifUnmodifiedSince = /** @type {string} */
  224. getRequestHeader(req, "if-unmodified-since");
  225. if (ifUnmodifiedSince) {
  226. const unmodifiedSince = parseHttpDate(ifUnmodifiedSince);
  227. // A recipient MUST ignore the If-Unmodified-Since header field if the
  228. // received field-value is not a valid HTTP-date.
  229. if (!isNaN(unmodifiedSince)) {
  230. const lastModified = parseHttpDate( /** @type {string} */getResponseHeader(res, "Last-Modified"));
  231. return isNaN(lastModified) || lastModified > unmodifiedSince;
  232. }
  233. }
  234. return false;
  235. }
  236. /**
  237. * @returns {boolean} is cachable
  238. */
  239. function isCachable() {
  240. const statusCode = getStatusCode(res);
  241. return statusCode >= 200 && statusCode < 300 || statusCode === 304 ||
  242. // For Koa and Hono, because by default status code is 404, but we already found a file
  243. statusCode === 404;
  244. }
  245. /**
  246. * @param {import("http").OutgoingHttpHeaders} resHeaders
  247. * @returns {boolean}
  248. */
  249. function isFresh(resHeaders) {
  250. // Always return stale when Cache-Control: no-cache to support end-to-end reload requests
  251. // https://tools.ietf.org/html/rfc2616#section-14.9.4
  252. const cacheControl = /** @type {string} */
  253. getRequestHeader(req, "cache-control");
  254. if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
  255. return false;
  256. }
  257. // fields
  258. const noneMatch = /** @type {string} */
  259. getRequestHeader(req, "if-none-match");
  260. const modifiedSince = /** @type {string} */
  261. getRequestHeader(req, "if-modified-since");
  262. // unconditional request
  263. if (!noneMatch && !modifiedSince) {
  264. return false;
  265. }
  266. // if-none-match
  267. if (noneMatch && noneMatch !== "*") {
  268. if (!resHeaders.etag) {
  269. return false;
  270. }
  271. const matches = parseTokenList(noneMatch);
  272. let etagStale = true;
  273. for (let i = 0; i < matches.length; i++) {
  274. const match = matches[i];
  275. if (match === resHeaders.etag || match === `W/${resHeaders.etag}` || `W/${match}` === resHeaders.etag) {
  276. etagStale = false;
  277. break;
  278. }
  279. }
  280. if (etagStale) {
  281. return false;
  282. }
  283. }
  284. // A recipient MUST ignore If-Modified-Since if the request contains an If-None-Match header field;
  285. // the condition in If-None-Match is considered to be a more accurate replacement for the condition in If-Modified-Since,
  286. // and the two are only combined for the sake of interoperating with older intermediaries that might not implement If-None-Match.
  287. if (noneMatch) {
  288. return true;
  289. }
  290. // if-modified-since
  291. if (modifiedSince) {
  292. const lastModified = resHeaders["last-modified"];
  293. // A recipient MUST ignore the If-Modified-Since header field if the
  294. // received field-value is not a valid HTTP-date, or if the request
  295. // method is neither GET nor HEAD.
  296. const modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince));
  297. if (modifiedStale) {
  298. return false;
  299. }
  300. }
  301. return true;
  302. }
  303. function isRangeFresh() {
  304. const ifRange = /** @type {string | undefined} */
  305. getRequestHeader(req, "if-range");
  306. if (!ifRange) {
  307. return true;
  308. }
  309. // if-range as etag
  310. if (ifRange.indexOf('"') !== -1) {
  311. const etag = /** @type {string | undefined} */
  312. getResponseHeader(res, "ETag");
  313. if (!etag) {
  314. return true;
  315. }
  316. return Boolean(etag && ifRange.indexOf(etag) !== -1);
  317. }
  318. // if-range as modified date
  319. const lastModified = /** @type {string | undefined} */
  320. getResponseHeader(res, "Last-Modified");
  321. if (!lastModified) {
  322. return true;
  323. }
  324. return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
  325. }
  326. /**
  327. * @returns {string | undefined}
  328. */
  329. function getRangeHeader() {
  330. const range = /** @type {string} */getRequestHeader(req, "range");
  331. if (range && BYTES_RANGE_REGEXP.test(range)) {
  332. return range;
  333. }
  334. // eslint-disable-next-line no-undefined
  335. return undefined;
  336. }
  337. /**
  338. * @param {import("range-parser").Range} range
  339. * @returns {[number, number]}
  340. */
  341. function getOffsetAndLenFromRange(range) {
  342. const offset = range.start;
  343. const len = range.end - range.start + 1;
  344. return [offset, len];
  345. }
  346. /**
  347. * @param {number} offset
  348. * @param {number} len
  349. * @returns {[number, number]}
  350. */
  351. function calcStartAndEnd(offset, len) {
  352. const start = offset;
  353. const end = Math.max(offset, offset + len - 1);
  354. return [start, end];
  355. }
  356. async function processRequest() {
  357. // Pipe and SendFile
  358. /** @type {import("./utils/getFilenameFromUrl").Extra} */
  359. const extra = {};
  360. const filename = getFilenameFromUrl(context, /** @type {string} */getRequestURL(req), extra);
  361. if (extra.errorCode) {
  362. if (extra.errorCode === 403) {
  363. context.logger.error(`Malicious path "${filename}".`);
  364. }
  365. sendError(extra.errorCode, {
  366. modifyResponseData: context.options.modifyResponseData
  367. });
  368. await goNext();
  369. return;
  370. }
  371. if (!filename) {
  372. await goNext();
  373. return;
  374. }
  375. const {
  376. size
  377. } = /** @type {import("fs").Stats} */extra.stats;
  378. let len = size;
  379. let offset = 0;
  380. // Send logic
  381. let {
  382. headers
  383. } = context.options;
  384. if (typeof headers === "function") {
  385. headers = /** @type {NormalizedHeaders} */headers(req, res, context);
  386. }
  387. /**
  388. * @type {{key: string, value: string | number}[]}
  389. */
  390. const allHeaders = [];
  391. if (typeof headers !== "undefined") {
  392. if (!Array.isArray(headers)) {
  393. // eslint-disable-next-line guard-for-in
  394. for (const name in headers) {
  395. allHeaders.push({
  396. key: name,
  397. value: headers[name]
  398. });
  399. }
  400. headers = allHeaders;
  401. }
  402. headers.forEach(header => {
  403. setResponseHeader(res, header.key, header.value);
  404. });
  405. }
  406. if (!getResponseHeader(res, "Content-Type") || getStatusCode(res) === 404) {
  407. removeResponseHeader(res, "Content-Type");
  408. // content-type name(like application/javascript; charset=utf-8) or false
  409. const contentType = mime.contentType(path.extname(filename));
  410. // Only set content-type header if media type is known
  411. // https://tools.ietf.org/html/rfc7231#section-3.1.1.5
  412. if (contentType) {
  413. setResponseHeader(res, "Content-Type", contentType);
  414. } else if (context.options.mimeTypeDefault) {
  415. setResponseHeader(res, "Content-Type", context.options.mimeTypeDefault);
  416. }
  417. }
  418. if (!getResponseHeader(res, "Accept-Ranges")) {
  419. setResponseHeader(res, "Accept-Ranges", "bytes");
  420. }
  421. if (context.options.lastModified && !getResponseHeader(res, "Last-Modified")) {
  422. const modified = /** @type {import("fs").Stats} */
  423. extra.stats.mtime.toUTCString();
  424. setResponseHeader(res, "Last-Modified", modified);
  425. }
  426. /** @type {number} */
  427. let start;
  428. /** @type {number} */
  429. let end;
  430. /** @type {undefined | Buffer | ReadStream} */
  431. let bufferOrStream;
  432. /** @type {number} */
  433. let byteLength;
  434. const rangeHeader = getRangeHeader();
  435. if (context.options.etag && !getResponseHeader(res, "ETag")) {
  436. /** @type {import("fs").Stats | Buffer | ReadStream | undefined} */
  437. let value;
  438. // TODO cache etag generation?
  439. if (context.options.etag === "weak") {
  440. value = /** @type {import("fs").Stats} */extra.stats;
  441. } else {
  442. if (rangeHeader) {
  443. const parsedRanges = /** @type {import("range-parser").Ranges | import("range-parser").Result} */
  444. parseRangeHeaders(`${size}|${rangeHeader}`);
  445. if (parsedRanges !== -2 && parsedRanges !== -1 && parsedRanges.length === 1) {
  446. [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]);
  447. }
  448. }
  449. [start, end] = calcStartAndEnd(offset, len);
  450. try {
  451. const result = createReadStreamOrReadFileSync(filename, context.outputFileSystem, start, end);
  452. value = result.bufferOrStream;
  453. ({
  454. bufferOrStream,
  455. byteLength
  456. } = result);
  457. } catch (error) {
  458. errorHandler( /** @type {NodeJS.ErrnoException} */error);
  459. await goNext();
  460. return;
  461. }
  462. }
  463. if (value) {
  464. // eslint-disable-next-line global-require
  465. const result = await require("./utils/etag")(value);
  466. // Because we already read stream, we can cache buffer to avoid extra read from fs
  467. if (result.buffer) {
  468. bufferOrStream = result.buffer;
  469. }
  470. setResponseHeader(res, "ETag", result.hash);
  471. }
  472. }
  473. // Conditional GET support
  474. if (isConditionalGET()) {
  475. if (isPreconditionFailure()) {
  476. sendError(412, {
  477. modifyResponseData: context.options.modifyResponseData
  478. });
  479. await goNext();
  480. return;
  481. }
  482. if (isCachable() && isFresh({
  483. etag: ( /** @type {string | undefined} */
  484. getResponseHeader(res, "ETag")),
  485. "last-modified": ( /** @type {string | undefined} */
  486. getResponseHeader(res, "Last-Modified"))
  487. })) {
  488. setStatusCode(res, 304);
  489. // Remove content header fields
  490. removeResponseHeader(res, "Content-Encoding");
  491. removeResponseHeader(res, "Content-Language");
  492. removeResponseHeader(res, "Content-Length");
  493. removeResponseHeader(res, "Content-Range");
  494. removeResponseHeader(res, "Content-Type");
  495. finish(res);
  496. await goNext();
  497. return;
  498. }
  499. }
  500. let isPartialContent = false;
  501. if (rangeHeader) {
  502. let parsedRanges = /** @type {import("range-parser").Ranges | import("range-parser").Result | []} */
  503. parseRangeHeaders(`${size}|${rangeHeader}`);
  504. // If-Range support
  505. if (!isRangeFresh()) {
  506. parsedRanges = [];
  507. }
  508. if (parsedRanges === -1) {
  509. context.logger.error("Unsatisfiable range for 'Range' header.");
  510. setResponseHeader(res, "Content-Range", getValueContentRangeHeader("bytes", size));
  511. sendError(416, {
  512. headers: {
  513. "Content-Range": getResponseHeader(res, "Content-Range")
  514. },
  515. modifyResponseData: context.options.modifyResponseData
  516. });
  517. await goNext();
  518. return;
  519. } else if (parsedRanges === -2) {
  520. context.logger.error("A malformed 'Range' header was provided. A regular response will be sent for this request.");
  521. } else if (parsedRanges.length > 1) {
  522. 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.");
  523. }
  524. if (parsedRanges !== -2 && parsedRanges.length === 1) {
  525. // Content-Range
  526. setStatusCode(res, 206);
  527. setResponseHeader(res, "Content-Range", getValueContentRangeHeader("bytes", size, /** @type {import("range-parser").Ranges} */parsedRanges[0]));
  528. isPartialContent = true;
  529. [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]);
  530. }
  531. }
  532. // When strong Etag generation is enabled we already read file, so we can skip extra fs call
  533. if (!bufferOrStream) {
  534. [start, end] = calcStartAndEnd(offset, len);
  535. try {
  536. ({
  537. bufferOrStream,
  538. byteLength
  539. } = createReadStreamOrReadFileSync(filename, context.outputFileSystem, start, end));
  540. } catch (error) {
  541. errorHandler( /** @type {NodeJS.ErrnoException} */error);
  542. await goNext();
  543. return;
  544. }
  545. }
  546. if (context.options.modifyResponseData) {
  547. ({
  548. data: bufferOrStream,
  549. byteLength
  550. } = context.options.modifyResponseData(req, res, bufferOrStream,
  551. // @ts-ignore
  552. byteLength));
  553. }
  554. // @ts-ignore
  555. setResponseHeader(res, "Content-Length", byteLength);
  556. if (method === "HEAD") {
  557. if (!isPartialContent) {
  558. setStatusCode(res, 200);
  559. }
  560. finish(res);
  561. await goNext();
  562. return;
  563. }
  564. if (!isPartialContent) {
  565. setStatusCode(res, 200);
  566. }
  567. const isPipeSupports = typeof ( /** @type {import("fs").ReadStream} */bufferOrStream.pipe) === "function";
  568. if (!isPipeSupports) {
  569. send(res, /** @type {Buffer} */bufferOrStream);
  570. await goNext();
  571. return;
  572. }
  573. // Cleanup
  574. const cleanup = () => {
  575. destroyStream( /** @type {import("fs").ReadStream} */bufferOrStream, true);
  576. };
  577. // Error handling
  578. /** @type {import("fs").ReadStream} */
  579. bufferOrStream.on("error", error => {
  580. // clean up stream early
  581. cleanup();
  582. errorHandler(error);
  583. goNext();
  584. }).on(getReadyReadableStreamState(res), () => {
  585. goNext();
  586. });
  587. pipe(res, /** @type {ReadStream} */bufferOrStream);
  588. const outgoing = getOutgoing(res);
  589. if (outgoing) {
  590. // Response finished, cleanup
  591. onFinishedStream(outgoing, cleanup);
  592. }
  593. }
  594. ready(context, processRequest, req);
  595. };
  596. }
  597. module.exports = wrapper;