browserclaw 0.3.8 → 0.3.10
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 +87 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +87 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -382,6 +382,51 @@ async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
|
|
|
382
382
|
clearTimeout(t);
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
|
+
async function isChromeCdpReady(cdpUrl, timeoutMs = 500, handshakeTimeoutMs = 800) {
|
|
386
|
+
const wsUrl = await getChromeWebSocketUrl(cdpUrl, timeoutMs);
|
|
387
|
+
if (!wsUrl) return false;
|
|
388
|
+
return await canRunCdpHealthCommand(wsUrl, handshakeTimeoutMs);
|
|
389
|
+
}
|
|
390
|
+
async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
|
|
391
|
+
return new Promise((resolve2) => {
|
|
392
|
+
let settled = false;
|
|
393
|
+
const finish = (value) => {
|
|
394
|
+
if (settled) return;
|
|
395
|
+
settled = true;
|
|
396
|
+
clearTimeout(timer);
|
|
397
|
+
try {
|
|
398
|
+
ws.close();
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
resolve2(value);
|
|
402
|
+
};
|
|
403
|
+
const timer = setTimeout(() => finish(false), Math.max(50, timeoutMs + 25));
|
|
404
|
+
let ws;
|
|
405
|
+
try {
|
|
406
|
+
ws = new WebSocket(wsUrl);
|
|
407
|
+
} catch {
|
|
408
|
+
finish(false);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
ws.onopen = () => {
|
|
412
|
+
try {
|
|
413
|
+
ws.send(JSON.stringify({ id: 1, method: "Browser.getVersion" }));
|
|
414
|
+
} catch {
|
|
415
|
+
finish(false);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
ws.onmessage = (event) => {
|
|
419
|
+
try {
|
|
420
|
+
const parsed = JSON.parse(String(event.data));
|
|
421
|
+
if (parsed?.id !== 1) return;
|
|
422
|
+
finish(Boolean(parsed.result && typeof parsed.result === "object"));
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
ws.onerror = () => finish(false);
|
|
427
|
+
ws.onclose = () => finish(false);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
385
430
|
async function launchChrome(opts = {}) {
|
|
386
431
|
const cdpPort = opts.cdpPort ?? DEFAULT_CDP_PORT;
|
|
387
432
|
await ensurePortAvailable(cdpPort);
|
|
@@ -456,18 +501,30 @@ async function launchChrome(opts = {}) {
|
|
|
456
501
|
}
|
|
457
502
|
const proc = spawnChrome();
|
|
458
503
|
const cdpUrl = `http://127.0.0.1:${cdpPort}`;
|
|
504
|
+
const stderrChunks = [];
|
|
505
|
+
const onStderr = (chunk) => {
|
|
506
|
+
stderrChunks.push(chunk);
|
|
507
|
+
};
|
|
508
|
+
proc.stderr?.on("data", onStderr);
|
|
459
509
|
const readyDeadline = Date.now() + 15e3;
|
|
460
510
|
while (Date.now() < readyDeadline) {
|
|
461
511
|
if (await isChromeReachable(cdpUrl, 500)) break;
|
|
462
512
|
await new Promise((r) => setTimeout(r, 200));
|
|
463
513
|
}
|
|
464
514
|
if (!await isChromeReachable(cdpUrl, 500)) {
|
|
515
|
+
const stderrOutput = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
516
|
+
const stderrHint = stderrOutput ? `
|
|
517
|
+
Chrome stderr:
|
|
518
|
+
${stderrOutput.slice(0, 2e3)}` : "";
|
|
519
|
+
const sandboxHint = process.platform === "linux" && !opts.noSandbox ? "\nHint: If running in a container or as root, try setting noSandbox: true." : "";
|
|
465
520
|
try {
|
|
466
521
|
proc.kill("SIGKILL");
|
|
467
522
|
} catch {
|
|
468
523
|
}
|
|
469
|
-
throw new Error(`Failed to start Chrome CDP on port ${cdpPort}
|
|
524
|
+
throw new Error(`Failed to start Chrome CDP on port ${cdpPort}.${sandboxHint}${stderrHint}`);
|
|
470
525
|
}
|
|
526
|
+
proc.stderr?.off("data", onStderr);
|
|
527
|
+
stderrChunks.length = 0;
|
|
471
528
|
return {
|
|
472
529
|
pid: proc.pid ?? -1,
|
|
473
530
|
exe,
|
|
@@ -1232,7 +1289,8 @@ async function assertBrowserNavigationAllowed(opts) {
|
|
|
1232
1289
|
const hostname = parsed.hostname.toLowerCase();
|
|
1233
1290
|
if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
|
|
1234
1291
|
}
|
|
1235
|
-
|
|
1292
|
+
const ipOpts = { allowRfc2544BenchmarkRange: policy?.allowRfc2544BenchmarkRange };
|
|
1293
|
+
if (await isInternalUrlResolved(rawUrl, opts.lookupFn, ipOpts)) {
|
|
1236
1294
|
throw new InvalidBrowserNavigationUrlError(
|
|
1237
1295
|
`Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1238
1296
|
);
|
|
@@ -1347,7 +1405,7 @@ function isUnsupportedIPv4Literal(ip) {
|
|
|
1347
1405
|
if (!parts.every(isStrictDecimalOctet)) return true;
|
|
1348
1406
|
return false;
|
|
1349
1407
|
}
|
|
1350
|
-
function isInternalIP(ip) {
|
|
1408
|
+
function isInternalIP(ip, opts) {
|
|
1351
1409
|
if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
|
|
1352
1410
|
if (/^127\./.test(ip)) return true;
|
|
1353
1411
|
if (/^10\./.test(ip)) return true;
|
|
@@ -1356,6 +1414,7 @@ function isInternalIP(ip) {
|
|
|
1356
1414
|
if (/^169\.254\./.test(ip)) return true;
|
|
1357
1415
|
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
|
|
1358
1416
|
if (ip === "0.0.0.0") return true;
|
|
1417
|
+
if (!opts?.allowRfc2544BenchmarkRange && /^198\.1[89]\./.test(ip)) return true;
|
|
1359
1418
|
const lower = ip.toLowerCase();
|
|
1360
1419
|
if (lower === "::1") return true;
|
|
1361
1420
|
if (lower.startsWith("fe80:")) return true;
|
|
@@ -1364,11 +1423,11 @@ function isInternalIP(ip) {
|
|
|
1364
1423
|
const embedded = extractEmbeddedIPv4(lower);
|
|
1365
1424
|
if (embedded !== null) {
|
|
1366
1425
|
if (embedded === "") return true;
|
|
1367
|
-
return isInternalIP(embedded);
|
|
1426
|
+
return isInternalIP(embedded, opts);
|
|
1368
1427
|
}
|
|
1369
1428
|
return false;
|
|
1370
1429
|
}
|
|
1371
|
-
function isInternalUrl(url) {
|
|
1430
|
+
function isInternalUrl(url, opts) {
|
|
1372
1431
|
let parsed;
|
|
1373
1432
|
try {
|
|
1374
1433
|
parsed = new URL(url);
|
|
@@ -1377,7 +1436,7 @@ function isInternalUrl(url) {
|
|
|
1377
1436
|
}
|
|
1378
1437
|
const hostname = parsed.hostname.toLowerCase();
|
|
1379
1438
|
if (hostname === "localhost") return true;
|
|
1380
|
-
if (isInternalIP(hostname)) return true;
|
|
1439
|
+
if (isInternalIP(hostname, opts)) return true;
|
|
1381
1440
|
if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
|
|
1382
1441
|
return true;
|
|
1383
1442
|
}
|
|
@@ -1399,8 +1458,8 @@ async function assertSafeUploadPaths(paths) {
|
|
|
1399
1458
|
}
|
|
1400
1459
|
}
|
|
1401
1460
|
}
|
|
1402
|
-
async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
|
|
1403
|
-
if (isInternalUrl(url)) return true;
|
|
1461
|
+
async function isInternalUrlResolved(url, lookupFn = promises.lookup, opts) {
|
|
1462
|
+
if (isInternalUrl(url, opts)) return true;
|
|
1404
1463
|
let parsed;
|
|
1405
1464
|
try {
|
|
1406
1465
|
parsed = new URL(url);
|
|
@@ -1409,12 +1468,25 @@ async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
|
|
|
1409
1468
|
}
|
|
1410
1469
|
try {
|
|
1411
1470
|
const { address } = await lookupFn(parsed.hostname);
|
|
1412
|
-
if (isInternalIP(address)) return true;
|
|
1471
|
+
if (isInternalIP(address, opts)) return true;
|
|
1413
1472
|
} catch {
|
|
1414
1473
|
return true;
|
|
1415
1474
|
}
|
|
1416
1475
|
return false;
|
|
1417
1476
|
}
|
|
1477
|
+
async function assertBrowserNavigationResultAllowed(opts) {
|
|
1478
|
+
const rawUrl = String(opts.url ?? "").trim();
|
|
1479
|
+
if (!rawUrl) return;
|
|
1480
|
+
let parsed;
|
|
1481
|
+
try {
|
|
1482
|
+
parsed = new URL(rawUrl);
|
|
1483
|
+
} catch {
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol) || SAFE_NON_NETWORK_URLS.has(parsed.href)) {
|
|
1487
|
+
await assertBrowserNavigationAllowed(opts);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1418
1490
|
|
|
1419
1491
|
// src/actions/interaction.ts
|
|
1420
1492
|
async function clickViaPlaywright(opts) {
|
|
@@ -1620,7 +1692,9 @@ async function navigateViaPlaywright(opts) {
|
|
|
1620
1692
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1621
1693
|
ensurePageState(page);
|
|
1622
1694
|
await page.goto(url, { timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
|
|
1623
|
-
|
|
1695
|
+
const finalUrl = page.url();
|
|
1696
|
+
await assertBrowserNavigationResultAllowed({ url: finalUrl, ssrfPolicy: policy });
|
|
1697
|
+
return { url: finalUrl };
|
|
1624
1698
|
}
|
|
1625
1699
|
async function listPagesViaPlaywright(opts) {
|
|
1626
1700
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
@@ -1639,8 +1713,8 @@ async function listPagesViaPlaywright(opts) {
|
|
|
1639
1713
|
}
|
|
1640
1714
|
async function createPageViaPlaywright(opts) {
|
|
1641
1715
|
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
1716
|
+
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
1642
1717
|
if (targetUrl !== "about:blank") {
|
|
1643
|
-
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
1644
1718
|
await assertBrowserNavigationAllowed({ url: targetUrl, ssrfPolicy: policy });
|
|
1645
1719
|
}
|
|
1646
1720
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
@@ -1649,6 +1723,7 @@ async function createPageViaPlaywright(opts) {
|
|
|
1649
1723
|
ensurePageState(page);
|
|
1650
1724
|
if (targetUrl !== "about:blank") {
|
|
1651
1725
|
await page.goto(targetUrl, { timeout: normalizeTimeoutMs(void 0, 2e4) });
|
|
1726
|
+
await assertBrowserNavigationResultAllowed({ url: page.url(), ssrfPolicy: policy });
|
|
1652
1727
|
}
|
|
1653
1728
|
const tid = await pageTargetId(page).catch(() => null);
|
|
1654
1729
|
if (!tid) throw new Error("Failed to get targetId for new page");
|
|
@@ -3177,6 +3252,7 @@ exports.BrowserClaw = BrowserClaw;
|
|
|
3177
3252
|
exports.CrawlPage = CrawlPage;
|
|
3178
3253
|
exports.InvalidBrowserNavigationUrlError = InvalidBrowserNavigationUrlError;
|
|
3179
3254
|
exports.assertBrowserNavigationAllowed = assertBrowserNavigationAllowed;
|
|
3255
|
+
exports.isChromeCdpReady = isChromeCdpReady;
|
|
3180
3256
|
exports.withBrowserNavigationPolicy = withBrowserNavigationPolicy;
|
|
3181
3257
|
//# sourceMappingURL=index.cjs.map
|
|
3182
3258
|
//# sourceMappingURL=index.cjs.map
|