171 lines
4.6 KiB
JavaScript
171 lines
4.6 KiB
JavaScript
|
"use strict";
|
||
|
|
||
|
const path = require("path");
|
||
|
const {
|
||
|
parse
|
||
|
} = require("url");
|
||
|
const querystring = require("querystring");
|
||
|
const getPaths = require("./getPaths");
|
||
|
|
||
|
/** @typedef {import("../index.js").IncomingMessage} IncomingMessage */
|
||
|
/** @typedef {import("../index.js").ServerResponse} ServerResponse */
|
||
|
|
||
|
const cacheStore = new WeakMap();
|
||
|
|
||
|
/**
|
||
|
* @template T
|
||
|
* @param {Function} fn
|
||
|
* @param {{ cache?: Map<string, { data: T }> } | undefined} cache
|
||
|
* @param {(value: T) => T} callback
|
||
|
* @returns {any}
|
||
|
*/
|
||
|
// @ts-ignore
|
||
|
const mem = (fn, {
|
||
|
cache = new Map()
|
||
|
} = {}, callback) => {
|
||
|
/**
|
||
|
* @param {any} arguments_
|
||
|
* @return {any}
|
||
|
*/
|
||
|
const memoized = (...arguments_) => {
|
||
|
const [key] = arguments_;
|
||
|
const cacheItem = cache.get(key);
|
||
|
if (cacheItem) {
|
||
|
return cacheItem.data;
|
||
|
}
|
||
|
let result = fn.apply(void 0, arguments_);
|
||
|
result = callback(result);
|
||
|
cache.set(key, {
|
||
|
data: result
|
||
|
});
|
||
|
return result;
|
||
|
};
|
||
|
cacheStore.set(memoized, cache);
|
||
|
return memoized;
|
||
|
};
|
||
|
// eslint-disable-next-line no-undefined
|
||
|
const memoizedParse = mem(parse, undefined, value => {
|
||
|
if (value.pathname) {
|
||
|
// eslint-disable-next-line no-param-reassign
|
||
|
value.pathname = decode(value.pathname);
|
||
|
}
|
||
|
return value;
|
||
|
});
|
||
|
const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} Extra
|
||
|
* @property {import("fs").Stats=} stats
|
||
|
* @property {number=} errorCode
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* decodeURIComponent.
|
||
|
*
|
||
|
* Allows V8 to only deoptimize this fn instead of all of send().
|
||
|
*
|
||
|
* @param {string} input
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
|
||
|
function decode(input) {
|
||
|
return querystring.unescape(input);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @template {IncomingMessage} Request
|
||
|
* @template {ServerResponse} Response
|
||
|
* @param {import("../index.js").Context<Request, Response>} context
|
||
|
* @param {string} url
|
||
|
* @param {Extra=} extra
|
||
|
* @returns {string | undefined}
|
||
|
*/
|
||
|
function getFilenameFromUrl(context, url, extra = {}) {
|
||
|
const {
|
||
|
options
|
||
|
} = context;
|
||
|
const paths = getPaths(context);
|
||
|
|
||
|
/** @type {string | undefined} */
|
||
|
let foundFilename;
|
||
|
/** @type {URL} */
|
||
|
let urlObject;
|
||
|
try {
|
||
|
// The `url` property of the `request` is contains only `pathname`, `search` and `hash`
|
||
|
urlObject = memoizedParse(url, false, true);
|
||
|
} catch (_ignoreError) {
|
||
|
return;
|
||
|
}
|
||
|
for (const {
|
||
|
publicPath,
|
||
|
outputPath
|
||
|
} of paths) {
|
||
|
/** @type {string | undefined} */
|
||
|
let filename;
|
||
|
/** @type {URL} */
|
||
|
let publicPathObject;
|
||
|
try {
|
||
|
publicPathObject = memoizedParse(publicPath !== "auto" && publicPath ? publicPath : "/", false, true);
|
||
|
} catch (_ignoreError) {
|
||
|
// eslint-disable-next-line no-continue
|
||
|
continue;
|
||
|
}
|
||
|
const {
|
||
|
pathname
|
||
|
} = urlObject;
|
||
|
const {
|
||
|
pathname: publicPathPathname
|
||
|
} = publicPathObject;
|
||
|
if (pathname && pathname.startsWith(publicPathPathname)) {
|
||
|
// Null byte(s)
|
||
|
if (pathname.includes("\0")) {
|
||
|
// eslint-disable-next-line no-param-reassign
|
||
|
extra.errorCode = 400;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// ".." is malicious
|
||
|
if (UP_PATH_REGEXP.test(path.normalize(`./${pathname}`))) {
|
||
|
// eslint-disable-next-line no-param-reassign
|
||
|
extra.errorCode = 403;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Strip the `pathname` property from the `publicPath` option from the start of requested url
|
||
|
// `/complex/foo.js` => `foo.js`
|
||
|
// and add outputPath
|
||
|
// `foo.js` => `/home/user/my-project/dist/foo.js`
|
||
|
filename = path.join(outputPath, pathname.slice(publicPathPathname.length));
|
||
|
try {
|
||
|
// eslint-disable-next-line no-param-reassign
|
||
|
extra.stats = /** @type {import("fs").statSync} */
|
||
|
context.outputFileSystem.statSync(filename);
|
||
|
} catch (_ignoreError) {
|
||
|
// eslint-disable-next-line no-continue
|
||
|
continue;
|
||
|
}
|
||
|
if (extra.stats.isFile()) {
|
||
|
foundFilename = filename;
|
||
|
break;
|
||
|
} else if (extra.stats.isDirectory() && (typeof options.index === "undefined" || options.index)) {
|
||
|
const indexValue = typeof options.index === "undefined" || typeof options.index === "boolean" ? "index.html" : options.index;
|
||
|
filename = path.join(filename, indexValue);
|
||
|
try {
|
||
|
extra.stats = /** @type {import("fs").statSync} */
|
||
|
context.outputFileSystem.statSync(filename);
|
||
|
} catch (__ignoreError) {
|
||
|
// eslint-disable-next-line no-continue
|
||
|
continue;
|
||
|
}
|
||
|
if (extra.stats.isFile()) {
|
||
|
foundFilename = filename;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// eslint-disable-next-line consistent-return
|
||
|
return foundFilename;
|
||
|
}
|
||
|
module.exports = getFilenameFromUrl;
|