source: frontend/node_modules/webpack/lib/schemes/HttpUriPlugin.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 44.0 KB
Line 
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const EventEmitter = require("events");
9const { basename, extname } = require("path");
10const {
11 // eslint-disable-next-line n/no-unsupported-features/node-builtins
12 createBrotliDecompress,
13 createGunzip,
14 createInflate
15} = require("zlib");
16const NormalModule = require("../NormalModule");
17const createHash = require("../util/createHash");
18const { dirname, join, mkdirp } = require("../util/fs");
19const memoize = require("../util/memoize");
20
21/** @typedef {import("http").IncomingMessage} IncomingMessage */
22/** @typedef {import("http").OutgoingHttpHeaders} OutgoingHttpHeaders */
23/** @typedef {import("http").RequestOptions} RequestOptions */
24/** @typedef {import("net").Socket} Socket */
25/** @typedef {import("stream").Readable} Readable */
26/** @typedef {import("../../declarations/plugins/schemes/HttpUriPlugin").HttpUriPluginOptions} HttpUriPluginOptions */
27/** @typedef {import("../Compiler")} Compiler */
28/** @typedef {import("../FileSystemInfo").Snapshot} Snapshot */
29/** @typedef {import("../Module").BuildInfo} BuildInfo */
30/** @typedef {import("../NormalModuleFactory").ResourceDataWithData} ResourceDataWithData */
31/** @typedef {import("../util/fs").IntermediateFileSystem} IntermediateFileSystem */
32
33const getHttp = memoize(() => require("http"));
34const getHttps = memoize(() => require("https"));
35
36const MAX_REDIRECTS = 5;
37
38/** @typedef {(url: URL, requestOptions: RequestOptions, callback: (incomingMessage: IncomingMessage) => void) => EventEmitter} Fetch */
39
40/**
41 * Defines the events map type used by this module.
42 * @typedef {object} EventsMap
43 * @property {[Error]} error
44 */
45
46/**
47 * Returns fn.
48 * @param {typeof import("http") | typeof import("https")} request request
49 * @param {string | URL | undefined} proxy proxy
50 * @returns {Fetch} fn
51 */
52const proxyFetch = (request, proxy) => (url, options, callback) => {
53 /** @type {EventEmitter<EventsMap>} */
54 const eventEmitter = new EventEmitter();
55
56 /**
57 * Processes the provided socket.
58 * @param {Socket=} socket socket
59 * @returns {void}
60 */
61 const doRequest = (socket) => {
62 request
63 .get(url, { ...options, ...(socket && { socket }) }, callback)
64 .on("error", eventEmitter.emit.bind(eventEmitter, "error"));
65 };
66
67 if (proxy) {
68 const { hostname: host, port } = new URL(proxy);
69
70 getHttp()
71 .request({
72 host, // IP address of proxy server
73 port, // port of proxy server
74 method: "CONNECT",
75 path: url.host
76 })
77 .on("connect", (res, socket) => {
78 if (res.statusCode === 200) {
79 // connected to proxy server
80 doRequest(socket);
81 } else {
82 eventEmitter.emit(
83 "error",
84 new Error(
85 `Failed to connect to proxy server "${proxy}": ${res.statusCode} ${res.statusMessage}`
86 )
87 );
88 }
89 })
90 .on("error", (err) => {
91 eventEmitter.emit(
92 "error",
93 new Error(
94 `Failed to connect to proxy server "${proxy}": ${err.message}`
95 )
96 );
97 })
98 .end();
99 } else {
100 doRequest();
101 }
102
103 return eventEmitter;
104};
105
106/** @typedef {() => void} InProgressWriteItem */
107/** @type {InProgressWriteItem[] | undefined} */
108let inProgressWrite;
109
110/**
111 * Returns safe path.
112 * @param {string} str path
113 * @returns {string} safe path
114 */
115const toSafePath = (str) =>
116 str.replace(/^[^a-z0-9]+|[^a-z0-9]+$/gi, "").replace(/[^a-z0-9._-]+/gi, "_");
117
118/**
119 * Returns integrity.
120 * @param {Buffer} content content
121 * @returns {string} integrity
122 */
123const computeIntegrity = (content) => {
124 const hash = createHash("sha512");
125 hash.update(content);
126 const integrity = `sha512-${hash.digest("base64")}`;
127 return integrity;
128};
129
130/**
131 * Returns true, if integrity matches.
132 * @param {Buffer} content content
133 * @param {string} integrity integrity
134 * @returns {boolean} true, if integrity matches
135 */
136const verifyIntegrity = (content, integrity) => {
137 if (integrity === "ignore") return true;
138 return computeIntegrity(content) === integrity;
139};
140
141/**
142 * Parses key value pairs.
143 * @param {string} str input
144 * @returns {Record<string, string>} parsed
145 */
146const parseKeyValuePairs = (str) => {
147 /** @type {Record<string, string>} */
148 const result = {};
149 for (const item of str.split(",")) {
150 const i = item.indexOf("=");
151 if (i >= 0) {
152 const key = item.slice(0, i).trim();
153 const value = item.slice(i + 1).trim();
154 result[key] = value;
155 } else {
156 const key = item.trim();
157 if (!key) continue;
158 result[key] = key;
159 }
160 }
161 return result;
162};
163
164/**
165 * Parses cache control.
166 * @param {string | undefined} cacheControl Cache-Control header
167 * @param {number} requestTime timestamp of request
168 * @returns {{ storeCache: boolean, storeLock: boolean, validUntil: number }} Logic for storing in cache and lockfile cache
169 */
170const parseCacheControl = (cacheControl, requestTime) => {
171 // When false resource is not stored in cache
172 let storeCache = true;
173 // When false resource is not stored in lockfile cache
174 let storeLock = true;
175 // Resource is only revalidated, after that timestamp and when upgrade is chosen
176 let validUntil = 0;
177 if (cacheControl) {
178 const parsed = parseKeyValuePairs(cacheControl);
179 if (parsed["no-cache"]) storeCache = storeLock = false;
180 if (parsed["max-age"] && !Number.isNaN(Number(parsed["max-age"]))) {
181 validUntil = requestTime + Number(parsed["max-age"]) * 1000;
182 }
183 if (parsed["must-revalidate"]) validUntil = 0;
184 }
185 return {
186 storeLock,
187 storeCache,
188 validUntil
189 };
190};
191
192/**
193 * Defines the lockfile entry type used by this module.
194 * @typedef {object} LockfileEntry
195 * @property {string} resolved
196 * @property {string} integrity
197 * @property {string} contentType
198 */
199
200/**
201 * Are lockfile entries equal.
202 * @param {LockfileEntry} a first lockfile entry
203 * @param {LockfileEntry} b second lockfile entry
204 * @returns {boolean} true when equal, otherwise false
205 */
206const areLockfileEntriesEqual = (a, b) =>
207 a.resolved === b.resolved &&
208 a.integrity === b.integrity &&
209 a.contentType === b.contentType;
210
211/**
212 * Returns , integrity: ${string}, contentType: ${string}`} stringified entry.
213 * @param {LockfileEntry} entry lockfile entry
214 * @returns {`resolved: ${string}, integrity: ${string}, contentType: ${string}`} stringified entry
215 */
216const entryToString = (entry) =>
217 `resolved: ${entry.resolved}, integrity: ${entry.integrity}, contentType: ${entry.contentType}`;
218
219/**
220 * Sanitize URL for inclusion in error messages
221 * @param {string} href URL string to sanitize
222 * @returns {string} sanitized URL text for logs/errors
223 */
224const sanitizeUrlForError = (href) => {
225 try {
226 const u = new URL(href);
227 return `${u.protocol}//${u.host}`;
228 } catch (_err) {
229 return String(href)
230 .slice(0, 200)
231 .replace(/[\r\n]/g, "");
232 }
233};
234
235class Lockfile {
236 constructor() {
237 /** @type {number} */
238 this.version = 1;
239 /** @type {Map<string, LockfileEntry | "ignore" | "no-cache">} */
240 this.entries = new Map();
241 }
242
243 /**
244 * Parses the provided source and updates the parser state.
245 * @param {string} content content of the lockfile
246 * @returns {Lockfile} lockfile
247 */
248 static parse(content) {
249 // TODO handle merge conflicts
250 const data = JSON.parse(content);
251 if (data.version !== 1) {
252 throw new Error(`Unsupported lockfile version ${data.version}`);
253 }
254 const lockfile = new Lockfile();
255 for (const key of Object.keys(data)) {
256 if (key === "version") continue;
257 const entry = data[key];
258 lockfile.entries.set(
259 key,
260 typeof entry === "string"
261 ? entry
262 : {
263 resolved: key,
264 ...entry
265 }
266 );
267 }
268 return lockfile;
269 }
270
271 /**
272 * Returns a string representation.
273 * @returns {string} stringified lockfile
274 */
275 toString() {
276 let str = "{\n";
277 const entries = [...this.entries].sort(([a], [b]) => (a < b ? -1 : 1));
278 for (const [key, entry] of entries) {
279 if (typeof entry === "string") {
280 str += ` ${JSON.stringify(key)}: ${JSON.stringify(entry)},\n`;
281 } else {
282 str += ` ${JSON.stringify(key)}: { `;
283 if (entry.resolved !== key) {
284 str += `"resolved": ${JSON.stringify(entry.resolved)}, `;
285 }
286 str += `"integrity": ${JSON.stringify(
287 entry.integrity
288 )}, "contentType": ${JSON.stringify(entry.contentType)} },\n`;
289 }
290 }
291 str += ` "version": ${this.version}\n}\n`;
292 return str;
293 }
294}
295
296/**
297 * Defines the fn without key callback type used by this module.
298 * @template R
299 * @typedef {(err: Error | null, result?: R) => void} FnWithoutKeyCallback
300 */
301
302/**
303 * Defines the fn without key type used by this module.
304 * @template R
305 * @typedef {(callback: FnWithoutKeyCallback<R>) => void} FnWithoutKey
306 */
307
308/**
309 * Caches d without key.
310 * @template R
311 * @param {FnWithoutKey<R>} fn function
312 * @returns {FnWithoutKey<R>} cached function
313 */
314const cachedWithoutKey = (fn) => {
315 let inFlight = false;
316 /** @type {Error | undefined} */
317 let cachedError;
318 /** @type {R | undefined} */
319 let cachedResult;
320 /** @type {FnWithoutKeyCallback<R>[] | undefined} */
321 let cachedCallbacks;
322 return (callback) => {
323 if (inFlight) {
324 if (cachedResult !== undefined) return callback(null, cachedResult);
325 if (cachedError !== undefined) return callback(cachedError);
326 if (cachedCallbacks === undefined) cachedCallbacks = [callback];
327 else cachedCallbacks.push(callback);
328 return;
329 }
330 inFlight = true;
331 fn((err, result) => {
332 if (err) cachedError = err;
333 else cachedResult = result;
334 const callbacks = cachedCallbacks;
335 cachedCallbacks = undefined;
336 callback(err, result);
337 if (callbacks !== undefined) for (const cb of callbacks) cb(err, result);
338 });
339 };
340};
341
342/**
343 * Defines the fn with key callback type used by this module.
344 * @template R
345 * @typedef {(err: Error | null, result?: R) => void} FnWithKeyCallback
346 */
347
348/**
349 * Defines the fn with key type used by this module.
350 * @template T
351 * @template R
352 * @typedef {(item: T, callback: FnWithKeyCallback<R>) => void} FnWithKey
353 */
354
355/**
356 * Returns } cached function.
357 * @template T
358 * @template R
359 * @param {FnWithKey<T, R>} fn function
360 * @param {FnWithKey<T, R>=} forceFn function for the second try
361 * @returns {FnWithKey<T, R> & { force: FnWithKey<T, R> }} cached function
362 */
363const cachedWithKey = (fn, forceFn = fn) => {
364 /**
365 * Defines the cache entry type used by this module.
366 * @template R
367 * @typedef {{ result?: R, error?: Error, callbacks?: FnWithKeyCallback<R>[], force?: true }} CacheEntry
368 */
369 /** @type {Map<T, CacheEntry<R>>} */
370 const cache = new Map();
371 /**
372 * Processes the provided arg.
373 * @param {T} arg arg
374 * @param {FnWithKeyCallback<R>} callback callback
375 * @returns {void}
376 */
377 const resultFn = (arg, callback) => {
378 const cacheEntry = cache.get(arg);
379 if (cacheEntry !== undefined) {
380 if (cacheEntry.result !== undefined) {
381 return callback(null, cacheEntry.result);
382 }
383 if (cacheEntry.error !== undefined) return callback(cacheEntry.error);
384 if (cacheEntry.callbacks === undefined) cacheEntry.callbacks = [callback];
385 else cacheEntry.callbacks.push(callback);
386 return;
387 }
388 /** @type {CacheEntry<R>} */
389 const newCacheEntry = {
390 result: undefined,
391 error: undefined,
392 callbacks: undefined
393 };
394 cache.set(arg, newCacheEntry);
395 fn(arg, (err, result) => {
396 if (err) newCacheEntry.error = err;
397 else newCacheEntry.result = result;
398 const callbacks = newCacheEntry.callbacks;
399 newCacheEntry.callbacks = undefined;
400 callback(err, result);
401 if (callbacks !== undefined) for (const cb of callbacks) cb(err, result);
402 });
403 };
404 /**
405 * Processes the provided arg.
406 * @param {T} arg arg
407 * @param {FnWithKeyCallback<R>} callback callback
408 * @returns {void}
409 */
410 resultFn.force = (arg, callback) => {
411 const cacheEntry = cache.get(arg);
412 if (cacheEntry !== undefined && cacheEntry.force) {
413 if (cacheEntry.result !== undefined) {
414 return callback(null, cacheEntry.result);
415 }
416 if (cacheEntry.error !== undefined) return callback(cacheEntry.error);
417 if (cacheEntry.callbacks === undefined) cacheEntry.callbacks = [callback];
418 else cacheEntry.callbacks.push(callback);
419 return;
420 }
421 /** @type {CacheEntry<R>} */
422 const newCacheEntry = {
423 result: undefined,
424 error: undefined,
425 callbacks: undefined,
426 force: true
427 };
428 cache.set(arg, newCacheEntry);
429 forceFn(arg, (err, result) => {
430 if (err) newCacheEntry.error = err;
431 else newCacheEntry.result = result;
432 const callbacks = newCacheEntry.callbacks;
433 newCacheEntry.callbacks = undefined;
434 callback(err, result);
435 if (callbacks !== undefined) for (const cb of callbacks) cb(err, result);
436 });
437 };
438 return resultFn;
439};
440
441/**
442 * Defines the lockfile cache type used by this module.
443 * @typedef {object} LockfileCache
444 * @property {Lockfile} lockfile lockfile
445 * @property {Snapshot} snapshot snapshot
446 */
447
448/**
449 * Defines the resolve content result type used by this module.
450 * @typedef {object} ResolveContentResult
451 * @property {LockfileEntry} entry lockfile entry
452 * @property {Buffer} content content
453 * @property {boolean} storeLock need store lockfile
454 */
455
456/** @typedef {{ storeCache: boolean, storeLock: boolean, validUntil: number, etag: string | undefined, fresh: boolean }} FetchResultMeta */
457/** @typedef {FetchResultMeta & { location: string }} RedirectFetchResult */
458/** @typedef {FetchResultMeta & { entry: LockfileEntry, content: Buffer }} ContentFetchResult */
459/** @typedef {RedirectFetchResult | ContentFetchResult} FetchResult */
460
461/** @typedef {(uri: string) => boolean} AllowedUriFn */
462
463const PLUGIN_NAME = "HttpUriPlugin";
464
465class HttpUriPlugin {
466 /**
467 * Creates an instance of HttpUriPlugin.
468 * @param {HttpUriPluginOptions} options options
469 */
470 constructor(options) {
471 /** @type {HttpUriPluginOptions} */
472 this.options = options;
473 }
474
475 /**
476 * Applies the plugin by registering its hooks on the compiler.
477 * @param {Compiler} compiler the compiler instance
478 * @returns {void}
479 */
480 apply(compiler) {
481 compiler.hooks.validate.tap(PLUGIN_NAME, () => {
482 compiler.validate(
483 () => require("../../schemas/plugins/schemes/HttpUriPlugin.json"),
484 this.options,
485 {
486 name: "Http Uri Plugin",
487 baseDataPath: "options"
488 },
489 (options) =>
490 require("../../schemas/plugins/schemes/HttpUriPlugin.check")(options)
491 );
492 });
493
494 const proxy =
495 this.options.proxy || process.env.http_proxy || process.env.HTTP_PROXY;
496 /**
497 * @type {{ scheme: "http" | "https", fetch: Fetch }[]}
498 */
499 const schemes = [
500 {
501 scheme: "http",
502 fetch: proxyFetch(getHttp(), proxy)
503 },
504 {
505 scheme: "https",
506 fetch: proxyFetch(getHttps(), proxy)
507 }
508 ];
509 /** @type {LockfileCache} */
510 let lockfileCache;
511 compiler.hooks.compilation.tap(
512 PLUGIN_NAME,
513 (compilation, { normalModuleFactory }) => {
514 const intermediateFs =
515 /** @type {IntermediateFileSystem} */
516 (compiler.intermediateFileSystem);
517 const fs = compilation.inputFileSystem;
518 const cache = compilation.getCache(`webpack.${PLUGIN_NAME}`);
519 const logger = compilation.getLogger(`webpack.${PLUGIN_NAME}`);
520 /** @type {string} */
521 const lockfileLocation =
522 this.options.lockfileLocation ||
523 join(
524 intermediateFs,
525 compiler.context,
526 compiler.name
527 ? `${toSafePath(compiler.name)}.webpack.lock`
528 : "webpack.lock"
529 );
530 /** @type {string | false} */
531 const cacheLocation =
532 this.options.cacheLocation !== undefined
533 ? this.options.cacheLocation
534 : `${lockfileLocation}.data`;
535 const upgrade = this.options.upgrade || false;
536 const frozen = this.options.frozen || false;
537 const hashFunction = "sha512";
538 const hashDigest = "hex";
539 const hashDigestLength = 20;
540 const allowedUris = this.options.allowedUris;
541
542 let warnedAboutEol = false;
543
544 /** @type {Map<string, string>} */
545 const cacheKeyCache = new Map();
546 /**
547 * Returns the key.
548 * @param {string} url the url
549 * @returns {string} the key
550 */
551 const getCacheKey = (url) => {
552 const cachedResult = cacheKeyCache.get(url);
553 if (cachedResult !== undefined) return cachedResult;
554 const result = _getCacheKey(url);
555 cacheKeyCache.set(url, result);
556 return result;
557 };
558
559 /**
560 * Returns the key.
561 * @param {string} url the url
562 * @returns {string} the key
563 */
564 const _getCacheKey = (url) => {
565 const parsedUrl = new URL(url);
566 const folder = toSafePath(parsedUrl.origin);
567 const name = toSafePath(parsedUrl.pathname);
568 const query = toSafePath(parsedUrl.search);
569 let ext = extname(name);
570 if (ext.length > 20) ext = "";
571 const basename = ext ? name.slice(0, -ext.length) : name;
572 const hash = createHash(hashFunction);
573 hash.update(url);
574 const digest = hash.digest(hashDigest).slice(0, hashDigestLength);
575 return `${folder.slice(-50)}/${`${basename}${
576 query ? `_${query}` : ""
577 }`.slice(0, 150)}_${digest}${ext}`;
578 };
579
580 const getLockfile = cachedWithoutKey(
581 /**
582 * Handles the callback logic for this hook.
583 * @param {(err: Error | null, lockfile?: Lockfile) => void} callback callback
584 * @returns {void}
585 */
586 (callback) => {
587 const readLockfile = () => {
588 intermediateFs.readFile(lockfileLocation, (err, buffer) => {
589 if (err && err.code !== "ENOENT") {
590 compilation.missingDependencies.add(lockfileLocation);
591 return callback(err);
592 }
593 compilation.fileDependencies.add(lockfileLocation);
594 compilation.fileSystemInfo.createSnapshot(
595 compiler.fsStartTime,
596 buffer ? [lockfileLocation] : [],
597 [],
598 buffer ? [] : [lockfileLocation],
599 { timestamp: true },
600 (err, s) => {
601 if (err) return callback(err);
602 const lockfile = buffer
603 ? Lockfile.parse(buffer.toString("utf8"))
604 : new Lockfile();
605 lockfileCache = {
606 lockfile,
607 snapshot: /** @type {Snapshot} */ (s)
608 };
609 callback(null, lockfile);
610 }
611 );
612 });
613 };
614 if (lockfileCache) {
615 compilation.fileSystemInfo.checkSnapshotValid(
616 lockfileCache.snapshot,
617 (err, valid) => {
618 if (err) return callback(err);
619 if (!valid) return readLockfile();
620 callback(null, lockfileCache.lockfile);
621 }
622 );
623 } else {
624 readLockfile();
625 }
626 }
627 );
628
629 /** @typedef {Map<string, LockfileEntry | "ignore" | "no-cache">} LockfileUpdates */
630
631 /** @type {LockfileUpdates | undefined} */
632 let lockfileUpdates;
633
634 /**
635 * Stores the provided lockfile.
636 * @param {Lockfile} lockfile lockfile instance
637 * @param {string} url url to store
638 * @param {LockfileEntry | "ignore" | "no-cache"} entry lockfile entry
639 */
640 const storeLockEntry = (lockfile, url, entry) => {
641 const oldEntry = lockfile.entries.get(url);
642 if (lockfileUpdates === undefined) lockfileUpdates = new Map();
643 lockfileUpdates.set(url, entry);
644 lockfile.entries.set(url, entry);
645 if (!oldEntry) {
646 logger.log(`${url} added to lockfile`);
647 } else if (typeof oldEntry === "string") {
648 if (typeof entry === "string") {
649 logger.log(`${url} updated in lockfile: ${oldEntry} -> ${entry}`);
650 } else {
651 logger.log(
652 `${url} updated in lockfile: ${oldEntry} -> ${entry.resolved}`
653 );
654 }
655 } else if (typeof entry === "string") {
656 logger.log(
657 `${url} updated in lockfile: ${oldEntry.resolved} -> ${entry}`
658 );
659 } else if (oldEntry.resolved !== entry.resolved) {
660 logger.log(
661 `${url} updated in lockfile: ${oldEntry.resolved} -> ${entry.resolved}`
662 );
663 } else if (oldEntry.integrity !== entry.integrity) {
664 logger.log(`${url} updated in lockfile: content changed`);
665 } else if (oldEntry.contentType !== entry.contentType) {
666 logger.log(
667 `${url} updated in lockfile: ${oldEntry.contentType} -> ${entry.contentType}`
668 );
669 } else {
670 logger.log(`${url} updated in lockfile`);
671 }
672 };
673
674 /**
675 * Stores the provided lockfile.
676 * @param {Lockfile} lockfile lockfile
677 * @param {string} url url
678 * @param {ResolveContentResult} result result
679 * @param {(err: Error | null, result?: ResolveContentResult) => void} callback callback
680 * @returns {void}
681 */
682 const storeResult = (lockfile, url, result, callback) => {
683 if (result.storeLock) {
684 storeLockEntry(lockfile, url, result.entry);
685 if (!cacheLocation || !result.content) {
686 return callback(null, result);
687 }
688 const key = getCacheKey(result.entry.resolved);
689 const filePath = join(intermediateFs, cacheLocation, key);
690 mkdirp(intermediateFs, dirname(intermediateFs, filePath), (err) => {
691 if (err) return callback(err);
692 intermediateFs.writeFile(filePath, result.content, (err) => {
693 if (err) return callback(err);
694 callback(null, result);
695 });
696 });
697 } else {
698 storeLockEntry(lockfile, url, "no-cache");
699 callback(null, result);
700 }
701 };
702
703 for (const { scheme, fetch } of schemes) {
704 /**
705 * Validate redirect location.
706 * @param {string} location Location header value (relative or absolute)
707 * @param {string} base current absolute URL
708 * @returns {string} absolute, validated redirect target
709 */
710 const validateRedirectLocation = (location, base) => {
711 /** @type {URL} */
712 let nextUrl;
713 try {
714 nextUrl = new URL(location, base);
715 } catch (err) {
716 throw new Error(
717 `Invalid redirect URL: ${sanitizeUrlForError(location)}`,
718 { cause: err }
719 );
720 }
721 if (nextUrl.protocol !== "http:" && nextUrl.protocol !== "https:") {
722 throw new Error(
723 `Redirected URL uses disallowed protocol: ${sanitizeUrlForError(nextUrl.href)}`
724 );
725 }
726 if (!isAllowed(nextUrl.href)) {
727 throw new Error(
728 `${nextUrl.href} doesn't match the allowedUris policy after redirect. These URIs are allowed:\n${allowedUris
729 .map((uri) => ` - ${uri}`)
730 .join("\n")}`
731 );
732 }
733 return nextUrl.href;
734 };
735 /**
736 * Processes the provided url.
737 * @param {string} url URL
738 * @param {string | null} integrity integrity
739 * @param {(err: Error | null, resolveContentResult?: ResolveContentResult) => void} callback callback
740 * @param {number=} redirectCount number of followed redirects
741 */
742 const resolveContent = (
743 url,
744 integrity,
745 callback,
746 redirectCount = 0
747 ) => {
748 /**
749 * Processes the provided err.
750 * @param {Error | null} err error
751 * @param {FetchResult=} _result fetch result
752 * @returns {void}
753 */
754 const handleResult = (err, _result) => {
755 if (err) return callback(err);
756
757 const result = /** @type {FetchResult} */ (_result);
758
759 if ("location" in result) {
760 // Validate redirect target before following
761 /** @type {string} */
762 let absolute;
763 try {
764 absolute = validateRedirectLocation(result.location, url);
765 } catch (err_) {
766 return callback(/** @type {Error} */ (err_));
767 }
768 if (redirectCount >= MAX_REDIRECTS) {
769 return callback(new Error("Too many redirects"));
770 }
771 return resolveContent(
772 absolute,
773 integrity,
774 (err, innerResult) => {
775 if (err) return callback(err);
776 const { entry, content, storeLock } =
777 /** @type {ResolveContentResult} */ (innerResult);
778 callback(null, {
779 entry,
780 content,
781 storeLock: storeLock && result.storeLock
782 });
783 },
784 redirectCount + 1
785 );
786 }
787
788 if (
789 !result.fresh &&
790 integrity &&
791 result.entry.integrity !== integrity &&
792 !verifyIntegrity(result.content, integrity)
793 ) {
794 return fetchContent.force(url, handleResult);
795 }
796
797 return callback(null, {
798 entry: result.entry,
799 content: result.content,
800 storeLock: result.storeLock
801 });
802 };
803
804 fetchContent(url, handleResult);
805 };
806
807 /**
808 * Processes the provided url.
809 * @param {string} url URL
810 * @param {FetchResult | RedirectFetchResult | undefined} cachedResult result from cache
811 * @param {(err: Error | null, fetchResult?: FetchResult) => void} callback callback
812 * @returns {void}
813 */
814 const fetchContentRaw = (url, cachedResult, callback) => {
815 const requestTime = Date.now();
816 /** @type {OutgoingHttpHeaders} */
817 const headers = {
818 "accept-encoding": "gzip, deflate, br",
819 "user-agent": "webpack"
820 };
821
822 if (cachedResult && cachedResult.etag) {
823 headers["if-none-match"] = cachedResult.etag;
824 }
825
826 fetch(new URL(url), { headers }, (res) => {
827 const etag = res.headers.etag;
828 const location = res.headers.location;
829 const cacheControl = res.headers["cache-control"];
830 const { storeLock, storeCache, validUntil } = parseCacheControl(
831 cacheControl,
832 requestTime
833 );
834 /**
835 * Processes the provided partial result.
836 * @param {Partial<Pick<FetchResultMeta, "fresh">> & (Pick<RedirectFetchResult, "location"> | Pick<ContentFetchResult, "content" | "entry">)} partialResult result
837 * @returns {void}
838 */
839 const finishWith = (partialResult) => {
840 if ("location" in partialResult) {
841 logger.debug(
842 `GET ${url} [${res.statusCode}] -> ${partialResult.location}`
843 );
844 } else {
845 logger.debug(
846 `GET ${url} [${res.statusCode}] ${Math.ceil(
847 partialResult.content.length / 1024
848 )} kB${!storeLock ? " no-cache" : ""}`
849 );
850 }
851 const result = {
852 ...partialResult,
853 fresh: true,
854 storeLock,
855 storeCache,
856 validUntil,
857 etag
858 };
859 if (!storeCache) {
860 logger.log(
861 `${url} can't be stored in cache, due to Cache-Control header: ${cacheControl}`
862 );
863 return callback(null, result);
864 }
865 cache.store(
866 url,
867 null,
868 {
869 ...result,
870 fresh: false
871 },
872 (err) => {
873 if (err) {
874 logger.warn(
875 `${url} can't be stored in cache: ${err.message}`
876 );
877 logger.debug(err.stack);
878 }
879 callback(null, result);
880 }
881 );
882 };
883 if (res.statusCode === 304) {
884 const result = /** @type {FetchResult} */ (cachedResult);
885 if (
886 result.validUntil < validUntil ||
887 result.storeLock !== storeLock ||
888 result.storeCache !== storeCache ||
889 result.etag !== etag
890 ) {
891 return finishWith(result);
892 }
893 logger.debug(`GET ${url} [${res.statusCode}] (unchanged)`);
894 return callback(null, { ...result, fresh: true });
895 }
896 if (
897 location &&
898 res.statusCode &&
899 res.statusCode >= 301 &&
900 res.statusCode <= 308
901 ) {
902 /** @type {string} */
903 let absolute;
904 try {
905 absolute = validateRedirectLocation(location, url);
906 } catch (err) {
907 logger.log(
908 `GET ${url} [${res.statusCode}] -> ${String(location)} (rejected: ${/** @type {Error} */ (err).message})`
909 );
910 return callback(/** @type {Error} */ (err));
911 }
912 const result = { location: absolute };
913 if (
914 !cachedResult ||
915 !("location" in cachedResult) ||
916 cachedResult.location !== result.location ||
917 cachedResult.validUntil < validUntil ||
918 cachedResult.storeLock !== storeLock ||
919 cachedResult.storeCache !== storeCache ||
920 cachedResult.etag !== etag
921 ) {
922 return finishWith(result);
923 }
924 logger.debug(`GET ${url} [${res.statusCode}] (unchanged)`);
925 return callback(null, {
926 ...result,
927 fresh: true,
928 storeLock,
929 storeCache,
930 validUntil,
931 etag
932 });
933 }
934 const contentType = res.headers["content-type"] || "";
935 /** @type {Buffer[]} */
936 const bufferArr = [];
937
938 const contentEncoding = res.headers["content-encoding"];
939 /** @type {Readable} */
940 let stream = res;
941 if (contentEncoding === "gzip") {
942 stream = stream.pipe(createGunzip());
943 } else if (contentEncoding === "br") {
944 stream = stream.pipe(createBrotliDecompress());
945 } else if (contentEncoding === "deflate") {
946 stream = stream.pipe(createInflate());
947 }
948
949 stream.on(
950 "data",
951 /**
952 * Handles the callback logic for this hook.
953 * @param {Buffer} chunk chunk
954 */
955 (chunk) => {
956 bufferArr.push(chunk);
957 }
958 );
959
960 stream.on("end", () => {
961 if (!res.complete) {
962 logger.log(`GET ${url} [${res.statusCode}] (terminated)`);
963 return callback(new Error(`${url} request was terminated`));
964 }
965
966 const content = Buffer.concat(bufferArr);
967
968 if (res.statusCode !== 200) {
969 logger.log(`GET ${url} [${res.statusCode}]`);
970 return callback(
971 new Error(
972 `${url} request status code = ${
973 res.statusCode
974 }\n${content.toString("utf8")}`
975 )
976 );
977 }
978
979 const integrity = computeIntegrity(content);
980 const entry = { resolved: url, integrity, contentType };
981
982 finishWith({
983 entry,
984 content
985 });
986 });
987 }).on("error", (err) => {
988 logger.log(`GET ${url} (error)`);
989 err.message += `\nwhile fetching ${url}`;
990 callback(err);
991 });
992 };
993
994 const fetchContent = cachedWithKey(
995 /**
996 * Handles the callback logic for this hook.
997 * @param {string} url URL
998 * @param {(err: Error | null, result?: FetchResult) => void} callback callback
999 * @returns {void}
1000 */
1001 (url, callback) => {
1002 cache.get(url, null, (err, cachedResult) => {
1003 if (err) return callback(err);
1004 if (cachedResult) {
1005 const isValid = cachedResult.validUntil >= Date.now();
1006 if (isValid) return callback(null, cachedResult);
1007 }
1008 fetchContentRaw(url, cachedResult, callback);
1009 });
1010 },
1011 (url, callback) => fetchContentRaw(url, undefined, callback)
1012 );
1013
1014 /**
1015 * Checks whether this http uri plugin is allowed.
1016 * @param {string} uri uri
1017 * @returns {boolean} true when allowed, otherwise false
1018 */
1019 const isAllowed = (uri) => {
1020 /** @type {URL} */
1021 let parsedUri;
1022 try {
1023 // Parse the URI to prevent userinfo bypass attacks
1024 // (e.g., http://allowed@malicious/path where @malicious is the actual host)
1025 parsedUri = new URL(uri);
1026 } catch (_err) {
1027 return false;
1028 }
1029 for (const allowed of allowedUris) {
1030 if (typeof allowed === "string") {
1031 /** @type {URL} */
1032 let parsedAllowed;
1033 try {
1034 parsedAllowed = new URL(allowed);
1035 } catch (_err) {
1036 continue;
1037 }
1038 if (parsedUri.href.startsWith(parsedAllowed.href)) {
1039 return true;
1040 }
1041 } else if (typeof allowed === "function") {
1042 if (allowed(parsedUri.href)) return true;
1043 } else if (allowed.test(parsedUri.href)) {
1044 return true;
1045 }
1046 }
1047 return false;
1048 };
1049
1050 /** @typedef {{ entry: LockfileEntry, content: Buffer }} Info */
1051
1052 const getInfo = cachedWithKey(
1053 /**
1054 * Processes the provided url.
1055 * @param {string} url the url
1056 * @param {(err: Error | null, info?: Info) => void} callback callback
1057 * @returns {void}
1058 */
1059 // eslint-disable-next-line no-loop-func
1060 (url, callback) => {
1061 if (!isAllowed(url)) {
1062 return callback(
1063 new Error(
1064 `${url} doesn't match the allowedUris policy. These URIs are allowed:\n${allowedUris
1065 .map((uri) => ` - ${uri}`)
1066 .join("\n")}`
1067 )
1068 );
1069 }
1070 getLockfile((err, _lockfile) => {
1071 if (err) return callback(err);
1072 const lockfile = /** @type {Lockfile} */ (_lockfile);
1073 const entryOrString = lockfile.entries.get(url);
1074 if (!entryOrString) {
1075 if (frozen) {
1076 return callback(
1077 new Error(
1078 `${url} has no lockfile entry and lockfile is frozen`
1079 )
1080 );
1081 }
1082 resolveContent(url, null, (err, result) => {
1083 if (err) return callback(err);
1084 storeResult(
1085 /** @type {Lockfile} */
1086 (lockfile),
1087 url,
1088 /** @type {ResolveContentResult} */
1089 (result),
1090 callback
1091 );
1092 });
1093 return;
1094 }
1095 if (typeof entryOrString === "string") {
1096 const entryTag = entryOrString;
1097 resolveContent(url, null, (err, _result) => {
1098 if (err) return callback(err);
1099 const result =
1100 /** @type {ResolveContentResult} */
1101 (_result);
1102 if (!result.storeLock || entryTag === "ignore") {
1103 return callback(null, result);
1104 }
1105 if (frozen) {
1106 return callback(
1107 new Error(
1108 `${url} used to have ${entryTag} lockfile entry and has content now, but lockfile is frozen`
1109 )
1110 );
1111 }
1112 if (!upgrade) {
1113 return callback(
1114 new Error(
1115 `${url} used to have ${entryTag} lockfile entry and has content now.
1116This should be reflected in the lockfile, so this lockfile entry must be upgraded, but upgrading is not enabled.
1117Remove this line from the lockfile to force upgrading.`
1118 )
1119 );
1120 }
1121 storeResult(lockfile, url, result, callback);
1122 });
1123 return;
1124 }
1125 let entry = entryOrString;
1126 /**
1127 * Processes the provided locked content.
1128 * @param {Buffer=} lockedContent locked content
1129 */
1130 const doFetch = (lockedContent) => {
1131 resolveContent(url, entry.integrity, (err, _result) => {
1132 if (err) {
1133 if (lockedContent) {
1134 logger.warn(
1135 `Upgrade request to ${url} failed: ${err.message}`
1136 );
1137 logger.debug(err.stack);
1138 return callback(null, {
1139 entry,
1140 content: lockedContent
1141 });
1142 }
1143 return callback(err);
1144 }
1145 const result =
1146 /** @type {ResolveContentResult} */
1147 (_result);
1148 if (!result.storeLock) {
1149 // When the lockfile entry should be no-cache
1150 // we need to update the lockfile
1151 if (frozen) {
1152 return callback(
1153 new Error(
1154 `${url} has a lockfile entry and is no-cache now, but lockfile is frozen\nLockfile: ${entryToString(
1155 entry
1156 )}`
1157 )
1158 );
1159 }
1160 storeResult(lockfile, url, result, callback);
1161 return;
1162 }
1163 if (!areLockfileEntriesEqual(result.entry, entry)) {
1164 // When the lockfile entry is outdated
1165 // we need to update the lockfile
1166 if (frozen) {
1167 return callback(
1168 new Error(
1169 `${url} has an outdated lockfile entry, but lockfile is frozen\nLockfile: ${entryToString(
1170 entry
1171 )}\nExpected: ${entryToString(result.entry)}`
1172 )
1173 );
1174 }
1175 storeResult(lockfile, url, result, callback);
1176 return;
1177 }
1178 if (!lockedContent && cacheLocation) {
1179 // When the lockfile cache content is missing
1180 // we need to update the lockfile
1181 if (frozen) {
1182 return callback(
1183 new Error(
1184 `${url} is missing content in the lockfile cache, but lockfile is frozen\nLockfile: ${entryToString(
1185 entry
1186 )}`
1187 )
1188 );
1189 }
1190 storeResult(lockfile, url, result, callback);
1191 return;
1192 }
1193 return callback(null, result);
1194 });
1195 };
1196 if (cacheLocation) {
1197 // When there is a lockfile cache
1198 // we read the content from there
1199 const key = getCacheKey(entry.resolved);
1200 const filePath = join(intermediateFs, cacheLocation, key);
1201 fs.readFile(filePath, (err, result) => {
1202 if (err) {
1203 if (err.code === "ENOENT") return doFetch();
1204 return callback(err);
1205 }
1206 const content = /** @type {Buffer} */ (result);
1207 /**
1208 * Continue with cached content.
1209 * @param {Buffer | undefined} _result result
1210 * @returns {void}
1211 */
1212 const continueWithCachedContent = (_result) => {
1213 if (!upgrade) {
1214 // When not in upgrade mode, we accept the result from the lockfile cache
1215 return callback(null, { entry, content });
1216 }
1217 return doFetch(content);
1218 };
1219 if (!verifyIntegrity(content, entry.integrity)) {
1220 /** @type {Buffer | undefined} */
1221 let contentWithChangedEol;
1222 let isEolChanged = false;
1223 try {
1224 contentWithChangedEol = Buffer.from(
1225 content.toString("utf8").replace(/\r\n/g, "\n")
1226 );
1227 isEolChanged = verifyIntegrity(
1228 contentWithChangedEol,
1229 entry.integrity
1230 );
1231 } catch (_err) {
1232 // ignore
1233 }
1234 if (isEolChanged) {
1235 if (!warnedAboutEol) {
1236 const explainer = `Incorrect end of line sequence was detected in the lockfile cache.
1237The lockfile cache is protected by integrity checks, so any external modification will lead to a corrupted lockfile cache.
1238When using git make sure to configure .gitattributes correctly for the lockfile cache:
1239 **/*webpack.lock.data/** -text
1240This will avoid that the end of line sequence is changed by git on Windows.`;
1241 if (frozen) {
1242 logger.error(explainer);
1243 } else {
1244 logger.warn(explainer);
1245 logger.info(
1246 "Lockfile cache will be automatically fixed now, but when lockfile is frozen this would result in an error."
1247 );
1248 }
1249 warnedAboutEol = true;
1250 }
1251 if (!frozen) {
1252 // "fix" the end of line sequence of the lockfile content
1253 logger.log(
1254 `${filePath} fixed end of line sequence (\\r\\n instead of \\n).`
1255 );
1256 intermediateFs.writeFile(
1257 filePath,
1258 /** @type {Buffer} */
1259 (contentWithChangedEol),
1260 (err) => {
1261 if (err) return callback(err);
1262 continueWithCachedContent(
1263 /** @type {Buffer} */
1264 (contentWithChangedEol)
1265 );
1266 }
1267 );
1268 return;
1269 }
1270 }
1271 if (frozen) {
1272 return callback(
1273 new Error(
1274 `${
1275 entry.resolved
1276 } integrity mismatch, expected content with integrity ${
1277 entry.integrity
1278 } but got ${computeIntegrity(content)}.
1279Lockfile corrupted (${
1280 isEolChanged
1281 ? "end of line sequence was unexpectedly changed"
1282 : "incorrectly merged? changed by other tools?"
1283 }).
1284Run build with un-frozen lockfile to automatically fix lockfile.`
1285 )
1286 );
1287 }
1288 // "fix" the lockfile entry to the correct integrity
1289 // the content has priority over the integrity value
1290 entry = {
1291 ...entry,
1292 integrity: computeIntegrity(content)
1293 };
1294 storeLockEntry(lockfile, url, entry);
1295 }
1296 continueWithCachedContent(result);
1297 });
1298 } else {
1299 doFetch();
1300 }
1301 });
1302 }
1303 );
1304
1305 /**
1306 * Respond with url module.
1307 * @param {URL} url url
1308 * @param {ResourceDataWithData} resourceData resource data
1309 * @param {(err: Error | null, result: true | void) => void} callback callback
1310 */
1311 const respondWithUrlModule = (url, resourceData, callback) => {
1312 getInfo(url.href, (err, _result) => {
1313 if (err) return callback(err);
1314 const result = /** @type {Info} */ (_result);
1315 resourceData.resource = url.href;
1316 resourceData.path = url.origin + url.pathname;
1317 resourceData.query = url.search;
1318 resourceData.fragment = url.hash;
1319 resourceData.context = new URL(
1320 ".",
1321 result.entry.resolved
1322 ).href.slice(0, -1);
1323 resourceData.data.mimetype = result.entry.contentType;
1324 callback(null, true);
1325 });
1326 };
1327 normalModuleFactory.hooks.resolveForScheme
1328 .for(scheme)
1329 .tapAsync(PLUGIN_NAME, (resourceData, resolveData, callback) => {
1330 respondWithUrlModule(
1331 new URL(resourceData.resource),
1332 resourceData,
1333 callback
1334 );
1335 });
1336 normalModuleFactory.hooks.resolveInScheme
1337 .for(scheme)
1338 .tapAsync(PLUGIN_NAME, (resourceData, data, callback) => {
1339 // Only handle relative urls (./xxx, ../xxx, /xxx, //xxx)
1340 if (
1341 data.dependencyType !== "url" &&
1342 !/^\.{0,2}\//.test(resourceData.resource)
1343 ) {
1344 return callback();
1345 }
1346 respondWithUrlModule(
1347 new URL(resourceData.resource, `${data.context}/`),
1348 resourceData,
1349 callback
1350 );
1351 });
1352 const hooks = NormalModule.getCompilationHooks(compilation);
1353 hooks.readResourceForScheme
1354 .for(scheme)
1355 .tapAsync(PLUGIN_NAME, (resource, module, callback) =>
1356 getInfo(resource, (err, _result) => {
1357 if (err) return callback(err);
1358 const result = /** @type {Info} */ (_result);
1359 if (module) {
1360 /** @type {BuildInfo} */
1361 (module.buildInfo).resourceIntegrity = result.entry.integrity;
1362 }
1363 callback(null, result.content);
1364 })
1365 );
1366 hooks.needBuild.tapAsync(PLUGIN_NAME, (module, context, callback) => {
1367 if (module.resource && module.resource.startsWith(`${scheme}://`)) {
1368 getInfo(module.resource, (err, _result) => {
1369 if (err) return callback(err);
1370 const result = /** @type {Info} */ (_result);
1371 if (
1372 result.entry.integrity !==
1373 /** @type {BuildInfo} */
1374 (module.buildInfo).resourceIntegrity
1375 ) {
1376 return callback(null, true);
1377 }
1378 callback();
1379 });
1380 } else {
1381 return callback();
1382 }
1383 });
1384 }
1385 compilation.hooks.finishModules.tapAsync(
1386 PLUGIN_NAME,
1387 (modules, callback) => {
1388 if (!lockfileUpdates) return callback();
1389 const ext = extname(lockfileLocation);
1390 const tempFile = join(
1391 intermediateFs,
1392 dirname(intermediateFs, lockfileLocation),
1393 `.${basename(lockfileLocation, ext)}.${
1394 (Math.random() * 10000) | 0
1395 }${ext}`
1396 );
1397
1398 const writeDone = () => {
1399 const nextOperation =
1400 /** @type {InProgressWriteItem[]} */
1401 (inProgressWrite).shift();
1402 if (nextOperation) {
1403 nextOperation();
1404 } else {
1405 inProgressWrite = undefined;
1406 }
1407 };
1408 const runWrite = () => {
1409 intermediateFs.readFile(lockfileLocation, (err, buffer) => {
1410 if (err && err.code !== "ENOENT") {
1411 writeDone();
1412 return callback(err);
1413 }
1414 const lockfile = buffer
1415 ? Lockfile.parse(buffer.toString("utf8"))
1416 : new Lockfile();
1417 for (const [key, value] of /** @type {LockfileUpdates} */ (
1418 lockfileUpdates
1419 )) {
1420 lockfile.entries.set(key, value);
1421 }
1422 intermediateFs.writeFile(
1423 tempFile,
1424 lockfile.toString(),
1425 (err) => {
1426 if (err) {
1427 writeDone();
1428 return (
1429 /** @type {NonNullable<IntermediateFileSystem["unlink"]>} */
1430 (intermediateFs.unlink)(tempFile, () => callback(err))
1431 );
1432 }
1433 intermediateFs.rename(tempFile, lockfileLocation, (err) => {
1434 if (err) {
1435 writeDone();
1436 return (
1437 /** @type {NonNullable<IntermediateFileSystem["unlink"]>} */
1438 (intermediateFs.unlink)(tempFile, () => callback(err))
1439 );
1440 }
1441 writeDone();
1442 callback();
1443 });
1444 }
1445 );
1446 });
1447 };
1448 if (inProgressWrite) {
1449 inProgressWrite.push(runWrite);
1450 } else {
1451 inProgressWrite = [];
1452 runWrite();
1453 }
1454 }
1455 );
1456 }
1457 );
1458 }
1459}
1460
1461module.exports = HttpUriPlugin;
Note: See TracBrowser for help on using the repository browser.