getFilenameFromUrl.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. "use strict";
  2. const path = require("path");
  3. const {
  4. parse
  5. } = require("url");
  6. const querystring = require("querystring");
  7. const getPaths = require("./getPaths");
  8. /** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
  9. /** @typedef {import("../index.js").ServerResponse} ServerResponse */
  10. const cacheStore = new WeakMap();
  11. /**
  12. * @template T
  13. * @param {Function} fn
  14. * @param {{ cache?: Map<string, { data: T }> } | undefined} cache
  15. * @param {(value: T) => T} callback
  16. * @returns {any}
  17. */
  18. // @ts-ignore
  19. const mem = (fn, {
  20. cache = new Map()
  21. } = {}, callback) => {
  22. /**
  23. * @param {any} arguments_
  24. * @return {any}
  25. */
  26. const memoized = (...arguments_) => {
  27. const [key] = arguments_;
  28. const cacheItem = cache.get(key);
  29. if (cacheItem) {
  30. return cacheItem.data;
  31. }
  32. let result = fn.apply(void 0, arguments_);
  33. result = callback(result);
  34. cache.set(key, {
  35. data: result
  36. });
  37. return result;
  38. };
  39. cacheStore.set(memoized, cache);
  40. return memoized;
  41. }; // eslint-disable-next-line no-undefined
  42. const memoizedParse = mem(parse, undefined, value => {
  43. if (value.pathname) {
  44. // eslint-disable-next-line no-param-reassign
  45. value.pathname = decode(value.pathname);
  46. }
  47. return value;
  48. });
  49. const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
  50. /**
  51. * @typedef {Object} Extra
  52. * @property {import("fs").Stats=} stats
  53. * @property {number=} errorCode
  54. */
  55. /**
  56. * decodeURIComponent.
  57. *
  58. * Allows V8 to only deoptimize this fn instead of all of send().
  59. *
  60. * @param {string} input
  61. * @returns {string}
  62. */
  63. function decode(input) {
  64. return querystring.unescape(input);
  65. }
  66. /**
  67. * @template {IncomingMessage} Request
  68. * @template {ServerResponse} Response
  69. * @param {import("../index.js").Context<Request, Response>} context
  70. * @param {string} url
  71. * @param {Extra=} extra
  72. * @returns {string | undefined}
  73. */
  74. function getFilenameFromUrl(context, url, extra = {}) {
  75. const {
  76. options
  77. } = context;
  78. const paths = getPaths(context);
  79. /** @type {string | undefined} */
  80. let foundFilename;
  81. /** @type {URL} */
  82. let urlObject;
  83. try {
  84. // The `url` property of the `request` is contains only `pathname`, `search` and `hash`
  85. urlObject = memoizedParse(url, false, true);
  86. } catch (_ignoreError) {
  87. return;
  88. }
  89. for (const {
  90. publicPath,
  91. outputPath
  92. } of paths) {
  93. /** @type {string | undefined} */
  94. let filename;
  95. /** @type {URL} */
  96. let publicPathObject;
  97. try {
  98. publicPathObject = memoizedParse(publicPath !== "auto" && publicPath ? publicPath : "/", false, true);
  99. } catch (_ignoreError) {
  100. // eslint-disable-next-line no-continue
  101. continue;
  102. }
  103. const {
  104. pathname
  105. } = urlObject;
  106. const {
  107. pathname: publicPathPathname
  108. } = publicPathObject;
  109. if (pathname && pathname.startsWith(publicPathPathname)) {
  110. // Null byte(s)
  111. if (pathname.includes("\0")) {
  112. // eslint-disable-next-line no-param-reassign
  113. extra.errorCode = 400;
  114. return;
  115. } // ".." is malicious
  116. if (UP_PATH_REGEXP.test(path.normalize(`./${pathname}`))) {
  117. // eslint-disable-next-line no-param-reassign
  118. extra.errorCode = 403;
  119. return;
  120. } // Strip the `pathname` property from the `publicPath` option from the start of requested url
  121. // `/complex/foo.js` => `foo.js`
  122. // and add outputPath
  123. // `foo.js` => `/home/user/my-project/dist/foo.js`
  124. filename = path.join(outputPath, pathname.slice(publicPathPathname.length));
  125. try {
  126. // eslint-disable-next-line no-param-reassign
  127. extra.stats =
  128. /** @type {import("fs").statSync} */
  129. context.outputFileSystem.statSync(filename);
  130. } catch (_ignoreError) {
  131. // eslint-disable-next-line no-continue
  132. continue;
  133. }
  134. if (extra.stats.isFile()) {
  135. foundFilename = filename;
  136. break;
  137. } else if (extra.stats.isDirectory() && (typeof options.index === "undefined" || options.index)) {
  138. const indexValue = typeof options.index === "undefined" || typeof options.index === "boolean" ? "index.html" : options.index;
  139. filename = path.join(filename, indexValue);
  140. try {
  141. // eslint-disable-next-line no-param-reassign
  142. extra.stats =
  143. /** @type {import("fs").statSync} */
  144. context.outputFileSystem.statSync(filename);
  145. } catch (__ignoreError) {
  146. // eslint-disable-next-line no-continue
  147. continue;
  148. }
  149. if (extra.stats.isFile()) {
  150. foundFilename = filename;
  151. break;
  152. }
  153. }
  154. }
  155. } // eslint-disable-next-line consistent-return
  156. return foundFilename;
  157. }
  158. module.exports = getFilenameFromUrl;