| | |
| | | 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"); |
| | |
| | | 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} |
| | | */ |
| | |
| | | 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) => { |
| | |
| | | /** @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 |
| | | */ |
| | |
| | | }; |
| | | |
| | | /** |
| | | * Returns true, if integrity matches. |
| | | * @param {Buffer} content content |
| | | * @param {string} integrity integrity |
| | | * @returns {boolean} true, if integrity matches |
| | |
| | | }; |
| | | |
| | | /** |
| | | * Parses key value pairs. |
| | | * @param {string} str input |
| | | * @returns {Record<string, string>} parsed |
| | | */ |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 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 |
| | |
| | | }; |
| | | |
| | | /** |
| | | * Defines the lockfile entry type used by this module. |
| | | * @typedef {object} LockfileEntry |
| | | * @property {string} resolved |
| | | * @property {string} integrity |
| | |
| | | */ |
| | | |
| | | /** |
| | | * Are lockfile entries equal. |
| | | * @param {LockfileEntry} a first lockfile entry |
| | | * @param {LockfileEntry} b second lockfile entry |
| | | * @returns {boolean} true when equal, otherwise false |
| | |
| | | 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 |
| | | */ |
| | |
| | | } |
| | | |
| | | /** |
| | | * Returns a string representation. |
| | | * @returns {string} stringified lockfile |
| | | */ |
| | | toString() { |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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 |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 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} |
| | |
| | | }); |
| | | }; |
| | | /** |
| | | * Processes the provided arg. |
| | | * @param {T} arg arg |
| | | * @param {FnWithKeyCallback<R>} callback callback |
| | | * @returns {void} |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 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 |
| | |
| | | /** @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", |
| | |
| | | const logger = compilation.getLogger(`webpack.${PLUGIN_NAME}`); |
| | | /** @type {string} */ |
| | | const lockfileLocation = |
| | | this._lockfileLocation || |
| | | this.options.lockfileLocation || |
| | | join( |
| | | intermediateFs, |
| | | compiler.context, |
| | |
| | | ); |
| | | /** @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 |
| | | */ |
| | |
| | | }; |
| | | |
| | | /** |
| | | * Returns the key. |
| | | * @param {string} url the url |
| | | * @returns {string} the key |
| | | */ |
| | |
| | | |
| | | const getLockfile = cachedWithoutKey( |
| | | /** |
| | | * Handles the callback logic for this hook. |
| | | * @param {(err: Error | null, lockfile?: Lockfile) => void} callback callback |
| | | * @returns {void} |
| | | */ |
| | |
| | | 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 |
| | |
| | | }; |
| | | |
| | | /** |
| | | * Stores the provided lockfile. |
| | | * @param {Lockfile} lockfile lockfile |
| | | * @param {string} url url |
| | | * @param {ResolveContentResult} result result |
| | |
| | | |
| | | 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} |
| | |
| | | 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); |
| | |
| | | content, |
| | | storeLock: storeLock && result.storeLock |
| | | }); |
| | | } |
| | | }, |
| | | redirectCount + 1 |
| | | ); |
| | | } |
| | | |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 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 |
| | |
| | | requestTime |
| | | ); |
| | | /** |
| | | * Processes the provided partial result. |
| | | * @param {Partial<Pick<FetchResultMeta, "fresh">> & (Pick<RedirectFetchResult, "location"> | Pick<ContentFetchResult, "content" | "entry">)} partialResult result |
| | | * @returns {void} |
| | | */ |
| | |
| | | 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) || |
| | |
| | | stream = stream.pipe(createInflate()); |
| | | } |
| | | |
| | | stream.on("data", (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) { |
| | |
| | | |
| | | 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} |
| | |
| | | ); |
| | | |
| | | /** |
| | | * 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; |
| | | } |
| | | } |
| | |
| | | |
| | | const getInfo = cachedWithKey( |
| | | /** |
| | | * Processes the provided url. |
| | | * @param {string} url the url |
| | | * @param {(err: Error | null, info?: Info) => void} callback callback |
| | | * @returns {void} |
| | |
| | | } |
| | | let entry = entryOrString; |
| | | /** |
| | | * Processes the provided locked content. |
| | | * @param {Buffer=} lockedContent locked content |
| | | */ |
| | | const doFetch = (lockedContent) => { |
| | |
| | | } |
| | | const content = /** @type {Buffer} */ (result); |
| | | /** |
| | | * Continue with cached content. |
| | | * @param {Buffer | undefined} _result result |
| | | * @returns {void} |
| | | */ |
| | |
| | | ); |
| | | |
| | | /** |
| | | * Respond with url module. |
| | | * @param {URL} url url |
| | | * @param {ResourceDataWithData} resourceData resource data |
| | | * @param {(err: Error | null, result: true | void) => void} callback callback |