single-file-cli 2.3.0 → 2.4.0
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/README.MD +9 -0
- package/build.sh +4 -5
- package/deno.json +1 -1
- package/dev-build.sh +9 -13
- package/lib/browser.js +150 -17
- package/lib/cdp-client.js +133 -57
- package/lib/constants.js +0 -1
- package/lib/deno-polyfill.js +42 -0
- package/lib/single-file-archive.js +10 -6
- package/lib/single-file-bundle.js +1 -1
- package/lib/version.js +1 -1
- package/options.js +14 -2
- package/package.json +1 -1
- package/single-file-cli-api.js +7 -1
- package/single-file-launcher.js +14 -2
- package/test/e2e/automation-detection.test.js +103 -0
- package/test/e2e/browser-profile.test.js +67 -0
- package/test/e2e/oopif.test.js +86 -0
- package/test/unit/archive-packager.test.js +1 -2
- package/test/unit/browser-profile.test.js +70 -0
- package/test/unit/cli-usage.test.js +15 -0
- package/lib/archive-packager.js +0 -228
package/lib/cdp-client.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import {
|
|
27
27
|
launchBrowser,
|
|
28
|
+
getBrowserOptions,
|
|
28
29
|
closeBrowser,
|
|
29
30
|
browserExited
|
|
30
31
|
} from "./browser.js";
|
|
@@ -59,6 +60,7 @@ const SET_PDF_FUNCTION_NAME = "setPDF";
|
|
|
59
60
|
const SET_PAGE_DATA_FUNCTION_NAME = "setPageData";
|
|
60
61
|
const BINDING_CALLED_EVENT_TYPE = "bindingCalled";
|
|
61
62
|
const LOCALHOST = "http://localhost:";
|
|
63
|
+
const HEADLESS_USER_AGENT_TOKEN = "Headless";
|
|
62
64
|
const BROWSER_EXITED_MAX_DELAY = 2000;
|
|
63
65
|
|
|
64
66
|
let browserOptions, relaunchBrowserPromise;
|
|
@@ -73,17 +75,7 @@ async function initialize(singleFileOptions) {
|
|
|
73
75
|
if (singleFileOptions.browserServer) {
|
|
74
76
|
options.apiUrl = singleFileOptions.browserServer;
|
|
75
77
|
} else {
|
|
76
|
-
browserOptions =
|
|
77
|
-
browserOptions.args = singleFileOptions.browserArgs;
|
|
78
|
-
browserOptions.headless = singleFileOptions.browserHeadless;
|
|
79
|
-
browserOptions.executablePath = singleFileOptions.browserExecutablePath;
|
|
80
|
-
browserOptions.debug = singleFileOptions.browserDebug;
|
|
81
|
-
browserOptions.singleProcess = singleFileOptions.browserSingleProcess;
|
|
82
|
-
browserOptions.disableWebSecurity = singleFileOptions.browserDisableWebSecurity;
|
|
83
|
-
browserOptions.width = singleFileOptions.browserWidth;
|
|
84
|
-
browserOptions.height = singleFileOptions.browserHeight;
|
|
85
|
-
browserOptions.userAgent = singleFileOptions.userAgent;
|
|
86
|
-
browserOptions.httpProxyServer = singleFileOptions.httpProxyServer;
|
|
78
|
+
browserOptions = getBrowserOptions(singleFileOptions);
|
|
87
79
|
options.apiUrl = LOCALHOST + (await launchBrowser(browserOptions));
|
|
88
80
|
}
|
|
89
81
|
}
|
|
@@ -102,7 +94,10 @@ function relaunchBrowser() {
|
|
|
102
94
|
|
|
103
95
|
async function getPageData(options) {
|
|
104
96
|
const EMPTY_PAGE_URL = "about:blank";
|
|
105
|
-
|
|
97
|
+
// compiled here so that an invalid pattern is reported before the page is
|
|
98
|
+
// loaded, instead of throwing for every request that is intercepted
|
|
99
|
+
const blockedURLPatterns = (options.blockedURLPatterns || []).map(pattern => new RegExp(pattern));
|
|
100
|
+
const pageContext = { options, consoleMessages: [], debugMessages: [], httpInfo: {}, blockedURLPatterns, fetchAbortController: new AbortController() };
|
|
106
101
|
let targetInfo, cdp;
|
|
107
102
|
try {
|
|
108
103
|
logData(["Loading page", EMPTY_PAGE_URL], pageContext);
|
|
@@ -205,23 +200,24 @@ async function setupBrowserWindow({ Browser }, targetId, { options, debugMessage
|
|
|
205
200
|
}
|
|
206
201
|
}
|
|
207
202
|
|
|
208
|
-
async function setupSecurity({ Security }, { options, debugMessages }) {
|
|
203
|
+
async function setupSecurity({ Security }, { options, debugMessages }, sessionId) {
|
|
209
204
|
if (options.browserIgnoreHTTPSErrors !== undefined && options.browserIgnoreHTTPSErrors) {
|
|
210
205
|
logData(["Ignoring HTTPS errors"], { options, debugMessages });
|
|
211
|
-
await Security.setIgnoreCertificateErrors({ ignore: true });
|
|
206
|
+
await Security.setIgnoreCertificateErrors({ ignore: true }, sessionId);
|
|
212
207
|
}
|
|
213
208
|
}
|
|
214
209
|
|
|
215
210
|
async function setupDeviceEmulation({ Browser, Emulation, Runtime }, { options, debugMessages }) {
|
|
216
211
|
const needsDeviceMetrics = options.browserMobileEmulation || options.browserDeviceWidth ||
|
|
217
212
|
options.browserDeviceHeight || options.browserDeviceScaleFactor;
|
|
218
|
-
const needsUserAgent = options.browserMobileEmulation || options.platform || options.acceptLanguage;
|
|
219
213
|
if (needsDeviceMetrics) {
|
|
220
214
|
await setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessages });
|
|
221
215
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
216
|
+
await setupUserAgent({ Browser, Emulation }, { options, debugMessages });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function needsUserAgentOverride(options) {
|
|
220
|
+
return Boolean(options.browserMobileEmulation || options.platform || options.acceptLanguage);
|
|
225
221
|
}
|
|
226
222
|
|
|
227
223
|
async function setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessages }) {
|
|
@@ -244,12 +240,12 @@ async function setupDeviceMetrics({ Emulation, Runtime }, { options, debugMessag
|
|
|
244
240
|
await Emulation.setDeviceMetricsOverride(deviceMetricsOptions);
|
|
245
241
|
}
|
|
246
242
|
|
|
247
|
-
async function setupUserAgent({ Browser, Emulation }, { options, debugMessages }) {
|
|
243
|
+
async function setupUserAgent({ Browser, Emulation }, { options, debugMessages }, sessionId) {
|
|
248
244
|
const ANDROID_PLATFORM = "Android";
|
|
249
245
|
const { userAgent, product } = await Browser.getVersion();
|
|
250
|
-
const defaultMobileUA = `Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) ${product} Mobile Safari/537.36`;
|
|
246
|
+
const defaultMobileUA = `Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) ${removeHeadlessToken(product)} Mobile Safari/537.36`;
|
|
251
247
|
const agentOptions = {
|
|
252
|
-
userAgent: options.userAgent || (options.browserMobileEmulation ? defaultMobileUA : userAgent)
|
|
248
|
+
userAgent: options.userAgent || (options.browserMobileEmulation ? defaultMobileUA : removeHeadlessToken(userAgent))
|
|
253
249
|
};
|
|
254
250
|
if (options.acceptLanguage) {
|
|
255
251
|
agentOptions.acceptLanguage = options.acceptLanguage;
|
|
@@ -257,23 +253,24 @@ async function setupUserAgent({ Browser, Emulation }, { options, debugMessages }
|
|
|
257
253
|
if (options.platform || options.browserMobileEmulation) {
|
|
258
254
|
agentOptions.platform = options.platform || ANDROID_PLATFORM;
|
|
259
255
|
}
|
|
260
|
-
|
|
261
|
-
|
|
256
|
+
if (needsUserAgentOverride(options) || agentOptions.userAgent !== userAgent) {
|
|
257
|
+
logData(["Emulating user agent", JSON.stringify(agentOptions)], { options, debugMessages });
|
|
258
|
+
await Emulation.setUserAgentOverride(agentOptions, sessionId);
|
|
259
|
+
}
|
|
262
260
|
}
|
|
263
261
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
262
|
+
function removeHeadlessToken(userAgent) {
|
|
263
|
+
return userAgent.replace(HEADLESS_USER_AGENT_TOKEN, "");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function setupNetworkInterception({ Browser, Emulation, Fetch, Network }, { options, debugMessages, httpInfo, blockedURLPatterns }) {
|
|
267
267
|
const DENY_BEHAVIOR = "deny";
|
|
268
|
-
const handleAuthRequests =
|
|
269
|
-
const patterns = handleAuthRequests ?
|
|
270
|
-
[{ requestStage: REQUEST_STAGE }, { requestStage: RESPONSE_STAGE }] :
|
|
271
|
-
[{ requestStage: RESPONSE_STAGE }];
|
|
268
|
+
const { handleAuthRequests, patterns } = getInterceptionOptions(options);
|
|
272
269
|
await Fetch.enable({ handleAuthRequests, patterns });
|
|
273
270
|
if (handleAuthRequests) {
|
|
274
271
|
setupProxyAuth({ Fetch }, { options, debugMessages });
|
|
275
272
|
}
|
|
276
|
-
setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo });
|
|
273
|
+
setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo, blockedURLPatterns });
|
|
277
274
|
if (options.httpHeaders) {
|
|
278
275
|
await setupHttpHeaders({ Network }, { options, debugMessages });
|
|
279
276
|
}
|
|
@@ -286,10 +283,20 @@ async function setupNetworkInterception({ Browser, Emulation, Fetch, Network },
|
|
|
286
283
|
await Browser.setDownloadBehavior({ behavior: DENY_BEHAVIOR });
|
|
287
284
|
}
|
|
288
285
|
|
|
286
|
+
function getInterceptionOptions(options) {
|
|
287
|
+
const REQUEST_STAGE = "Request";
|
|
288
|
+
const RESPONSE_STAGE = "Response";
|
|
289
|
+
const handleAuthRequests = Boolean(options.httpProxyUsername);
|
|
290
|
+
const patterns = handleAuthRequests ?
|
|
291
|
+
[{ requestStage: REQUEST_STAGE }, { requestStage: RESPONSE_STAGE }] :
|
|
292
|
+
[{ requestStage: RESPONSE_STAGE }];
|
|
293
|
+
return { handleAuthRequests, patterns };
|
|
294
|
+
}
|
|
295
|
+
|
|
289
296
|
function setupProxyAuth({ Fetch }, { options, debugMessages }) {
|
|
290
297
|
const AUTH_REQUIRED_EVENT_TYPE = "authRequired";
|
|
291
298
|
const PROVIDE_CREDENTIALS_RESPONSE = "ProvideCredentials";
|
|
292
|
-
Fetch.addEventListener(AUTH_REQUIRED_EVENT_TYPE, ignoringErrors(async ({ params }) => {
|
|
299
|
+
Fetch.addEventListener(AUTH_REQUIRED_EVENT_TYPE, ignoringErrors(async ({ params, sessionId }) => {
|
|
293
300
|
logData(["Authenticating"], { options, debugMessages });
|
|
294
301
|
await Fetch.continueWithAuth({
|
|
295
302
|
requestId: params.requestId,
|
|
@@ -298,38 +305,37 @@ function setupProxyAuth({ Fetch }, { options, debugMessages }) {
|
|
|
298
305
|
username: options.httpProxyUsername,
|
|
299
306
|
password: options.httpProxyPassword
|
|
300
307
|
}
|
|
301
|
-
});
|
|
308
|
+
}, sessionId);
|
|
302
309
|
}, { options, debugMessages }));
|
|
303
310
|
}
|
|
304
311
|
|
|
305
|
-
function setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo }) {
|
|
312
|
+
function setupRequestInterception({ Fetch }, { options, debugMessages, httpInfo, blockedURLPatterns }) {
|
|
306
313
|
const REQUEST_PAUSED_EVENT_TYPE = "requestPaused";
|
|
307
314
|
const ABORTED_ERROR_REASON = "Aborted";
|
|
308
315
|
const urlState = { url: options.url, alternativeUrl: getAlternativeUrl(options.url) };
|
|
309
|
-
|
|
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 }) => {
|
|
316
|
+
Fetch.addEventListener(REQUEST_PAUSED_EVENT_TYPE, ignoringErrors(async ({ params, sessionId }) => {
|
|
313
317
|
const { requestId, request } = params;
|
|
314
318
|
// the request is always resumed below, otherwise the page waits for it
|
|
315
319
|
// until the load timeout expires
|
|
316
320
|
let blocked = false;
|
|
317
321
|
try {
|
|
318
|
-
|
|
322
|
+
if (!sessionId) {
|
|
323
|
+
captureHttpInfo(params, urlState, { options, debugMessages, httpInfo });
|
|
324
|
+
}
|
|
319
325
|
blocked = shouldBlockRequest(request.url);
|
|
320
326
|
} catch (error) {
|
|
321
327
|
logData(["Ignoring request interception error", error.message], { options, debugMessages });
|
|
322
328
|
}
|
|
323
329
|
if (blocked) {
|
|
324
330
|
try {
|
|
325
|
-
await Fetch.failRequest({ requestId, errorReason: ABORTED_ERROR_REASON });
|
|
331
|
+
await Fetch.failRequest({ requestId, errorReason: ABORTED_ERROR_REASON }, sessionId);
|
|
326
332
|
return;
|
|
327
333
|
} catch {
|
|
328
334
|
// ignored
|
|
329
335
|
}
|
|
330
336
|
}
|
|
331
337
|
try {
|
|
332
|
-
await Fetch.continueRequest({ requestId });
|
|
338
|
+
await Fetch.continueRequest({ requestId }, sessionId);
|
|
333
339
|
} catch {
|
|
334
340
|
// ignored
|
|
335
341
|
}
|
|
@@ -381,13 +387,13 @@ function captureHttpInfo(params, urlState, { options, debugMessages, httpInfo })
|
|
|
381
387
|
}
|
|
382
388
|
}
|
|
383
389
|
|
|
384
|
-
async function setupHttpHeaders({ Network }, { options, debugMessages }) {
|
|
390
|
+
async function setupHttpHeaders({ Network }, { options, debugMessages }, sessionId) {
|
|
385
391
|
logData(["Setting HTTP headers", JSON.stringify(options.httpHeaders)], { options, debugMessages });
|
|
386
|
-
await Network.enable();
|
|
387
|
-
await Network.setExtraHTTPHeaders({ headers: options.httpHeaders });
|
|
392
|
+
await Network.enable({}, sessionId);
|
|
393
|
+
await Network.setExtraHTTPHeaders({ headers: options.httpHeaders }, sessionId);
|
|
388
394
|
}
|
|
389
395
|
|
|
390
|
-
async function setupMediaFeatures({ Emulation }, { options, debugMessages }) {
|
|
396
|
+
async function setupMediaFeatures({ Emulation }, { options, debugMessages }, sessionId) {
|
|
391
397
|
const features = [];
|
|
392
398
|
for (const mediaFeature of options.emulateMediaFeatures) {
|
|
393
399
|
logData(["Emulating media feature", mediaFeature.name, mediaFeature.value], { options, debugMessages });
|
|
@@ -395,7 +401,7 @@ async function setupMediaFeatures({ Emulation }, { options, debugMessages }) {
|
|
|
395
401
|
features.push({ name: mediaFeature.name, value: value.trim() });
|
|
396
402
|
}
|
|
397
403
|
}
|
|
398
|
-
await Emulation.setEmulatedMedia({ features });
|
|
404
|
+
await Emulation.setEmulatedMedia({ features }, sessionId);
|
|
399
405
|
}
|
|
400
406
|
|
|
401
407
|
async function setupCookies({ Network }, { options, debugMessages }) {
|
|
@@ -403,16 +409,75 @@ async function setupCookies({ Network }, { options, debugMessages }) {
|
|
|
403
409
|
await Network.setCookies({ cookies: options.browserCookies });
|
|
404
410
|
}
|
|
405
411
|
|
|
406
|
-
async function setupScriptInjection(
|
|
412
|
+
async function setupScriptInjection(cdp, { options, debugMessages }) {
|
|
413
|
+
const { Page } = cdp;
|
|
414
|
+
const scriptSource = await getScriptSource(options);
|
|
407
415
|
await Page.addScriptToEvaluateOnNewDocument({
|
|
408
416
|
source: getHookScriptSource(),
|
|
409
417
|
runImmediately: true
|
|
410
418
|
});
|
|
411
419
|
await Page.addScriptToEvaluateOnNewDocument({
|
|
412
|
-
source:
|
|
420
|
+
source: scriptSource,
|
|
413
421
|
runImmediately: true,
|
|
414
422
|
worldName: SINGLE_FILE_WORLD_NAME
|
|
415
423
|
});
|
|
424
|
+
await setupFrameScriptInjection(cdp, scriptSource, { options, debugMessages });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// out-of-process frames are separate targets, so the scripts registered above
|
|
428
|
+
// do not reach them; they are attached paused, injected and resumed instead
|
|
429
|
+
async function setupFrameScriptInjection(cdp, scriptSource, { options, debugMessages }) {
|
|
430
|
+
const { Page, Runtime, Target } = cdp;
|
|
431
|
+
const ATTACHED_TO_TARGET_EVENT_TYPE = "attachedToTarget";
|
|
432
|
+
const IFRAME_TARGET_TYPE = "iframe";
|
|
433
|
+
const autoAttachOptions = { autoAttach: true, waitForDebuggerOnStart: true, flatten: true };
|
|
434
|
+
Target.addEventListener(ATTACHED_TO_TARGET_EVENT_TYPE, ignoringErrors(async ({ params }) => {
|
|
435
|
+
const { sessionId, targetInfo, waitingForDebugger } = params;
|
|
436
|
+
try {
|
|
437
|
+
if (targetInfo.type === IFRAME_TARGET_TYPE) {
|
|
438
|
+
logData(["Injecting scripts into frame", targetInfo.url], { options, debugMessages });
|
|
439
|
+
// the scripts registered below only run when the Page domain is
|
|
440
|
+
// enabled on the session
|
|
441
|
+
await Page.enable({}, sessionId);
|
|
442
|
+
await Target.setAutoAttach(autoAttachOptions, sessionId);
|
|
443
|
+
await Page.addScriptToEvaluateOnNewDocument({
|
|
444
|
+
source: getHookScriptSource(),
|
|
445
|
+
runImmediately: true
|
|
446
|
+
}, sessionId);
|
|
447
|
+
await Page.addScriptToEvaluateOnNewDocument({
|
|
448
|
+
source: scriptSource,
|
|
449
|
+
runImmediately: true,
|
|
450
|
+
worldName: SINGLE_FILE_WORLD_NAME
|
|
451
|
+
}, sessionId);
|
|
452
|
+
await Runtime.addBinding({ name: FETCH_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME }, sessionId);
|
|
453
|
+
await setupFrameSession(cdp, sessionId, { options, debugMessages });
|
|
454
|
+
}
|
|
455
|
+
} finally {
|
|
456
|
+
// the target is always resumed, even when a setup command fails,
|
|
457
|
+
// otherwise it would stay paused until the load timeout expires
|
|
458
|
+
if (waitingForDebugger) {
|
|
459
|
+
await Runtime.runIfWaitingForDebugger({}, sessionId);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}, { options, debugMessages }));
|
|
463
|
+
await Target.setAutoAttach(autoAttachOptions);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function setupFrameSession({ Browser, Emulation, Fetch, Network, Security }, sessionId, { options, debugMessages }) {
|
|
467
|
+
const { handleAuthRequests, patterns } = getInterceptionOptions(options);
|
|
468
|
+
if (handleAuthRequests || (options.blockedURLPatterns && options.blockedURLPatterns.length)) {
|
|
469
|
+
// the requestPaused and authRequired listeners are registered once on
|
|
470
|
+
// the connection and receive the events of every session
|
|
471
|
+
await Fetch.enable({ handleAuthRequests, patterns }, sessionId);
|
|
472
|
+
}
|
|
473
|
+
await setupSecurity({ Security }, { options, debugMessages }, sessionId);
|
|
474
|
+
if (options.httpHeaders) {
|
|
475
|
+
await setupHttpHeaders({ Network }, { options, debugMessages }, sessionId);
|
|
476
|
+
}
|
|
477
|
+
if (options.emulateMediaFeatures) {
|
|
478
|
+
await setupMediaFeatures({ Emulation }, { options, debugMessages }, sessionId);
|
|
479
|
+
}
|
|
480
|
+
await setupUserAgent({ Browser, Emulation }, { options, debugMessages }, sessionId);
|
|
416
481
|
}
|
|
417
482
|
|
|
418
483
|
async function getContextId({ Debugger, Page, Runtime }, { options, debugMessages }) {
|
|
@@ -578,7 +643,7 @@ function setupPageDataCapture({ Runtime }, { options, debugMessages }) {
|
|
|
578
643
|
});
|
|
579
644
|
}
|
|
580
645
|
|
|
581
|
-
async function setupBindings({ Page, Runtime }, { options, debugMessages, fetchAbortController }) {
|
|
646
|
+
async function setupBindings({ Page, Runtime }, { options, debugMessages, blockedURLPatterns, fetchAbortController }) {
|
|
582
647
|
await Runtime.addBinding({ name: SET_PAGE_DATA_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME });
|
|
583
648
|
if (options.embedScreenshot && options.compressContent) {
|
|
584
649
|
await setupScreenshotCapture({ Page, Runtime }, { options, debugMessages });
|
|
@@ -587,9 +652,9 @@ async function setupBindings({ Page, Runtime }, { options, debugMessages, fetchA
|
|
|
587
652
|
await setupPdfCapture({ Page, Runtime }, { options, debugMessages });
|
|
588
653
|
}
|
|
589
654
|
await Runtime.addBinding({ name: FETCH_FUNCTION_NAME, executionContextName: SINGLE_FILE_WORLD_NAME });
|
|
590
|
-
Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ignoringErrors(async ({ params }) => {
|
|
655
|
+
Runtime.addEventListener(BINDING_CALLED_EVENT_TYPE, ignoringErrors(async ({ params, sessionId }) => {
|
|
591
656
|
if (params.name === FETCH_FUNCTION_NAME) {
|
|
592
|
-
await handleFetchRequest({ Runtime }, params, { options, debugMessages, fetchAbortController });
|
|
657
|
+
await handleFetchRequest({ Runtime }, params, { options, debugMessages, blockedURLPatterns, fetchAbortController }, sessionId);
|
|
593
658
|
}
|
|
594
659
|
}, { options, debugMessages }));
|
|
595
660
|
}
|
|
@@ -652,12 +717,23 @@ async function setupPdfCapture({ Page, Runtime }, { options, debugMessages }) {
|
|
|
652
717
|
}
|
|
653
718
|
}
|
|
654
719
|
|
|
655
|
-
async function handleFetchRequest({ Runtime }, params, { options, debugMessages, fetchAbortController }) {
|
|
720
|
+
async function handleFetchRequest({ Runtime }, params, { options, debugMessages, blockedURLPatterns, fetchAbortController }, sessionId) {
|
|
721
|
+
const BLOCKED_URL_ERROR_MESSAGE = "Blocked URL";
|
|
656
722
|
const { executionContextId: contextId, payload } = params;
|
|
657
723
|
const { requestId, url, options: fetchOptions } = JSON.parse(payload);
|
|
658
724
|
logData(["Fetching URL", url], { options, debugMessages });
|
|
659
725
|
try {
|
|
660
|
-
|
|
726
|
+
// this fetch does not go through the browser, so the URL blocking and
|
|
727
|
+
// the extra HTTP headers must be applied here too
|
|
728
|
+
if (blockedURLPatterns.some(pattern => pattern.test(url))) {
|
|
729
|
+
logData(["Blocking request", url], { options, debugMessages });
|
|
730
|
+
throw new Error(BLOCKED_URL_ERROR_MESSAGE);
|
|
731
|
+
}
|
|
732
|
+
const headers = Object.assign({}, fetchOptions.headers, options.httpHeaders);
|
|
733
|
+
if (options.userAgent) {
|
|
734
|
+
headers["user-agent"] = options.userAgent;
|
|
735
|
+
}
|
|
736
|
+
const response = await fetch(url, Object.assign({}, fetchOptions, { headers, signal: fetchAbortController.signal }));
|
|
661
737
|
const arrayBuffer = await response.arrayBuffer();
|
|
662
738
|
const base64Data = arrayBufferToBase64(arrayBuffer);
|
|
663
739
|
const result = {
|
|
@@ -665,22 +741,22 @@ async function handleFetchRequest({ Runtime }, params, { options, debugMessages,
|
|
|
665
741
|
headers: Object.fromEntries(response.headers.entries()),
|
|
666
742
|
data: base64Data
|
|
667
743
|
};
|
|
668
|
-
await callBrowserFunction({ Runtime }, contextId, RESOLVE_FETCH_FUNCTION_NAME, [requestId, result]);
|
|
744
|
+
await callBrowserFunction({ Runtime }, contextId, RESOLVE_FETCH_FUNCTION_NAME, [requestId, result], sessionId);
|
|
669
745
|
} catch (error) {
|
|
670
746
|
const errorResult = {
|
|
671
747
|
error: error.message,
|
|
672
748
|
code: error.code
|
|
673
749
|
};
|
|
674
|
-
await callBrowserFunction({ Runtime }, contextId, REJECT_FETCH_FUNCTION_NAME, [requestId, errorResult]);
|
|
750
|
+
await callBrowserFunction({ Runtime }, contextId, REJECT_FETCH_FUNCTION_NAME, [requestId, errorResult], sessionId);
|
|
675
751
|
}
|
|
676
752
|
}
|
|
677
753
|
|
|
678
|
-
async function callBrowserFunction({ Runtime }, contextId, functionName, args) {
|
|
754
|
+
async function callBrowserFunction({ Runtime }, contextId, functionName, args, sessionId) {
|
|
679
755
|
const serializedArgs = args.map(arg => JSON.stringify(arg)).join(", ");
|
|
680
756
|
await Runtime.evaluate({
|
|
681
757
|
expression: `globalThis.${functionName}(${serializedArgs})`,
|
|
682
758
|
contextId
|
|
683
|
-
});
|
|
759
|
+
}, sessionId);
|
|
684
760
|
}
|
|
685
761
|
|
|
686
762
|
async function capturePageData({ Runtime }, contextId, { options, debugMessages }) {
|
package/lib/constants.js
CHANGED
package/lib/deno-polyfill.js
CHANGED
|
@@ -101,8 +101,10 @@ const DenoAPI = {
|
|
|
101
101
|
args,
|
|
102
102
|
readFile,
|
|
103
103
|
readTextFile,
|
|
104
|
+
readDir,
|
|
104
105
|
writeTextFile,
|
|
105
106
|
writeFile,
|
|
107
|
+
copyFile,
|
|
106
108
|
mkdir,
|
|
107
109
|
makeTempDir,
|
|
108
110
|
stat,
|
|
@@ -118,6 +120,7 @@ const DenoAPI = {
|
|
|
118
120
|
|
|
119
121
|
const pathAPI = {
|
|
120
122
|
dirname,
|
|
123
|
+
join,
|
|
121
124
|
toFileUrl,
|
|
122
125
|
fromFileUrl,
|
|
123
126
|
SEPARATOR: DENO_RUNTIME_DETECTED ? path.SEPARATOR : path.sep
|
|
@@ -154,6 +157,41 @@ async function readTextFile(path) {
|
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
159
|
|
|
160
|
+
async function readDir(path) {
|
|
161
|
+
if (DENO_RUNTIME_DETECTED) {
|
|
162
|
+
const entries = [];
|
|
163
|
+
try {
|
|
164
|
+
for await (const entry of Deno.readDir(path)) {
|
|
165
|
+
entries.push({ name: entry.name, isDirectory: entry.isDirectory, isSymlink: entry.isSymlink });
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw mapNotFoundError(error);
|
|
169
|
+
}
|
|
170
|
+
return entries;
|
|
171
|
+
} else {
|
|
172
|
+
const fsPromise = await import(NODE_MODULES["fs"]);
|
|
173
|
+
try {
|
|
174
|
+
const entries = await fsPromise.readdir(path, { withFileTypes: true });
|
|
175
|
+
return entries.map(entry => ({ name: entry.name, isDirectory: entry.isDirectory(), isSymlink: entry.isSymbolicLink() }));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw mapNotFoundError(error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function copyFile(sourcePath, destinationPath) {
|
|
183
|
+
if (DENO_RUNTIME_DETECTED) {
|
|
184
|
+
return Deno.copyFile(sourcePath, destinationPath);
|
|
185
|
+
} else {
|
|
186
|
+
const fsPromise = await import(NODE_MODULES["fs"]);
|
|
187
|
+
return fsPromise.copyFile(sourcePath, destinationPath);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function mapNotFoundError(error) {
|
|
192
|
+
return error.code == "ENOENT" || error.code == "ENOTDIR" ? new errors.NotFound(error.message) : error;
|
|
193
|
+
}
|
|
194
|
+
|
|
157
195
|
async function writeTextFile(path, data, options = {}) {
|
|
158
196
|
if (DENO_RUNTIME_DETECTED) {
|
|
159
197
|
return Deno.writeTextFile(path, data, options);
|
|
@@ -259,6 +297,10 @@ function dirname(filePath) {
|
|
|
259
297
|
return path.dirname(filePath);
|
|
260
298
|
}
|
|
261
299
|
|
|
300
|
+
function join(...filePaths) {
|
|
301
|
+
return path.join(...filePaths);
|
|
302
|
+
}
|
|
303
|
+
|
|
262
304
|
async function toFileUrl(filePath) {
|
|
263
305
|
if (DENO_RUNTIME_DETECTED) {
|
|
264
306
|
return path.toFileUrl(filePath);
|