HttpUriPlugin.js 34 KB

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