195 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			195 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 {
 | 
						|
          // 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;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  } // eslint-disable-next-line consistent-return
 | 
						|
 | 
						|
 | 
						|
  return foundFilename;
 | 
						|
}
 | 
						|
 | 
						|
module.exports = getFilenameFromUrl; |