middleware.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915
  1. "use strict";
  2. const path = require("node:path");
  3. const querystring = require("node:querystring");
  4. const {
  5. finished
  6. } = require("node:stream");
  7. const mime = require("mime-types");
  8. const {
  9. createReadStreamOrReadFile,
  10. destroyStream,
  11. escapeHtml,
  12. etag,
  13. finish,
  14. getHeadersSent,
  15. getOutgoing,
  16. getRequestHeader,
  17. getRequestMethod,
  18. getRequestURL,
  19. getResponseHeader,
  20. getResponseHeaders,
  21. getStatusCode,
  22. getValueContentRangeHeader,
  23. initState,
  24. memorize,
  25. parseHttpDate,
  26. parseTokenList,
  27. pipe,
  28. removeResponseHeader,
  29. send,
  30. setResponseHeader,
  31. setState,
  32. setStatusCode
  33. } = require("./utils");
  34. /** @typedef {import("fs").ReadStream} ReadStream */
  35. /** @typedef {import("webpack").Compiler} Compiler */
  36. /** @typedef {import("webpack").Stats} Stats */
  37. /** @typedef {import("webpack").MultiStats} MultiStats */
  38. /** @typedef {import("webpack").Asset} Asset */
  39. /** @typedef {import("./index.js").NextFunction} NextFunction */
  40. /** @typedef {import("./index.js").IncomingMessage} IncomingMessage */
  41. /** @typedef {import("./index.js").ServerResponse} ServerResponse */
  42. /** @typedef {import("./index.js").NormalizedHeaders} NormalizedHeaders */
  43. /** @typedef {import("./index.js").OutputFileSystem} OutputFileSystem */
  44. const BYTES_RANGE_REGEXP = /^ *bytes/i;
  45. /**
  46. * @param {string} input input
  47. * @returns {string} unescape input
  48. */
  49. function decode(input) {
  50. return querystring.unescape(input);
  51. }
  52. const memoizedParse = memorize(url => {
  53. const urlObject = new URL(url, "http://localhost");
  54. // We can't change pathname in URL object directly because don't decode correctly
  55. return {
  56. ...urlObject,
  57. pathname: decode(urlObject.pathname)
  58. };
  59. }, undefined);
  60. const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
  61. /** @typedef {import("fs").Stats} FSStats */
  62. /**
  63. * @typedef {object} Extra
  64. * @property {FSStats} stats stats
  65. * @property {boolean=} immutable true when immutable, otherwise false
  66. * @property {OutputFileSystem} outputFileSystem outputFileSystem
  67. */
  68. /**
  69. * decodeURIComponent.
  70. *
  71. * Allows V8 to only deoptimize this fn instead of all of send().
  72. * @param {string} input
  73. * @returns {string}
  74. */
  75. class FilenameError extends Error {
  76. /**
  77. * @param {string} message message
  78. * @param {number=} code error code
  79. */
  80. constructor(message, code) {
  81. super(message);
  82. this.name = "FilenameError";
  83. this.statusCode = code;
  84. }
  85. }
  86. /** @typedef {{ filename: string, extra: Extra }} FilenameWithExtra */
  87. /**
  88. * @param {unknown} error error
  89. * @returns {boolean} true when error is like not found, otherwise false
  90. */
  91. function isNotFoundError(error) {
  92. switch (/** @type {NodeJS.ErrnoException} */error.code) {
  93. case "ENAMETOOLONG":
  94. case "ENOENT":
  95. case "ENOTDIR":
  96. return true;
  97. default:
  98. return false;
  99. }
  100. }
  101. /**
  102. * @template {IncomingMessage} Request
  103. * @template {ServerResponse} Response
  104. * @param {import("./index.js").FilledContext<Request, Response>} context context
  105. * @param {string} url url
  106. * @returns {Promise<FilenameWithExtra | undefined>} result of get filename from url
  107. */
  108. async function getFilenameFromUrl(context, url) {
  109. /** @type {URL} */
  110. let urlObject;
  111. try {
  112. // The `url` property of the `request` is contains only `pathname`, `search` and `hash`
  113. urlObject = memoizedParse(url);
  114. } catch {
  115. return;
  116. }
  117. const {
  118. options,
  119. stats
  120. } = context;
  121. /** @type {Stats[]} */
  122. const allStats = /** @type {MultiStats} */
  123. stats.stats || [(/** @type {Stats} */stats)];
  124. const index = options.index === false ? (/** @type {string[]} */[]) : typeof options.index === "undefined" || options.index === true ? ["index.html"] : [options.index];
  125. for (const {
  126. compilation
  127. } of allStats) {
  128. if (compilation.options.devServer === false) {
  129. continue;
  130. }
  131. /** @type {URL} */
  132. let publicPathObject;
  133. const publicPath = options.publicPath || compilation.options.output.publicPath || "";
  134. try {
  135. publicPathObject = memoizedParse(publicPath === "auto" ? "/" : compilation.getPath(publicPath));
  136. } catch {
  137. continue;
  138. }
  139. const {
  140. pathname
  141. } = urlObject;
  142. const {
  143. pathname: publicPathPathname
  144. } = publicPathObject;
  145. /** @type {string | undefined} */
  146. let filename;
  147. if (pathname && publicPathPathname && pathname.startsWith(publicPathPathname)) {
  148. // Null byte(s)
  149. if (pathname.includes("\0")) {
  150. throw new FilenameError("Bad Request", 400);
  151. }
  152. // ".." is malicious
  153. if (UP_PATH_REGEXP.test(path.normalize(`./${pathname}`))) {
  154. throw new FilenameError("Forbidden", 403);
  155. }
  156. // send file logic
  157. // The `output.path` is always present and always absolute
  158. const outputPath = compilation.getPath(compilation.outputOptions.path || "");
  159. // Strip the `pathname` property from the `publicPath` option from the start of requested url
  160. // `/complex/foo.js` => `foo.js`
  161. // and add outputPath
  162. // `foo.js` => `/home/user/my-project/dist/foo.js`
  163. filename = path.join(outputPath, pathname.slice(publicPathPathname.length));
  164. const {
  165. assetsInfo
  166. } = compilation;
  167. const {
  168. outputFileSystem
  169. } = /** @type {Compiler & { outputFileSystem: OutputFileSystem }} */
  170. compilation.compiler;
  171. /**
  172. * @param {string} filename filename
  173. * @returns {Promise<FilenameWithExtra | undefined>} filename when found, otherwise undefined
  174. */
  175. const resolveIndex = async filename => {
  176. if (index.length === 0) {
  177. return;
  178. }
  179. filename = path.join(filename, index[0]);
  180. let stats;
  181. try {
  182. stats = await new Promise((resolve, reject) => {
  183. outputFileSystem.stat(filename, (err, res) => {
  184. if (err) {
  185. reject(err);
  186. return;
  187. }
  188. resolve(res);
  189. });
  190. });
  191. } catch (err) {
  192. if (isNotFoundError(err)) return;
  193. throw err;
  194. }
  195. if (/** @type {FSStats} */stats.isDirectory()) {
  196. return resolveIndex(filename);
  197. }
  198. const extra = {
  199. immutable: assetsInfo ? assetsInfo.get(pathname.slice(publicPathPathname.length))?.immutable : false,
  200. outputFileSystem,
  201. stats: (/** @type {FSStats} */stats)
  202. };
  203. return {
  204. filename,
  205. extra
  206. };
  207. };
  208. /**
  209. * @param {string} filename filename
  210. * @returns {Promise<FilenameWithExtra | undefined>} filename when found, otherwise undefined
  211. */
  212. const resolveFile = async filename => {
  213. let stats;
  214. try {
  215. stats = await new Promise((resolve, reject) => {
  216. outputFileSystem.stat(filename, (err, res) => {
  217. if (err) {
  218. reject(err);
  219. return;
  220. }
  221. resolve(res);
  222. });
  223. });
  224. } catch (err) {
  225. if (isNotFoundError(err)) return;
  226. throw err;
  227. }
  228. if (/** @type {FSStats} */stats.isDirectory()) {
  229. // Different between `send` and our logic is here, `send` makes a redirect, we just return a file.
  230. return resolveIndex(filename);
  231. }
  232. if (filename.endsWith(path.sep)) {
  233. return;
  234. }
  235. /** @type {Extra} */
  236. const extra = {
  237. immutable: assetsInfo ? assetsInfo.get(pathname.slice(publicPathPathname.length))?.immutable : false,
  238. outputFileSystem,
  239. stats: (/** @type {FSStats} */stats)
  240. };
  241. return {
  242. filename,
  243. extra
  244. };
  245. };
  246. // send index logic
  247. if (index.length > 0 && pathname.endsWith("/")) {
  248. const result = await resolveIndex(filename);
  249. if (!result) {
  250. continue;
  251. }
  252. return result;
  253. }
  254. // send file logic
  255. const result = await resolveFile(filename);
  256. if (!result) {
  257. continue;
  258. }
  259. return result;
  260. }
  261. }
  262. }
  263. const CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/;
  264. /** @type {Record<number, string>} */
  265. const statuses = {
  266. 400: "Bad Request",
  267. 403: "Forbidden",
  268. 404: "Not Found",
  269. 416: "Range Not Satisfiable",
  270. 500: "Internal Server Error"
  271. };
  272. const parseRangeHeaders = memorize(
  273. /**
  274. * @param {string} value value
  275. * @returns {import("range-parser").Result | import("range-parser").Ranges} ranges
  276. */
  277. value => {
  278. const [len, rangeHeader] = value.split("|");
  279. return require("range-parser")(Number(len), rangeHeader, {
  280. combine: true
  281. });
  282. });
  283. const MAX_MAX_AGE = 31536000000;
  284. /**
  285. * @template {IncomingMessage} Request
  286. * @template {ServerResponse} Response
  287. * @typedef {object} SendErrorOptions send error options
  288. * @property {Record<string, number | string | string[] | undefined>=} headers headers
  289. * @property {import("./index").ModifyResponseData<Request, Response>=} modifyResponseData modify response data callback
  290. */
  291. /**
  292. * @template {IncomingMessage} Request
  293. * @template {ServerResponse} Response
  294. * @param {import("./index.js").FilledContext<Request, Response>} context context
  295. * @param {import("./index.js").Callback} callback callback
  296. * @param {Request=} req req
  297. * @returns {void}
  298. */
  299. function ready(context, callback, req) {
  300. if (context.state) {
  301. callback(context.stats);
  302. return;
  303. }
  304. const name = req && req.url || callback.name;
  305. context.logger.info(`wait until bundle finished${name ? `: ${name}` : ""}`);
  306. context.callbacks.push(callback);
  307. }
  308. /**
  309. * @template {IncomingMessage} Request
  310. * @template {ServerResponse} Response
  311. * @param {import("./index.js").FilledContext<Request, Response>} context context
  312. * @returns {import("./index.js").Middleware<Request, Response>} wrapper
  313. */
  314. function wrapper(context) {
  315. return async function middleware(req, res, next) {
  316. /**
  317. * @param {NodeJS.ErrnoException=} err an error
  318. * @returns {Promise<void>}
  319. */
  320. async function goNext(err) {
  321. if (!context.options.serverSideRender) {
  322. return next(err);
  323. }
  324. return new Promise(resolve => {
  325. ready(context, () => {
  326. setState(res, "webpack", {
  327. devMiddleware: context
  328. });
  329. resolve(next(err));
  330. }, req);
  331. });
  332. }
  333. const acceptedMethods = context.options.methods || ["GET", "HEAD"];
  334. initState(res);
  335. const method = getRequestMethod(req);
  336. if (method && !acceptedMethods.includes(method)) {
  337. await goNext();
  338. return;
  339. }
  340. /**
  341. * @param {string} message an error message
  342. * @param {number} status status
  343. * @param {Partial<SendErrorOptions<Request, Response>>=} options options
  344. * @returns {Promise<void>}
  345. */
  346. async function sendError(message, status, options) {
  347. if (context.options.forwardError) {
  348. if (!getHeadersSent(res)) {
  349. const headers = getResponseHeaders(res);
  350. for (let i = 0; i < headers.length; i++) {
  351. removeResponseHeader(res, headers[i]);
  352. }
  353. }
  354. const error = /** @type {Error & { statusCode: number }} */
  355. new Error(message);
  356. error.statusCode = status;
  357. await goNext(error);
  358. // need the return for prevent to execute the code below and override the status and body set by user in the next middleware
  359. return;
  360. }
  361. const content = statuses[status] || String(status);
  362. let document = Buffer.from(`<!DOCTYPE html>
  363. <html lang="en">
  364. <head>
  365. <meta charset="utf-8">
  366. <title>Error</title>
  367. </head>
  368. <body>
  369. <pre>${escapeHtml(content)}</pre>
  370. </body>
  371. </html>`, "utf8");
  372. // Clear existing headers
  373. const headers = getResponseHeaders(res);
  374. for (let i = 0; i < headers.length; i++) {
  375. removeResponseHeader(res, headers[i]);
  376. }
  377. if (options && options.headers) {
  378. const keys = Object.keys(options.headers);
  379. for (let i = 0; i < keys.length; i++) {
  380. const key = keys[i];
  381. const value = options.headers[key];
  382. if (typeof value !== "undefined") {
  383. setResponseHeader(res, key, value);
  384. }
  385. }
  386. }
  387. // Send basic response
  388. setStatusCode(res, status);
  389. setResponseHeader(res, "Content-Type", "text/html; charset=utf-8");
  390. setResponseHeader(res, "Content-Security-Policy", "default-src 'none'");
  391. setResponseHeader(res, "X-Content-Type-Options", "nosniff");
  392. let byteLength = Buffer.byteLength(document);
  393. if (options && options.modifyResponseData) {
  394. ({
  395. data: document,
  396. byteLength
  397. } = /** @type {{ data: Buffer<ArrayBuffer>, byteLength: number }} */
  398. options.modifyResponseData(req, res, document, byteLength));
  399. }
  400. setResponseHeader(res, "Content-Length", byteLength);
  401. finish(res, document);
  402. }
  403. /**
  404. * @param {NodeJS.ErrnoException} error error
  405. * @param {string=} message override message
  406. * @param {number=} code override code
  407. * @returns {Promise<void>}
  408. */
  409. async function errorHandler(error, message, code) {
  410. switch (error.code) {
  411. case "ENAMETOOLONG":
  412. case "ENOENT":
  413. case "ENOTDIR":
  414. await sendError(error.message, 404, {
  415. modifyResponseData: context.options.modifyResponseData
  416. });
  417. break;
  418. default:
  419. await sendError(message || error.message, code || 500, {
  420. modifyResponseData: context.options.modifyResponseData
  421. });
  422. break;
  423. }
  424. }
  425. /**
  426. * @returns {string | string[] | undefined} something when conditional get exist
  427. */
  428. function isConditionalGET() {
  429. return getRequestHeader(req, "if-match") || getRequestHeader(req, "if-unmodified-since") || getRequestHeader(req, "if-none-match") || getRequestHeader(req, "if-modified-since");
  430. }
  431. /**
  432. * @returns {boolean} true when precondition failure, otherwise false
  433. */
  434. function isPreconditionFailure() {
  435. // if-match
  436. const ifMatch = /** @type {string} */getRequestHeader(req, "if-match");
  437. // A recipient MUST ignore If-Unmodified-Since if the request contains
  438. // an If-Match header field; the condition in If-Match is considered to
  439. // be a more accurate replacement for the condition in
  440. // If-Unmodified-Since, and the two are only combined for the sake of
  441. // interoperating with older intermediaries that might not implement If-Match.
  442. if (ifMatch) {
  443. const etag = getResponseHeader(res, "ETag");
  444. return !etag || ifMatch !== "*" && parseTokenList(ifMatch).every(match => match !== etag && match !== `W/${etag}` && `W/${match}` !== etag);
  445. }
  446. // if-unmodified-since
  447. const ifUnmodifiedSince = /** @type {string} */
  448. getRequestHeader(req, "if-unmodified-since");
  449. if (ifUnmodifiedSince) {
  450. const unmodifiedSince = parseHttpDate(ifUnmodifiedSince);
  451. // A recipient MUST ignore the If-Unmodified-Since header field if the
  452. // received field-value is not a valid HTTP-date.
  453. if (!Number.isNaN(unmodifiedSince)) {
  454. const lastModified = parseHttpDate(/** @type {string} */getResponseHeader(res, "Last-Modified"));
  455. return Number.isNaN(lastModified) || lastModified > unmodifiedSince;
  456. }
  457. }
  458. return false;
  459. }
  460. /**
  461. * @returns {boolean} is cachable
  462. */
  463. function isCachable() {
  464. const statusCode = getStatusCode(res);
  465. return statusCode >= 200 && statusCode < 300 || statusCode === 304 ||
  466. // For Koa and Hono, because by default status code is 404, but we already found a file
  467. statusCode === 404;
  468. }
  469. /**
  470. * @param {import("http").OutgoingHttpHeaders} resHeaders res header
  471. * @returns {boolean} true when fresh, otherwise false
  472. */
  473. function isFresh(resHeaders) {
  474. // Always return stale when Cache-Control: no-cache to support end-to-end reload requests
  475. // https://tools.ietf.org/html/rfc2616#section-14.9.4
  476. const cacheControl = /** @type {string} */
  477. getRequestHeader(req, "cache-control");
  478. if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
  479. return false;
  480. }
  481. // fields
  482. const noneMatch = /** @type {string} */
  483. getRequestHeader(req, "if-none-match");
  484. const modifiedSince = /** @type {string} */
  485. getRequestHeader(req, "if-modified-since");
  486. // unconditional request
  487. if (!noneMatch && !modifiedSince) {
  488. return false;
  489. }
  490. // if-none-match
  491. if (noneMatch && noneMatch !== "*") {
  492. if (!resHeaders.etag) {
  493. return false;
  494. }
  495. const matches = parseTokenList(noneMatch);
  496. let etagStale = true;
  497. for (let i = 0; i < matches.length; i++) {
  498. const match = matches[i];
  499. if (match === resHeaders.etag || match === `W/${resHeaders.etag}` || `W/${match}` === resHeaders.etag) {
  500. etagStale = false;
  501. break;
  502. }
  503. }
  504. if (etagStale) {
  505. return false;
  506. }
  507. }
  508. // A recipient MUST ignore If-Modified-Since if the request contains an If-None-Match header field;
  509. // the condition in If-None-Match is considered to be a more accurate replacement for the condition in If-Modified-Since,
  510. // and the two are only combined for the sake of interoperating with older intermediaries that might not implement If-None-Match.
  511. if (noneMatch) {
  512. return true;
  513. }
  514. // if-modified-since
  515. if (modifiedSince) {
  516. const lastModified = resHeaders["last-modified"];
  517. // A recipient MUST ignore the If-Modified-Since header field if the
  518. // received field-value is not a valid HTTP-date, or if the request
  519. // method is neither GET nor HEAD.
  520. const modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince));
  521. if (modifiedStale) {
  522. return false;
  523. }
  524. }
  525. return true;
  526. }
  527. /**
  528. * @returns {boolean} true when range is fresh, otherwise false
  529. */
  530. function isRangeFresh() {
  531. const ifRange = /** @type {string | undefined} */
  532. getRequestHeader(req, "if-range");
  533. if (!ifRange) {
  534. return true;
  535. }
  536. // if-range as etag
  537. if (ifRange.includes('"')) {
  538. const etag = /** @type {string | undefined} */
  539. getResponseHeader(res, "ETag");
  540. if (!etag) {
  541. return true;
  542. }
  543. return Boolean(etag && ifRange.includes(etag));
  544. }
  545. // if-range as modified date
  546. const lastModified = /** @type {string | undefined} */
  547. getResponseHeader(res, "Last-Modified");
  548. if (!lastModified) {
  549. return true;
  550. }
  551. return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
  552. }
  553. /**
  554. * @returns {string | undefined} range header
  555. */
  556. function getRangeHeader() {
  557. const range = /** @type {string} */getRequestHeader(req, "range");
  558. if (range && BYTES_RANGE_REGEXP.test(range)) {
  559. return range;
  560. }
  561. return undefined;
  562. }
  563. /**
  564. * @param {import("range-parser").Range} range range
  565. * @returns {[number, number]} offset and length
  566. */
  567. function getOffsetAndLenFromRange(range) {
  568. const offset = range.start;
  569. const len = range.end - range.start + 1;
  570. return [offset, len];
  571. }
  572. /**
  573. * @param {number} offset offset
  574. * @param {number} len len
  575. * @returns {[number, number]} start and end
  576. */
  577. function calcStartAndEnd(offset, len) {
  578. const start = offset;
  579. const end = Math.max(offset, offset + len - 1);
  580. return [start, end];
  581. }
  582. /**
  583. * @returns {Promise<void>}
  584. */
  585. async function processRequest() {
  586. // Pipe and SendFile
  587. /** @type {FilenameWithExtra | undefined} */
  588. let resolved;
  589. const requestUrl = /** @type {string} */getRequestURL(req);
  590. try {
  591. resolved = await getFilenameFromUrl(context, requestUrl);
  592. } catch (err) {
  593. // Fallback to 403 for unknown errors
  594. const errorCode = typeof err === "object" && err !== null && typeof (/** @type {FilenameError} */err.statusCode) !== "undefined" ? /** @type {FilenameError} */err.statusCode : undefined;
  595. if (errorCode === 403) {
  596. context.logger.error(`Malicious path "${requestUrl}".`);
  597. }
  598. await errorHandler(/** @type {NodeJS.ErrnoException} */err, errorCode === 400 ? "Bad Request" : errorCode === 403 ? "Forbidden" : undefined, errorCode);
  599. return;
  600. }
  601. if (!resolved) {
  602. await goNext();
  603. return;
  604. }
  605. if (getHeadersSent(res)) {
  606. await goNext();
  607. return;
  608. }
  609. const {
  610. extra,
  611. filename
  612. } = resolved;
  613. const {
  614. size
  615. } = extra.stats;
  616. let len = size;
  617. let offset = 0;
  618. // Send logic
  619. if (context.options.headers) {
  620. let {
  621. headers
  622. } = context.options;
  623. if (typeof headers === "function") {
  624. headers = /** @type {NormalizedHeaders} */
  625. headers(req, res, context);
  626. }
  627. /**
  628. * @type {{ key: string, value: string | number }[]}
  629. */
  630. const allHeaders = [];
  631. if (typeof headers !== "undefined") {
  632. if (!Array.isArray(headers)) {
  633. for (const name in headers) {
  634. allHeaders.push({
  635. key: name,
  636. value: headers[name]
  637. });
  638. }
  639. headers = allHeaders;
  640. }
  641. for (const {
  642. key,
  643. value
  644. } of headers) {
  645. setResponseHeader(res, key, value);
  646. }
  647. }
  648. }
  649. if (!getResponseHeader(res, "Accept-Ranges")) {
  650. setResponseHeader(res, "Accept-Ranges", "bytes");
  651. }
  652. if (!getResponseHeader(res, "Cache-Control")) {
  653. const {
  654. cacheControl,
  655. cacheImmutable
  656. } = context.options;
  657. let cacheControlValue;
  658. if ((cacheImmutable === undefined || cacheImmutable) && extra.immutable) {
  659. cacheControlValue = `public, max-age=${Math.floor(MAX_MAX_AGE / 1000)}, immutable`;
  660. } else if (typeof cacheControl === "boolean") {
  661. cacheControlValue = `public, max-age=${Math.floor(MAX_MAX_AGE / 1000)}`;
  662. } else if (typeof cacheControl === "number") {
  663. const maxAge = Math.min(Math.max(0, cacheControl), MAX_MAX_AGE);
  664. cacheControlValue = `public, max-age=${Math.floor(maxAge / 1000)}`;
  665. } else if (typeof cacheControl === "string") {
  666. cacheControlValue = cacheControl;
  667. } else if (cacheControl) {
  668. const maxAge = cacheControl.maxAge !== undefined ? Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) : MAX_MAX_AGE;
  669. cacheControlValue = `public, max-age=${Math.floor(maxAge / 1000)}`;
  670. if (cacheControl.immutable) {
  671. cacheControlValue += ", immutable";
  672. }
  673. }
  674. if (cacheControlValue) {
  675. setResponseHeader(res, "Cache-Control", cacheControlValue);
  676. }
  677. }
  678. if (context.options.lastModified && !getResponseHeader(res, "Last-Modified")) {
  679. const modified = extra.stats.mtime.toUTCString();
  680. setResponseHeader(res, "Last-Modified", modified);
  681. }
  682. /** @type {number} */
  683. let start;
  684. /** @type {number} */
  685. let end;
  686. /** @type {undefined | Buffer | ReadStream} */
  687. let bufferOrStream;
  688. /** @type {number | undefined} */
  689. let byteLength;
  690. const rangeHeader = getRangeHeader();
  691. if (context.options.etag && !getResponseHeader(res, "ETag")) {
  692. const isStrongETag = context.options.etag === "strong";
  693. // TODO cache strong etag generation?
  694. if (isStrongETag) {
  695. if (rangeHeader) {
  696. const parsedRanges = /** @type {import("range-parser").Ranges | import("range-parser").Result} */
  697. parseRangeHeaders(`${size}|${rangeHeader}`);
  698. if (parsedRanges !== -2 && parsedRanges !== -1 && parsedRanges.length === 1) {
  699. [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]);
  700. }
  701. }
  702. [start, end] = calcStartAndEnd(offset, len);
  703. try {
  704. const result = createReadStreamOrReadFile(filename, extra.outputFileSystem, start, end);
  705. ({
  706. bufferOrStream,
  707. byteLength
  708. } = result);
  709. } catch (error) {
  710. await errorHandler(/** @type {NodeJS.ErrnoException} */error);
  711. return;
  712. }
  713. }
  714. const result = await etag(isStrongETag ? (/** @type {Buffer | ReadStream} */bufferOrStream) : extra.stats);
  715. // Because we already read stream, we can cache buffer to avoid extra read from fs
  716. if (result.buffer) {
  717. bufferOrStream = result.buffer;
  718. }
  719. setResponseHeader(res, "ETag", result.hash);
  720. }
  721. if (!getResponseHeader(res, "Content-Type") || getStatusCode(res) === 404) {
  722. removeResponseHeader(res, "Content-Type");
  723. // content-type name (like application/javascript; charset=utf-8) or false
  724. const contentType = mime.contentType(path.extname(filename));
  725. // Only set content-type header if media type is known
  726. // https://tools.ietf.org/html/rfc7231#section-3.1.1.5
  727. if (contentType) {
  728. setResponseHeader(res, "Content-Type", contentType);
  729. } else if (context.options.mimeTypeDefault) {
  730. setResponseHeader(res, "Content-Type", context.options.mimeTypeDefault);
  731. }
  732. }
  733. // Conditional GET support
  734. if (isConditionalGET()) {
  735. if (isPreconditionFailure()) {
  736. await sendError("Precondition Failed", 412, {
  737. modifyResponseData: context.options.modifyResponseData
  738. });
  739. return;
  740. }
  741. if (isCachable() && isFresh({
  742. etag: (/** @type {string | undefined} */
  743. getResponseHeader(res, "ETag")),
  744. "last-modified": (/** @type {string | undefined} */
  745. getResponseHeader(res, "Last-Modified"))
  746. })) {
  747. setStatusCode(res, 304);
  748. // Remove content header fields
  749. removeResponseHeader(res, "Content-Encoding");
  750. removeResponseHeader(res, "Content-Language");
  751. removeResponseHeader(res, "Content-Length");
  752. removeResponseHeader(res, "Content-Range");
  753. removeResponseHeader(res, "Content-Type");
  754. finish(res);
  755. return;
  756. }
  757. }
  758. let isPartialContent = false;
  759. if (rangeHeader) {
  760. let parsedRanges = /** @type {import("range-parser").Ranges | import("range-parser").Result | []} */
  761. parseRangeHeaders(`${size}|${rangeHeader}`);
  762. // If-Range support
  763. if (!isRangeFresh()) {
  764. parsedRanges = [];
  765. }
  766. if (parsedRanges === -1) {
  767. context.logger.error("Unsatisfiable range for 'Range' header.");
  768. setResponseHeader(res, "Content-Range", getValueContentRangeHeader("bytes", size));
  769. await sendError("Range Not Satisfiable", 416, {
  770. headers: {
  771. "Content-Range": getResponseHeader(res, "Content-Range")
  772. },
  773. modifyResponseData: context.options.modifyResponseData
  774. });
  775. return;
  776. } else if (parsedRanges === -2) {
  777. context.logger.error("A malformed 'Range' header was provided. A regular response will be sent for this request.");
  778. } else if (parsedRanges.length > 1) {
  779. 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.");
  780. }
  781. if (parsedRanges !== -2 && parsedRanges.length === 1) {
  782. // Content-Range
  783. setStatusCode(res, 206);
  784. setResponseHeader(res, "Content-Range", getValueContentRangeHeader("bytes", size, /** @type {import("range-parser").Ranges} */parsedRanges[0]));
  785. isPartialContent = true;
  786. [offset, len] = getOffsetAndLenFromRange(parsedRanges[0]);
  787. }
  788. }
  789. // When strong Etag generation is enabled we already read file, so we can skip extra fs call
  790. if (!bufferOrStream) {
  791. [start, end] = calcStartAndEnd(offset, len);
  792. try {
  793. ({
  794. bufferOrStream,
  795. byteLength
  796. } = createReadStreamOrReadFile(filename, extra.outputFileSystem, start, end));
  797. } catch (error) {
  798. await errorHandler(/** @type {NodeJS.ErrnoException} */error);
  799. return;
  800. }
  801. }
  802. if (context.options.modifyResponseData) {
  803. ({
  804. data: bufferOrStream,
  805. byteLength
  806. } = context.options.modifyResponseData(req, res, bufferOrStream, /** @type {number} */
  807. byteLength));
  808. }
  809. setResponseHeader(res, "Content-Length", /** @type {number} */
  810. byteLength);
  811. if (method === "HEAD") {
  812. if (!isPartialContent) {
  813. setStatusCode(res, 200);
  814. }
  815. finish(res);
  816. return;
  817. }
  818. if (!isPartialContent) {
  819. setStatusCode(res, 200);
  820. }
  821. const isPipeSupports = typeof (/** @type {import("fs").ReadStream} */bufferOrStream.pipe) === "function";
  822. if (!isPipeSupports) {
  823. send(res, /** @type {Buffer} */bufferOrStream);
  824. return;
  825. }
  826. // Error handling
  827. /** @type {import("fs").ReadStream} */
  828. bufferOrStream.on("error", error => {
  829. context.logger.error("Stream error:", error);
  830. // clean up stream early
  831. destroyStream(/** @type {import("fs").ReadStream} */bufferOrStream, true);
  832. errorHandler(error);
  833. });
  834. pipe(res, /** @type {ReadStream} */bufferOrStream);
  835. const outgoing = getOutgoing(res);
  836. if (outgoing) {
  837. // Response finished, cleanup
  838. finished(outgoing, err => {
  839. if (err) {
  840. context.logger.error("Stream error:", err);
  841. }
  842. destroyStream(/** @type {import("fs").ReadStream} */bufferOrStream, true);
  843. });
  844. }
  845. }
  846. ready(context, processRequest, req);
  847. };
  848. }
  849. module.exports = wrapper;
  850. module.exports.getFilenameFromUrl = getFilenameFromUrl;
  851. module.exports.ready = ready;