1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | "use strict";
|
7 |
|
8 | const EventEmitter = require("events");
|
9 | const { extname, basename } = require("path");
|
10 | const { URL } = require("url");
|
11 | const { createGunzip, createBrotliDecompress, createInflate } = require("zlib");
|
12 | const NormalModule = require("../NormalModule");
|
13 | const createSchemaValidation = require("../util/create-schema-validation");
|
14 | const createHash = require("../util/createHash");
|
15 | const { mkdirp, dirname, join } = require("../util/fs");
|
16 | const memoize = require("../util/memoize");
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | const getHttp = memoize(() => require("http"));
|
22 | const getHttps = memoize(() => require("https"));
|
23 | const proxyFetch = (request, proxy) => (url, options, callback) => {
|
24 | const eventEmitter = new EventEmitter();
|
25 | const doRequest = socket =>
|
26 | request
|
27 | .get(url, { ...options, ...(socket && { socket }) }, callback)
|
28 | .on("error", eventEmitter.emit.bind(eventEmitter, "error"));
|
29 |
|
30 | if (proxy) {
|
31 | const { hostname: host, port } = new URL(proxy);
|
32 |
|
33 | getHttp()
|
34 | .request({
|
35 | host,
|
36 | port,
|
37 | method: "CONNECT",
|
38 | path: url.host
|
39 | })
|
40 | .on("connect", (res, socket) => {
|
41 | if (res.statusCode === 200) {
|
42 |
|
43 | doRequest(socket);
|
44 | }
|
45 | })
|
46 | .on("error", err => {
|
47 | eventEmitter.emit(
|
48 | "error",
|
49 | new Error(
|
50 | `Failed to connect to proxy server "${proxy}": ${err.message}`
|
51 | )
|
52 | );
|
53 | })
|
54 | .end();
|
55 | } else {
|
56 | doRequest();
|
57 | }
|
58 |
|
59 | return eventEmitter;
|
60 | };
|
61 |
|
62 |
|
63 | let inProgressWrite = undefined;
|
64 |
|
65 | const validate = createSchemaValidation(
|
66 | require("../../schemas/plugins/schemes/HttpUriPlugin.check.js"),
|
67 | () => require("../../schemas/plugins/schemes/HttpUriPlugin.json"),
|
68 | {
|
69 | name: "Http Uri Plugin",
|
70 | baseDataPath: "options"
|
71 | }
|
72 | );
|
73 |
|
74 | const toSafePath = str =>
|
75 | str
|
76 | .replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, "")
|
77 | .replace(/[^a-zA-Z0-9._-]+/g, "_");
|
78 |
|
79 | const computeIntegrity = content => {
|
80 | const hash = createHash("sha512");
|
81 | hash.update(content);
|
82 | const integrity = "sha512-" + hash.digest("base64");
|
83 | return integrity;
|
84 | };
|
85 |
|
86 | const verifyIntegrity = (content, integrity) => {
|
87 | if (integrity === "ignore") return true;
|
88 | return computeIntegrity(content) === integrity;
|
89 | };
|
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 | const parseKeyValuePairs = str => {
|
96 |
|
97 | const result = {};
|
98 | for (const item of str.split(",")) {
|
99 | const i = item.indexOf("=");
|
100 | if (i >= 0) {
|
101 | const key = item.slice(0, i).trim();
|
102 | const value = item.slice(i + 1).trim();
|
103 | result[key] = value;
|
104 | } else {
|
105 | const key = item.trim();
|
106 | if (!key) continue;
|
107 | result[key] = key;
|
108 | }
|
109 | }
|
110 | return result;
|
111 | };
|
112 |
|
113 | const parseCacheControl = (cacheControl, requestTime) => {
|
114 |
|
115 | let storeCache = true;
|
116 |
|
117 | let storeLock = true;
|
118 |
|
119 | let validUntil = 0;
|
120 | if (cacheControl) {
|
121 | const parsed = parseKeyValuePairs(cacheControl);
|
122 | if (parsed["no-cache"]) storeCache = storeLock = false;
|
123 | if (parsed["max-age"] && !isNaN(+parsed["max-age"])) {
|
124 | validUntil = requestTime + +parsed["max-age"] * 1000;
|
125 | }
|
126 | if (parsed["must-revalidate"]) validUntil = 0;
|
127 | }
|
128 | return {
|
129 | storeLock,
|
130 | storeCache,
|
131 | validUntil
|
132 | };
|
133 | };
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 | const areLockfileEntriesEqual = (a, b) => {
|
143 | return (
|
144 | a.resolved === b.resolved &&
|
145 | a.integrity === b.integrity &&
|
146 | a.contentType === b.contentType
|
147 | );
|
148 | };
|
149 |
|
150 | const entryToString = entry => {
|
151 | return `resolved: ${entry.resolved}, integrity: ${entry.integrity}, contentType: ${entry.contentType}`;
|
152 | };
|
153 |
|
154 | class Lockfile {
|
155 | constructor() {
|
156 | this.version = 1;
|
157 |
|
158 | this.entries = new Map();
|
159 | }
|
160 |
|
161 | static parse(content) {
|
162 |
|
163 | const data = JSON.parse(content);
|
164 | if (data.version !== 1)
|
165 | throw new Error(`Unsupported lockfile version ${data.version}`);
|
166 | const lockfile = new Lockfile();
|
167 | for (const key of Object.keys(data)) {
|
168 | if (key === "version") continue;
|
169 | const entry = data[key];
|
170 | lockfile.entries.set(
|
171 | key,
|
172 | typeof entry === "string"
|
173 | ? entry
|
174 | : {
|
175 | resolved: key,
|
176 | ...entry
|
177 | }
|
178 | );
|
179 | }
|
180 | return lockfile;
|
181 | }
|
182 |
|
183 | toString() {
|
184 | let str = "{\n";
|
185 | const entries = Array.from(this.entries).sort(([a], [b]) =>
|
186 | a < b ? -1 : 1
|
187 | );
|
188 | for (const [key, entry] of entries) {
|
189 | if (typeof entry === "string") {
|
190 | str += ` ${JSON.stringify(key)}: ${JSON.stringify(entry)},\n`;
|
191 | } else {
|
192 | str += ` ${JSON.stringify(key)}: { `;
|
193 | if (entry.resolved !== key)
|
194 | str += `"resolved": ${JSON.stringify(entry.resolved)}, `;
|
195 | str += `"integrity": ${JSON.stringify(
|
196 | entry.integrity
|
197 | )}, "contentType": ${JSON.stringify(entry.contentType)} },\n`;
|
198 | }
|
199 | }
|
200 | str += ` "version": ${this.version}\n}\n`;
|
201 | return str;
|
202 | }
|
203 | }
|
204 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 | const cachedWithoutKey = fn => {
|
211 | let inFlight = false;
|
212 |
|
213 | let cachedError = undefined;
|
214 |
|
215 | let cachedResult = undefined;
|
216 |
|
217 | let cachedCallbacks = undefined;
|
218 | return callback => {
|
219 | if (inFlight) {
|
220 | if (cachedResult !== undefined) return callback(null, cachedResult);
|
221 | if (cachedError !== undefined) return callback(cachedError);
|
222 | if (cachedCallbacks === undefined) cachedCallbacks = [callback];
|
223 | else cachedCallbacks.push(callback);
|
224 | return;
|
225 | }
|
226 | inFlight = true;
|
227 | fn((err, result) => {
|
228 | if (err) cachedError = err;
|
229 | else cachedResult = result;
|
230 | const callbacks = cachedCallbacks;
|
231 | cachedCallbacks = undefined;
|
232 | callback(err, result);
|
233 | if (callbacks !== undefined) for (const cb of callbacks) cb(err, result);
|
234 | });
|
235 | };
|
236 | };
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 | const cachedWithKey = (fn, forceFn = fn) => {
|
246 |
|
247 |
|
248 | const cache = new Map();
|
249 | const resultFn = (arg, callback) => {
|
250 | const cacheEntry = cache.get(arg);
|
251 | if (cacheEntry !== undefined) {
|
252 | if (cacheEntry.result !== undefined)
|
253 | return callback(null, cacheEntry.result);
|
254 | if (cacheEntry.error !== undefined) return callback(cacheEntry.error);
|
255 | if (cacheEntry.callbacks === undefined) cacheEntry.callbacks = [callback];
|
256 | else cacheEntry.callbacks.push(callback);
|
257 | return;
|
258 | }
|
259 |
|
260 | const newCacheEntry = {
|
261 | result: undefined,
|
262 | error: undefined,
|
263 | callbacks: undefined
|
264 | };
|
265 | cache.set(arg, newCacheEntry);
|
266 | fn(arg, (err, result) => {
|
267 | if (err) newCacheEntry.error = err;
|
268 | else newCacheEntry.result = result;
|
269 | const callbacks = newCacheEntry.callbacks;
|
270 | newCacheEntry.callbacks = undefined;
|
271 | callback(err, result);
|
272 | if (callbacks !== undefined) for (const cb of callbacks) cb(err, result);
|
273 | });
|
274 | };
|
275 | resultFn.force = (arg, callback) => {
|
276 | const cacheEntry = cache.get(arg);
|
277 | if (cacheEntry !== undefined && cacheEntry.force) {
|
278 | if (cacheEntry.result !== undefined)
|
279 | return callback(null, cacheEntry.result);
|
280 | if (cacheEntry.error !== undefined) return callback(cacheEntry.error);
|
281 | if (cacheEntry.callbacks === undefined) cacheEntry.callbacks = [callback];
|
282 | else cacheEntry.callbacks.push(callback);
|
283 | return;
|
284 | }
|
285 |
|
286 | const newCacheEntry = {
|
287 | result: undefined,
|
288 | error: undefined,
|
289 | callbacks: undefined,
|
290 | force: true
|
291 | };
|
292 | cache.set(arg, newCacheEntry);
|
293 | forceFn(arg, (err, result) => {
|
294 | if (err) newCacheEntry.error = err;
|
295 | else newCacheEntry.result = result;
|
296 | const callbacks = newCacheEntry.callbacks;
|
297 | newCacheEntry.callbacks = undefined;
|
298 | callback(err, result);
|
299 | if (callbacks !== undefined) for (const cb of callbacks) cb(err, result);
|
300 | });
|
301 | };
|
302 | return resultFn;
|
303 | };
|
304 |
|
305 | class HttpUriPlugin {
|
306 | |
307 |
|
308 |
|
309 | constructor(options) {
|
310 | validate(options);
|
311 | this._lockfileLocation = options.lockfileLocation;
|
312 | this._cacheLocation = options.cacheLocation;
|
313 | this._upgrade = options.upgrade;
|
314 | this._frozen = options.frozen;
|
315 | this._allowedUris = options.allowedUris;
|
316 | this._proxy = options.proxy;
|
317 | }
|
318 |
|
319 | |
320 |
|
321 |
|
322 |
|
323 |
|
324 | apply(compiler) {
|
325 | const proxy =
|
326 | this._proxy || process.env["http_proxy"] || process.env["HTTP_PROXY"];
|
327 | const schemes = [
|
328 | {
|
329 | scheme: "http",
|
330 | fetch: proxyFetch(getHttp(), proxy)
|
331 | },
|
332 | {
|
333 | scheme: "https",
|
334 | fetch: proxyFetch(getHttps(), proxy)
|
335 | }
|
336 | ];
|
337 | let lockfileCache;
|
338 | compiler.hooks.compilation.tap(
|
339 | "HttpUriPlugin",
|
340 | (compilation, { normalModuleFactory }) => {
|
341 | const intermediateFs = compiler.intermediateFileSystem;
|
342 | const fs = compilation.inputFileSystem;
|
343 | const cache = compilation.getCache("webpack.HttpUriPlugin");
|
344 | const logger = compilation.getLogger("webpack.HttpUriPlugin");
|
345 | const lockfileLocation =
|
346 | this._lockfileLocation ||
|
347 | join(
|
348 | intermediateFs,
|
349 | compiler.context,
|
350 | compiler.name
|
351 | ? `${toSafePath(compiler.name)}.webpack.lock`
|
352 | : "webpack.lock"
|
353 | );
|
354 | const cacheLocation =
|
355 | this._cacheLocation !== undefined
|
356 | ? this._cacheLocation
|
357 | : lockfileLocation + ".data";
|
358 | const upgrade = this._upgrade || false;
|
359 | const frozen = this._frozen || false;
|
360 | const hashFunction = "sha512";
|
361 | const hashDigest = "hex";
|
362 | const hashDigestLength = 20;
|
363 | const allowedUris = this._allowedUris;
|
364 |
|
365 | let warnedAboutEol = false;
|
366 |
|
367 | const cacheKeyCache = new Map();
|
368 | |
369 |
|
370 |
|
371 |
|
372 | const getCacheKey = url => {
|
373 | const cachedResult = cacheKeyCache.get(url);
|
374 | if (cachedResult !== undefined) return cachedResult;
|
375 | const result = _getCacheKey(url);
|
376 | cacheKeyCache.set(url, result);
|
377 | return result;
|
378 | };
|
379 |
|
380 | |
381 |
|
382 |
|
383 |
|
384 | const _getCacheKey = url => {
|
385 | const parsedUrl = new URL(url);
|
386 | const folder = toSafePath(parsedUrl.origin);
|
387 | const name = toSafePath(parsedUrl.pathname);
|
388 | const query = toSafePath(parsedUrl.search);
|
389 | let ext = extname(name);
|
390 | if (ext.length > 20) ext = "";
|
391 | const basename = ext ? name.slice(0, -ext.length) : name;
|
392 | const hash = createHash(hashFunction);
|
393 | hash.update(url);
|
394 | const digest = hash.digest(hashDigest).slice(0, hashDigestLength);
|
395 | return `${folder.slice(-50)}/${`${basename}${
|
396 | query ? `_${query}` : ""
|
397 | }`.slice(0, 150)}_${digest}${ext}`;
|
398 | };
|
399 |
|
400 | const getLockfile = cachedWithoutKey(
|
401 | |
402 |
|
403 |
|
404 |
|
405 | callback => {
|
406 | const readLockfile = () => {
|
407 | intermediateFs.readFile(lockfileLocation, (err, buffer) => {
|
408 | if (err && err.code !== "ENOENT") {
|
409 | compilation.missingDependencies.add(lockfileLocation);
|
410 | return callback(err);
|
411 | }
|
412 | compilation.fileDependencies.add(lockfileLocation);
|
413 | compilation.fileSystemInfo.createSnapshot(
|
414 | compiler.fsStartTime,
|
415 | buffer ? [lockfileLocation] : [],
|
416 | [],
|
417 | buffer ? [] : [lockfileLocation],
|
418 | { timestamp: true },
|
419 | (err, snapshot) => {
|
420 | if (err) return callback(err);
|
421 | const lockfile = buffer
|
422 | ? Lockfile.parse(buffer.toString("utf-8"))
|
423 | : new Lockfile();
|
424 | lockfileCache = {
|
425 | lockfile,
|
426 | snapshot
|
427 | };
|
428 | callback(null, lockfile);
|
429 | }
|
430 | );
|
431 | });
|
432 | };
|
433 | if (lockfileCache) {
|
434 | compilation.fileSystemInfo.checkSnapshotValid(
|
435 | lockfileCache.snapshot,
|
436 | (err, valid) => {
|
437 | if (err) return callback(err);
|
438 | if (!valid) return readLockfile();
|
439 | callback(null, lockfileCache.lockfile);
|
440 | }
|
441 | );
|
442 | } else {
|
443 | readLockfile();
|
444 | }
|
445 | }
|
446 | );
|
447 |
|
448 |
|
449 | let lockfileUpdates = undefined;
|
450 | const storeLockEntry = (lockfile, url, entry) => {
|
451 | const oldEntry = lockfile.entries.get(url);
|
452 | if (lockfileUpdates === undefined) lockfileUpdates = new Map();
|
453 | lockfileUpdates.set(url, entry);
|
454 | lockfile.entries.set(url, entry);
|
455 | if (!oldEntry) {
|
456 | logger.log(`${url} added to lockfile`);
|
457 | } else if (typeof oldEntry === "string") {
|
458 | if (typeof entry === "string") {
|
459 | logger.log(`${url} updated in lockfile: ${oldEntry} -> ${entry}`);
|
460 | } else {
|
461 | logger.log(
|
462 | `${url} updated in lockfile: ${oldEntry} -> ${entry.resolved}`
|
463 | );
|
464 | }
|
465 | } else if (typeof entry === "string") {
|
466 | logger.log(
|
467 | `${url} updated in lockfile: ${oldEntry.resolved} -> ${entry}`
|
468 | );
|
469 | } else if (oldEntry.resolved !== entry.resolved) {
|
470 | logger.log(
|
471 | `${url} updated in lockfile: ${oldEntry.resolved} -> ${entry.resolved}`
|
472 | );
|
473 | } else if (oldEntry.integrity !== entry.integrity) {
|
474 | logger.log(`${url} updated in lockfile: content changed`);
|
475 | } else if (oldEntry.contentType !== entry.contentType) {
|
476 | logger.log(
|
477 | `${url} updated in lockfile: ${oldEntry.contentType} -> ${entry.contentType}`
|
478 | );
|
479 | } else {
|
480 | logger.log(`${url} updated in lockfile`);
|
481 | }
|
482 | };
|
483 |
|
484 | const storeResult = (lockfile, url, result, callback) => {
|
485 | if (result.storeLock) {
|
486 | storeLockEntry(lockfile, url, result.entry);
|
487 | if (!cacheLocation || !result.content)
|
488 | return callback(null, result);
|
489 | const key = getCacheKey(result.entry.resolved);
|
490 | const filePath = join(intermediateFs, cacheLocation, key);
|
491 | mkdirp(intermediateFs, dirname(intermediateFs, filePath), err => {
|
492 | if (err) return callback(err);
|
493 | intermediateFs.writeFile(filePath, result.content, err => {
|
494 | if (err) return callback(err);
|
495 | callback(null, result);
|
496 | });
|
497 | });
|
498 | } else {
|
499 | storeLockEntry(lockfile, url, "no-cache");
|
500 | callback(null, result);
|
501 | }
|
502 | };
|
503 |
|
504 | for (const { scheme, fetch } of schemes) {
|
505 | |
506 |
|
507 |
|
508 |
|
509 |
|
510 |
|
511 | const resolveContent = (url, integrity, callback) => {
|
512 | const handleResult = (err, result) => {
|
513 | if (err) return callback(err);
|
514 | if ("location" in result) {
|
515 | return resolveContent(
|
516 | result.location,
|
517 | integrity,
|
518 | (err, innerResult) => {
|
519 | if (err) return callback(err);
|
520 | callback(null, {
|
521 | entry: innerResult.entry,
|
522 | content: innerResult.content,
|
523 | storeLock: innerResult.storeLock && result.storeLock
|
524 | });
|
525 | }
|
526 | );
|
527 | } else {
|
528 | if (
|
529 | !result.fresh &&
|
530 | integrity &&
|
531 | result.entry.integrity !== integrity &&
|
532 | !verifyIntegrity(result.content, integrity)
|
533 | ) {
|
534 | return fetchContent.force(url, handleResult);
|
535 | }
|
536 | return callback(null, {
|
537 | entry: result.entry,
|
538 | content: result.content,
|
539 | storeLock: result.storeLock
|
540 | });
|
541 | }
|
542 | };
|
543 | fetchContent(url, handleResult);
|
544 | };
|
545 |
|
546 |
|
547 |
|
548 |
|
549 |
|
550 |
|
551 | |
552 |
|
553 |
|
554 |
|
555 |
|
556 |
|
557 | const fetchContentRaw = (url, cachedResult, callback) => {
|
558 | const requestTime = Date.now();
|
559 | fetch(
|
560 | new URL(url),
|
561 | {
|
562 | headers: {
|
563 | "accept-encoding": "gzip, deflate, br",
|
564 | "user-agent": "webpack",
|
565 | "if-none-match": cachedResult
|
566 | ? cachedResult.etag || null
|
567 | : null
|
568 | }
|
569 | },
|
570 | res => {
|
571 | const etag = res.headers["etag"];
|
572 | const location = res.headers["location"];
|
573 | const cacheControl = res.headers["cache-control"];
|
574 | const { storeLock, storeCache, validUntil } = parseCacheControl(
|
575 | cacheControl,
|
576 | requestTime
|
577 | );
|
578 | |
579 |
|
580 |
|
581 |
|
582 | const finishWith = partialResult => {
|
583 | if ("location" in partialResult) {
|
584 | logger.debug(
|
585 | `GET ${url} [${res.statusCode}] -> ${partialResult.location}`
|
586 | );
|
587 | } else {
|
588 | logger.debug(
|
589 | `GET ${url} [${res.statusCode}] ${Math.ceil(
|
590 | partialResult.content.length / 1024
|
591 | )} kB${!storeLock ? " no-cache" : ""}`
|
592 | );
|
593 | }
|
594 | const result = {
|
595 | ...partialResult,
|
596 | fresh: true,
|
597 | storeLock,
|
598 | storeCache,
|
599 | validUntil,
|
600 | etag
|
601 | };
|
602 | if (!storeCache) {
|
603 | logger.log(
|
604 | `${url} can't be stored in cache, due to Cache-Control header: ${cacheControl}`
|
605 | );
|
606 | return callback(null, result);
|
607 | }
|
608 | cache.store(
|
609 | url,
|
610 | null,
|
611 | {
|
612 | ...result,
|
613 | fresh: false
|
614 | },
|
615 | err => {
|
616 | if (err) {
|
617 | logger.warn(
|
618 | `${url} can't be stored in cache: ${err.message}`
|
619 | );
|
620 | logger.debug(err.stack);
|
621 | }
|
622 | callback(null, result);
|
623 | }
|
624 | );
|
625 | };
|
626 | if (res.statusCode === 304) {
|
627 | if (
|
628 | cachedResult.validUntil < validUntil ||
|
629 | cachedResult.storeLock !== storeLock ||
|
630 | cachedResult.storeCache !== storeCache ||
|
631 | cachedResult.etag !== etag
|
632 | ) {
|
633 | return finishWith(cachedResult);
|
634 | } else {
|
635 | logger.debug(`GET ${url} [${res.statusCode}] (unchanged)`);
|
636 | return callback(null, {
|
637 | ...cachedResult,
|
638 | fresh: true
|
639 | });
|
640 | }
|
641 | }
|
642 | if (
|
643 | location &&
|
644 | res.statusCode >= 301 &&
|
645 | res.statusCode <= 308
|
646 | ) {
|
647 | const result = {
|
648 | location: new URL(location, url).href
|
649 | };
|
650 | if (
|
651 | !cachedResult ||
|
652 | !("location" in cachedResult) ||
|
653 | cachedResult.location !== result.location ||
|
654 | cachedResult.validUntil < validUntil ||
|
655 | cachedResult.storeLock !== storeLock ||
|
656 | cachedResult.storeCache !== storeCache ||
|
657 | cachedResult.etag !== etag
|
658 | ) {
|
659 | return finishWith(result);
|
660 | } else {
|
661 | logger.debug(`GET ${url} [${res.statusCode}] (unchanged)`);
|
662 | return callback(null, {
|
663 | ...result,
|
664 | fresh: true,
|
665 | storeLock,
|
666 | storeCache,
|
667 | validUntil,
|
668 | etag
|
669 | });
|
670 | }
|
671 | }
|
672 | const contentType = res.headers["content-type"] || "";
|
673 | const bufferArr = [];
|
674 |
|
675 | const contentEncoding = res.headers["content-encoding"];
|
676 | let stream = res;
|
677 | if (contentEncoding === "gzip") {
|
678 | stream = stream.pipe(createGunzip());
|
679 | } else if (contentEncoding === "br") {
|
680 | stream = stream.pipe(createBrotliDecompress());
|
681 | } else if (contentEncoding === "deflate") {
|
682 | stream = stream.pipe(createInflate());
|
683 | }
|
684 |
|
685 | stream.on("data", chunk => {
|
686 | bufferArr.push(chunk);
|
687 | });
|
688 |
|
689 | stream.on("end", () => {
|
690 | if (!res.complete) {
|
691 | logger.log(`GET ${url} [${res.statusCode}] (terminated)`);
|
692 | return callback(new Error(`${url} request was terminated`));
|
693 | }
|
694 |
|
695 | const content = Buffer.concat(bufferArr);
|
696 |
|
697 | if (res.statusCode !== 200) {
|
698 | logger.log(`GET ${url} [${res.statusCode}]`);
|
699 | return callback(
|
700 | new Error(
|
701 | `${url} request status code = ${
|
702 | res.statusCode
|
703 | }\n${content.toString("utf-8")}`
|
704 | )
|
705 | );
|
706 | }
|
707 |
|
708 | const integrity = computeIntegrity(content);
|
709 | const entry = { resolved: url, integrity, contentType };
|
710 |
|
711 | finishWith({
|
712 | entry,
|
713 | content
|
714 | });
|
715 | });
|
716 | }
|
717 | ).on("error", err => {
|
718 | logger.log(`GET ${url} (error)`);
|
719 | err.message += `\nwhile fetching ${url}`;
|
720 | callback(err);
|
721 | });
|
722 | };
|
723 |
|
724 | const fetchContent = cachedWithKey(
|
725 | |
726 |
|
727 |
|
728 |
|
729 | (url, callback) => {
|
730 | cache.get(url, null, (err, cachedResult) => {
|
731 | if (err) return callback(err);
|
732 | if (cachedResult) {
|
733 | const isValid = cachedResult.validUntil >= Date.now();
|
734 | if (isValid) return callback(null, cachedResult);
|
735 | }
|
736 | fetchContentRaw(url, cachedResult, callback);
|
737 | });
|
738 | },
|
739 | (url, callback) => fetchContentRaw(url, undefined, callback)
|
740 | );
|
741 |
|
742 | const isAllowed = uri => {
|
743 | for (const allowed of allowedUris) {
|
744 | if (typeof allowed === "string") {
|
745 | if (uri.startsWith(allowed)) return true;
|
746 | } else if (typeof allowed === "function") {
|
747 | if (allowed(uri)) return true;
|
748 | } else {
|
749 | if (allowed.test(uri)) return true;
|
750 | }
|
751 | }
|
752 | return false;
|
753 | };
|
754 |
|
755 | const getInfo = cachedWithKey(
|
756 | |
757 |
|
758 |
|
759 |
|
760 |
|
761 | (url, callback) => {
|
762 | if (!isAllowed(url)) {
|
763 | return callback(
|
764 | new Error(
|
765 | `${url} doesn't match the allowedUris policy. These URIs are allowed:\n${allowedUris
|
766 | .map(uri => ` - ${uri}`)
|
767 | .join("\n")}`
|
768 | )
|
769 | );
|
770 | }
|
771 | getLockfile((err, lockfile) => {
|
772 | if (err) return callback(err);
|
773 | const entryOrString = lockfile.entries.get(url);
|
774 | if (!entryOrString) {
|
775 | if (frozen) {
|
776 | return callback(
|
777 | new Error(
|
778 | `${url} has no lockfile entry and lockfile is frozen`
|
779 | )
|
780 | );
|
781 | }
|
782 | resolveContent(url, null, (err, result) => {
|
783 | if (err) return callback(err);
|
784 | storeResult(lockfile, url, result, callback);
|
785 | });
|
786 | return;
|
787 | }
|
788 | if (typeof entryOrString === "string") {
|
789 | const entryTag = entryOrString;
|
790 | resolveContent(url, null, (err, result) => {
|
791 | if (err) return callback(err);
|
792 | if (!result.storeLock || entryTag === "ignore")
|
793 | return callback(null, result);
|
794 | if (frozen) {
|
795 | return callback(
|
796 | new Error(
|
797 | `${url} used to have ${entryTag} lockfile entry and has content now, but lockfile is frozen`
|
798 | )
|
799 | );
|
800 | }
|
801 | if (!upgrade) {
|
802 | return callback(
|
803 | new Error(
|
804 | `${url} used to have ${entryTag} lockfile entry and has content now.
|
805 | This should be reflected in the lockfile, so this lockfile entry must be upgraded, but upgrading is not enabled.
|
806 | Remove this line from the lockfile to force upgrading.`
|
807 | )
|
808 | );
|
809 | }
|
810 | storeResult(lockfile, url, result, callback);
|
811 | });
|
812 | return;
|
813 | }
|
814 | let entry = entryOrString;
|
815 | const doFetch = lockedContent => {
|
816 | resolveContent(url, entry.integrity, (err, result) => {
|
817 | if (err) {
|
818 | if (lockedContent) {
|
819 | logger.warn(
|
820 | `Upgrade request to ${url} failed: ${err.message}`
|
821 | );
|
822 | logger.debug(err.stack);
|
823 | return callback(null, {
|
824 | entry,
|
825 | content: lockedContent
|
826 | });
|
827 | }
|
828 | return callback(err);
|
829 | }
|
830 | if (!result.storeLock) {
|
831 |
|
832 |
|
833 | if (frozen) {
|
834 | return callback(
|
835 | new Error(
|
836 | `${url} has a lockfile entry and is no-cache now, but lockfile is frozen\nLockfile: ${entryToString(
|
837 | entry
|
838 | )}`
|
839 | )
|
840 | );
|
841 | }
|
842 | storeResult(lockfile, url, result, callback);
|
843 | return;
|
844 | }
|
845 | if (!areLockfileEntriesEqual(result.entry, entry)) {
|
846 |
|
847 |
|
848 | if (frozen) {
|
849 | return callback(
|
850 | new Error(
|
851 | `${url} has an outdated lockfile entry, but lockfile is frozen\nLockfile: ${entryToString(
|
852 | entry
|
853 | )}\nExpected: ${entryToString(result.entry)}`
|
854 | )
|
855 | );
|
856 | }
|
857 | storeResult(lockfile, url, result, callback);
|
858 | return;
|
859 | }
|
860 | if (!lockedContent && cacheLocation) {
|
861 |
|
862 |
|
863 | if (frozen) {
|
864 | return callback(
|
865 | new Error(
|
866 | `${url} is missing content in the lockfile cache, but lockfile is frozen\nLockfile: ${entryToString(
|
867 | entry
|
868 | )}`
|
869 | )
|
870 | );
|
871 | }
|
872 | storeResult(lockfile, url, result, callback);
|
873 | return;
|
874 | }
|
875 | return callback(null, result);
|
876 | });
|
877 | };
|
878 | if (cacheLocation) {
|
879 |
|
880 |
|
881 | const key = getCacheKey(entry.resolved);
|
882 | const filePath = join(intermediateFs, cacheLocation, key);
|
883 | fs.readFile(filePath, (err, result) => {
|
884 | const content = (result);
|
885 | if (err) {
|
886 | if (err.code === "ENOENT") return doFetch();
|
887 | return callback(err);
|
888 | }
|
889 | const continueWithCachedContent = result => {
|
890 | if (!upgrade) {
|
891 |
|
892 | return callback(null, { entry, content });
|
893 | }
|
894 | return doFetch(content);
|
895 | };
|
896 | if (!verifyIntegrity(content, entry.integrity)) {
|
897 | let contentWithChangedEol;
|
898 | let isEolChanged = false;
|
899 | try {
|
900 | contentWithChangedEol = Buffer.from(
|
901 | content.toString("utf-8").replace(/\r\n/g, "\n")
|
902 | );
|
903 | isEolChanged = verifyIntegrity(
|
904 | contentWithChangedEol,
|
905 | entry.integrity
|
906 | );
|
907 | } catch (e) {
|
908 |
|
909 | }
|
910 | if (isEolChanged) {
|
911 | if (!warnedAboutEol) {
|
912 | const explainer = `Incorrect end of line sequence was detected in the lockfile cache.
|
913 | The lockfile cache is protected by integrity checks, so any external modification will lead to a corrupted lockfile cache.
|
914 | When using git make sure to configure .gitattributes correctly for the lockfile cache:
|
915 | **/*webpack.lock.data/** -text
|
916 | This will avoid that the end of line sequence is changed by git on Windows.`;
|
917 | if (frozen) {
|
918 | logger.error(explainer);
|
919 | } else {
|
920 | logger.warn(explainer);
|
921 | logger.info(
|
922 | "Lockfile cache will be automatically fixed now, but when lockfile is frozen this would result in an error."
|
923 | );
|
924 | }
|
925 | warnedAboutEol = true;
|
926 | }
|
927 | if (!frozen) {
|
928 |
|
929 | logger.log(
|
930 | `${filePath} fixed end of line sequence (\\r\\n instead of \\n).`
|
931 | );
|
932 | intermediateFs.writeFile(
|
933 | filePath,
|
934 | contentWithChangedEol,
|
935 | err => {
|
936 | if (err) return callback(err);
|
937 | continueWithCachedContent(contentWithChangedEol);
|
938 | }
|
939 | );
|
940 | return;
|
941 | }
|
942 | }
|
943 | if (frozen) {
|
944 | return callback(
|
945 | new Error(
|
946 | `${
|
947 | entry.resolved
|
948 | } integrity mismatch, expected content with integrity ${
|
949 | entry.integrity
|
950 | } but got ${computeIntegrity(content)}.
|
951 | Lockfile corrupted (${
|
952 | isEolChanged
|
953 | ? "end of line sequence was unexpectedly changed"
|
954 | : "incorrectly merged? changed by other tools?"
|
955 | }).
|
956 | Run build with un-frozen lockfile to automatically fix lockfile.`
|
957 | )
|
958 | );
|
959 | } else {
|
960 |
|
961 |
|
962 | entry = {
|
963 | ...entry,
|
964 | integrity: computeIntegrity(content)
|
965 | };
|
966 | storeLockEntry(lockfile, url, entry);
|
967 | }
|
968 | }
|
969 | continueWithCachedContent(result);
|
970 | });
|
971 | } else {
|
972 | doFetch();
|
973 | }
|
974 | });
|
975 | }
|
976 | );
|
977 |
|
978 | const respondWithUrlModule = (url, resourceData, callback) => {
|
979 | getInfo(url.href, (err, result) => {
|
980 | if (err) return callback(err);
|
981 | resourceData.resource = url.href;
|
982 | resourceData.path = url.origin + url.pathname;
|
983 | resourceData.query = url.search;
|
984 | resourceData.fragment = url.hash;
|
985 | resourceData.context = new URL(
|
986 | ".",
|
987 | result.entry.resolved
|
988 | ).href.slice(0, -1);
|
989 | resourceData.data.mimetype = result.entry.contentType;
|
990 | callback(null, true);
|
991 | });
|
992 | };
|
993 | normalModuleFactory.hooks.resolveForScheme
|
994 | .for(scheme)
|
995 | .tapAsync(
|
996 | "HttpUriPlugin",
|
997 | (resourceData, resolveData, callback) => {
|
998 | respondWithUrlModule(
|
999 | new URL(resourceData.resource),
|
1000 | resourceData,
|
1001 | callback
|
1002 | );
|
1003 | }
|
1004 | );
|
1005 | normalModuleFactory.hooks.resolveInScheme
|
1006 | .for(scheme)
|
1007 | .tapAsync("HttpUriPlugin", (resourceData, data, callback) => {
|
1008 |
|
1009 | if (
|
1010 | data.dependencyType !== "url" &&
|
1011 | !/^\.{0,2}\//.test(resourceData.resource)
|
1012 | ) {
|
1013 | return callback();
|
1014 | }
|
1015 | respondWithUrlModule(
|
1016 | new URL(resourceData.resource, data.context + "/"),
|
1017 | resourceData,
|
1018 | callback
|
1019 | );
|
1020 | });
|
1021 | const hooks = NormalModule.getCompilationHooks(compilation);
|
1022 | hooks.readResourceForScheme
|
1023 | .for(scheme)
|
1024 | .tapAsync("HttpUriPlugin", (resource, module, callback) => {
|
1025 | return getInfo(resource, (err, result) => {
|
1026 | if (err) return callback(err);
|
1027 | module.buildInfo.resourceIntegrity = result.entry.integrity;
|
1028 | callback(null, result.content);
|
1029 | });
|
1030 | });
|
1031 | hooks.needBuild.tapAsync(
|
1032 | "HttpUriPlugin",
|
1033 | (module, context, callback) => {
|
1034 | if (
|
1035 | module.resource &&
|
1036 | module.resource.startsWith(`${scheme}://`)
|
1037 | ) {
|
1038 | getInfo(module.resource, (err, result) => {
|
1039 | if (err) return callback(err);
|
1040 | if (
|
1041 | result.entry.integrity !==
|
1042 | module.buildInfo.resourceIntegrity
|
1043 | ) {
|
1044 | return callback(null, true);
|
1045 | }
|
1046 | callback();
|
1047 | });
|
1048 | } else {
|
1049 | return callback();
|
1050 | }
|
1051 | }
|
1052 | );
|
1053 | }
|
1054 | compilation.hooks.finishModules.tapAsync(
|
1055 | "HttpUriPlugin",
|
1056 | (modules, callback) => {
|
1057 | if (!lockfileUpdates) return callback();
|
1058 | const ext = extname(lockfileLocation);
|
1059 | const tempFile = join(
|
1060 | intermediateFs,
|
1061 | dirname(intermediateFs, lockfileLocation),
|
1062 | `.${basename(lockfileLocation, ext)}.${
|
1063 | (Math.random() * 10000) | 0
|
1064 | }${ext}`
|
1065 | );
|
1066 |
|
1067 | const writeDone = () => {
|
1068 | const nextOperation = inProgressWrite.shift();
|
1069 | if (nextOperation) {
|
1070 | nextOperation();
|
1071 | } else {
|
1072 | inProgressWrite = undefined;
|
1073 | }
|
1074 | };
|
1075 | const runWrite = () => {
|
1076 | intermediateFs.readFile(lockfileLocation, (err, buffer) => {
|
1077 | if (err && err.code !== "ENOENT") {
|
1078 | writeDone();
|
1079 | return callback(err);
|
1080 | }
|
1081 | const lockfile = buffer
|
1082 | ? Lockfile.parse(buffer.toString("utf-8"))
|
1083 | : new Lockfile();
|
1084 | for (const [key, value] of lockfileUpdates) {
|
1085 | lockfile.entries.set(key, value);
|
1086 | }
|
1087 | intermediateFs.writeFile(tempFile, lockfile.toString(), err => {
|
1088 | if (err) {
|
1089 | writeDone();
|
1090 | return intermediateFs.unlink(tempFile, () => callback(err));
|
1091 | }
|
1092 | intermediateFs.rename(tempFile, lockfileLocation, err => {
|
1093 | if (err) {
|
1094 | writeDone();
|
1095 | return intermediateFs.unlink(tempFile, () =>
|
1096 | callback(err)
|
1097 | );
|
1098 | }
|
1099 | writeDone();
|
1100 | callback();
|
1101 | });
|
1102 | });
|
1103 | });
|
1104 | };
|
1105 | if (inProgressWrite) {
|
1106 | inProgressWrite.push(runWrite);
|
1107 | } else {
|
1108 | inProgressWrite = [];
|
1109 | runWrite();
|
1110 | }
|
1111 | }
|
1112 | );
|
1113 | }
|
1114 | );
|
1115 | }
|
1116 | }
|
1117 |
|
1118 | module.exports = HttpUriPlugin;
|