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