WXL
4 天以前 2cc85c64f1c64a2dbaeae276a3e2ca8420de76b7
node_modules/webpack/lib/schemes/HttpUriPlugin.js
@@ -14,7 +14,6 @@
   createInflate
} = require("zlib");
const NormalModule = require("../NormalModule");
const createSchemaValidation = require("../util/create-schema-validation");
const createHash = require("../util/createHash");
const { dirname, join, mkdirp } = require("../util/fs");
const memoize = require("../util/memoize");
@@ -34,15 +33,28 @@
const getHttp = memoize(() => require("http"));
const getHttps = memoize(() => require("https"));
const MAX_REDIRECTS = 5;
/** @typedef {(url: URL, requestOptions: RequestOptions, callback: (incomingMessage: IncomingMessage) => void) => EventEmitter} Fetch */
/**
 * Defines the events map type used by this module.
 * @typedef {object} EventsMap
 * @property {[Error]} error
 */
/**
 * Returns fn.
 * @param {typeof import("http") | typeof import("https")} request request
 * @param {string | URL | undefined} proxy proxy
 * @returns {(url: URL, requestOptions: RequestOptions, callback: (incomingMessage: IncomingMessage) => void) => EventEmitter} fn
 * @returns {Fetch} fn
 */
const proxyFetch = (request, proxy) => (url, options, callback) => {
   /** @type {EventEmitter<EventsMap>} */
   const eventEmitter = new EventEmitter();
   /**
    * Processes the provided socket.
    * @param {Socket=} socket socket
    * @returns {void}
    */
@@ -66,6 +78,13 @@
            if (res.statusCode === 200) {
               // connected to proxy server
               doRequest(socket);
            } else {
               eventEmitter.emit(
                  "error",
                  new Error(
                     `Failed to connect to proxy server "${proxy}": ${res.statusCode} ${res.statusMessage}`
                  )
               );
            }
         })
         .on("error", (err) => {
@@ -88,25 +107,16 @@
/** @type {InProgressWriteItem[] | undefined} */
let inProgressWrite;
const validate = createSchemaValidation(
   require("../../schemas/plugins/schemes/HttpUriPlugin.check"),
   () => require("../../schemas/plugins/schemes/HttpUriPlugin.json"),
   {
      name: "Http Uri Plugin",
      baseDataPath: "options"
   }
);
/**
 * Returns safe path.
 * @param {string} str path
 * @returns {string} safe path
 */
const toSafePath = (str) =>
   str
      .replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "")
      .replace(/[^a-zA-Z0-9._-]+/g, "_");
   str.replace(/^[^a-z0-9]+|[^a-z0-9]+$/gi, "").replace(/[^a-z0-9._-]+/gi, "_");
/**
 * Returns integrity.
 * @param {Buffer} content content
 * @returns {string} integrity
 */
@@ -118,6 +128,7 @@
};
/**
 * Returns true, if integrity matches.
 * @param {Buffer} content content
 * @param {string} integrity integrity
 * @returns {boolean} true, if integrity matches
@@ -128,6 +139,7 @@
};
/**
 * Parses key value pairs.
 * @param {string} str input
 * @returns {Record<string, string>} parsed
 */
@@ -150,6 +162,7 @@
};
/**
 * Parses cache control.
 * @param {string | undefined} cacheControl Cache-Control header
 * @param {number} requestTime timestamp of request
 * @returns {{ storeCache: boolean, storeLock: boolean, validUntil: number }} Logic for storing in cache and lockfile cache
@@ -177,6 +190,7 @@
};
/**
 * Defines the lockfile entry type used by this module.
 * @typedef {object} LockfileEntry
 * @property {string} resolved
 * @property {string} integrity
@@ -184,6 +198,7 @@
 */
/**
 * Are lockfile entries equal.
 * @param {LockfileEntry} a first lockfile entry
 * @param {LockfileEntry} b second lockfile entry
 * @returns {boolean} true when equal, otherwise false
@@ -194,20 +209,39 @@
   a.contentType === b.contentType;
/**
 * Returns , integrity: ${string}, contentType: ${string}`} stringified entry.
 * @param {LockfileEntry} entry lockfile entry
 * @returns {`resolved: ${string}, integrity: ${string}, contentType: ${string}`} stringified entry
 */
const entryToString = (entry) =>
   `resolved: ${entry.resolved}, integrity: ${entry.integrity}, contentType: ${entry.contentType}`;
/**
 * Sanitize URL for inclusion in error messages
 * @param {string} href URL string to sanitize
 * @returns {string} sanitized URL text for logs/errors
 */
