getFilenameFromUrl.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. "use strict";
  2. const path = require("node:path");
  3. const querystring = require("node:querystring");
  4. // eslint-disable-next-line n/no-deprecated-api
  5. const {
  6. parse
  7. } = require("node:url");
  8. const getPaths = require("./getPaths");
  9. const memorize = require("./memorize");
  10. /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
  11. /** @typedef {import("../index.js").ServerResponse} ServerResponse */
  12. /**
  13. * @param {string} input input
  14. * @returns {string} unescape input
  15. */
  16. function decode(input) {
  17. return querystring.unescape(input);
  18. }
  19. const memoizedParse = memorize(parse, undefined, value => {
  20. if (value.pathname) {
  21. value.pathname = decode(value.pathname);
  22. }
  23. return value;
  24. });
  25. const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
  26. /**
  27. * @typedef {object} Extra
  28. * @property {import("fs").Stats=} stats stats
  29. * @property {number=} errorCode error code
  30. * @property {boolean=} immutable true when immutable, otherwise false
  31. */
  32. /**
  33. * decodeURIComponent.
  34. *
  35. * Allows V8 to only deoptimize this fn instead of all of send().
  36. * @param {string} input
  37. * @returns {string}
  38. */
  39. // TODO refactor me in the next major release, this function should return `{ filename, stats, error }`
  40. // TODO fix redirect logic when `/` at the end, like https://github.com/pillarjs/send/blob/master/index.js#L586
  41. /**
  42. * @template {IncomingMessage} Request
  43. * @template {ServerResponse} Response
  44. * @param {import("../index.js").FilledContext<Request, Response>} context context
  45. * @param {string} url url
  46. * @param {Extra=} extra extra
  47. * @returns {string | undefined} filename
  48. */
  49. function getFilenameFromUrl(context, url, extra = {}) {
  50. const {
  51. options
  52. } = context;
  53. const paths = getPaths(context);
  54. /** @type {string | undefined} */
  55. let foundFilename;
  56. /** @type {import("node:url").Url} */
  57. let urlObject;
  58. try {
  59. // The `url` property of the `request` is contains only `pathname`, `search` and `hash`
  60. urlObject = memoizedParse(url, false, true);
  61. } catch {
  62. return;
  63. }
  64. for (const {
  65. publicPath,
  66. outputPath,
  67. assetsInfo
  68. } of paths) {
  69. /** @type {string | undefined} */
  70. let filename;
  71. /** @type {import("node:url").Url} */
  72. let publicPathObject;
  73. try {
  74. publicPathObject = memoizedParse(publicPath !== "auto" && publicPath ? publicPath : "/", false, true);
  75. } catch {
  76. continue;
  77. }
  78. const {
  79. pathname
  80. } = urlObject;
  81. const {
  82. pathname: publicPathPathname
  83. } = publicPathObject;
  84. if (pathname && publicPathPathname && pathname.startsWith(publicPathPathname)) {
  85. // Null byte(s)
  86. if (pathname.includes("\0")) {
  87. extra.errorCode = 400;
  88. return;
  89. }
  90. // ".." is malicious
  91. if (UP_PATH_REGEXP.test(path.normalize(`./${pathname}`))) {
  92. extra.errorCode = 403;
  93. return;
  94. }
  95. // Strip the `pathname` property from the `publicPath` option from the start of requested url
  96. // `/complex/foo.js` => `foo.js`
  97. // and add outputPath
  98. // `foo.js` => `/home/user/my-project/dist/foo.js`
  99. filename = path.join(outputPath, pathname.slice(publicPathPathname.length));
  100. try {
  101. extra.stats = context.outputFileSystem.statSync(filename);
  102. } catch {
  103. continue;
  104. }
  105. if (extra.stats.isFile()) {
  106. foundFilename = filename;
  107. // Rspack does not yet support `assetsInfo`, so we need to check if `assetsInfo` exists here
  108. if (assetsInfo) {
  109. const assetInfo = assetsInfo.get(pathname.slice(publicPathPathname.length));
  110. extra.immutable = assetInfo ? assetInfo.immutable : false;
  111. }
  112. break;
  113. } else if (extra.stats.isDirectory() && (typeof options.index === "undefined" || options.index)) {
  114. const indexValue = typeof options.index === "undefined" || typeof options.index === "boolean" ? "index.html" : options.index;
  115. filename = path.join(filename, indexValue);
  116. try {
  117. extra.stats = context.outputFileSystem.statSync(filename);
  118. } catch {
  119. continue;
  120. }
  121. if (extra.stats.isFile()) {
  122. foundFilename = filename;
  123. break;
  124. }
  125. }
  126. }
  127. }
  128. return foundFilename;
  129. }
  130. module.exports = getFilenameFromUrl;