single-file-cli 2.3.0 → 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/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.95"
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.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "SingleFile CLI",
5
5
  "exports": {
6
6
  ".": "./single-file-cli-api.js"
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);
@@ -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 }) {
@@ -578,7 +649,7 @@ function setupPageDataCapture({ Runtime }, { options, debugMessages }) {
578
649
  });
579
650
  }
580
651
 
581
- async function setupBindings({ Page, Runtime }, { options, debugMessages, fetchAbortController }) {
652
+ async function setupBindings({ Page, Runtime }, { options, debugMessages, blockedURLPatterns, fetchAbortController }) {
582
653
  await Runtime.addBinding({ name: SET_PAGE_DATA_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME });
583
654
  if (options.embedScreenshot && options.compressContent) {
584
655
  await setupScreenshotCapture({ Page, Runtime }, { options, debugMessages });
@@ -587,9 +658,9 @@ async function setupBindings({ Page, Runtime }, { options, debugMessages, fetchA
587
658
  await setupPdfCapture({ Page, Runtime }, { options, debugMessages });
588
659
  }
589
660
  await Runtime.addBinding({ name: FETCH_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME });
590
- Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ignoringErrors(async ({ params }) => {
661
+ Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ignoringErrors(async ({ params, sessionId }) => {
591
662
  if (params.name === FETCH_FUNCTION_NAME) {
592
- await handleFetchRequest({ Runtime }, params, { options, debugMessages, fetchAbortController });
663
+ await handleFetchRequest({ Runtime }, params, { options, debugMessages, blockedURLPatterns, fetchAbortController }, sessionId);
593
664
  }
594
665
  }, { options, debugMessages }));
595
666
  }
@@ -652,12 +723,23 @@ async function setupPdfCapture({ Page, Runtime }, { options, debugMessages }) {
652
723
  }
653
724
  }
654
725
 
655
- async function handleFetchRequest({ Runtime }, params, { options, debugMessages, fetchAbortController }) {
726
+ async function handleFetchRequest({ Runtime }, params, { options, debugMessages, blockedURLPatterns, fetchAbortController }, sessionId) {
727
+ const BLOCKED_URL_ERROR_MESSAGE = "Blocked URL";
656
728
  const { executionContextId: contextId, payload } = params;
657
729
  const { requestId, url, options: fetchOptions } = JSON.parse(payload);
658
730
  logData(["Fetching URL", url], { options, debugMessages });
659
731
  try {
660
- 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 }));
661
743
  const arrayBuffer = await response.arrayBuffer();
662
744
  const base64Data = arrayBufferToBase64(arrayBuffer);
663
745
  const result = {
@@ -665,22 +747,22 @@ async function handleFetchRequest({ Runtime }, params, { options, debugMessages,
665
747
  headers: Object.fromEntries(response.headers.entries()),
666
748
  data: base64Data
667
749
  };
668
- await callBrowserFunction({ Runtime }, contextId, RESOLVE_FETCH_FUNCTION_NAME, [requestId, result]);
750
+ await callBrowserFunction({ Runtime }, contextId, RESOLVE_FETCH_FUNCTION_NAME, [requestId, result], sessionId);
669
751
  } catch (error) {
670
752
  const errorResult = {
671
753
  error: error.message,
672
754
  code: error.code
673
755
  };
674
- await callBrowserFunction({ Runtime }, contextId, REJECT_FETCH_FUNCTION_NAME, [requestId, errorResult]);
756
+ await callBrowserFunction({ Runtime }, contextId, REJECT_FETCH_FUNCTION_NAME, [requestId, errorResult], sessionId);
675
757
  }
676
758
  }
677
759
 
678
- async function callBrowserFunction({ Runtime }, contextId, functionName, args) {
760
+ async function callBrowserFunction({ Runtime }, contextId, functionName, args, sessionId) {
679
761
  const serializedArgs = args.map(arg => JSON.stringify(arg)).join(", ");
680
762
  await Runtime.evaluate({
681
763
  expression: `globalThis.${functionName}(${serializedArgs})`,
682
764
  contextId
683
- });
765
+ }, sessionId);
684
766
  }
685
767
 
686
768
  async function capturePageData({ Runtime }, contextId, { options, debugMessages }) {