@apifuse/provider-sdk 2.2.0-beta.32 → 2.2.0-beta.35
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/CHANGELOG.md +14 -0
- package/dist/error-resolution.js +0 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/auth-flow.d.ts +3 -1
- package/dist/runtime/auth-flow.js +1 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +350 -27
- package/dist/runtime/choice.d.ts +0 -1
- package/dist/runtime/choice.js +10 -126
- package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
- package/dist/runtime/resolver-vendors/browser.js +68 -16
- package/dist/runtime/resolver-vendors/capsolver.d.ts +1 -3
- package/dist/runtime/resolver-vendors/capsolver.js +148 -24
- package/dist/runtime/resolver-vendors/twocaptcha.js +57 -18
- package/dist/runtime/resolver-vendors/types.d.ts +5 -2
- package/dist/runtime/resolver-vendors/types.js +16 -4
- package/dist/runtime/resolver.d.ts +1 -1
- package/dist/runtime/resolver.js +24 -5
- package/dist/server/serve-implementation.d.ts +2 -1
- package/dist/server/serve-implementation.js +27 -17
- package/dist/testing/run.js +1 -0
- package/dist/types.d.ts +25 -3
- package/package.json +3 -2
- package/src/error-resolution.ts +0 -1
- package/src/index.ts +0 -1
- package/src/provider.ts +0 -1
- package/src/runtime/auth-flow.ts +4 -0
- package/src/runtime/browser.ts +438 -31
- package/src/runtime/choice.ts +10 -151
- package/src/runtime/resolver-vendors/browser.ts +83 -16
- package/src/runtime/resolver-vendors/capsolver.ts +170 -33
- package/src/runtime/resolver-vendors/twocaptcha.ts +54 -15
- package/src/runtime/resolver-vendors/types.ts +22 -4
- package/src/runtime/resolver.ts +31 -7
- package/src/server/serve-implementation.ts +74 -17
- package/src/testing/run.ts +1 -0
- package/src/types.ts +26 -3
package/src/runtime/browser.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import type { Frame, LaunchOptions, Locator, Page, Request, Route } from "playwright";
|
|
2
|
+
import type { CDPSession, Frame, LaunchOptions, Locator, Page, Request, Route } from "playwright";
|
|
3
3
|
|
|
4
4
|
import { ProviderError } from "../errors.js";
|
|
5
5
|
import type {
|
|
@@ -87,13 +87,28 @@ type CdpFrameTreeNode = {
|
|
|
87
87
|
type CdpFetchFulfillParams = {
|
|
88
88
|
readonly requestId: string;
|
|
89
89
|
readonly responseCode: number;
|
|
90
|
-
readonly responseHeaders?:
|
|
90
|
+
readonly responseHeaders?: {
|
|
91
91
|
readonly name: string;
|
|
92
92
|
readonly value: string;
|
|
93
93
|
}[];
|
|
94
94
|
readonly body?: string;
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
+
type CdpPausedDocumentResponse = {
|
|
98
|
+
readonly hasBody: boolean;
|
|
99
|
+
readonly requestId: string;
|
|
100
|
+
readonly responseCode: number;
|
|
101
|
+
readonly responseHeaders: readonly {
|
|
102
|
+
readonly name: string;
|
|
103
|
+
readonly value: string;
|
|
104
|
+
}[];
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
type CdpResponseBody = {
|
|
108
|
+
readonly base64Encoded?: boolean;
|
|
109
|
+
readonly body?: string;
|
|
110
|
+
};
|
|
111
|
+
|
|
97
112
|
type BrowserPageContract = BrowserPage;
|
|
98
113
|
|
|
99
114
|
function toResourceBody(body: BrowserResourceBody | undefined): Buffer | string | undefined {
|
|
@@ -109,7 +124,7 @@ function toResourceBody(body: BrowserResourceBody | undefined): Buffer | string
|
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
function isResourceMethod(method: string): method is BrowserResourceMethod {
|
|
112
|
-
return method === "GET" || method === "HEAD";
|
|
127
|
+
return method === "GET" || method === "HEAD" || method === "POST";
|
|
113
128
|
}
|
|
114
129
|
|
|
115
130
|
async function toResourceRequest(request: Request): Promise<BrowserResourceRequest | null> {
|
|
@@ -164,6 +179,85 @@ function getCdpPausedRequestId(params: unknown): string | null {
|
|
|
164
179
|
return params.requestId;
|
|
165
180
|
}
|
|
166
181
|
|
|
182
|
+
function isCdpResponseStage(params: unknown): boolean {
|
|
183
|
+
return (
|
|
184
|
+
isRecord(params) &&
|
|
185
|
+
(typeof params.responseStatusCode === "number" ||
|
|
186
|
+
typeof params.responseErrorReason === "string")
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function toCdpPausedDocumentResponse(params: unknown): CdpPausedDocumentResponse | null {
|
|
191
|
+
if (
|
|
192
|
+
!isRecord(params) ||
|
|
193
|
+
typeof params.requestId !== "string" ||
|
|
194
|
+
typeof params.responseStatusCode !== "number" ||
|
|
195
|
+
params.resourceType !== "Document" ||
|
|
196
|
+
!isRecord(params.request)
|
|
197
|
+
) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const responseCode = params.responseStatusCode;
|
|
202
|
+
const responseHeaders = Array.isArray(params.responseHeaders)
|
|
203
|
+
? params.responseHeaders.flatMap((header) => {
|
|
204
|
+
if (
|
|
205
|
+
!isRecord(header) ||
|
|
206
|
+
typeof header.name !== "string" ||
|
|
207
|
+
typeof header.value !== "string"
|
|
208
|
+
) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
return [{ name: header.name, value: header.value }];
|
|
212
|
+
})
|
|
213
|
+
: [];
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
hasBody:
|
|
217
|
+
String(params.request.method ?? "").toUpperCase() !== "HEAD" &&
|
|
218
|
+
responseCode >= 200 &&
|
|
219
|
+
(responseCode < 300 || responseCode >= 400) &&
|
|
220
|
+
responseCode !== 204 &&
|
|
221
|
+
responseCode !== 205 &&
|
|
222
|
+
responseCode !== 304,
|
|
223
|
+
requestId: params.requestId,
|
|
224
|
+
responseCode,
|
|
225
|
+
responseHeaders,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function toCdpDocumentFulfillParams(
|
|
230
|
+
response: CdpPausedDocumentResponse,
|
|
231
|
+
body: CdpResponseBody,
|
|
232
|
+
contentSecurityPolicy: string,
|
|
233
|
+
): CdpFetchFulfillParams {
|
|
234
|
+
const responseBody = typeof body.body === "string" ? body.body : "";
|
|
235
|
+
return {
|
|
236
|
+
body: body.base64Encoded ? responseBody : Buffer.from(responseBody).toString("base64"),
|
|
237
|
+
requestId: response.requestId,
|
|
238
|
+
responseCode: response.responseCode,
|
|
239
|
+
responseHeaders: [
|
|
240
|
+
...response.responseHeaders,
|
|
241
|
+
{ name: "Content-Security-Policy", value: contentSecurityPolicy },
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function resourcePolicyFetchPatterns(policy: BrowserResourcePolicy) {
|
|
247
|
+
return [
|
|
248
|
+
{ requestStage: "Request" as const, urlPattern: "*" },
|
|
249
|
+
...(policy.documentContentSecurityPolicy
|
|
250
|
+
? [
|
|
251
|
+
{
|
|
252
|
+
requestStage: "Response" as const,
|
|
253
|
+
resourceType: "Document" as const,
|
|
254
|
+
urlPattern: "*",
|
|
255
|
+
},
|
|
256
|
+
]
|
|
257
|
+
: []),
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
|
|
167
261
|
function toCdpResourceHeaders(value: unknown): Record<string, string> {
|
|
168
262
|
if (!isRecord(value)) {
|
|
169
263
|
return {};
|
|
@@ -225,11 +319,171 @@ async function fulfillResourceRoute(
|
|
|
225
319
|
});
|
|
226
320
|
}
|
|
227
321
|
|
|
322
|
+
async function decideResourceRequest(
|
|
323
|
+
policy: BrowserResourcePolicy,
|
|
324
|
+
request: BrowserResourceRequest,
|
|
325
|
+
): Promise<BrowserResourceDecision> {
|
|
326
|
+
for (const resourceRoute of policy.routes) {
|
|
327
|
+
if (matchesResourceRoute(resourceRoute.match, request)) {
|
|
328
|
+
return await resourceRoute.handle(request);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return { action: "block" };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function abortResourceRoute(route: Route): Promise<void> {
|
|
336
|
+
await route.abort("blockedbyclient");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function handleCdpResourceRequest(
|
|
340
|
+
session: CDPSession,
|
|
341
|
+
policy: BrowserResourcePolicy,
|
|
342
|
+
allowedMethods: ReadonlySet<BrowserResourceMethod>,
|
|
343
|
+
params: unknown,
|
|
344
|
+
): Promise<void> {
|
|
345
|
+
const paused = toCdpResourceRequest(params);
|
|
346
|
+
const requestId = paused?.requestId ?? getCdpPausedRequestId(params);
|
|
347
|
+
if (!requestId) return;
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
if (!paused || !allowedMethods.has(paused.request.method)) {
|
|
351
|
+
await session.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const decision = await decideResourceRequest(policy, paused.request);
|
|
356
|
+
switch (decision.action) {
|
|
357
|
+
case "continue":
|
|
358
|
+
await session.send("Fetch.continueRequest", { requestId });
|
|
359
|
+
return;
|
|
360
|
+
case "fulfill":
|
|
361
|
+
await session.send("Fetch.fulfillRequest", toCdpFulfillParams(requestId, decision));
|
|
362
|
+
return;
|
|
363
|
+
case "block":
|
|
364
|
+
await session.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
} catch {
|
|
368
|
+
await session
|
|
369
|
+
.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId })
|
|
370
|
+
.catch(() => undefined);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function handleCdpDocumentResponse(
|
|
375
|
+
session: CDPSession,
|
|
376
|
+
contentSecurityPolicy: string,
|
|
377
|
+
params: unknown,
|
|
378
|
+
): Promise<void> {
|
|
379
|
+
const requestId = getCdpPausedRequestId(params);
|
|
380
|
+
if (!requestId) return;
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const response = toCdpPausedDocumentResponse(params);
|
|
384
|
+
if (!response) throw new Error("CDP document response metadata is unavailable");
|
|
385
|
+
if (!response.hasBody) {
|
|
386
|
+
await session.send("Fetch.continueResponse", {
|
|
387
|
+
requestId,
|
|
388
|
+
responseCode: response.responseCode,
|
|
389
|
+
responseHeaders: [...response.responseHeaders],
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const body = await session.send("Fetch.getResponseBody", {
|
|
395
|
+
requestId: response.requestId,
|
|
396
|
+
});
|
|
397
|
+
await session.send(
|
|
398
|
+
"Fetch.fulfillRequest",
|
|
399
|
+
toCdpDocumentFulfillParams(response, body, contentSecurityPolicy),
|
|
400
|
+
);
|
|
401
|
+
} catch {
|
|
402
|
+
await session
|
|
403
|
+
.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId })
|
|
404
|
+
.catch(() => undefined);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function getCdpAuthRequiredRequestId(params: unknown): string | null {
|
|
409
|
+
if (!isRecord(params) || typeof params.requestId !== "string") {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return params.requestId;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isCdpProxyAuthChallenge(params: unknown): boolean {
|
|
417
|
+
return (
|
|
418
|
+
isRecord(params) &&
|
|
419
|
+
isRecord(params.authChallenge) &&
|
|
420
|
+
params.authChallenge.source === "Proxy"
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function handleCdpAuthRequired(
|
|
425
|
+
session: CDPSession,
|
|
426
|
+
proxy: PlaywrightProxy & { username: string; password: string },
|
|
427
|
+
authAttempts: Set<string>,
|
|
428
|
+
params: unknown,
|
|
429
|
+
): Promise<void> {
|
|
430
|
+
const requestId = getCdpAuthRequiredRequestId(params);
|
|
431
|
+
if (!requestId) return;
|
|
432
|
+
|
|
433
|
+
// Chromium can emit another challenge after rejected credentials. Cancel a
|
|
434
|
+
// retry so incorrect credentials fail promptly instead of looping forever.
|
|
435
|
+
const response =
|
|
436
|
+
isCdpProxyAuthChallenge(params) && !authAttempts.has(requestId)
|
|
437
|
+
? (() => {
|
|
438
|
+
authAttempts.add(requestId);
|
|
439
|
+
return {
|
|
440
|
+
authChallengeResponse: {
|
|
441
|
+
password: proxy.password,
|
|
442
|
+
response: "ProvideCredentials" as const,
|
|
443
|
+
username: proxy.username,
|
|
444
|
+
},
|
|
445
|
+
requestId,
|
|
446
|
+
};
|
|
447
|
+
})()
|
|
448
|
+
: {
|
|
449
|
+
authChallengeResponse: { response: "CancelAuth" as const },
|
|
450
|
+
requestId,
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
await session.send("Fetch.continueWithAuth", response).catch(() => undefined);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function createResourcePolicyHandler(
|
|
457
|
+
policy: BrowserResourcePolicy,
|
|
458
|
+
allowedMethods: ReadonlySet<BrowserResourceMethod>,
|
|
459
|
+
): (route: Route) => Promise<void> {
|
|
460
|
+
return async (route: Route): Promise<void> => {
|
|
461
|
+
const request = await toResourceRequest(route.request());
|
|
462
|
+
if (!request || !allowedMethods.has(request.method)) {
|
|
463
|
+
await abortResourceRoute(route);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const decision = await decideResourceRequest(policy, request);
|
|
468
|
+
switch (decision.action) {
|
|
469
|
+
case "continue":
|
|
470
|
+
await route.continue();
|
|
471
|
+
return;
|
|
472
|
+
case "fulfill":
|
|
473
|
+
await fulfillResourceRoute(route, decision);
|
|
474
|
+
return;
|
|
475
|
+
case "block":
|
|
476
|
+
await abortResourceRoute(route);
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
228
481
|
export type BrowserClientOptions = BrowserOptions & {
|
|
229
482
|
allowedHosts?: string[];
|
|
230
483
|
cdpUrl?: string;
|
|
231
484
|
executablePath?: string;
|
|
232
485
|
extraArgs?: string[];
|
|
486
|
+
serviceWorkers?: "allow" | "block";
|
|
233
487
|
};
|
|
234
488
|
|
|
235
489
|
type SupportedBrowserClient = {
|
|
@@ -288,7 +542,10 @@ function formatExpression<T>(fn: string | (() => T)): string {
|
|
|
288
542
|
return `(${fn.toString()})()`;
|
|
289
543
|
}
|
|
290
544
|
|
|
291
|
-
function toLaunchOptions(
|
|
545
|
+
function toLaunchOptions(
|
|
546
|
+
options: BrowserClientOptions,
|
|
547
|
+
proxy: LaunchOptions["proxy"],
|
|
548
|
+
): LaunchOptions {
|
|
292
549
|
return {
|
|
293
550
|
// `extraArgs` is optional, but playwright-extra's stealth evasions mutate
|
|
294
551
|
// `options.args` unguarded (navigator.webdriver does
|
|
@@ -299,7 +556,56 @@ function toLaunchOptions(options: BrowserClientOptions): LaunchOptions {
|
|
|
299
556
|
args: options.extraArgs ?? [],
|
|
300
557
|
executablePath: options.executablePath,
|
|
301
558
|
headless: options.headless ?? true,
|
|
302
|
-
proxy
|
|
559
|
+
proxy,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
type PlaywrightProxy = NonNullable<LaunchOptions["proxy"]>;
|
|
564
|
+
|
|
565
|
+
function hasProxyCredentials(
|
|
566
|
+
proxy: LaunchOptions["proxy"] | undefined,
|
|
567
|
+
): proxy is PlaywrightProxy & { username: string; password: string } {
|
|
568
|
+
return proxy?.username !== undefined && proxy.password !== undefined;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function toPlaywrightProxy(proxy: string | undefined): LaunchOptions["proxy"] {
|
|
572
|
+
if (!proxy) return undefined;
|
|
573
|
+
|
|
574
|
+
const schemeEnd = proxy.indexOf("://");
|
|
575
|
+
const authorityStart = schemeEnd >= 0 ? schemeEnd + 3 : 0;
|
|
576
|
+
const remainder = proxy.slice(authorityStart);
|
|
577
|
+
const authorityEnd = remainder.search(/[/?#]/);
|
|
578
|
+
const authority = remainder.slice(0, authorityEnd >= 0 ? authorityEnd : undefined);
|
|
579
|
+
if (!authority.includes("@")) {
|
|
580
|
+
return { server: proxy };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let parsed: URL;
|
|
584
|
+
try {
|
|
585
|
+
parsed = new URL(proxy);
|
|
586
|
+
} catch {
|
|
587
|
+
throw new ProviderError("Browser proxy URL is invalid", {
|
|
588
|
+
code: "BROWSER_PROXY_INVALID",
|
|
589
|
+
fix: "Use a valid proxy URL.",
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
let username: string;
|
|
594
|
+
let password: string;
|
|
595
|
+
try {
|
|
596
|
+
username = decodeURIComponent(parsed.username);
|
|
597
|
+
password = decodeURIComponent(parsed.password);
|
|
598
|
+
} catch {
|
|
599
|
+
throw new ProviderError("Browser proxy credentials use invalid percent-encoding", {
|
|
600
|
+
code: "BROWSER_PROXY_INVALID",
|
|
601
|
+
fix: "Percent-encode the proxy username and password as URL userinfo.",
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
server: `${parsed.protocol}//${parsed.host}`,
|
|
607
|
+
username,
|
|
608
|
+
password,
|
|
303
609
|
};
|
|
304
610
|
}
|
|
305
611
|
|
|
@@ -493,7 +799,10 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
493
799
|
readonly id = "main";
|
|
494
800
|
readonly pageId?: string;
|
|
495
801
|
|
|
496
|
-
constructor(
|
|
802
|
+
constructor(
|
|
803
|
+
private readonly page: Page,
|
|
804
|
+
private readonly proxy: LaunchOptions["proxy"] = undefined,
|
|
805
|
+
) {}
|
|
497
806
|
|
|
498
807
|
async goto(url: string): Promise<void> {
|
|
499
808
|
await this.page.goto(url);
|
|
@@ -507,6 +816,10 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
507
816
|
return await this.page.evaluate(fn);
|
|
508
817
|
}
|
|
509
818
|
|
|
819
|
+
async userAgent(): Promise<string> {
|
|
820
|
+
return await this.evaluate<string>("navigator.userAgent");
|
|
821
|
+
}
|
|
822
|
+
|
|
510
823
|
async waitForSelector(selector: string, options?: { timeout?: number }): Promise<void> {
|
|
511
824
|
await this.page.waitForSelector(selector, options);
|
|
512
825
|
}
|
|
@@ -558,43 +871,102 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
558
871
|
|
|
559
872
|
async withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T> {
|
|
560
873
|
const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
|
|
874
|
+
const policyHandler = createResourcePolicyHandler(policy, allowedMethods);
|
|
875
|
+
const context = this.page.context();
|
|
561
876
|
const handler = async (route: Route): Promise<void> => {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
877
|
+
try {
|
|
878
|
+
// Context routing sees a popup's first request before the late `page`
|
|
879
|
+
// event. Fail every secondary page closed so no redirect can escape
|
|
880
|
+
// the per-page CDP interception while that session is being attached.
|
|
881
|
+
if (route.request().frame().page() !== this.page) {
|
|
882
|
+
await abortResourceRoute(route);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
} catch {
|
|
886
|
+
await abortResourceRoute(route);
|
|
565
887
|
return;
|
|
566
888
|
}
|
|
567
889
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
890
|
+
await policyHandler(route);
|
|
891
|
+
};
|
|
892
|
+
const sessions = new Set<CDPSession>();
|
|
893
|
+
const pendingAttachments = new Set<Promise<void>>();
|
|
894
|
+
const proxy = hasProxyCredentials(this.proxy) ? this.proxy : undefined;
|
|
895
|
+
let active = true;
|
|
896
|
+
const attachPage = async (page: Page): Promise<void> => {
|
|
897
|
+
const session = await context.newCDPSession(page);
|
|
898
|
+
if (!active) {
|
|
899
|
+
await session.detach();
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
572
902
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
return;
|
|
578
|
-
case "block":
|
|
579
|
-
await route.abort("blockedbyclient");
|
|
580
|
-
return;
|
|
903
|
+
const onRequestPaused = (params: unknown) => {
|
|
904
|
+
if (isCdpResponseStage(params) && policy.documentContentSecurityPolicy) {
|
|
905
|
+
void handleCdpDocumentResponse(session, policy.documentContentSecurityPolicy, params);
|
|
906
|
+
return;
|
|
581
907
|
}
|
|
908
|
+
void handleCdpResourceRequest(session, policy, allowedMethods, params);
|
|
909
|
+
};
|
|
910
|
+
const authAttempts = new Set<string>();
|
|
911
|
+
const onAuthRequired = (params: unknown) => {
|
|
912
|
+
if (!proxy) return;
|
|
913
|
+
void handleCdpAuthRequired(session, proxy, authAttempts, params);
|
|
914
|
+
};
|
|
915
|
+
session.on("Fetch.requestPaused", onRequestPaused);
|
|
916
|
+
if (proxy) {
|
|
917
|
+
session.on("Fetch.authRequired", onAuthRequired);
|
|
582
918
|
}
|
|
583
|
-
|
|
584
|
-
|
|
919
|
+
try {
|
|
920
|
+
await session.send("Fetch.enable", {
|
|
921
|
+
...(proxy ? { handleAuthRequests: true } : {}),
|
|
922
|
+
patterns: resourcePolicyFetchPatterns(policy),
|
|
923
|
+
});
|
|
924
|
+
sessions.add(session);
|
|
925
|
+
} catch (error) {
|
|
926
|
+
session.off("Fetch.requestPaused", onRequestPaused);
|
|
927
|
+
if (proxy) {
|
|
928
|
+
session.off("Fetch.authRequired", onAuthRequired);
|
|
929
|
+
}
|
|
930
|
+
await session.detach().catch(() => undefined);
|
|
931
|
+
throw error;
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
const onPage = (page: Page) => {
|
|
935
|
+
const attachment = attachPage(page)
|
|
936
|
+
.catch(async () => {
|
|
937
|
+
await page.close().catch(() => undefined);
|
|
938
|
+
})
|
|
939
|
+
.finally(() => pendingAttachments.delete(attachment));
|
|
940
|
+
pendingAttachments.add(attachment);
|
|
585
941
|
};
|
|
586
942
|
|
|
587
|
-
await
|
|
943
|
+
await context.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
|
|
588
944
|
try {
|
|
589
|
-
|
|
945
|
+
context.on("page", onPage);
|
|
946
|
+
try {
|
|
947
|
+
await attachPage(this.page);
|
|
948
|
+
return await run();
|
|
949
|
+
} finally {
|
|
950
|
+
context.off("page", onPage);
|
|
951
|
+
}
|
|
590
952
|
} finally {
|
|
591
|
-
|
|
953
|
+
active = false;
|
|
954
|
+
await context.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
|
|
955
|
+
await Promise.allSettled(pendingAttachments);
|
|
956
|
+
await Promise.all(
|
|
957
|
+
[...sessions].map(async (session) => {
|
|
958
|
+
await session.send("Fetch.disable").catch(() => undefined);
|
|
959
|
+
await session.detach().catch(() => undefined);
|
|
960
|
+
}),
|
|
961
|
+
);
|
|
592
962
|
}
|
|
593
963
|
}
|
|
594
964
|
}
|
|
595
965
|
|
|
596
966
|
class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
597
967
|
private browser: import("playwright").Browser | null = null;
|
|
968
|
+
private parsedProxy: LaunchOptions["proxy"] | undefined;
|
|
969
|
+
private proxyParsed = false;
|
|
598
970
|
readonly engine = "playwright-stealth" satisfies BrowserEngine;
|
|
599
971
|
|
|
600
972
|
constructor(private readonly options: BrowserClientOptions = {}) {}
|
|
@@ -605,7 +977,11 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
605
977
|
}
|
|
606
978
|
|
|
607
979
|
const chromium = await loadChromiumLauncher(this.options);
|
|
608
|
-
|
|
980
|
+
if (!this.proxyParsed) {
|
|
981
|
+
this.parsedProxy = toPlaywrightProxy(this.options.proxy);
|
|
982
|
+
this.proxyParsed = true;
|
|
983
|
+
}
|
|
984
|
+
this.browser = await chromium.launch(toLaunchOptions(this.options, this.parsedProxy));
|
|
609
985
|
return this.browser;
|
|
610
986
|
}
|
|
611
987
|
|
|
@@ -613,7 +989,7 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
613
989
|
const browser = await this.ensureBrowser();
|
|
614
990
|
const page = await browser.newPage();
|
|
615
991
|
|
|
616
|
-
return new PlaywrightBrowserPage(page);
|
|
992
|
+
return new PlaywrightBrowserPage(page, this.parsedProxy);
|
|
617
993
|
}
|
|
618
994
|
|
|
619
995
|
async rawPage(): Promise<BrowserPageContract> {
|
|
@@ -625,9 +1001,9 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
625
1001
|
|
|
626
1002
|
async withIsolatedContext<T>(handler: (page: BrowserPageContract) => Promise<T>): Promise<T> {
|
|
627
1003
|
const browser = await this.ensureBrowser();
|
|
628
|
-
const context = await browser.newContext();
|
|
1004
|
+
const context = await browser.newContext({ serviceWorkers: this.options.serviceWorkers });
|
|
629
1005
|
const page = await context.newPage();
|
|
630
|
-
const browserPage = new PlaywrightBrowserPage(page);
|
|
1006
|
+
const browserPage = new PlaywrightBrowserPage(page, this.parsedProxy);
|
|
631
1007
|
|
|
632
1008
|
try {
|
|
633
1009
|
return await handler(browserPage);
|
|
@@ -1137,6 +1513,10 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1137
1513
|
return await this.evaluateWithContext<T>(fn);
|
|
1138
1514
|
}
|
|
1139
1515
|
|
|
1516
|
+
async userAgent(): Promise<string> {
|
|
1517
|
+
return await this.evaluate<string>("navigator.userAgent");
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1140
1520
|
async evaluateInFrame<T>(frameId: string, fn: string | (() => T)): Promise<T> {
|
|
1141
1521
|
await this.initialize();
|
|
1142
1522
|
const contextId = await this.getFrameExecutionContextId(frameId);
|
|
@@ -1329,7 +1709,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1329
1709
|
|
|
1330
1710
|
try {
|
|
1331
1711
|
await this.pageClient.send("Fetch.enable", {
|
|
1332
|
-
patterns:
|
|
1712
|
+
patterns: resourcePolicyFetchPatterns(policy),
|
|
1333
1713
|
});
|
|
1334
1714
|
} catch (error) {
|
|
1335
1715
|
unsubscribe();
|
|
@@ -1362,6 +1742,28 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1362
1742
|
}
|
|
1363
1743
|
|
|
1364
1744
|
try {
|
|
1745
|
+
if (isCdpResponseStage(params) && policy.documentContentSecurityPolicy) {
|
|
1746
|
+
const response = toCdpPausedDocumentResponse(params);
|
|
1747
|
+
if (!response) throw new Error("CDP document response metadata is unavailable");
|
|
1748
|
+
if (!response.hasBody) {
|
|
1749
|
+
await this.pageClient.send("Fetch.continueResponse", {
|
|
1750
|
+
requestId,
|
|
1751
|
+
responseCode: response.responseCode,
|
|
1752
|
+
responseHeaders: [...response.responseHeaders],
|
|
1753
|
+
});
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
const body = await this.pageClient.send("Fetch.getResponseBody", {
|
|
1758
|
+
requestId: response.requestId,
|
|
1759
|
+
});
|
|
1760
|
+
await this.pageClient.send(
|
|
1761
|
+
"Fetch.fulfillRequest",
|
|
1762
|
+
toCdpDocumentFulfillParams(response, body, policy.documentContentSecurityPolicy),
|
|
1763
|
+
);
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1365
1767
|
const parsed = toCdpResourceRequest(params);
|
|
1366
1768
|
if (!parsed || !allowedMethods.has(parsed.request.method)) {
|
|
1367
1769
|
await this.failCdpResourceRequest(requestId);
|
|
@@ -1375,6 +1777,11 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1375
1777
|
|
|
1376
1778
|
const decision = await resourceRoute.handle(parsed.request);
|
|
1377
1779
|
switch (decision.action) {
|
|
1780
|
+
case "continue":
|
|
1781
|
+
await this.pageClient.send("Fetch.continueRequest", {
|
|
1782
|
+
requestId: parsed.requestId,
|
|
1783
|
+
});
|
|
1784
|
+
return;
|
|
1378
1785
|
case "fulfill":
|
|
1379
1786
|
await this.pageClient.send(
|
|
1380
1787
|
"Fetch.fulfillRequest",
|