browserclaw 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +206 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +206 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1276,7 +1276,8 @@ function isWebSocketUrl(url) {
|
|
|
1276
1276
|
}
|
|
1277
1277
|
}
|
|
1278
1278
|
function isLoopbackHost(hostname) {
|
|
1279
|
-
|
|
1279
|
+
const h = hostname.replace(/\.+$/, "");
|
|
1280
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "[::1]";
|
|
1280
1281
|
}
|
|
1281
1282
|
var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
1282
1283
|
function hasProxyEnvConfigured(env = process.env) {
|
|
@@ -1928,6 +1929,56 @@ function bumpDownloadArmId() {
|
|
|
1928
1929
|
nextDownloadArmId += 1;
|
|
1929
1930
|
return nextDownloadArmId;
|
|
1930
1931
|
}
|
|
1932
|
+
var BlockedBrowserTargetError = class extends Error {
|
|
1933
|
+
constructor() {
|
|
1934
|
+
super("Browser target is unavailable after SSRF policy blocked its navigation.");
|
|
1935
|
+
this.name = "BlockedBrowserTargetError";
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
var blockedTargetsByCdpUrl = /* @__PURE__ */ new Set();
|
|
1939
|
+
var blockedPageRefsByCdpUrl = /* @__PURE__ */ new Map();
|
|
1940
|
+
function blockedTargetKey(cdpUrl, targetId) {
|
|
1941
|
+
return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
|
|
1942
|
+
}
|
|
1943
|
+
function isBlockedTarget(cdpUrl, targetId) {
|
|
1944
|
+
const normalized = targetId?.trim() ?? "";
|
|
1945
|
+
if (normalized === "") return false;
|
|
1946
|
+
return blockedTargetsByCdpUrl.has(blockedTargetKey(cdpUrl, normalized));
|
|
1947
|
+
}
|
|
1948
|
+
function markTargetBlocked(cdpUrl, targetId) {
|
|
1949
|
+
const normalized = targetId?.trim() ?? "";
|
|
1950
|
+
if (normalized === "") return;
|
|
1951
|
+
blockedTargetsByCdpUrl.add(blockedTargetKey(cdpUrl, normalized));
|
|
1952
|
+
}
|
|
1953
|
+
function clearBlockedTarget(cdpUrl, targetId) {
|
|
1954
|
+
const normalized = targetId?.trim() ?? "";
|
|
1955
|
+
if (normalized === "") return;
|
|
1956
|
+
blockedTargetsByCdpUrl.delete(blockedTargetKey(cdpUrl, normalized));
|
|
1957
|
+
}
|
|
1958
|
+
function hasBlockedTargetsForCdpUrl(cdpUrl) {
|
|
1959
|
+
const prefix = `${normalizeCdpUrl(cdpUrl)}::`;
|
|
1960
|
+
for (const key of blockedTargetsByCdpUrl) {
|
|
1961
|
+
if (key.startsWith(prefix)) return true;
|
|
1962
|
+
}
|
|
1963
|
+
return false;
|
|
1964
|
+
}
|
|
1965
|
+
function blockedPageRefsForCdpUrl(cdpUrl) {
|
|
1966
|
+
const normalized = normalizeCdpUrl(cdpUrl);
|
|
1967
|
+
const existing = blockedPageRefsByCdpUrl.get(normalized);
|
|
1968
|
+
if (existing) return existing;
|
|
1969
|
+
const created = /* @__PURE__ */ new WeakSet();
|
|
1970
|
+
blockedPageRefsByCdpUrl.set(normalized, created);
|
|
1971
|
+
return created;
|
|
1972
|
+
}
|
|
1973
|
+
function isBlockedPageRef(cdpUrl, page) {
|
|
1974
|
+
return blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.has(page) ?? false;
|
|
1975
|
+
}
|
|
1976
|
+
function markPageRefBlocked(cdpUrl, page) {
|
|
1977
|
+
blockedPageRefsForCdpUrl(cdpUrl).add(page);
|
|
1978
|
+
}
|
|
1979
|
+
function clearBlockedPageRef(cdpUrl, page) {
|
|
1980
|
+
blockedPageRefsByCdpUrl.get(normalizeCdpUrl(cdpUrl))?.delete(page);
|
|
1981
|
+
}
|
|
1931
1982
|
function ensureContextState(context) {
|
|
1932
1983
|
const existing = contextStates.get(context);
|
|
1933
1984
|
if (existing) return existing;
|
|
@@ -2368,11 +2419,43 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
2368
2419
|
if (!resolvedViaCdp && pages.length === 1) return pages[0] ?? null;
|
|
2369
2420
|
return null;
|
|
2370
2421
|
}
|
|
2422
|
+
async function partitionAccessiblePages(opts) {
|
|
2423
|
+
const accessible = [];
|
|
2424
|
+
let blockedCount = 0;
|
|
2425
|
+
for (const page of opts.pages) {
|
|
2426
|
+
if (isBlockedPageRef(opts.cdpUrl, page)) {
|
|
2427
|
+
blockedCount += 1;
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
const targetId = await pageTargetId(page).catch(() => null);
|
|
2431
|
+
if (targetId === null || targetId === "") {
|
|
2432
|
+
if (hasBlockedTargetsForCdpUrl(opts.cdpUrl)) {
|
|
2433
|
+
blockedCount += 1;
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
accessible.push(page);
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2439
|
+
if (isBlockedTarget(opts.cdpUrl, targetId)) {
|
|
2440
|
+
blockedCount += 1;
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
accessible.push(page);
|
|
2444
|
+
}
|
|
2445
|
+
return { accessible, blockedCount };
|
|
2446
|
+
}
|
|
2371
2447
|
async function getPageForTargetId(opts) {
|
|
2448
|
+
if (opts.targetId !== void 0 && opts.targetId !== "" && isBlockedTarget(opts.cdpUrl, opts.targetId))
|
|
2449
|
+
throw new BlockedBrowserTargetError();
|
|
2372
2450
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
2373
2451
|
const pages = getAllPages(browser);
|
|
2374
2452
|
if (!pages.length) throw new Error("No pages available in the connected browser.");
|
|
2375
|
-
const
|
|
2453
|
+
const { accessible, blockedCount } = await partitionAccessiblePages({ cdpUrl: opts.cdpUrl, pages });
|
|
2454
|
+
if (!accessible.length) {
|
|
2455
|
+
if (blockedCount > 0) throw new BlockedBrowserTargetError();
|
|
2456
|
+
throw new Error("No pages available in the connected browser.");
|
|
2457
|
+
}
|
|
2458
|
+
const first = accessible[0];
|
|
2376
2459
|
if (opts.targetId === void 0 || opts.targetId === "") return first;
|
|
2377
2460
|
const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
2378
2461
|
if (!found) {
|
|
@@ -2381,6 +2464,10 @@ async function getPageForTargetId(opts) {
|
|
|
2381
2464
|
`Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`
|
|
2382
2465
|
);
|
|
2383
2466
|
}
|
|
2467
|
+
if (isBlockedPageRef(opts.cdpUrl, found)) throw new BlockedBrowserTargetError();
|
|
2468
|
+
const foundTargetId = await pageTargetId(found).catch(() => null);
|
|
2469
|
+
if (foundTargetId !== null && foundTargetId !== "" && isBlockedTarget(opts.cdpUrl, foundTargetId))
|
|
2470
|
+
throw new BlockedBrowserTargetError();
|
|
2384
2471
|
return found;
|
|
2385
2472
|
}
|
|
2386
2473
|
async function resolvePageByTargetIdOrThrow(opts) {
|
|
@@ -3508,6 +3595,82 @@ function isRetryableNavigateError(err) {
|
|
|
3508
3595
|
const msg = typeof err === "string" ? err.toLowerCase() : err instanceof Error ? err.message.toLowerCase() : "";
|
|
3509
3596
|
return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
|
|
3510
3597
|
}
|
|
3598
|
+
function isPolicyDenyNavigationError(err) {
|
|
3599
|
+
return err instanceof InvalidBrowserNavigationUrlError;
|
|
3600
|
+
}
|
|
3601
|
+
function isTopLevelNavigationRequest(page, request) {
|
|
3602
|
+
if (!request.isNavigationRequest()) return false;
|
|
3603
|
+
try {
|
|
3604
|
+
return request.frame() === page.mainFrame();
|
|
3605
|
+
} catch {
|
|
3606
|
+
return true;
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
async function closeBlockedNavigationTarget(opts) {
|
|
3610
|
+
markPageRefBlocked(opts.cdpUrl, opts.page);
|
|
3611
|
+
const resolvedTargetId = await pageTargetId(opts.page).catch(() => null);
|
|
3612
|
+
const fallbackTargetId = opts.targetId?.trim() ?? "";
|
|
3613
|
+
const targetIdToBlock = resolvedTargetId ?? fallbackTargetId;
|
|
3614
|
+
if (targetIdToBlock) markTargetBlocked(opts.cdpUrl, targetIdToBlock);
|
|
3615
|
+
await opts.page.close().catch((e) => {
|
|
3616
|
+
console.warn("[browserclaw] failed to close blocked page", e);
|
|
3617
|
+
});
|
|
3618
|
+
}
|
|
3619
|
+
async function assertPageNavigationCompletedSafely(opts) {
|
|
3620
|
+
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
|
|
3621
|
+
try {
|
|
3622
|
+
await assertBrowserNavigationRedirectChainAllowed({ request: opts.response?.request(), ...navigationPolicy });
|
|
3623
|
+
await assertBrowserNavigationResultAllowed({ url: opts.page.url(), ...navigationPolicy });
|
|
3624
|
+
} catch (err) {
|
|
3625
|
+
if (isPolicyDenyNavigationError(err))
|
|
3626
|
+
await closeBlockedNavigationTarget({ cdpUrl: opts.cdpUrl, page: opts.page, targetId: opts.targetId });
|
|
3627
|
+
throw err;
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
async function gotoPageWithNavigationGuard(opts) {
|
|
3631
|
+
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
|
|
3632
|
+
const state = { blocked: null };
|
|
3633
|
+
const handler = async (route, request) => {
|
|
3634
|
+
if (state.blocked !== null) {
|
|
3635
|
+
await route.abort().catch((e) => {
|
|
3636
|
+
console.warn("[browserclaw] route abort failed", e);
|
|
3637
|
+
});
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
if (!isTopLevelNavigationRequest(opts.page, request)) {
|
|
3641
|
+
await route.continue();
|
|
3642
|
+
return;
|
|
3643
|
+
}
|
|
3644
|
+
try {
|
|
3645
|
+
await assertBrowserNavigationAllowed({ url: request.url(), ...navigationPolicy });
|
|
3646
|
+
} catch (err) {
|
|
3647
|
+
if (isPolicyDenyNavigationError(err)) {
|
|
3648
|
+
state.blocked = err;
|
|
3649
|
+
await route.abort().catch((e) => {
|
|
3650
|
+
console.warn("[browserclaw] route abort failed", e);
|
|
3651
|
+
});
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
throw err;
|
|
3655
|
+
}
|
|
3656
|
+
await route.continue();
|
|
3657
|
+
};
|
|
3658
|
+
await opts.page.route("**", handler);
|
|
3659
|
+
try {
|
|
3660
|
+
const response = await opts.page.goto(opts.url, { timeout: opts.timeoutMs });
|
|
3661
|
+
if (state.blocked !== null) throw state.blocked;
|
|
3662
|
+
return response;
|
|
3663
|
+
} catch (err) {
|
|
3664
|
+
if (state.blocked !== null) throw state.blocked;
|
|
3665
|
+
throw err;
|
|
3666
|
+
} finally {
|
|
3667
|
+
await opts.page.unroute("**", handler).catch((e) => {
|
|
3668
|
+
console.warn("[browserclaw] route unroute failed", e);
|
|
3669
|
+
});
|
|
3670
|
+
if (state.blocked !== null)
|
|
3671
|
+
await closeBlockedNavigationTarget({ cdpUrl: opts.cdpUrl, page: opts.page, targetId: opts.targetId });
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3511
3674
|
async function navigateViaPlaywright(opts) {
|
|
3512
3675
|
const url = opts.url.trim();
|
|
3513
3676
|
if (!url) throw new Error("url is required");
|
|
@@ -3516,7 +3679,14 @@ async function navigateViaPlaywright(opts) {
|
|
|
3516
3679
|
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
|
|
3517
3680
|
let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3518
3681
|
ensurePageState(page);
|
|
3519
|
-
const navigate = async () => await
|
|
3682
|
+
const navigate = async () => await gotoPageWithNavigationGuard({
|
|
3683
|
+
cdpUrl: opts.cdpUrl,
|
|
3684
|
+
page,
|
|
3685
|
+
url,
|
|
3686
|
+
timeoutMs: timeout,
|
|
3687
|
+
ssrfPolicy: policy,
|
|
3688
|
+
targetId: opts.targetId
|
|
3689
|
+
});
|
|
3520
3690
|
let response;
|
|
3521
3691
|
try {
|
|
3522
3692
|
response = await navigate();
|
|
@@ -3530,21 +3700,23 @@ async function navigateViaPlaywright(opts) {
|
|
|
3530
3700
|
ensurePageState(page);
|
|
3531
3701
|
response = await navigate();
|
|
3532
3702
|
}
|
|
3533
|
-
await
|
|
3534
|
-
|
|
3535
|
-
|
|
3703
|
+
await assertPageNavigationCompletedSafely({
|
|
3704
|
+
cdpUrl: opts.cdpUrl,
|
|
3705
|
+
page,
|
|
3706
|
+
response,
|
|
3707
|
+
ssrfPolicy: policy,
|
|
3708
|
+
targetId: opts.targetId
|
|
3536
3709
|
});
|
|
3537
|
-
|
|
3538
|
-
await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
|
|
3539
|
-
return { url: finalUrl };
|
|
3710
|
+
return { url: page.url() };
|
|
3540
3711
|
}
|
|
3541
3712
|
async function listPagesViaPlaywright(opts) {
|
|
3542
3713
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3543
3714
|
const pages = getAllPages(browser);
|
|
3544
3715
|
const results = [];
|
|
3545
3716
|
for (const page of pages) {
|
|
3717
|
+
if (isBlockedPageRef(opts.cdpUrl, page)) continue;
|
|
3546
3718
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3547
|
-
if (tid !== null && tid !== "")
|
|
3719
|
+
if (tid !== null && tid !== "" && !isBlockedTarget(opts.cdpUrl, tid))
|
|
3548
3720
|
results.push({
|
|
3549
3721
|
targetId: tid,
|
|
3550
3722
|
title: await page.title().catch(() => ""),
|
|
@@ -3560,18 +3732,35 @@ async function createPageViaPlaywright(opts) {
|
|
|
3560
3732
|
ensureContextState(context);
|
|
3561
3733
|
const page = await context.newPage();
|
|
3562
3734
|
ensurePageState(page);
|
|
3735
|
+
clearBlockedPageRef(opts.cdpUrl, page);
|
|
3736
|
+
const createdTargetId = await pageTargetId(page).catch(() => null);
|
|
3737
|
+
clearBlockedTarget(opts.cdpUrl, createdTargetId ?? void 0);
|
|
3563
3738
|
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
3564
3739
|
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3565
3740
|
if (targetUrl !== "about:blank") {
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3741
|
+
await assertBrowserNavigationAllowed({ url: targetUrl, ...withBrowserNavigationPolicy(policy) });
|
|
3742
|
+
let response = null;
|
|
3743
|
+
try {
|
|
3744
|
+
response = await gotoPageWithNavigationGuard({
|
|
3745
|
+
cdpUrl: opts.cdpUrl,
|
|
3746
|
+
page,
|
|
3747
|
+
url: targetUrl,
|
|
3748
|
+
timeoutMs: 3e4,
|
|
3749
|
+
ssrfPolicy: policy,
|
|
3750
|
+
targetId: createdTargetId ?? void 0
|
|
3751
|
+
});
|
|
3752
|
+
} catch (err) {
|
|
3753
|
+
if (isPolicyDenyNavigationError(err) || err instanceof BlockedBrowserTargetError) throw err;
|
|
3754
|
+
}
|
|
3755
|
+
await assertPageNavigationCompletedSafely({
|
|
3756
|
+
cdpUrl: opts.cdpUrl,
|
|
3757
|
+
page,
|
|
3758
|
+
response,
|
|
3759
|
+
ssrfPolicy: policy,
|
|
3760
|
+
targetId: createdTargetId ?? void 0
|
|
3571
3761
|
});
|
|
3572
|
-
await assertBrowserNavigationResultAllowed({ url: page.url(), ...navigationPolicy });
|
|
3573
3762
|
}
|
|
3574
|
-
const tid = await pageTargetId(page).catch(() => null);
|
|
3763
|
+
const tid = createdTargetId ?? await pageTargetId(page).catch(() => null);
|
|
3575
3764
|
if (tid === null || tid === "") throw new Error("Failed to get targetId for new page");
|
|
3576
3765
|
return {
|
|
3577
3766
|
targetId: tid,
|