@push.rocks/smartpuppeteer 2.1.0 → 2.2.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/smartpuppeteer.classes.livebrowsersession.d.ts +23 -15
- package/dist_ts/smartpuppeteer.classes.livebrowsersession.js +619 -57
- package/dist_ts/smartpuppeteer.classes.smartpuppeteer.d.ts +1 -0
- package/dist_ts/smartpuppeteer.classes.smartpuppeteer.js +23 -4
- package/dist_ts/smartpuppeteer.interfaces.livebrowser.d.ts +24 -0
- package/package.json +2 -2
- package/readme.md +37 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartpuppeteer.classes.livebrowsersession.ts +726 -49
- package/ts/smartpuppeteer.classes.smartpuppeteer.ts +27 -4
- package/ts/smartpuppeteer.interfaces.livebrowser.ts +32 -0
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ILiveBrowserClickOptions,
|
|
4
4
|
ILiveBrowserCreateTabOptions,
|
|
5
5
|
ILiveBrowserError,
|
|
6
|
+
ILiveBrowserEvaluateOptions,
|
|
6
7
|
ILiveBrowserFillOptions,
|
|
7
8
|
ILiveBrowserFrame,
|
|
8
9
|
ILiveBrowserFrameAcknowledgement,
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
ILiveBrowserNavigationOptions,
|
|
16
17
|
ILiveBrowserObservation,
|
|
17
18
|
ILiveBrowserObserveOptions,
|
|
19
|
+
ILiveBrowserOperationOptions,
|
|
18
20
|
ILiveBrowserPressOptions,
|
|
19
21
|
ILiveBrowserSessionOptions,
|
|
20
22
|
ILiveBrowserSnapshot,
|
|
@@ -26,6 +28,7 @@ import type {
|
|
|
26
28
|
TLiveBrowserEvent,
|
|
27
29
|
TLiveBrowserEventListener,
|
|
28
30
|
TLiveBrowserImageFormat,
|
|
31
|
+
TLiveBrowserJsonValue,
|
|
29
32
|
TLiveBrowserWaitUntil,
|
|
30
33
|
} from './smartpuppeteer.interfaces.livebrowser.js';
|
|
31
34
|
import * as plugins from './smartpuppeteer.plugins.js';
|
|
@@ -47,6 +50,16 @@ const maxTimeoutMs = 60000;
|
|
|
47
50
|
const maxOutstandingFrames = 3;
|
|
48
51
|
const maxQueuedPublicOperations = 64;
|
|
49
52
|
const maxQueuedInternalOperations = 128;
|
|
53
|
+
const maxEvaluationScriptBytes = 262144;
|
|
54
|
+
const maxEvaluationOutputBytes = 1048576;
|
|
55
|
+
const maxEvaluationDepth = 32;
|
|
56
|
+
const maxEvaluationNodes = 50000;
|
|
57
|
+
const maxEvaluationStringBytes = 262144;
|
|
58
|
+
const maxEvaluationArrayLength = 10000;
|
|
59
|
+
const maxEvaluationObjectKeys = 10000;
|
|
60
|
+
const evaluationBootstrapKey = '__smartpuppeteerEvaluate';
|
|
61
|
+
const evaluationCancelKey = '__smartpuppeteerCancel';
|
|
62
|
+
const evaluationCleanupKey = '__smartpuppeteerCleanup';
|
|
50
63
|
|
|
51
64
|
type TScreencastFrameEvent = plugins.puppeteer.Protocol.Page.ScreencastFrameEvent;
|
|
52
65
|
type TScreencastFrameListener = (event: TScreencastFrameEvent) => void;
|
|
@@ -67,6 +80,8 @@ interface IPrivateLiveBrowserTab {
|
|
|
67
80
|
stateUpdateQueued: boolean;
|
|
68
81
|
stateUpdatePending: boolean;
|
|
69
82
|
navigationResetPending: boolean;
|
|
83
|
+
evaluationExecutionContextId?: number;
|
|
84
|
+
securityCdpSession?: plugins.puppeteer.CDPSession;
|
|
70
85
|
cdpSession?: plugins.puppeteer.CDPSession;
|
|
71
86
|
cdpConnection?: plugins.puppeteer.Connection;
|
|
72
87
|
screencastFrameListener?: TScreencastFrameListener;
|
|
@@ -87,14 +102,34 @@ interface IImageDimensions {
|
|
|
87
102
|
height: number;
|
|
88
103
|
}
|
|
89
104
|
|
|
105
|
+
interface INormalizedEvaluationOptions {
|
|
106
|
+
timeoutMs: number;
|
|
107
|
+
maxOutputBytes: number;
|
|
108
|
+
maxDepth: number;
|
|
109
|
+
maxNodes: number;
|
|
110
|
+
maxStringBytes: number;
|
|
111
|
+
maxArrayLength: number;
|
|
112
|
+
maxObjectKeys: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface IEvaluationEnvelope {
|
|
116
|
+
ok: boolean;
|
|
117
|
+
json?: string;
|
|
118
|
+
error?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
90
121
|
type TQueuedOperationKind = 'public' | 'internal' | 'shutdown';
|
|
122
|
+
type TQueuedOperationState = 'queued' | 'active' | 'settled';
|
|
91
123
|
|
|
92
124
|
interface IQueuedOperation {
|
|
93
125
|
kind: TQueuedOperationKind;
|
|
126
|
+
state: TQueuedOperationState;
|
|
94
127
|
controller: AbortController;
|
|
95
128
|
run: (signal: AbortSignal) => Promise<unknown>;
|
|
96
129
|
resolve: (value: unknown) => void;
|
|
97
130
|
reject: (error: unknown) => void;
|
|
131
|
+
capacityReleased: boolean;
|
|
132
|
+
removeCallerAbortListener?: () => void;
|
|
98
133
|
}
|
|
99
134
|
|
|
100
135
|
const validateBoundedString = (
|
|
@@ -163,6 +198,10 @@ const normalizeErrorMessage = (error: unknown): string => {
|
|
|
163
198
|
return truncate(String(error), 2048);
|
|
164
199
|
};
|
|
165
200
|
|
|
201
|
+
const normalizeAbortReason = (signal: AbortSignal): unknown => {
|
|
202
|
+
return signal.reason ?? new Error('The browser operation was aborted');
|
|
203
|
+
};
|
|
204
|
+
|
|
166
205
|
const readUint32 = (data: Uint8Array, offset: number): number => {
|
|
167
206
|
return (
|
|
168
207
|
data[offset]! * 0x1000000
|
|
@@ -291,6 +330,7 @@ export class LiveBrowserSession {
|
|
|
291
330
|
private viewportRevision = 1;
|
|
292
331
|
private tabSequence = 0;
|
|
293
332
|
private frameSequence = 0;
|
|
333
|
+
private evaluationSequence = 0;
|
|
294
334
|
private normalStopRequested = false;
|
|
295
335
|
private lastError?: ILiveBrowserError;
|
|
296
336
|
|
|
@@ -310,6 +350,10 @@ export class LiveBrowserSession {
|
|
|
310
350
|
if (optionsArg.launchOptions && 'signal' in optionsArg.launchOptions) {
|
|
311
351
|
throw new Error('LiveBrowserSession owns launch cancellation; launchOptions.signal is unsupported');
|
|
312
352
|
}
|
|
353
|
+
validateOptionalBoolean(optionsArg.allowEvaluation, 'allowEvaluation');
|
|
354
|
+
for (const [name, value] of Object.entries(optionsArg.security ?? {})) {
|
|
355
|
+
validateOptionalBoolean(value, `security.${name}`);
|
|
356
|
+
}
|
|
313
357
|
const launchViewport = optionsArg.launchOptions?.defaultViewport;
|
|
314
358
|
const viewport = normalizeViewport(
|
|
315
359
|
optionsArg.viewport
|
|
@@ -327,9 +371,13 @@ export class LiveBrowserSession {
|
|
|
327
371
|
...optionsArg.launchOptions,
|
|
328
372
|
args: [...(optionsArg.launchOptions?.args ?? [])],
|
|
329
373
|
defaultViewport: createPuppeteerViewport(viewport),
|
|
374
|
+
...(optionsArg.security?.denyDownloads
|
|
375
|
+
? { downloadBehavior: { policy: 'deny' as const } }
|
|
376
|
+
: {}),
|
|
330
377
|
},
|
|
331
378
|
viewport,
|
|
332
379
|
screencast: optionsArg.screencast ? { ...optionsArg.screencast } : undefined,
|
|
380
|
+
security: optionsArg.security ? { ...optionsArg.security } : undefined,
|
|
333
381
|
};
|
|
334
382
|
this.viewport = { ...viewport };
|
|
335
383
|
this.validateScreencastOptions();
|
|
@@ -356,7 +404,7 @@ export class LiveBrowserSession {
|
|
|
356
404
|
};
|
|
357
405
|
}
|
|
358
406
|
|
|
359
|
-
public async start(): Promise<void> {
|
|
407
|
+
public async start(operationOptions: ILiveBrowserOperationOptions = {}): Promise<void> {
|
|
360
408
|
return this.enqueuePublicOperation(async (signal) => {
|
|
361
409
|
if (this.status === 'running' || this.status === 'starting') {
|
|
362
410
|
return;
|
|
@@ -377,19 +425,31 @@ export class LiveBrowserSession {
|
|
|
377
425
|
if (browserLifetimeController.signal.aborted) {
|
|
378
426
|
throw browserLifetimeController.signal.reason;
|
|
379
427
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
428
|
+
const abortBrowserLaunch = (): void => {
|
|
429
|
+
if (!browserLifetimeController.signal.aborted) {
|
|
430
|
+
browserLifetimeController.abort(normalizeAbortReason(signal));
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
signal.addEventListener('abort', abortBrowserLaunch, { once: true });
|
|
434
|
+
try {
|
|
435
|
+
this.browser = await getEnvAwareBrowserInstance({
|
|
436
|
+
forceNoSandbox: this.options.forceNoSandbox,
|
|
437
|
+
requireSandbox: this.options.requireSandbox,
|
|
438
|
+
usePipe: this.options.usePipe,
|
|
439
|
+
launchOptions: {
|
|
440
|
+
...this.options.launchOptions,
|
|
441
|
+
protocol: 'cdp',
|
|
442
|
+
signal: browserLifetimeController.signal,
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
} finally {
|
|
446
|
+
signal.removeEventListener('abort', abortBrowserLaunch);
|
|
447
|
+
}
|
|
389
448
|
if (signal.aborted) {
|
|
390
449
|
throw signal.reason;
|
|
391
450
|
}
|
|
392
451
|
this.browserContext = this.browser.defaultBrowserContext();
|
|
452
|
+
await this.configureBrowserSecurity(this.browser);
|
|
393
453
|
this.browserDisconnectedListener = () => {
|
|
394
454
|
if (this.normalStopRequested || this.status === 'stopped' || this.status === 'stopping') {
|
|
395
455
|
return;
|
|
@@ -432,6 +492,9 @@ export class LiveBrowserSession {
|
|
|
432
492
|
this.status = 'running';
|
|
433
493
|
this.emitState();
|
|
434
494
|
await this.startScreencast(firstTab);
|
|
495
|
+
if (signal.aborted) {
|
|
496
|
+
throw normalizeAbortReason(signal);
|
|
497
|
+
}
|
|
435
498
|
} catch (error) {
|
|
436
499
|
if (signal.aborted || this.normalStopRequested) {
|
|
437
500
|
await this.stopInternal();
|
|
@@ -446,7 +509,7 @@ export class LiveBrowserSession {
|
|
|
446
509
|
await this.stopInternal(startError);
|
|
447
510
|
throw error;
|
|
448
511
|
}
|
|
449
|
-
});
|
|
512
|
+
}, operationOptions);
|
|
450
513
|
}
|
|
451
514
|
|
|
452
515
|
public async stop(): Promise<void> {
|
|
@@ -508,6 +571,7 @@ export class LiveBrowserSession {
|
|
|
508
571
|
|
|
509
572
|
public async createTab(
|
|
510
573
|
optionsArg: ILiveBrowserCreateTabOptions = {},
|
|
574
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
511
575
|
): Promise<ILiveBrowserTabState> {
|
|
512
576
|
const url = optionsArg.url === undefined ? undefined : this.validateUrl(optionsArg.url);
|
|
513
577
|
const activate = optionsArg.activate ?? true;
|
|
@@ -563,16 +627,22 @@ export class LiveBrowserSession {
|
|
|
563
627
|
}
|
|
564
628
|
throw error;
|
|
565
629
|
}
|
|
566
|
-
});
|
|
630
|
+
}, operationOptions);
|
|
567
631
|
}
|
|
568
632
|
|
|
569
|
-
public async activateTab(
|
|
633
|
+
public async activateTab(
|
|
634
|
+
tabId: string,
|
|
635
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
636
|
+
): Promise<void> {
|
|
570
637
|
return this.enqueuePublicOperation(async () => {
|
|
571
638
|
await this.activateTabInternal(tabId);
|
|
572
|
-
});
|
|
639
|
+
}, operationOptions);
|
|
573
640
|
}
|
|
574
641
|
|
|
575
|
-
public async closeTab(
|
|
642
|
+
public async closeTab(
|
|
643
|
+
tabId: string,
|
|
644
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
645
|
+
): Promise<void> {
|
|
576
646
|
return this.enqueuePublicOperation(async () => {
|
|
577
647
|
const tab = this.requireTab(tabId);
|
|
578
648
|
const wasActive = this.activeTabId === tab.id;
|
|
@@ -609,10 +679,13 @@ export class LiveBrowserSession {
|
|
|
609
679
|
} else {
|
|
610
680
|
this.emitState();
|
|
611
681
|
}
|
|
612
|
-
});
|
|
682
|
+
}, operationOptions);
|
|
613
683
|
}
|
|
614
684
|
|
|
615
|
-
public async navigate(
|
|
685
|
+
public async navigate(
|
|
686
|
+
optionsArg: ILiveBrowserNavigateOptions,
|
|
687
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
688
|
+
): Promise<void> {
|
|
616
689
|
const url = this.validateUrl(optionsArg.url);
|
|
617
690
|
return this.enqueuePublicOperation(async (signal) => {
|
|
618
691
|
const tab = this.resolveNavigationTab(optionsArg.tabId);
|
|
@@ -623,10 +696,13 @@ export class LiveBrowserSession {
|
|
|
623
696
|
},
|
|
624
697
|
signal,
|
|
625
698
|
);
|
|
626
|
-
});
|
|
699
|
+
}, operationOptions);
|
|
627
700
|
}
|
|
628
701
|
|
|
629
|
-
public async back(
|
|
702
|
+
public async back(
|
|
703
|
+
optionsArg: ILiveBrowserNavigationOptions = {},
|
|
704
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
705
|
+
): Promise<void> {
|
|
630
706
|
return this.enqueuePublicOperation(async (signal) => {
|
|
631
707
|
const tab = this.resolveNavigationTab(optionsArg.tabId);
|
|
632
708
|
await this.navigateTab(
|
|
@@ -636,10 +712,13 @@ export class LiveBrowserSession {
|
|
|
636
712
|
},
|
|
637
713
|
signal,
|
|
638
714
|
);
|
|
639
|
-
});
|
|
715
|
+
}, operationOptions);
|
|
640
716
|
}
|
|
641
717
|
|
|
642
|
-
public async forward(
|
|
718
|
+
public async forward(
|
|
719
|
+
optionsArg: ILiveBrowserNavigationOptions = {},
|
|
720
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
721
|
+
): Promise<void> {
|
|
643
722
|
return this.enqueuePublicOperation(async (signal) => {
|
|
644
723
|
const tab = this.resolveNavigationTab(optionsArg.tabId);
|
|
645
724
|
await this.navigateTab(
|
|
@@ -649,10 +728,13 @@ export class LiveBrowserSession {
|
|
|
649
728
|
},
|
|
650
729
|
signal,
|
|
651
730
|
);
|
|
652
|
-
});
|
|
731
|
+
}, operationOptions);
|
|
653
732
|
}
|
|
654
733
|
|
|
655
|
-
public async reload(
|
|
734
|
+
public async reload(
|
|
735
|
+
optionsArg: ILiveBrowserNavigationOptions = {},
|
|
736
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
737
|
+
): Promise<void> {
|
|
656
738
|
return this.enqueuePublicOperation(async (signal) => {
|
|
657
739
|
const tab = this.resolveNavigationTab(optionsArg.tabId);
|
|
658
740
|
await this.navigateTab(
|
|
@@ -662,10 +744,13 @@ export class LiveBrowserSession {
|
|
|
662
744
|
},
|
|
663
745
|
signal,
|
|
664
746
|
);
|
|
665
|
-
});
|
|
747
|
+
}, operationOptions);
|
|
666
748
|
}
|
|
667
749
|
|
|
668
|
-
public async setViewport(
|
|
750
|
+
public async setViewport(
|
|
751
|
+
viewportArg: ILiveBrowserViewport,
|
|
752
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
753
|
+
): Promise<void> {
|
|
669
754
|
const viewport = normalizeViewport(viewportArg);
|
|
670
755
|
return this.enqueuePublicOperation(async (signal) => {
|
|
671
756
|
const tab = this.requireActiveTab();
|
|
@@ -684,7 +769,7 @@ export class LiveBrowserSession {
|
|
|
684
769
|
await this.startScreencast(tab);
|
|
685
770
|
}
|
|
686
771
|
}
|
|
687
|
-
});
|
|
772
|
+
}, operationOptions);
|
|
688
773
|
}
|
|
689
774
|
|
|
690
775
|
public async dispatchMouse(input: ILiveBrowserMouseInput): Promise<void> {
|
|
@@ -815,6 +900,7 @@ export class LiveBrowserSession {
|
|
|
815
900
|
|
|
816
901
|
public async captureSnapshot(
|
|
817
902
|
optionsArg: ILiveBrowserSnapshotOptions = {},
|
|
903
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
818
904
|
): Promise<ILiveBrowserSnapshot> {
|
|
819
905
|
const format = optionsArg.format ?? 'jpeg';
|
|
820
906
|
if (format !== 'jpeg' && format !== 'png') {
|
|
@@ -858,10 +944,13 @@ export class LiveBrowserSession {
|
|
|
858
944
|
...dimensions,
|
|
859
945
|
data,
|
|
860
946
|
};
|
|
861
|
-
});
|
|
947
|
+
}, operationOptions);
|
|
862
948
|
}
|
|
863
949
|
|
|
864
|
-
public async observe(
|
|
950
|
+
public async observe(
|
|
951
|
+
optionsArg: ILiveBrowserObserveOptions = {},
|
|
952
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
953
|
+
): Promise<ILiveBrowserObservation> {
|
|
865
954
|
const maxCharacters = optionsArg.maxCharacters === undefined
|
|
866
955
|
? 12000
|
|
867
956
|
: validateInteger(optionsArg.maxCharacters, 'maxCharacters', 256, 50000);
|
|
@@ -948,10 +1037,202 @@ export class LiveBrowserSession {
|
|
|
948
1037
|
text: truncatedText,
|
|
949
1038
|
truncated: reachedTraversalLimit || unboundedText.length > maxCharacters,
|
|
950
1039
|
};
|
|
951
|
-
});
|
|
1040
|
+
}, operationOptions);
|
|
952
1041
|
}
|
|
953
1042
|
|
|
954
|
-
public async
|
|
1043
|
+
public async evaluate(
|
|
1044
|
+
expressionArg: string,
|
|
1045
|
+
optionsArg: ILiveBrowserEvaluateOptions = {},
|
|
1046
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
1047
|
+
): Promise<TLiveBrowserJsonValue> {
|
|
1048
|
+
if (!this.options.allowEvaluation) {
|
|
1049
|
+
throw new Error('LiveBrowserSession evaluation is disabled');
|
|
1050
|
+
}
|
|
1051
|
+
const expression = validateBoundedString(
|
|
1052
|
+
expressionArg,
|
|
1053
|
+
'expression',
|
|
1054
|
+
1,
|
|
1055
|
+
maxEvaluationScriptBytes,
|
|
1056
|
+
);
|
|
1057
|
+
const expressionBytes = new TextEncoder().encode(expression).byteLength;
|
|
1058
|
+
if (expressionBytes > maxEvaluationScriptBytes) {
|
|
1059
|
+
throw new Error(`expression must not exceed ${maxEvaluationScriptBytes} UTF-8 bytes`);
|
|
1060
|
+
}
|
|
1061
|
+
const evaluationOptions = this.normalizeEvaluationOptions(optionsArg);
|
|
1062
|
+
const evaluationId = ++this.evaluationSequence;
|
|
1063
|
+
const cancellationKey = `__smartpuppeteerCancel${evaluationId}`;
|
|
1064
|
+
const evaluationExpression = this.createEvaluationExpression(
|
|
1065
|
+
expression,
|
|
1066
|
+
evaluationOptions,
|
|
1067
|
+
cancellationKey,
|
|
1068
|
+
);
|
|
1069
|
+
|
|
1070
|
+
return this.enqueuePublicOperation(async (signal) => {
|
|
1071
|
+
const tab = this.resolveActionTab(optionsArg.tabId);
|
|
1072
|
+
const cdpSession = await tab.page.createCDPSession();
|
|
1073
|
+
let executionContextId: number | undefined;
|
|
1074
|
+
let cancellationPromise: Promise<void> | undefined;
|
|
1075
|
+
let terminationPromise: Promise<void> | undefined;
|
|
1076
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
1077
|
+
let evaluationTimedOut = false;
|
|
1078
|
+
const terminateEvaluation = (): void => {
|
|
1079
|
+
terminationPromise ??= cdpSession.send('Runtime.terminateExecution').then(() => undefined);
|
|
1080
|
+
};
|
|
1081
|
+
const cancelEvaluation = (): void => {
|
|
1082
|
+
if (executionContextId === undefined) {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
cancellationPromise ??= cdpSession.send('Runtime.evaluate', {
|
|
1086
|
+
expression: `globalThis[${JSON.stringify(evaluationCancelKey)}]?.(${JSON.stringify(cancellationKey)})`,
|
|
1087
|
+
contextId: executionContextId,
|
|
1088
|
+
returnByValue: true,
|
|
1089
|
+
awaitPromise: false,
|
|
1090
|
+
includeCommandLineAPI: false,
|
|
1091
|
+
userGesture: false,
|
|
1092
|
+
disableBreaks: true,
|
|
1093
|
+
}).then(() => undefined);
|
|
1094
|
+
};
|
|
1095
|
+
signal.addEventListener('abort', cancelEvaluation, { once: true });
|
|
1096
|
+
let evaluationError: unknown;
|
|
1097
|
+
let evaluationFailed = false;
|
|
1098
|
+
try {
|
|
1099
|
+
if (signal.aborted) {
|
|
1100
|
+
throw normalizeAbortReason(signal);
|
|
1101
|
+
}
|
|
1102
|
+
executionContextId = tab.evaluationExecutionContextId;
|
|
1103
|
+
if (executionContextId === undefined) {
|
|
1104
|
+
const frameTreeResponse = await cdpSession.send('Page.getFrameTree');
|
|
1105
|
+
if (signal.aborted) {
|
|
1106
|
+
throw normalizeAbortReason(signal);
|
|
1107
|
+
}
|
|
1108
|
+
const isolatedWorld = await cdpSession.send('Page.createIsolatedWorld', {
|
|
1109
|
+
frameId: frameTreeResponse.frameTree.frame.id,
|
|
1110
|
+
worldName: 'smartpuppeteer-evaluation',
|
|
1111
|
+
grantUniveralAccess: false,
|
|
1112
|
+
});
|
|
1113
|
+
executionContextId = isolatedWorld.executionContextId;
|
|
1114
|
+
tab.evaluationExecutionContextId = executionContextId;
|
|
1115
|
+
}
|
|
1116
|
+
if (signal.aborted) {
|
|
1117
|
+
throw normalizeAbortReason(signal);
|
|
1118
|
+
}
|
|
1119
|
+
const bootstrapResponse = await cdpSession.send('Runtime.evaluate', {
|
|
1120
|
+
expression: this.createEvaluationBootstrapExpression(),
|
|
1121
|
+
contextId: executionContextId,
|
|
1122
|
+
returnByValue: true,
|
|
1123
|
+
awaitPromise: true,
|
|
1124
|
+
includeCommandLineAPI: false,
|
|
1125
|
+
userGesture: false,
|
|
1126
|
+
disableBreaks: true,
|
|
1127
|
+
});
|
|
1128
|
+
if (bootstrapResponse.exceptionDetails) {
|
|
1129
|
+
throw new Error(this.readCdpExceptionMessage(bootstrapResponse.exceptionDetails));
|
|
1130
|
+
}
|
|
1131
|
+
if (signal.aborted) {
|
|
1132
|
+
throw normalizeAbortReason(signal);
|
|
1133
|
+
}
|
|
1134
|
+
timeoutHandle = setTimeout(() => {
|
|
1135
|
+
evaluationTimedOut = true;
|
|
1136
|
+
terminateEvaluation();
|
|
1137
|
+
}, evaluationOptions.timeoutMs + 250);
|
|
1138
|
+
const evaluationResponse = await cdpSession.send('Runtime.evaluate', {
|
|
1139
|
+
expression: evaluationExpression,
|
|
1140
|
+
contextId: executionContextId,
|
|
1141
|
+
returnByValue: true,
|
|
1142
|
+
awaitPromise: true,
|
|
1143
|
+
timeout: evaluationOptions.timeoutMs + 250,
|
|
1144
|
+
includeCommandLineAPI: false,
|
|
1145
|
+
userGesture: false,
|
|
1146
|
+
disableBreaks: true,
|
|
1147
|
+
allowUnsafeEvalBlockedByCSP: true,
|
|
1148
|
+
});
|
|
1149
|
+
if (evaluationTimedOut) {
|
|
1150
|
+
throw new Error(`Evaluation timed out after ${evaluationOptions.timeoutMs}ms`);
|
|
1151
|
+
}
|
|
1152
|
+
if (signal.aborted) {
|
|
1153
|
+
throw normalizeAbortReason(signal);
|
|
1154
|
+
}
|
|
1155
|
+
if (evaluationResponse.exceptionDetails) {
|
|
1156
|
+
throw new Error(this.readCdpExceptionMessage(evaluationResponse.exceptionDetails));
|
|
1157
|
+
}
|
|
1158
|
+
const envelope = evaluationResponse.result.value as IEvaluationEnvelope | undefined;
|
|
1159
|
+
if (!envelope || typeof envelope !== 'object' || typeof envelope.ok !== 'boolean') {
|
|
1160
|
+
throw new Error('Evaluation returned an invalid result envelope');
|
|
1161
|
+
}
|
|
1162
|
+
if (!envelope.ok) {
|
|
1163
|
+
throw new Error(
|
|
1164
|
+
typeof envelope.error === 'string'
|
|
1165
|
+
? truncate(envelope.error, 2048)
|
|
1166
|
+
: 'Evaluation failed',
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
if (typeof envelope.json !== 'string') {
|
|
1170
|
+
throw new Error('Evaluation returned an invalid JSON result');
|
|
1171
|
+
}
|
|
1172
|
+
if (new TextEncoder().encode(envelope.json).byteLength > evaluationOptions.maxOutputBytes) {
|
|
1173
|
+
throw new Error('Evaluation result exceeded maxOutputBytes during transfer');
|
|
1174
|
+
}
|
|
1175
|
+
return JSON.parse(envelope.json) as TLiveBrowserJsonValue;
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
evaluationError = error;
|
|
1178
|
+
evaluationFailed = true;
|
|
1179
|
+
throw error;
|
|
1180
|
+
} finally {
|
|
1181
|
+
signal.removeEventListener('abort', cancelEvaluation);
|
|
1182
|
+
if (timeoutHandle) {
|
|
1183
|
+
clearTimeout(timeoutHandle);
|
|
1184
|
+
}
|
|
1185
|
+
const cleanupErrors: unknown[] = [];
|
|
1186
|
+
if (cancellationPromise) {
|
|
1187
|
+
try {
|
|
1188
|
+
await cancellationPromise;
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
cleanupErrors.push(error);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (terminationPromise) {
|
|
1194
|
+
try {
|
|
1195
|
+
await terminationPromise;
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
cleanupErrors.push(error);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (executionContextId !== undefined && !cdpSession.detached) {
|
|
1201
|
+
try {
|
|
1202
|
+
await cdpSession.send('Runtime.evaluate', {
|
|
1203
|
+
expression: `globalThis[${JSON.stringify(evaluationCleanupKey)}]?.(${JSON.stringify(cancellationKey)})`,
|
|
1204
|
+
contextId: executionContextId,
|
|
1205
|
+
returnByValue: true,
|
|
1206
|
+
awaitPromise: false,
|
|
1207
|
+
includeCommandLineAPI: false,
|
|
1208
|
+
userGesture: false,
|
|
1209
|
+
disableBreaks: true,
|
|
1210
|
+
});
|
|
1211
|
+
} catch {
|
|
1212
|
+
// Navigation destroys the old execution context and its cancellation registry.
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (!cdpSession.detached) {
|
|
1216
|
+
try {
|
|
1217
|
+
await cdpSession.detach();
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
cleanupErrors.push(error);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (cleanupErrors.length > 0) {
|
|
1223
|
+
throw new AggregateError(
|
|
1224
|
+
evaluationFailed ? [evaluationError, ...cleanupErrors] : cleanupErrors,
|
|
1225
|
+
'Evaluation cleanup was incomplete',
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}, operationOptions);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
public async click(
|
|
1233
|
+
optionsArg: ILiveBrowserClickOptions,
|
|
1234
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
1235
|
+
): Promise<void> {
|
|
955
1236
|
const selector = validateBoundedString(
|
|
956
1237
|
optionsArg.selector,
|
|
957
1238
|
'selector',
|
|
@@ -977,10 +1258,13 @@ export class LiveBrowserSession {
|
|
|
977
1258
|
});
|
|
978
1259
|
await this.refreshTab(actionTab);
|
|
979
1260
|
this.emitState();
|
|
980
|
-
});
|
|
1261
|
+
}, operationOptions);
|
|
981
1262
|
}
|
|
982
1263
|
|
|
983
|
-
public async fill(
|
|
1264
|
+
public async fill(
|
|
1265
|
+
optionsArg: ILiveBrowserFillOptions,
|
|
1266
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
1267
|
+
): Promise<void> {
|
|
984
1268
|
const selector = validateBoundedString(
|
|
985
1269
|
optionsArg.selector,
|
|
986
1270
|
'selector',
|
|
@@ -996,10 +1280,13 @@ export class LiveBrowserSession {
|
|
|
996
1280
|
await actionTab.page.locator(selector).setTimeout(timeout).fill(text, { signal });
|
|
997
1281
|
await this.refreshTab(actionTab);
|
|
998
1282
|
this.emitState();
|
|
999
|
-
});
|
|
1283
|
+
}, operationOptions);
|
|
1000
1284
|
}
|
|
1001
1285
|
|
|
1002
|
-
public async press(
|
|
1286
|
+
public async press(
|
|
1287
|
+
optionsArg: ILiveBrowserPressOptions,
|
|
1288
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
1289
|
+
): Promise<void> {
|
|
1003
1290
|
const selector = validateBoundedString(
|
|
1004
1291
|
optionsArg.selector,
|
|
1005
1292
|
'selector',
|
|
@@ -1027,12 +1314,27 @@ export class LiveBrowserSession {
|
|
|
1027
1314
|
}
|
|
1028
1315
|
await this.refreshTab(actionTab);
|
|
1029
1316
|
this.emitState();
|
|
1030
|
-
});
|
|
1317
|
+
}, operationOptions);
|
|
1031
1318
|
}
|
|
1032
1319
|
|
|
1033
1320
|
private enqueuePublicOperation<T>(
|
|
1034
1321
|
operation: (signal: AbortSignal) => Promise<T>,
|
|
1322
|
+
operationOptions: ILiveBrowserOperationOptions = {},
|
|
1035
1323
|
): Promise<T> {
|
|
1324
|
+
const callerSignal = operationOptions.signal;
|
|
1325
|
+
if (
|
|
1326
|
+
callerSignal !== undefined
|
|
1327
|
+
&& (
|
|
1328
|
+
typeof callerSignal !== 'object'
|
|
1329
|
+
|| typeof callerSignal.addEventListener !== 'function'
|
|
1330
|
+
|| typeof callerSignal.removeEventListener !== 'function'
|
|
1331
|
+
)
|
|
1332
|
+
) {
|
|
1333
|
+
return Promise.reject(new Error('operationOptions.signal must be an AbortSignal'));
|
|
1334
|
+
}
|
|
1335
|
+
if (callerSignal?.aborted) {
|
|
1336
|
+
return Promise.reject(normalizeAbortReason(callerSignal));
|
|
1337
|
+
}
|
|
1036
1338
|
if (
|
|
1037
1339
|
this.status === 'stopping'
|
|
1038
1340
|
|| this.normalStopRequested
|
|
@@ -1044,7 +1346,7 @@ export class LiveBrowserSession {
|
|
|
1044
1346
|
return Promise.reject(new Error('LiveBrowserSession operation queue is full'));
|
|
1045
1347
|
}
|
|
1046
1348
|
this.admittedPublicOperations += 1;
|
|
1047
|
-
return this.enqueueQueuedOperation('public', operation);
|
|
1349
|
+
return this.enqueueQueuedOperation('public', operation, false, callerSignal);
|
|
1048
1350
|
}
|
|
1049
1351
|
|
|
1050
1352
|
private enqueueInternalOperation(
|
|
@@ -1062,15 +1364,45 @@ export class LiveBrowserSession {
|
|
|
1062
1364
|
kind: TQueuedOperationKind,
|
|
1063
1365
|
operation: (signal: AbortSignal) => Promise<T>,
|
|
1064
1366
|
priority = false,
|
|
1367
|
+
callerSignal?: AbortSignal,
|
|
1065
1368
|
): Promise<T> {
|
|
1066
1369
|
return new Promise<T>((resolve, reject) => {
|
|
1067
1370
|
const queuedOperation: IQueuedOperation = {
|
|
1068
1371
|
kind,
|
|
1372
|
+
state: 'queued',
|
|
1069
1373
|
controller: new AbortController(),
|
|
1070
1374
|
run: operation,
|
|
1071
1375
|
resolve: (value) => resolve(value as T),
|
|
1072
1376
|
reject,
|
|
1377
|
+
capacityReleased: false,
|
|
1073
1378
|
};
|
|
1379
|
+
if (callerSignal) {
|
|
1380
|
+
const onCallerAbort = (): void => {
|
|
1381
|
+
const abortReason = normalizeAbortReason(callerSignal);
|
|
1382
|
+
if (queuedOperation.state === 'queued') {
|
|
1383
|
+
const operationIndex = this.operationQueue.indexOf(queuedOperation);
|
|
1384
|
+
if (operationIndex >= 0) {
|
|
1385
|
+
this.operationQueue.splice(operationIndex, 1);
|
|
1386
|
+
}
|
|
1387
|
+
queuedOperation.controller.abort(abortReason);
|
|
1388
|
+
queuedOperation.reject(abortReason);
|
|
1389
|
+
this.finalizeQueuedOperation(queuedOperation);
|
|
1390
|
+
this.drainOperationQueue();
|
|
1391
|
+
} else if (queuedOperation.state === 'active') {
|
|
1392
|
+
queuedOperation.controller.abort(abortReason);
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
callerSignal.addEventListener('abort', onCallerAbort, { once: true });
|
|
1396
|
+
queuedOperation.removeCallerAbortListener = () => {
|
|
1397
|
+
callerSignal.removeEventListener('abort', onCallerAbort);
|
|
1398
|
+
};
|
|
1399
|
+
if (callerSignal.aborted) {
|
|
1400
|
+
onCallerAbort();
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (queuedOperation.state !== 'queued') {
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1074
1406
|
if (priority) {
|
|
1075
1407
|
this.operationQueue.unshift(queuedOperation);
|
|
1076
1408
|
} else {
|
|
@@ -1090,6 +1422,7 @@ export class LiveBrowserSession {
|
|
|
1090
1422
|
}
|
|
1091
1423
|
this.operationRunning = true;
|
|
1092
1424
|
this.activeOperation = queuedOperation;
|
|
1425
|
+
queuedOperation.state = 'active';
|
|
1093
1426
|
void this.executeQueuedOperation(queuedOperation).catch((error) => {
|
|
1094
1427
|
this.operationRunning = false;
|
|
1095
1428
|
this.activeOperation = undefined;
|
|
@@ -1107,15 +1440,15 @@ export class LiveBrowserSession {
|
|
|
1107
1440
|
if (queuedOperation.controller.signal.aborted) {
|
|
1108
1441
|
throw queuedOperation.controller.signal.reason;
|
|
1109
1442
|
}
|
|
1110
|
-
|
|
1443
|
+
const value = await queuedOperation.run(queuedOperation.controller.signal);
|
|
1444
|
+
if (queuedOperation.controller.signal.aborted) {
|
|
1445
|
+
throw queuedOperation.controller.signal.reason;
|
|
1446
|
+
}
|
|
1447
|
+
queuedOperation.resolve(value);
|
|
1111
1448
|
} catch (error) {
|
|
1112
1449
|
queuedOperation.reject(error);
|
|
1113
1450
|
} finally {
|
|
1114
|
-
|
|
1115
|
-
this.admittedPublicOperations -= 1;
|
|
1116
|
-
} else if (queuedOperation.kind === 'internal') {
|
|
1117
|
-
this.admittedInternalOperations -= 1;
|
|
1118
|
-
}
|
|
1451
|
+
this.finalizeQueuedOperation(queuedOperation);
|
|
1119
1452
|
if (this.activeOperation === queuedOperation) {
|
|
1120
1453
|
this.activeOperation = undefined;
|
|
1121
1454
|
}
|
|
@@ -1130,15 +1463,29 @@ export class LiveBrowserSession {
|
|
|
1130
1463
|
}
|
|
1131
1464
|
}
|
|
1132
1465
|
|
|
1466
|
+
private finalizeQueuedOperation(queuedOperation: IQueuedOperation): void {
|
|
1467
|
+
if (queuedOperation.state === 'settled') {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
queuedOperation.state = 'settled';
|
|
1471
|
+
queuedOperation.removeCallerAbortListener?.();
|
|
1472
|
+
queuedOperation.removeCallerAbortListener = undefined;
|
|
1473
|
+
if (queuedOperation.capacityReleased) {
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
queuedOperation.capacityReleased = true;
|
|
1477
|
+
if (queuedOperation.kind === 'public') {
|
|
1478
|
+
this.admittedPublicOperations -= 1;
|
|
1479
|
+
} else if (queuedOperation.kind === 'internal') {
|
|
1480
|
+
this.admittedInternalOperations -= 1;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1133
1484
|
private cancelQueuedOperations(error: Error): void {
|
|
1134
1485
|
for (const queuedOperation of this.operationQueue.splice(0)) {
|
|
1135
1486
|
queuedOperation.controller.abort(error);
|
|
1136
|
-
if (queuedOperation.kind === 'public') {
|
|
1137
|
-
this.admittedPublicOperations -= 1;
|
|
1138
|
-
} else if (queuedOperation.kind === 'internal') {
|
|
1139
|
-
this.admittedInternalOperations -= 1;
|
|
1140
|
-
}
|
|
1141
1487
|
queuedOperation.reject(error);
|
|
1488
|
+
this.finalizeQueuedOperation(queuedOperation);
|
|
1142
1489
|
}
|
|
1143
1490
|
}
|
|
1144
1491
|
|
|
@@ -1297,6 +1644,15 @@ export class LiveBrowserSession {
|
|
|
1297
1644
|
const shutdownErrors: unknown[] = [];
|
|
1298
1645
|
for (const tab of [...this.tabs.values()]) {
|
|
1299
1646
|
tab.closing = true;
|
|
1647
|
+
const securityCdpSession = tab.securityCdpSession;
|
|
1648
|
+
tab.securityCdpSession = undefined;
|
|
1649
|
+
if (securityCdpSession && !securityCdpSession.detached) {
|
|
1650
|
+
try {
|
|
1651
|
+
await securityCdpSession.detach();
|
|
1652
|
+
} catch {
|
|
1653
|
+
// Browser lifetime cancellation may close the target before explicit detach settles.
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1300
1656
|
try {
|
|
1301
1657
|
await this.stopScreencast(tab);
|
|
1302
1658
|
} catch (cleanupError) {
|
|
@@ -1338,6 +1694,22 @@ export class LiveBrowserSession {
|
|
|
1338
1694
|
}
|
|
1339
1695
|
}
|
|
1340
1696
|
|
|
1697
|
+
private async configureBrowserSecurity(
|
|
1698
|
+
browser: plugins.puppeteer.Browser,
|
|
1699
|
+
): Promise<void> {
|
|
1700
|
+
if (!this.options.security?.denyPermissions) {
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
const cdpSession = await browser.target().createCDPSession();
|
|
1704
|
+
try {
|
|
1705
|
+
await cdpSession.send('Browser.grantPermissions', { permissions: [] });
|
|
1706
|
+
} finally {
|
|
1707
|
+
if (!cdpSession.detached) {
|
|
1708
|
+
await cdpSession.detach();
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1341
1713
|
private requireBrowserContext(): plugins.puppeteer.BrowserContext {
|
|
1342
1714
|
if (this.status !== 'running' || !this.browserContext) {
|
|
1343
1715
|
throw new Error('LiveBrowserSession is not running');
|
|
@@ -1433,6 +1805,17 @@ export class LiveBrowserSession {
|
|
|
1433
1805
|
this.tabs.set(tab.id, tab);
|
|
1434
1806
|
this.tabIdsByPage.set(page, tab.id);
|
|
1435
1807
|
try {
|
|
1808
|
+
if (this.options.security?.denyFileChoosers) {
|
|
1809
|
+
const securityCdpSession = await page.createCDPSession();
|
|
1810
|
+
tab.securityCdpSession = securityCdpSession;
|
|
1811
|
+
await securityCdpSession.send('Page.enable', {
|
|
1812
|
+
enableFileChooserOpenedEvent: true,
|
|
1813
|
+
});
|
|
1814
|
+
await securityCdpSession.send('Page.setInterceptFileChooserDialog', {
|
|
1815
|
+
enabled: true,
|
|
1816
|
+
cancel: true,
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1436
1819
|
await this.ensureTabViewport(tab);
|
|
1437
1820
|
await this.refreshTab(tab);
|
|
1438
1821
|
|
|
@@ -1488,6 +1871,7 @@ export class LiveBrowserSession {
|
|
|
1488
1871
|
if (frame !== page.mainFrame() || tab.closing) {
|
|
1489
1872
|
return;
|
|
1490
1873
|
}
|
|
1874
|
+
tab.evaluationExecutionContextId = undefined;
|
|
1491
1875
|
const navigationReset = !tab.navigationInProgress;
|
|
1492
1876
|
if (navigationReset) {
|
|
1493
1877
|
tab.streamInvalidated = true;
|
|
@@ -1563,6 +1947,13 @@ export class LiveBrowserSession {
|
|
|
1563
1947
|
);
|
|
1564
1948
|
return tab;
|
|
1565
1949
|
} catch (error) {
|
|
1950
|
+
if (tab.securityCdpSession && !tab.securityCdpSession.detached) {
|
|
1951
|
+
try {
|
|
1952
|
+
await tab.securityCdpSession.detach();
|
|
1953
|
+
} catch {
|
|
1954
|
+
// The page may have closed while security setup was failing.
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1566
1957
|
this.removePageListeners(tab);
|
|
1567
1958
|
this.tabs.delete(tab.id);
|
|
1568
1959
|
this.tabIdsByPage.delete(page);
|
|
@@ -1813,6 +2204,7 @@ export class LiveBrowserSession {
|
|
|
1813
2204
|
signal: AbortSignal,
|
|
1814
2205
|
): Promise<void> {
|
|
1815
2206
|
const isActive = this.activeTabId === tab.id;
|
|
2207
|
+
tab.evaluationExecutionContextId = undefined;
|
|
1816
2208
|
tab.navigationInProgress = true;
|
|
1817
2209
|
if (isActive) {
|
|
1818
2210
|
await this.stopScreencast(tab);
|
|
@@ -1832,7 +2224,6 @@ export class LiveBrowserSession {
|
|
|
1832
2224
|
&& this.activeTabId === tab.id
|
|
1833
2225
|
&& this.status === 'running'
|
|
1834
2226
|
&& tab.status === 'open'
|
|
1835
|
-
&& !signal.aborted
|
|
1836
2227
|
) {
|
|
1837
2228
|
await this.startScreencast(tab);
|
|
1838
2229
|
}
|
|
@@ -2242,14 +2633,300 @@ export class LiveBrowserSession {
|
|
|
2242
2633
|
|
|
2243
2634
|
private validateUrl(url: unknown): string {
|
|
2244
2635
|
const validatedUrl = validateBoundedString(url, 'url', 1, maxUrlLength);
|
|
2636
|
+
let parsedUrl: URL;
|
|
2245
2637
|
try {
|
|
2246
|
-
new URL(validatedUrl);
|
|
2638
|
+
parsedUrl = new URL(validatedUrl);
|
|
2247
2639
|
} catch {
|
|
2248
2640
|
throw new Error('url must be absolute');
|
|
2249
2641
|
}
|
|
2642
|
+
if (
|
|
2643
|
+
this.options.security?.httpNavigationOnly
|
|
2644
|
+
&& parsedUrl.protocol !== 'http:'
|
|
2645
|
+
&& parsedUrl.protocol !== 'https:'
|
|
2646
|
+
) {
|
|
2647
|
+
throw new Error('url protocol must be http or https');
|
|
2648
|
+
}
|
|
2250
2649
|
return validatedUrl;
|
|
2251
2650
|
}
|
|
2252
2651
|
|
|
2652
|
+
private normalizeEvaluationOptions(
|
|
2653
|
+
options: ILiveBrowserEvaluateOptions,
|
|
2654
|
+
): INormalizedEvaluationOptions {
|
|
2655
|
+
return {
|
|
2656
|
+
timeoutMs: options.timeoutMs === undefined
|
|
2657
|
+
? 5000
|
|
2658
|
+
: validateInteger(options.timeoutMs, 'timeoutMs', 1, 30000),
|
|
2659
|
+
maxOutputBytes: options.maxOutputBytes === undefined
|
|
2660
|
+
? 262144
|
|
2661
|
+
: validateInteger(
|
|
2662
|
+
options.maxOutputBytes,
|
|
2663
|
+
'maxOutputBytes',
|
|
2664
|
+
1,
|
|
2665
|
+
maxEvaluationOutputBytes,
|
|
2666
|
+
),
|
|
2667
|
+
maxDepth: options.maxDepth === undefined
|
|
2668
|
+
? 16
|
|
2669
|
+
: validateInteger(options.maxDepth, 'maxDepth', 1, maxEvaluationDepth),
|
|
2670
|
+
maxNodes: options.maxNodes === undefined
|
|
2671
|
+
? 10000
|
|
2672
|
+
: validateInteger(options.maxNodes, 'maxNodes', 1, maxEvaluationNodes),
|
|
2673
|
+
maxStringBytes: options.maxStringBytes === undefined
|
|
2674
|
+
? 65536
|
|
2675
|
+
: validateInteger(
|
|
2676
|
+
options.maxStringBytes,
|
|
2677
|
+
'maxStringBytes',
|
|
2678
|
+
0,
|
|
2679
|
+
maxEvaluationStringBytes,
|
|
2680
|
+
),
|
|
2681
|
+
maxArrayLength: options.maxArrayLength === undefined
|
|
2682
|
+
? 1000
|
|
2683
|
+
: validateInteger(
|
|
2684
|
+
options.maxArrayLength,
|
|
2685
|
+
'maxArrayLength',
|
|
2686
|
+
0,
|
|
2687
|
+
maxEvaluationArrayLength,
|
|
2688
|
+
),
|
|
2689
|
+
maxObjectKeys: options.maxObjectKeys === undefined
|
|
2690
|
+
? 1000
|
|
2691
|
+
: validateInteger(
|
|
2692
|
+
options.maxObjectKeys,
|
|
2693
|
+
'maxObjectKeys',
|
|
2694
|
+
0,
|
|
2695
|
+
maxEvaluationObjectKeys,
|
|
2696
|
+
),
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
private readCdpExceptionMessage(
|
|
2701
|
+
exceptionDetails: plugins.puppeteer.Protocol.Runtime.ExceptionDetails,
|
|
2702
|
+
): string {
|
|
2703
|
+
const description = exceptionDetails.exception?.description;
|
|
2704
|
+
if (typeof description === 'string' && description.length > 0) {
|
|
2705
|
+
return truncate(description, 2048);
|
|
2706
|
+
}
|
|
2707
|
+
return truncate(exceptionDetails.text, 2048);
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
private createEvaluationBootstrapExpression(): string {
|
|
2711
|
+
const bootstrapKeyLiteral = JSON.stringify(evaluationBootstrapKey);
|
|
2712
|
+
const cancelKeyLiteral = JSON.stringify(evaluationCancelKey);
|
|
2713
|
+
const cleanupKeyLiteral = JSON.stringify(evaluationCleanupKey);
|
|
2714
|
+
return `(() => {
|
|
2715
|
+
'use strict';
|
|
2716
|
+
if (typeof globalThis[${bootstrapKeyLiteral}] === 'function') {
|
|
2717
|
+
return true;
|
|
2718
|
+
}
|
|
2719
|
+
const safeGlobalThis = globalThis;
|
|
2720
|
+
const SafeArray = Array;
|
|
2721
|
+
const SafeError = Error;
|
|
2722
|
+
const SafeFunction = Function;
|
|
2723
|
+
const SafeJSON = JSON;
|
|
2724
|
+
const SafeMap = Map;
|
|
2725
|
+
const SafeNumber = Number;
|
|
2726
|
+
const SafeObject = Object;
|
|
2727
|
+
const SafePromise = Promise;
|
|
2728
|
+
const SafeString = String;
|
|
2729
|
+
const SafeTextEncoder = TextEncoder;
|
|
2730
|
+
const SafeWeakSet = WeakSet;
|
|
2731
|
+
const SafeClearTimeout = clearTimeout;
|
|
2732
|
+
const SafeSetTimeout = setTimeout;
|
|
2733
|
+
const safeCreate = SafeObject.create;
|
|
2734
|
+
const safeDefineProperty = SafeObject.defineProperty;
|
|
2735
|
+
const safeGetOwnPropertyDescriptor = SafeObject.getOwnPropertyDescriptor;
|
|
2736
|
+
const safeGetOwnPropertySymbols = SafeObject.getOwnPropertySymbols;
|
|
2737
|
+
const safeGetPrototypeOf = SafeObject.getPrototypeOf;
|
|
2738
|
+
const safeIsArray = SafeArray.isArray;
|
|
2739
|
+
const safeIsFinite = SafeNumber.isFinite;
|
|
2740
|
+
const safeKeys = SafeObject.keys;
|
|
2741
|
+
const safeSetPrototypeOf = SafeObject.setPrototypeOf;
|
|
2742
|
+
const safeStringify = SafeJSON.stringify;
|
|
2743
|
+
const safeMapDelete = SafeFunction.prototype.call.bind(SafeMap.prototype.delete);
|
|
2744
|
+
const safeMapGet = SafeFunction.prototype.call.bind(SafeMap.prototype.get);
|
|
2745
|
+
const safeMapSet = SafeFunction.prototype.call.bind(SafeMap.prototype.set);
|
|
2746
|
+
const safePromiseThen = SafeFunction.prototype.call.bind(SafePromise.prototype.then);
|
|
2747
|
+
const safeWeakSetAdd = SafeFunction.prototype.call.bind(SafeWeakSet.prototype.add);
|
|
2748
|
+
const safeWeakSetHas = SafeFunction.prototype.call.bind(SafeWeakSet.prototype.has);
|
|
2749
|
+
const safeEncode = SafeFunction.prototype.call.bind(SafeTextEncoder.prototype.encode);
|
|
2750
|
+
const encoder = new SafeTextEncoder();
|
|
2751
|
+
const cancellationHandlers = new SafeMap();
|
|
2752
|
+
|
|
2753
|
+
const createEnvelope = (ok, value) => {
|
|
2754
|
+
const envelope = safeCreate(null);
|
|
2755
|
+
envelope.ok = ok;
|
|
2756
|
+
if (ok) {
|
|
2757
|
+
envelope.json = value;
|
|
2758
|
+
} else {
|
|
2759
|
+
envelope.error = value;
|
|
2760
|
+
}
|
|
2761
|
+
return envelope;
|
|
2762
|
+
};
|
|
2763
|
+
const byteLength = (value) => safeEncode(encoder, value).byteLength;
|
|
2764
|
+
const cancelEvaluation = (cancellationKey) => {
|
|
2765
|
+
const handler = safeMapGet(cancellationHandlers, cancellationKey);
|
|
2766
|
+
if (typeof handler === 'function') {
|
|
2767
|
+
handler();
|
|
2768
|
+
}
|
|
2769
|
+
return safeMapDelete(cancellationHandlers, cancellationKey);
|
|
2770
|
+
};
|
|
2771
|
+
const evaluate = async (source, options, cancellationKey) => {
|
|
2772
|
+
const seen = new SafeWeakSet();
|
|
2773
|
+
let visitedNodes = 0;
|
|
2774
|
+
const normalize = (value, depth) => {
|
|
2775
|
+
visitedNodes += 1;
|
|
2776
|
+
if (visitedNodes > options.maxNodes) {
|
|
2777
|
+
throw new SafeError('Evaluation result exceeded maxNodes');
|
|
2778
|
+
}
|
|
2779
|
+
if (depth > options.maxDepth) {
|
|
2780
|
+
throw new SafeError('Evaluation result exceeded maxDepth');
|
|
2781
|
+
}
|
|
2782
|
+
if (value === null || typeof value === 'boolean') {
|
|
2783
|
+
return value;
|
|
2784
|
+
}
|
|
2785
|
+
if (typeof value === 'number') {
|
|
2786
|
+
if (!safeIsFinite(value)) {
|
|
2787
|
+
throw new SafeError('Evaluation result contains a non-finite number');
|
|
2788
|
+
}
|
|
2789
|
+
return value;
|
|
2790
|
+
}
|
|
2791
|
+
if (typeof value === 'string') {
|
|
2792
|
+
if (byteLength(value) > options.maxStringBytes) {
|
|
2793
|
+
throw new SafeError('Evaluation result string exceeded maxStringBytes');
|
|
2794
|
+
}
|
|
2795
|
+
return value;
|
|
2796
|
+
}
|
|
2797
|
+
if (typeof value !== 'object') {
|
|
2798
|
+
throw new SafeError('Evaluation result contains a non-JSON value');
|
|
2799
|
+
}
|
|
2800
|
+
if (safeWeakSetHas(seen, value)) {
|
|
2801
|
+
throw new SafeError('Evaluation result contains a cycle or repeated object');
|
|
2802
|
+
}
|
|
2803
|
+
safeWeakSetAdd(seen, value);
|
|
2804
|
+
|
|
2805
|
+
if (safeIsArray(value)) {
|
|
2806
|
+
if (value.length > options.maxArrayLength) {
|
|
2807
|
+
throw new SafeError('Evaluation result array exceeded maxArrayLength');
|
|
2808
|
+
}
|
|
2809
|
+
const keys = safeKeys(value);
|
|
2810
|
+
if (keys.length !== value.length) {
|
|
2811
|
+
throw new SafeError('Evaluation result contains a sparse or extended array');
|
|
2812
|
+
}
|
|
2813
|
+
const output = SafeArray(value.length);
|
|
2814
|
+
safeSetPrototypeOf(output, null);
|
|
2815
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
2816
|
+
const descriptor = safeGetOwnPropertyDescriptor(value, SafeString(index));
|
|
2817
|
+
if (!descriptor || !('value' in descriptor)) {
|
|
2818
|
+
throw new SafeError('Evaluation result contains an array accessor');
|
|
2819
|
+
}
|
|
2820
|
+
output[index] = normalize(descriptor.value, depth + 1);
|
|
2821
|
+
}
|
|
2822
|
+
return output;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
const prototype = safeGetPrototypeOf(value);
|
|
2826
|
+
if (prototype !== SafeObject.prototype && prototype !== null) {
|
|
2827
|
+
throw new SafeError('Evaluation result contains a non-plain object');
|
|
2828
|
+
}
|
|
2829
|
+
if (safeGetOwnPropertySymbols(value).length > 0) {
|
|
2830
|
+
throw new SafeError('Evaluation result contains symbol properties');
|
|
2831
|
+
}
|
|
2832
|
+
const keys = safeKeys(value);
|
|
2833
|
+
if (keys.length > options.maxObjectKeys) {
|
|
2834
|
+
throw new SafeError('Evaluation result object exceeded maxObjectKeys');
|
|
2835
|
+
}
|
|
2836
|
+
const output = safeCreate(null);
|
|
2837
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
2838
|
+
const key = keys[index];
|
|
2839
|
+
if (byteLength(key) > options.maxStringBytes) {
|
|
2840
|
+
throw new SafeError('Evaluation result key exceeded maxStringBytes');
|
|
2841
|
+
}
|
|
2842
|
+
const descriptor = safeGetOwnPropertyDescriptor(value, key);
|
|
2843
|
+
if (!descriptor || !('value' in descriptor)) {
|
|
2844
|
+
throw new SafeError('Evaluation result contains an object accessor');
|
|
2845
|
+
}
|
|
2846
|
+
output[key] = normalize(descriptor.value, depth + 1);
|
|
2847
|
+
}
|
|
2848
|
+
return output;
|
|
2849
|
+
};
|
|
2850
|
+
|
|
2851
|
+
try {
|
|
2852
|
+
const execute = SafeFunction(
|
|
2853
|
+
'\"use strict\"; return (async () => (' + source + '\\n))();',
|
|
2854
|
+
);
|
|
2855
|
+
const boundedExecution = new SafePromise((resolve, reject) => {
|
|
2856
|
+
let timeout;
|
|
2857
|
+
safeMapSet(cancellationHandlers, cancellationKey, () => {
|
|
2858
|
+
if (timeout !== undefined) {
|
|
2859
|
+
SafeClearTimeout(timeout);
|
|
2860
|
+
}
|
|
2861
|
+
reject(new SafeError('Evaluation cancelled'));
|
|
2862
|
+
});
|
|
2863
|
+
timeout = SafeSetTimeout(() => {
|
|
2864
|
+
reject(new SafeError('Evaluation timed out after ' + options.timeoutMs + 'ms'));
|
|
2865
|
+
}, options.timeoutMs);
|
|
2866
|
+
safePromiseThen(
|
|
2867
|
+
execute(),
|
|
2868
|
+
(value) => {
|
|
2869
|
+
SafeClearTimeout(timeout);
|
|
2870
|
+
resolve(value);
|
|
2871
|
+
},
|
|
2872
|
+
(error) => {
|
|
2873
|
+
SafeClearTimeout(timeout);
|
|
2874
|
+
reject(error);
|
|
2875
|
+
},
|
|
2876
|
+
);
|
|
2877
|
+
});
|
|
2878
|
+
let normalizedResult;
|
|
2879
|
+
try {
|
|
2880
|
+
normalizedResult = normalize(await boundedExecution, 0);
|
|
2881
|
+
} finally {
|
|
2882
|
+
safeMapDelete(cancellationHandlers, cancellationKey);
|
|
2883
|
+
}
|
|
2884
|
+
const json = safeStringify(normalizedResult);
|
|
2885
|
+
if (typeof json !== 'string' || byteLength(json) > options.maxOutputBytes) {
|
|
2886
|
+
throw new SafeError('Evaluation result exceeded maxOutputBytes');
|
|
2887
|
+
}
|
|
2888
|
+
return createEnvelope(true, json);
|
|
2889
|
+
} catch (error) {
|
|
2890
|
+
let message = 'Evaluation failed';
|
|
2891
|
+
if (typeof error === 'string') {
|
|
2892
|
+
message = error;
|
|
2893
|
+
} else if (error && typeof error === 'object') {
|
|
2894
|
+
const descriptor = safeGetOwnPropertyDescriptor(error, 'message');
|
|
2895
|
+
if (descriptor && 'value' in descriptor && typeof descriptor.value === 'string') {
|
|
2896
|
+
message = descriptor.value;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
if (byteLength(message) > 2048) {
|
|
2900
|
+
message = 'Evaluation failed with an oversized error';
|
|
2901
|
+
}
|
|
2902
|
+
return createEnvelope(false, message);
|
|
2903
|
+
}
|
|
2904
|
+
};
|
|
2905
|
+
|
|
2906
|
+
for (const [key, value] of [
|
|
2907
|
+
[${bootstrapKeyLiteral}, evaluate],
|
|
2908
|
+
[${cancelKeyLiteral}, cancelEvaluation],
|
|
2909
|
+
[${cleanupKeyLiteral}, cancelEvaluation],
|
|
2910
|
+
]) {
|
|
2911
|
+
safeDefineProperty(safeGlobalThis, key, {
|
|
2912
|
+
value,
|
|
2913
|
+
configurable: false,
|
|
2914
|
+
enumerable: false,
|
|
2915
|
+
writable: false,
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
return true;
|
|
2919
|
+
})()`;
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
private createEvaluationExpression(
|
|
2923
|
+
expression: string,
|
|
2924
|
+
options: INormalizedEvaluationOptions,
|
|
2925
|
+
cancellationKey: string,
|
|
2926
|
+
): string {
|
|
2927
|
+
return `globalThis[${JSON.stringify(evaluationBootstrapKey)}](${JSON.stringify(expression)}, ${JSON.stringify(options)}, ${JSON.stringify(cancellationKey)})`;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2253
2930
|
private validateTimeout(timeoutMs?: number, defaultValue = 5000): number {
|
|
2254
2931
|
if (timeoutMs === undefined) {
|
|
2255
2932
|
return defaultValue;
|