HttpUriPlugin.js 39 KB

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