HttpUriPlugin.js 39 KB

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