const sanitizeUrlForError = (href) => {
   try {
      const u = new URL(href);
      return `${u.protocol}//${u.host}`;
   } catch (_err) {
      return String(href)
         .slice(0, 200)
         .replace(/[\r\n]/g, "");
   }
};
class Lockfile {
   constructor() {
      /** @type {number} */
      this.version = 1;
      /** @type {Map<string, LockfileEntry | "ignore" | "no-cache">} */
      this.entries = new Map();
   }
   /**
    * Parses the provided source and updates the parser state.
    * @param {string} content content of the lockfile
    * @returns {Lockfile} lockfile
    */
@@ -235,6 +269,7 @@
   }
   /**
    * Returns a string representation.
    * @returns {string} stringified lockfile
    */
   toString() {
@@ -259,16 +294,19 @@
}
/**
 * Defines the fn without key callback type used by this module.
 * @template R
 * @typedef {(err: Error | null, result?: R) => void}  FnWithoutKeyCallback
 */
/**
 * Defines the fn without key type used by this module.
 * @template R
 * @typedef {(callback: FnWithoutKeyCallback<R>) => void} FnWithoutKey
 */
/**
 * Caches d without key.
 * @template R
 * @param {FnWithoutKey<R>} fn function
 * @returns {FnWithoutKey<R>} cached function
@@ -302,31 +340,36 @@
};
/**
 * Defines the fn with key callback type used by this module.
 * @template R
 * @typedef {(err: Error | null, result?: R) => void} FnWithKeyCallback
 */
/**
 * Defines the fn with key type used by this module.
 * @template T
 * @template R
 * @typedef {(item: T, callback: FnWithKeyCallback<R>) => void} FnWithKey
 */
/**
 * Returns } cached function.
 * @template T
 * @template R
 * @param {FnWithKey<T, R>} fn function
 * @param {FnWithKey<T, R>=} forceFn function for the second try
 * @returns {(FnWithKey<T, R>) & { force: FnWithKey<T, R> }} cached function
 * @returns {FnWithKey<T, R> & { force: FnWithKey<T, R> }} cached function
 */
