@apifuse/provider-sdk 2.2.0-beta.31 → 2.2.0-beta.33
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 +9 -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/browser.d.ts +1 -0
- package/dist/runtime/browser.js +344 -27
- package/dist/runtime/choice.d.ts +0 -1
- package/dist/runtime/choice.js +1 -78
- 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 +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +526 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +46 -13
- package/dist/runtime/resolver-vendors/types.d.ts +1 -1
- package/dist/runtime/resolver-vendors/types.js +6 -3
- package/dist/runtime/resolver.d.ts +1 -1
- package/dist/runtime/resolver.js +19 -4
- package/dist/server/serve-implementation.js +1 -2
- package/dist/types.d.ts +9 -1
- package/package.json +1 -1
- package/src/error-resolution.ts +0 -1
- package/src/index.ts +0 -1
- package/src/provider.ts +0 -1
- package/src/runtime/browser.ts +430 -31
- package/src/runtime/choice.ts +1 -91
- package/src/runtime/resolver-vendors/browser.ts +82 -15
- package/src/runtime/resolver-vendors/capsolver.ts +700 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +43 -8
- package/src/runtime/resolver-vendors/types.ts +6 -3
- package/src/runtime/resolver.ts +21 -6
- package/src/server/serve-implementation.ts +0 -2
- package/src/types.ts +10 -1
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);
|
|
@@ -558,43 +867,102 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
558
867
|
|
|
559
868
|
async withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T> {
|
|
560
869
|
const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
|
|
870
|
+
const policyHandler = createResourcePolicyHandler(policy, allowedMethods);
|
|
871
|
+
const context = this.page.context();
|
|
561
872
|
const handler = async (route: Route): Promise<void> => {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
873
|
+
try {
|
|
874
|
+
// Context routing sees a popup's first request before the late `page`
|
|
875
|
+
// event. Fail every secondary page closed so no redirect can escape
|
|
876
|
+
// the per-page CDP interception while that session is being attached.
|
|
877
|
+
if (route.request().frame().page() !== this.page) {
|
|
878
|
+
await abortResourceRoute(route);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
} catch {
|
|
882
|
+
await abortResourceRoute(route);
|
|
565
883
|
return;
|
|
566
884
|
}
|
|
567
885
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
886
|
+
await policyHandler(route);
|
|
887
|
+
};
|
|
888
|
+
const sessions = new Set<CDPSession>();
|
|
889
|
+
const pendingAttachments = new Set<Promise<void>>();
|
|
890
|
+
const proxy = hasProxyCredentials(this.proxy) ? this.proxy : undefined;
|
|
891
|
+
let active = true;
|
|
892
|
+
const attachPage = async (page: Page): Promise<void> => {
|
|
893
|
+
const session = await context.newCDPSession(page);
|
|
894
|
+
if (!active) {
|
|
895
|
+
await session.detach();
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
572
898
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
return;
|
|
578
|
-
case "block":
|
|
579
|
-
await route.abort("blockedbyclient");
|
|
580
|
-
return;
|
|
899
|
+
const onRequestPaused = (params: unknown) => {
|
|
900
|
+
if (isCdpResponseStage(params) && policy.documentContentSecurityPolicy) {
|
|
901
|
+
void handleCdpDocumentResponse(session, policy.documentContentSecurityPolicy, params);
|
|
902
|
+
return;
|
|
581
903
|
}
|
|
904
|
+
void handleCdpResourceRequest(session, policy, allowedMethods, params);
|
|
905
|
+
};
|
|
906
|
+
const authAttempts = new Set<string>();
|
|
907
|
+
const onAuthRequired = (params: unknown) => {
|
|
908
|
+
if (!proxy) return;
|
|
909
|
+
void handleCdpAuthRequired(session, proxy, authAttempts, params);
|
|
910
|
+
};
|
|
911
|
+
session.on("Fetch.requestPaused", onRequestPaused);
|
|
912
|
+
if (proxy) {
|
|
913
|
+
session.on("Fetch.authRequired", onAuthRequired);
|
|
582
914
|
}
|
|
583
|
-
|
|
584
|
-
|
|
915
|
+
try {
|
|
916
|
+
await session.send("Fetch.enable", {
|
|
917
|
+
...(proxy ? { handleAuthRequests: true } : {}),
|
|
918
|
+
patterns: resourcePolicyFetchPatterns(policy),
|
|
919
|
+
});
|
|
920
|
+
sessions.add(session);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
session.off("Fetch.requestPaused", onRequestPaused);
|
|
923
|
+
if (proxy) {
|
|
924
|
+
session.off("Fetch.authRequired", onAuthRequired);
|
|
925
|
+
}
|
|
926
|
+
await session.detach().catch(() => undefined);
|
|
927
|
+
throw error;
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
const onPage = (page: Page) => {
|
|
931
|
+
const attachment = attachPage(page)
|
|
932
|
+
.catch(async () => {
|
|
933
|
+
await page.close().catch(() => undefined);
|
|
934
|
+
})
|
|
935
|
+
.finally(() => pendingAttachments.delete(attachment));
|
|
936
|
+
pendingAttachments.add(attachment);
|
|
585
937
|
};
|
|
586
938
|
|
|
587
|
-
await
|
|
939
|
+
await context.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
|
|
588
940
|
try {
|
|
589
|
-
|
|
941
|
+
context.on("page", onPage);
|
|
942
|
+
try {
|
|
943
|
+
await attachPage(this.page);
|
|
944
|
+
return await run();
|
|
945
|
+
} finally {
|
|
946
|
+
context.off("page", onPage);
|
|
947
|
+
}
|
|
590
948
|
} finally {
|
|
591
|
-
|
|
949
|
+
active = false;
|
|
950
|
+
await context.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
|
|
951
|
+
await Promise.allSettled(pendingAttachments);
|
|
952
|
+
await Promise.all(
|
|
953
|
+
[...sessions].map(async (session) => {
|
|
954
|
+
await session.send("Fetch.disable").catch(() => undefined);
|
|
955
|
+
await session.detach().catch(() => undefined);
|
|
956
|
+
}),
|
|
957
|
+
);
|
|
592
958
|
}
|
|
593
959
|
}
|
|
594
960
|
}
|
|
595
961
|
|
|
596
962
|
class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
597
963
|
private browser: import("playwright").Browser | null = null;
|
|
964
|
+
private parsedProxy: LaunchOptions["proxy"] | undefined;
|
|
965
|
+
private proxyParsed = false;
|
|
598
966
|
readonly engine = "playwright-stealth" satisfies BrowserEngine;
|
|
599
967
|
|
|
600
968
|
constructor(private readonly options: BrowserClientOptions = {}) {}
|
|
@@ -605,7 +973,11 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
605
973
|
}
|
|
606
974
|
|
|
607
975
|
const chromium = await loadChromiumLauncher(this.options);
|
|
608
|
-
|
|
976
|
+
if (!this.proxyParsed) {
|
|
977
|
+
this.parsedProxy = toPlaywrightProxy(this.options.proxy);
|
|
978
|
+
this.proxyParsed = true;
|
|
979
|
+
}
|
|
980
|
+
this.browser = await chromium.launch(toLaunchOptions(this.options, this.parsedProxy));
|
|
609
981
|
return this.browser;
|
|
610
982
|
}
|
|
611
983
|
|
|
@@ -613,7 +985,7 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
613
985
|
const browser = await this.ensureBrowser();
|
|
614
986
|
const page = await browser.newPage();
|
|
615
987
|
|
|
616
|
-
return new PlaywrightBrowserPage(page);
|
|
988
|
+
return new PlaywrightBrowserPage(page, this.parsedProxy);
|
|
617
989
|
}
|
|
618
990
|
|
|
619
991
|
async rawPage(): Promise<BrowserPageContract> {
|
|
@@ -625,9 +997,9 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
625
997
|
|
|
626
998
|
async withIsolatedContext<T>(handler: (page: BrowserPageContract) => Promise<T>): Promise<T> {
|
|
627
999
|
const browser = await this.ensureBrowser();
|
|
628
|
-
const context = await browser.newContext();
|
|
1000
|
+
const context = await browser.newContext({ serviceWorkers: this.options.serviceWorkers });
|
|
629
1001
|
const page = await context.newPage();
|
|
630
|
-
const browserPage = new PlaywrightBrowserPage(page);
|
|
1002
|
+
const browserPage = new PlaywrightBrowserPage(page, this.parsedProxy);
|
|
631
1003
|
|
|
632
1004
|
try {
|
|
633
1005
|
return await handler(browserPage);
|
|
@@ -1329,7 +1701,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1329
1701
|
|
|
1330
1702
|
try {
|
|
1331
1703
|
await this.pageClient.send("Fetch.enable", {
|
|
1332
|
-
patterns:
|
|
1704
|
+
patterns: resourcePolicyFetchPatterns(policy),
|
|
1333
1705
|
});
|
|
1334
1706
|
} catch (error) {
|
|
1335
1707
|
unsubscribe();
|
|
@@ -1362,6 +1734,28 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1362
1734
|
}
|
|
1363
1735
|
|
|
1364
1736
|
try {
|
|
1737
|
+
if (isCdpResponseStage(params) && policy.documentContentSecurityPolicy) {
|
|
1738
|
+
const response = toCdpPausedDocumentResponse(params);
|
|
1739
|
+
if (!response) throw new Error("CDP document response metadata is unavailable");
|
|
1740
|
+
if (!response.hasBody) {
|
|
1741
|
+
await this.pageClient.send("Fetch.continueResponse", {
|
|
1742
|
+
requestId,
|
|
1743
|
+
responseCode: response.responseCode,
|
|
1744
|
+
responseHeaders: [...response.responseHeaders],
|
|
1745
|
+
});
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
const body = await this.pageClient.send("Fetch.getResponseBody", {
|
|
1750
|
+
requestId: response.requestId,
|
|
1751
|
+
});
|
|
1752
|
+
await this.pageClient.send(
|
|
1753
|
+
"Fetch.fulfillRequest",
|
|
1754
|
+
toCdpDocumentFulfillParams(response, body, policy.documentContentSecurityPolicy),
|
|
1755
|
+
);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1365
1759
|
const parsed = toCdpResourceRequest(params);
|
|
1366
1760
|
if (!parsed || !allowedMethods.has(parsed.request.method)) {
|
|
1367
1761
|
await this.failCdpResourceRequest(requestId);
|
|
@@ -1375,6 +1769,11 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1375
1769
|
|
|
1376
1770
|
const decision = await resourceRoute.handle(parsed.request);
|
|
1377
1771
|
switch (decision.action) {
|
|
1772
|
+
case "continue":
|
|
1773
|
+
await this.pageClient.send("Fetch.continueRequest", {
|
|
1774
|
+
requestId: parsed.requestId,
|
|
1775
|
+
});
|
|
1776
|
+
return;
|
|
1378
1777
|
case "fulfill":
|
|
1379
1778
|
await this.pageClient.send(
|
|
1380
1779
|
"Fetch.fulfillRequest",
|