HttpUriPlugin.js 35 KB

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