single-file-cli 2.2.2 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Dockerfile CHANGED
@@ -14,5 +14,6 @@ ENTRYPOINT [ \
14
14
  "npx", \
15
15
  "single-file", \
16
16
  "--browser-executable-path", "/usr/bin/chromium-browser", \
17
+ "--browser-single-process=false", \
17
18
  "--output-directory", "./out/", \
18
19
  "--dump-content" ]
package/README.MD CHANGED
@@ -143,12 +143,16 @@ Make sure Chrome or a Chromium-based browser is installed in the default folder.
143
143
  single-file https://www.wikipedia.org --crawl-links=true --crawl-inner-links-only=false --crawl-external-links-max-depth=1 --crawl-rewrite-rule="^.*wikipedia.*$"
144
144
  ```
145
145
 
146
- - Save https://www.wikipedia.org and its internal links into a single self-extracting ZIP file
146
+ - Save http://info.cern.ch (i.e. the first website) and its internal links into a single self-extracting ZIP file, and mark the links to pages missing from the archive
147
147
 
148
148
  ```sh
149
- single-file https://www.wikipedia.org wikipedia.html --crawl-links=true --crawl-save-archive=true --compress-content=true
149
+ single-file http://info.cern.ch first-website.html --crawl-links=true --crawl-max-depth=2 --crawl-save-archive=true --crawl-save-archive-mark-unarchived-links=true --compress-content=true
150
150
  ```
151
151
 
152
+ - Exit code
153
+
154
+ The exit code is 0 when all the pages have been saved, 1 when at least one capture failed (the other pages of the batch or the crawl are still saved), and 255 when an error prevented the process from running.
155
+
152
156
  ## Compile executables
153
157
 
154
158
  - Compile executables into `/dist`
package/build.sh CHANGED
@@ -3,7 +3,7 @@
3
3
  mv package.json package.json.tmp
4
4
  mv deno.json deno.json.tmp
5
5
  mv deno.lock deno.lock.tmp
6
- deno install --vendor --quiet --minimum-dependency-age=0 "npm:single-file-core@1.5.92"
6
+ deno install --vendor --quiet --minimum-dependency-age=0 "npm:single-file-core@1.5.98"
7
7
  mv package.json.tmp package.json
8
8
  mv deno.json.tmp deno.json
9
9
  mv deno.lock.tmp deno.lock
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@single-file/single-file-cli",
3
- "version": "2.2.2",
3
+ "version": "2.3.1",
4
4
  "description": "SingleFile CLI",
5
5
  "exports": {
6
6
  ".": "./single-file-cli-api.js"
@@ -59,6 +59,9 @@ async function createPagesArchive(pages, options) {
59
59
  if (options.markUnarchivedLinks) {
60
60
  manifest.markUnarchivedLinks = true;
61
61
  }
62
+ if (options.pageTransitions && options.pageTransitions != "auto") {
63
+ manifest.pageTransitions = options.pageTransitions;
64
+ }
62
65
  const pageData = {
63
66
  doctype: "<!DOCTYPE html>",
64
67
  content: "",
@@ -74,10 +74,13 @@ function waitForTimeout(abortSignal, maxDelay, errorMessage, errorCode) {
74
74
  reject(error);
75
75
  }, maxDelay);
76
76
 
77
+ // the promise must stay pending on abort: the abort fires after the
78
+ // Promise.race around this timeout has settled, and settling here would
79
+ // either make the race winner destructure undefined or raise an
80
+ // unhandled rejection
77
81
  function onAbort() {
78
82
  abortSignal.removeEventListener(ABORT_EVENT, onAbort);
79
83
  clearTimeout(timeoutId);
80
- resolve();
81
84
  }
82
85
  });
83
86
  }
package/lib/cdp-client.js CHANGED
@@ -102,7 +102,10 @@ function relaunchBrowser() {
102
102
 
103
103
  async function getPageData(options) {
104
104
  const EMPTY_PAGE_URL = "about:blank";
105
- const pageContext = { options, consoleMessages: [], debugMessages: [], httpInfo: {}, fetchAbortController: new AbortController() };
105
+ // compiled here so that an invalid pattern is reported before the page is
106
+ // loaded, instead of throwing for every request that is intercepted
107
+ const blockedURLPatterns = (options.blockedURLPatterns || []).map(pattern => new RegExp(pattern));
108
+ const pageContext = { options, consoleMessages: [], debugMessages: [], httpInfo: {}, blockedURLPatterns, fetchAbortController: new AbortController() };
106
109
  let targetInfo, cdp;
107
110
  try {
108
111
  logData(["Loading page", EMPTY_PAGE_URL], pageContext);
@@ -115,8 +118,8 @@ async function getPageData(options) {
115
118
  await setupNetworkInterception(cdp, pageContext);
116
119
  await setupScriptInjection(cdp, pageContext);
117
120
  const contextId = await getContextId(cdp, pageContext);
118
- const pageDataPromise = setupPageDataCapture(cdp, contextId, pageContext);
119
- await setupBindings(cdp, contextId, pageContext);
121
+ const pageDataPromise = setupPageDataCapture(cdp, pageContext);
122
+ await setupBindings(cdp, pageContext);
120
123
  await capturePageData(cdp, contextId, pageContext);
121
124
  await disableCdpDomains(cdp, pageContext);
122
125
  return await finalizePageData(pageDataPromise, pageContext);
@@ -205,25 +208,28 @@ async function setupBrowserWindow({ Browser }, targetId, { options, debugMessage
205
208
  }
206
209
  }
207
210
 
208
- async function setupSecurity({ Security }, { options, debugMessages }) {
211
+ async function setupSecurity({ Security }, { options, debugMessages }, sessionId) {
209
212
  if (options.browserIgnoreHTTPSErrors !== undefined && options.browserIgnoreHTTPSErrors) {
210
213
  logData(["Ignoring HTTPS errors"], { options, debugMessages });
211
- await Security.setIgnoreCertificateErrors({ ignore: true });
214
+ await Security.setIgnoreCertificateErrors({ ignore: true }, sessionId);
212
215
  }
213
216
  }
214
217
 
215
218
  async function setupDeviceEmulation({ Browser, Emulation, Runtime }, { options, debugMessages }) {
216
219
  const needsDeviceMetrics = options.browserMobileEmulation || options.browserDeviceWidth ||
217
220
  options.browserDeviceHeight || options.browserDeviceScaleFactor;
218
- const needsUserAgent = options.browserMobileEmulation || options.platform || options.acceptLanguage;
219
221
  if (needsDeviceMetrics) {
220
222
  await setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessages });
221
223
  }
222
- if (needsUserAgent) {
224
+ if (needsUserAgentOverride(options)) {
223
225
  await setupUserAgent({ Browser, Emulation }, { options, debugMessages });
224
226
  }
225
227
  }
226
228
 
229
+ function needsUserAgentOverride(options) {
230
+ return Boolean(options.browserMobileEmulation || options.platform || options.acceptLanguage);
231
+ }
232
+
227
233
  async function setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessages }) {
228
234
  const INNER_WIDTH_PROPERTY = "window.innerWidth";
229
235
  const INNER_HEIGHT_PROPERTY = "window.innerHeight";
@@ -244,7 +250,7 @@ async function setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessag
244
250
  await Emulation.setDeviceMetricsOverride(deviceMetricsOptions);
245
251
  }
246
252
 
247
- async function setupUserAgent({ Browser, Emulation }, { options, debugMessages }) {
253
+ async function setupUserAgent({ Browser, Emulation }, { options, debugMessages }, sessionId) {
248
254
  const ANDROID_PLATFORM = "Android";
249
255
  const { userAgent, product } = await Browser.getVersion();
250
256
  const defaultMobileUA = `Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) ${product} Mobile Safari/537.36`;
@@ -258,22 +264,17 @@ async function setupUserAgent({ Browser, Emulation }, { options, debugMessages }
258
264
  agentOptions.platform = options.platform || ANDROID_PLATFORM;
259
265
  }
260
266
  logData(["Emulating user agent", JSON.stringify(agentOptions)], { options, debugMessages });
261
- await Emulation.setUserAgentOverride(agentOptions);
267
+ await Emulation.setUserAgentOverride(agentOptions, sessionId);
262
268
  }
263
269
 
264
- async function setupNetworkInterception({ Browser, Emulation, Fetch, Network }, { options, debugMessages, httpInfo }) {
265
- const REQUEST_STAGE = "Request";
266
- const RESPONSE_STAGE = "Response";
270
+ async function setupNetworkInterception({ Browser, Emulation, Fetch, Network }, { options, debugMessages, httpInfo, blockedURLPatterns }) {
267
271
  const DENY_BEHAVIOR = "deny";
268
- const handleAuthRequests = Boolean(options.httpProxyUsername);
269
- const patterns = handleAuthRequests ?
270
- [{ requestStage: REQUEST_STAGE }, { requestStage: RESPONSE_STAGE }] :
271
- [{ requestStage: RESPONSE_STAGE }];
272
+ const { handleAuthRequests, patterns } = getInterceptionOptions(options);
272
273
  await Fetch.enable({ handleAuthRequests, patterns });
273
274
  if (handleAuthRequests) {
274
275
  setupProxyAuth({ Fetch }, { options, debugMessages });
275
276
  }
276
- setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo });
277
+ setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo, blockedURLPatterns });
277
278
  if (options.httpHeaders) {
278
279
  await setupHttpHeaders({ Network }, { options, debugMessages });
279
280
  }
@@ -286,10 +287,20 @@ async function setupNetworkInterception({ Browser, Emulation, Fetch, Network },
286
287
  await Browser.setDownloadBehavior({ behavior: DENY_BEHAVIOR });
287
288
  }
288
289
 
290
+ function getInterceptionOptions(options) {
291
+ const REQUEST_STAGE = "Request";
292
+ const RESPONSE_STAGE = "Response";
293
+ const handleAuthRequests = Boolean(options.httpProxyUsername);
294
+ const patterns = handleAuthRequests ?
295
+ [{ requestStage: REQUEST_STAGE }, { requestStage: RESPONSE_STAGE }] :
296
+ [{ requestStage: RESPONSE_STAGE }];
297
+ return { handleAuthRequests, patterns };
298
+ }
299
+
289
300
  function setupProxyAuth({ Fetch }, { options, debugMessages }) {
290
301
  const AUTH_REQUIRED_EVENT_TYPE = "authRequired";
291
302
  const PROVIDE_CREDENTIALS_RESPONSE = "ProvideCredentials";
292
- Fetch.addEventListener(AUTH_REQUIRED_EVENT_TYPE, ignoringErrors(async ({ params }) => {
303
+ Fetch.addEventListener(AUTH_REQUIRED_EVENT_TYPE, ignoringErrors(async ({ params, sessionId }) => {
293
304
  logData(["Authenticating"], { options, debugMessages });
294
305
  await Fetch.continueWithAuth({
295
306
  requestId: params.requestId,
@@ -298,38 +309,37 @@ function setupProxyAuth({ Fetch }, { options, debugMessages }) {
298
309
  username: options.httpProxyUsername,
299
310
  password: options.httpProxyPassword
300
311
  }
301
- });
312
+ }, sessionId);
302
313
  }, { options, debugMessages }));
303
314
  }
304
315
 
305
- function setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo }) {
316
+ function setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo, blockedURLPatterns }) {
306
317
  const REQUEST_PAUSED_EVENT_TYPE = "requestPaused";
307
318
  const ABORTED_ERROR_REASON = "Aborted";
308
319
  const urlState = { url: options.url, alternativeUrl: getAlternativeUrl(options.url) };
309
- // compiled here so that an invalid pattern is reported before the page is
310
- // loaded, instead of throwing for every request that is intercepted
311
- const blockedURLPatterns = (options.blockedURLPatterns || []).map(pattern => new RegExp(pattern));
312
- Fetch.addEventListener(REQUEST_PAUSED_EVENT_TYPE, ignoringErrors(async ({ params }) => {
320
+ Fetch.addEventListener(REQUEST_PAUSED_EVENT_TYPE, ignoringErrors(async ({ params, sessionId }) => {
313
321
  const { requestId, request } = params;
314
322
  // the request is always resumed below, otherwise the page waits for it
315
323
  // until the load timeout expires
316
324
  let blocked = false;
317
325
  try {
318
- captureHttpInfo(params, urlState, { options, debugMessages, httpInfo });
326
+ if (!sessionId) {
327
+ captureHttpInfo(params, urlState, { options, debugMessages, httpInfo });
328
+ }
319
329
  blocked = shouldBlockRequest(request.url);
320
330
  } catch (error) {
321
331
  logData(["Ignoring request interception error", error.message], { options, debugMessages });
322
332
  }
323
333
  if (blocked) {
324
334
  try {
325
- await Fetch.failRequest({ requestId, errorReason: ABORTED_ERROR_REASON });
335
+ await Fetch.failRequest({ requestId, errorReason: ABORTED_ERROR_REASON }, sessionId);
326
336
  return;
327
337
  } catch {
328
338
  // ignored
329
339
  }
330
340
  }
331
341
  try {
332
- await Fetch.continueRequest({ requestId });
342
+ await Fetch.continueRequest({ requestId }, sessionId);
333
343
  } catch {
334
344
  // ignored
335
345
  }
@@ -381,13 +391,13 @@ function captureHttpInfo(params, urlState, { options, debugMessages, httpInfo })
381
391
  }
382
392
  }
383
393
 
384
- async function setupHttpHeaders({ Network }, { options, debugMessages }) {
394
+ async function setupHttpHeaders({ Network }, { options, debugMessages }, sessionId) {
385
395
  logData(["Setting HTTP headers", JSON.stringify(options.httpHeaders)], { options, debugMessages });
386
- await Network.enable();
387
- await Network.setExtraHTTPHeaders({ headers: options.httpHeaders });
396
+ await Network.enable({}, sessionId);
397
+ await Network.setExtraHTTPHeaders({ headers: options.httpHeaders }, sessionId);
388
398
  }
389
399
 
390
- async function setupMediaFeatures({ Emulation }, { options, debugMessages }) {
400
+ async function setupMediaFeatures({ Emulation }, { options, debugMessages }, sessionId) {
391
401
  const features = [];
392
402
  for (const mediaFeature of options.emulateMediaFeatures) {
393
403
  logData(["Emulating media feature", mediaFeature.name, mediaFeature.value], { options, debugMessages });
@@ -395,7 +405,7 @@ async function setupMediaFeatures({ Emulation }, { options, debugMessages }) {
395
405
  features.push({ name: mediaFeature.name, value: value.trim() });
396
406
  }
397
407
  }
398
- await Emulation.setEmulatedMedia({ features });
408
+ await Emulation.setEmulatedMedia({ features }, sessionId);
399
409
  }
400
410
 
401
411
  async function setupCookies({ Network }, { options, debugMessages }) {
@@ -403,16 +413,77 @@ async function setupCookies({ Network }, { options, debugMessages }) {
403
413
  await Network.setCookies({ cookies: options.browserCookies });
404
414
  }
405
415
 
406
- async function setupScriptInjection({ Page }, { options }) {
416
+ async function setupScriptInjection(cdp, { options, debugMessages }) {
417
+ const { Page } = cdp;
418
+ const scriptSource = await getScriptSource(options);
407
419
  await Page.addScriptToEvaluateOnNewDocument({
408
420
  source: getHookScriptSource(),
409
421
  runImmediately: true
410
422
  });
411
423
  await Page.addScriptToEvaluateOnNewDocument({
412
- source: await getScriptSource(options),
424
+ source: scriptSource,
413
425
  runImmediately: true,
414
426
  worldName: SINGLE_FILE_WORLD_NAME
415
427
  });
428
+ await setupFrameScriptInjection(cdp, scriptSource, { options, debugMessages });
429
+ }
430
+
431
+ // out-of-process frames are separate targets, so the scripts registered above
432
+ // do not reach them; they are attached paused, injected and resumed instead
433
+ async function setupFrameScriptInjection(cdp, scriptSource, { options, debugMessages }) {
434
+ const { Page, Runtime, Target } = cdp;
435
+ const ATTACHED_TO_TARGET_EVENT_TYPE = "attachedToTarget";
436
+ const IFRAME_TARGET_TYPE = "iframe";
437
+ const autoAttachOptions = { autoAttach: true, waitForDebuggerOnStart: true, flatten: true };
438
+ Target.addEventListener(ATTACHED_TO_TARGET_EVENT_TYPE, ignoringErrors(async ({ params }) => {
439
+ const { sessionId, targetInfo, waitingForDebugger } = params;
440
+ try {
441
+ if (targetInfo.type === IFRAME_TARGET_TYPE) {
442
+ logData(["Injecting scripts into frame", targetInfo.url], { options, debugMessages });
443
+ // the scripts registered below only run when the Page domain is
444
+ // enabled on the session
445
+ await Page.enable({}, sessionId);
446
+ await Target.setAutoAttach(autoAttachOptions, sessionId);
447
+ await Page.addScriptToEvaluateOnNewDocument({
448
+ source: getHookScriptSource(),
449
+ runImmediately: true
450
+ }, sessionId);
451
+ await Page.addScriptToEvaluateOnNewDocument({
452
+ source: scriptSource,
453
+ runImmediately: true,
454
+ worldName: SINGLE_FILE_WORLD_NAME
455
+ }, sessionId);
456
+ await Runtime.addBinding({ name: FETCH_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME }, sessionId);
457
+ await setupFrameSession(cdp, sessionId, { options, debugMessages });
458
+ }
459
+ } finally {
460
+ // the target is always resumed, even when a setup command fails,
461
+ // otherwise it would stay paused until the load timeout expires
462
+ if (waitingForDebugger) {
463
+ await Runtime.runIfWaitingForDebugger({}, sessionId);
464
+ }
465
+ }
466
+ }, { options, debugMessages }));
467
+ await Target.setAutoAttach(autoAttachOptions);
468
+ }
469
+
470
+ async function setupFrameSession({ Browser, Emulation, Fetch, Network, Security }, sessionId, { options, debugMessages }) {
471
+ const { handleAuthRequests, patterns } = getInterceptionOptions(options);
472
+ if (handleAuthRequests || (options.blockedURLPatterns && options.blockedURLPatterns.length)) {
473
+ // the requestPaused and authRequired listeners are registered once on
474
+ // the connection and receive the events of every session
475
+ await Fetch.enable({ handleAuthRequests, patterns }, sessionId);
476
+ }
477
+ await setupSecurity({ Security }, { options, debugMessages }, sessionId);
478
+ if (options.httpHeaders) {
479
+ await setupHttpHeaders({ Network }, { options, debugMessages }, sessionId);
480
+ }
481
+ if (options.emulateMediaFeatures) {
482
+ await setupMediaFeatures({ Emulation }, { options, debugMessages }, sessionId);
483
+ }
484
+ if (needsUserAgentOverride(options)) {
485
+ await setupUserAgent({ Browser, Emulation }, { options, debugMessages }, sessionId);
486
+ }
416
487
  }
417
488
 
418
489
  async function getContextId({ Debugger, Page, Runtime }, { options, debugMessages }) {
@@ -440,6 +511,9 @@ async function loadPage({ Page, Runtime }, { options, debugMessages }) {
440
511
  const LOAD_TIMEOUT_ERROR_MESSAGE = "Load timeout";
441
512
  await Runtime.enable();
442
513
  await Page.enable();
514
+ if (options.browserBypassCSP) {
515
+ await Page.setBypassCSP({ enabled: true });
516
+ }
443
517
  await Page.setLifecycleEventsEnabled({ enabled: true });
444
518
  // the ID of the top frame is stable, and it is read before the navigation is
445
519
  // triggered so that no event is missed while the browser answers
@@ -535,7 +609,8 @@ async function getSingleFileContext({ Page, Runtime }, topFrameId, { options, de
535
609
  // injected script ran in instead of creating another one
536
610
  const { executionContextId } = await Page.createIsolatedWorld({
537
611
  frameId: topFrameId,
538
- worldName: SINGLE_FILE_WORLD_NAME
612
+ worldName: SINGLE_FILE_WORLD_NAME,
613
+ contentSecurityPolicy: ""
539
614
  });
540
615
  // an empty world is returned when the script could not be injected, so the
541
616
  // context is checked before it is used to capture the page
@@ -549,7 +624,7 @@ async function getSingleFileContext({ Page, Runtime }, topFrameId, { options, de
549
624
  return executionContextId;
550
625
  }
551
626
 
552
- function setupPageDataCapture({ Runtime }, _contextId, { options, debugMessages }) {
627
+ function setupPageDataCapture({ Runtime }, { options, debugMessages }) {
553
628
  return new Promise((resolve, reject) => {
554
629
  let pageDataResponse = "";
555
630
  Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ({ params }) => {
@@ -574,33 +649,33 @@ function setupPageDataCapture({ Runtime }, _contextId, { options, debugMessages
574
649
  });
575
650
  }
576
651
 
577
- async function setupBindings({ Page, Runtime }, contextId, { options, debugMessages, fetchAbortController }) {
578
- await Runtime.addBinding({ name: SET_PAGE_DATA_FUNCTION_NAME, executionContextId: contextId });
652
+ async function setupBindings({ Page, Runtime }, { options, debugMessages, blockedURLPatterns, fetchAbortController }) {
653
+ await Runtime.addBinding({ name: SET_PAGE_DATA_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME });
579
654
  if (options.embedScreenshot && options.compressContent) {
580
- await setupScreenshotCapture({ Page, Runtime }, contextId, { options, debugMessages });
655
+ await setupScreenshotCapture({ Page, Runtime }, { options, debugMessages });
581
656
  }
582
657
  if (options.embedPdf && options.compressContent) {
583
- await setupPdfCapture({ Page, Runtime }, contextId, { options, debugMessages });
658
+ await setupPdfCapture({ Page, Runtime }, { options, debugMessages });
584
659
  }
585
- await Runtime.addBinding({ name: FETCH_FUNCTION_NAME, executionContextId: contextId });
586
- Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ignoringErrors(async ({ params }) => {
660
+ await Runtime.addBinding({ name: FETCH_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME });
661
+ Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ignoringErrors(async ({ params, sessionId }) => {
587
662
  if (params.name === FETCH_FUNCTION_NAME) {
588
- await handleFetchRequest({ Runtime }, params, contextId, { options, debugMessages, fetchAbortController });
663
+ await handleFetchRequest({ Runtime }, params, { options, debugMessages, blockedURLPatterns, fetchAbortController }, sessionId);
589
664
  }
590
665
  }, { options, debugMessages }));
591
666
  }
592
667
 
593
- async function setupScreenshotCapture({ Page, Runtime }, contextId, { options, debugMessages }) {
594
- await Runtime.addBinding({ name: CAPTURE_SCREENSHOT_FUNCTION_NAME, executionContextId: contextId });
668
+ async function setupScreenshotCapture({ Page, Runtime }, { options, debugMessages }) {
669
+ await Runtime.addBinding({ name: CAPTURE_SCREENSHOT_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME });
595
670
  Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ignoringErrors(async ({ params }) => {
596
671
  if (params.name === CAPTURE_SCREENSHOT_FUNCTION_NAME) {
597
672
  logData(["Capturing screenshot"], { options, debugMessages });
598
673
  try {
599
674
  const screenshotOptions = parseScreenshotOptions(options.embedScreenshotOptions);
600
675
  const { data } = await Page.captureScreenshot(screenshotOptions);
601
- await callBrowserFunction({ Runtime }, contextId, SET_SCREENSHOT_FUNCTION_NAME, [data]);
676
+ await callBrowserFunction({ Runtime }, params.executionContextId, SET_SCREENSHOT_FUNCTION_NAME, [data]);
602
677
  } catch {
603
- await callBrowserFunction({ Runtime }, contextId, SET_SCREENSHOT_FUNCTION_NAME, [""]);
678
+ await callBrowserFunction({ Runtime }, params.executionContextId, SET_SCREENSHOT_FUNCTION_NAME, [""]);
604
679
  }
605
680
  }
606
681
  }, { options, debugMessages }));
@@ -620,17 +695,17 @@ async function setupScreenshotCapture({ Page, Runtime }, contextId, { options, d
620
695
  }
621
696
  }
622
697
 
623
- async function setupPdfCapture({ Page, Runtime }, contextId, { options, debugMessages }) {
624
- await Runtime.addBinding({ name: PRINT_TO_PDF_FUNCTION_NAME, executionContextId: contextId });
698
+ async function setupPdfCapture({ Page, Runtime }, { options, debugMessages }) {
699
+ await Runtime.addBinding({ name: PRINT_TO_PDF_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME });
625
700
  Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ignoringErrors(async ({ params }) => {
626
701
  if (params.name === PRINT_TO_PDF_FUNCTION_NAME) {
627
702
  logData(["Printing to PDF", options.embedPdfOptions || ""], { options, debugMessages });
628
703
  const pdfOptions = parsePdfOptions(options.embedPdfOptions);
629
704
  try {
630
705
  const { data } = await Page.printToPDF(pdfOptions);
631
- await callBrowserFunction({ Runtime }, contextId, SET_PDF_FUNCTION_NAME, [data]);
706
+ await callBrowserFunction({ Runtime }, params.executionContextId, SET_PDF_FUNCTION_NAME, [data]);
632
707
  } catch {
633
- await callBrowserFunction({ Runtime }, contextId, SET_PDF_FUNCTION_NAME, [""]);
708
+ await callBrowserFunction({ Runtime }, params.executionContextId, SET_PDF_FUNCTION_NAME, [""]);
634
709
  }
635
710
  }
636
711
  }, { options, debugMessages }));
@@ -648,12 +723,23 @@ async function setupPdfCapture({ Page, Runtime }, contextId, { options, debugMes
648
723
  }
649
724
  }
650
725
 
651
- async function handleFetchRequest({ Runtime }, params, contextId, { options, debugMessages, fetchAbortController }) {
652
- const { payload } = params;
726
+ async function handleFetchRequest({ Runtime }, params, { options, debugMessages, blockedURLPatterns, fetchAbortController }, sessionId) {
727
+ const BLOCKED_URL_ERROR_MESSAGE = "Blocked URL";
728
+ const { executionContextId: contextId, payload } = params;
653
729
  const { requestId, url, options: fetchOptions } = JSON.parse(payload);
654
730
  logData(["Fetching URL", url], { options, debugMessages });
655
731
  try {
656
- const response = await fetch(url, Object.assign({}, fetchOptions, { signal: fetchAbortController.signal }));
732
+ // this fetch does not go through the browser, so the URL blocking and
733
+ // the extra HTTP headers must be applied here too
734
+ if (blockedURLPatterns.some(pattern => pattern.test(url))) {
735
+ logData(["Blocking request", url], { options, debugMessages });
736
+ throw new Error(BLOCKED_URL_ERROR_MESSAGE);
737
+ }
738
+ const headers = Object.assign({}, fetchOptions.headers, options.httpHeaders);
739
+ if (options.userAgent) {
740
+ headers["user-agent"] = options.userAgent;
741
+ }
742
+ const response = await fetch(url, Object.assign({}, fetchOptions, { headers, signal: fetchAbortController.signal }));
657
743
  const arrayBuffer = await response.arrayBuffer();
658
744
  const base64Data = arrayBufferToBase64(arrayBuffer);
659
745
  const result = {
@@ -661,22 +747,22 @@ async function handleFetchRequest({ Runtime }, params, contextId, { options, deb
661
747
  headers: Object.fromEntries(response.headers.entries()),
662
748
  data: base64Data
663
749
  };
664
- await callBrowserFunction({ Runtime }, contextId, RESOLVE_FETCH_FUNCTION_NAME, [requestId, result]);
750
+ await callBrowserFunction({ Runtime }, contextId, RESOLVE_FETCH_FUNCTION_NAME, [requestId, result], sessionId);
665
751
  } catch (error) {
666
752
  const errorResult = {
667
753
  error: error.message,
668
754
  code: error.code
669
755
  };
670
- await callBrowserFunction({ Runtime }, contextId, REJECT_FETCH_FUNCTION_NAME, [requestId, errorResult]);
756
+ await callBrowserFunction({ Runtime }, contextId, REJECT_FETCH_FUNCTION_NAME, [requestId, errorResult], sessionId);
671
757
  }
672
758
  }
673
759
 
674
- async function callBrowserFunction({ Runtime }, contextId, functionName, args) {
760
+ async function callBrowserFunction({ Runtime }, contextId, functionName, args, sessionId) {
675
761
  const serializedArgs = args.map(arg => JSON.stringify(arg)).join(", ");
676
762
  await Runtime.evaluate({
677
763
  expression: `globalThis.${functionName}(${serializedArgs})`,
678
764
  contextId
679
- });
765
+ }, sessionId);
680
766
  }
681
767
 
682
768
  async function capturePageData({ Runtime }, contextId, { options, debugMessages }) {