const cachedWithKey = (fn, forceFn = fn) => {
   /**
    * Defines the cache entry type used by this module.
    * @template R
    * @typedef {{ result?: R, error?: Error, callbacks?: FnWithKeyCallback<R>[], force?: true }} CacheEntry
    */
   /** @type {Map<T, CacheEntry<R>>} */
   const cache = new Map();
   /**
    * Processes the provided arg.
    * @param {T} arg arg
    * @param {FnWithKeyCallback<R>} callback callback
    * @returns {void}
@@ -359,6 +402,7 @@
      });
   };
   /**
    * Processes the provided arg.
    * @param {T} arg arg
    * @param {FnWithKeyCallback<R>} callback callback
    * @returns {void}
@@ -395,12 +439,14 @@
};
/**
 * Defines the lockfile cache type used by this module.
 * @typedef {object} LockfileCache
 * @property {Lockfile} lockfile lockfile
 * @property {Snapshot} snapshot snapshot
 */
/**
 * Defines the resolve content result type used by this module.
 * @typedef {object} ResolveContentResult
 * @property {LockfileEntry} entry lockfile entry
 * @property {Buffer} content content
@@ -412,30 +458,44 @@
/** @typedef {FetchResultMeta & { entry: LockfileEntry, content: Buffer }} ContentFetchResult */
/** @typedef {RedirectFetchResult | ContentFetchResult} FetchResult */
/** @typedef {(uri: string) => boolean} AllowedUriFn */
const PLUGIN_NAME = "HttpUriPlugin";
class HttpUriPlugin {
   /**
    * Creates an instance of HttpUriPlugin.
    * @param {HttpUriPluginOptions} options options
    */
   constructor(options) {
      validate(options);
      this._lockfileLocation = options.lockfileLocation;
      this._cacheLocation = options.cacheLocation;
      this._upgrade = options.upgrade;
      this._frozen = options.frozen;
      this._allowedUris = options.allowedUris;
      this._proxy = options.proxy;
      /** @type {HttpUriPluginOptions} */
      this.options = options;
   }
   /**
    * Apply the plugin
    * Applies the plugin by registering its hooks on the compiler.
    * @param {Compiler} compiler the compiler instance
    * @returns {void}
    */
   apply(compiler) {
      compiler.hooks.validate.tap(PLUGIN_NAME, () => {
         compiler.validate(
            () => require("../../schemas/plugins/schemes/HttpUriPlugin.json"),
            this.options,
            {
               name: "Http Uri Plugin",
               baseDataPath: "options"
            },
            (options) =>
               require("../../schemas/plugins/schemes/HttpUriPlugin.check")(options)
         );
      });
      const proxy =
         this._proxy || process.env.http_proxy || process.env.HTTP_PROXY;
         this.options.proxy || process.env.http_proxy || process.env.HTTP_PROXY;
      /**
       * @type {{ scheme: "http" | "https", fetch: Fetch }[]}
       */
      const schemes = [
         {
            scheme: "http",
@@ -459,7 +519,7 @@
            const logger = compilation.getLogger(`webpack.${PLUGIN_NAME}`);
            /** @type {string} */
            const lockfileLocation =
               this._lockfileLocation ||
               this.options.lockfileLocation ||
               join(
                  intermediateFs,
                  compiler.context,
@@ -469,21 +529,22 @@
               );
            /** @type {string | false} */
            const cacheLocation =
               this._cacheLocation !== undefined
                  ? this._cacheLocation
               this.options.cacheLocation !== undefined
                  ? this.options.cacheLocation
                  : `${lockfileLocation}.data`;
            const upgrade = this._upgrade || false;
            const frozen = this._frozen || false;
            const upgrade = this.options.upgrade || false;
            const frozen = this.options.frozen || false;
            const hashFunction = "sha512";
            const hashDigest = "hex";
            const hashDigestLength = 20;
            const allowedUris = this._allowedUris;
            const allowedUris = this.options.allowedUris;
            let warnedAboutEol = false;
            /** @type {Map<string, string>} */
            const cacheKeyCache = new Map();
            /**
             * Returns the key.
             * @param {string} url the url
             * @returns {string} the key
             */
@@ -496,6 +557,7 @@
            };
            /**
             * Returns the key.
             * @param {string} url the url
             * @returns {string} the key
             */
@@ -517,6 +579,7 @@
            const getLockfile = cachedWithoutKey(
               /**
                * Handles the callback logic for this hook.
                * @param {(err: Error | null, lockfile?: Lockfile) => void} callback callback
                * @returns {void}
                */
@@ -569,6 +632,7 @@
            let lockfileUpdates;
            /**
             * Stores the provided lockfile.
             * @param {Lockfile} lockfile lockfile instance
             * @param {string} url url to store
             * @param {LockfileEntry | "ignore" | "no-cache"} entry lockfile entry
@@ -608,6 +672,7 @@
            };
            /**
             * Stores the provided lockfile.
             * @param {Lockfile} lockfile lockfile
             * @param {string} url url
             * @param {ResolveContentResult} result result
@@ -637,12 +702,51 @@
            for (const { scheme, fetch } of schemes) {
               /**
                * Validate redirect location.
                * @param {string} location Location header value (relative or absolute)
                * @param {string} base current absolute URL
                * @returns {string} absolute, validated redirect target
                */
               const validateRedirectLocation = (location, base) => {
                  /** @type {URL} */
                  let nextUrl;
                  try {
                     nextUrl = new URL(location, base);
                  } catch (err) {
                     throw new Error(
                        `Invalid redirect URL: ${sanitizeUrlForError(location)}`,
                        { cause: err }
                     );
                  }
                  if (nextUrl.protocol !== "http:" && nextUrl.protocol !== "https:") {
                     throw new Error(
                        `Redirected URL uses disallowed protocol: ${sanitizeUrlForError(nextUrl.href)}`
                     );
                  }
                  if (!isAllowed(nextUrl.href)) {
                     throw new Error(
                        `${nextUrl.href} doesn't match the allowedUris policy after redirect. These URIs are allowed:\n${allowedUris
                           .map((uri) => ` - ${uri}`)
                           .join("\n")}`
                     );
                  }
                  return nextUrl.href;
               };
               /**
                * Processes the provided url.
                * @param {string} url URL
                * @param {string | null} integrity integrity
                * @param {(err: Error | null, resolveContentResult?: ResolveContentResult) => void} callback callback
                * @param {number=} redirectCount number of followed redirects
                */
               const resolveContent = (url, integrity, callback) => {
               const resolveContent = (
                  url,
                  integrity,
                  callback,
                  redirectCount = 0
               ) => {
                  /**
                   * Processes the provided err.
                   * @param {Error | null} err error
                   * @param {FetchResult=} _result fetch result
                   * @returns {void}
@@ -653,8 +757,19 @@
                     const result = /** @type {FetchResult} */ (_result);
                     if ("location" in result) {
                        // Validate redirect target before following
                        /** @type {string} */
                        let absolute;
                        try {
                           absolute = validateRedirectLocation(result.location, url);
                        } catch (err_) {
                           return callback(/** @type {Error} */ (err_));
                        }
                        if (redirectCount >= MAX_REDIRECTS) {
                           return callback(new Error("Too many redirects"));
                        }
                        return resolveContent(
                           result.location,
                           absolute,
                           integrity,
                           (err, innerResult) => {
                              if (err) return callback(err);
@@ -665,7 +780,8 @@
                                 content,
                                 storeLock: storeLock && result.storeLock
                              });
                           }
                           },
                           redirectCount + 1
                        );
                     }
@@ -689,6 +805,7 @@
               };
               /**
                * Processes the provided url.
                * @param {string} url URL
                * @param {FetchResult | RedirectFetchResult | undefined} cachedResult result from cache
                * @param {(err: Error | null, fetchResult?: FetchResult) => void} callback callback
@@ -715,6 +832,7 @@
                        requestTime
                     );
                     /**
                      * Processes the provided partial result.
                      * @param {Partial<Pick<FetchResultMeta, "fresh">> & (Pick<RedirectFetchResult, "location"> | Pick<ContentFetchResult, "content" | "entry">)} partialResult result
                      * @returns {void}
                      */
@@ -781,9 +899,17 @@
                        res.statusCode >= 301 &&
                        res.statusCode <= 308
                     ) {
                        const result = {
                           location: new URL(location, url).href
                        };
                        /** @type {string} */
                        let absolute;
                        try {
                           absolute = validateRedirectLocation(location, url);
                        } catch (err) {
                           logger.log(
                              `GET ${url} [${res.statusCode}] -> ${String(location)} (rejected: ${/** @type {Error} */ (err).message})`
                           );
                           return callback(/** @type {Error} */ (err));
                        }
                        const result = { location: absolute };
                        if (
                           !cachedResult ||
                           !("location" in cachedResult) ||
@@ -820,9 +946,16 @@
                        stream = stream.pipe(createInflate());
                     }
                     stream.on("data", (chunk) => {
                        bufferArr.push(chunk);
                     });
                     stream.on(
                        "data",
                        /**
                         * Handles the callback logic for this hook.
                         * @param {Buffer} chunk chunk
                         */
                        (chunk) => {
                           bufferArr.push(chunk);
                        }
                     );
                     stream.on("end", () => {
                        if (!res.complete) {
@@ -860,6 +993,7 @@
               const fetchContent = cachedWithKey(
                  /**
                   * Handles the callback logic for this hook.
                   * @param {string} url URL
                   * @param {(err: Error | null, result?: FetchResult) => void} callback callback
                   * @returns {void}
@@ -878,16 +1012,35 @@
               );
               /**
                * Checks whether this http uri plugin is allowed.
                * @param {string} uri uri
                * @returns {boolean} true when allowed, otherwise false
                */
               const isAllowed = (uri) => {
                  /** @type {URL} */
                  let parsedUri;
                  try {
                     // Parse the URI to prevent userinfo bypass attacks
                     // (e.g., http://allowed@malicious/path where @malicious is the actual host)
                     parsedUri = new URL(uri);
                  } catch (_err) {
                     return false;
                  }
                  for (const allowed of allowedUris) {
                     if (typeof allowed === "string") {
                        if (uri.startsWith(allowed)) return true;
                        /** @type {URL} */
                        let parsedAllowed;
                        try {
                           parsedAllowed = new URL(allowed);
                        } catch (_err) {
                           continue;
                        }
                        if (parsedUri.href.startsWith(parsedAllowed.href)) {
                           return true;
                        }
                     } else if (typeof allowed === "function") {
                        if (allowed(uri)) return true;
                     } else if (allowed.test(uri)) {
                        if (allowed(parsedUri.href)) return true;
                     } else if (allowed.test(parsedUri.href)) {
                        return true;
                     }
                  }
@@ -898,6 +1051,7 @@
               const getInfo = cachedWithKey(
                  /**
                   * Processes the provided url.
                   * @param {string} url the url
                   * @param {(err: Error | null, info?: Info) => void} callback callback
                   * @returns {void}
@@ -970,6 +1124,7 @@
                        }
                        let entry = entryOrString;
                        /**
                         * Processes the provided locked content.
                         * @param {Buffer=} lockedContent locked content
                         */
                        const doFetch = (lockedContent) => {
@@ -1050,6 +1205,7 @@
                              }
                              const content = /** @type {Buffer} */ (result);
                              /**
                               * Continue with cached content.
                               * @param {Buffer | undefined} _result result
                               * @returns {void}
                               */
@@ -1147,6 +1303,7 @@
               );
               /**
                * Respond with url module.
                * @param {URL} url url
                * @param {ResourceDataWithData} resourceData resource data
                * @param {(err: Error | null, result: true | void) => void} callback callback