HttpUriPlugin.js 39 KB

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