browserclaw 0.4.0 → 0.4.2
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/README.md +2 -0
- package/dist/index.cjs +131 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +126 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1136,6 +1136,12 @@ declare class BrowserClaw {
|
|
|
1136
1136
|
stop(): Promise<void>;
|
|
1137
1137
|
}
|
|
1138
1138
|
|
|
1139
|
+
/**
|
|
1140
|
+
* Convert a WebSocket CDP URL to an HTTP base URL for `/json/*` endpoints.
|
|
1141
|
+
*/
|
|
1142
|
+
declare function normalizeCdpHttpBaseForJsonEndpoints(cdpUrl: string): string;
|
|
1143
|
+
declare function isChromeReachable(cdpUrl: string, timeoutMs?: number, authToken?: string): Promise<boolean>;
|
|
1144
|
+
declare function getChromeWebSocketUrl(cdpUrl: string, timeoutMs?: number, authToken?: string): Promise<string | null>;
|
|
1139
1145
|
declare function isChromeCdpReady(cdpUrl: string, timeoutMs?: number, handshakeTimeoutMs?: number): Promise<boolean>;
|
|
1140
1146
|
|
|
1141
1147
|
type LookupFn = typeof lookup;
|
|
@@ -1151,6 +1157,11 @@ declare class InvalidBrowserNavigationUrlError extends Error {
|
|
|
1151
1157
|
type BrowserNavigationPolicyOptions = {
|
|
1152
1158
|
ssrfPolicy?: SsrfPolicy;
|
|
1153
1159
|
};
|
|
1160
|
+
/** Playwright-compatible request interface for redirect chain inspection. */
|
|
1161
|
+
type BrowserNavigationRequestLike = {
|
|
1162
|
+
url(): string;
|
|
1163
|
+
redirectedFrom(): BrowserNavigationRequestLike | null;
|
|
1164
|
+
};
|
|
1154
1165
|
/** Build a BrowserNavigationPolicyOptions from an SsrfPolicy. */
|
|
1155
1166
|
declare function withBrowserNavigationPolicy(ssrfPolicy?: SsrfPolicy): BrowserNavigationPolicyOptions;
|
|
1156
1167
|
/**
|
|
@@ -1161,5 +1172,31 @@ declare function assertBrowserNavigationAllowed(opts: {
|
|
|
1161
1172
|
url: string;
|
|
1162
1173
|
lookupFn?: LookupFn;
|
|
1163
1174
|
} & BrowserNavigationPolicyOptions): Promise<void>;
|
|
1175
|
+
/**
|
|
1176
|
+
* Best-effort post-navigation guard for the final page URL.
|
|
1177
|
+
* Only validates http/https URLs and about:blank — swallows errors on
|
|
1178
|
+
* unparseable URLs and non-network protocols (e.g. chrome-error://) to avoid
|
|
1179
|
+
* false positives on browser-internal error pages.
|
|
1180
|
+
*
|
|
1181
|
+
* Call this after `page.goto()` to catch redirect-based SSRF bypasses.
|
|
1182
|
+
*/
|
|
1183
|
+
declare function assertBrowserNavigationResultAllowed(opts: {
|
|
1184
|
+
url: string;
|
|
1185
|
+
lookupFn?: LookupFn;
|
|
1186
|
+
} & BrowserNavigationPolicyOptions): Promise<void>;
|
|
1187
|
+
/**
|
|
1188
|
+
* Walk the full redirect chain and validate each hop against the SSRF policy.
|
|
1189
|
+
* Call this after `page.goto()` with `response?.request()` to catch intermediate
|
|
1190
|
+
* redirects that resolve to private/internal addresses.
|
|
1191
|
+
*/
|
|
1192
|
+
declare function assertBrowserNavigationRedirectChainAllowed(opts: {
|
|
1193
|
+
request?: BrowserNavigationRequestLike | null;
|
|
1194
|
+
lookupFn?: LookupFn;
|
|
1195
|
+
} & BrowserNavigationPolicyOptions): Promise<void>;
|
|
1196
|
+
/**
|
|
1197
|
+
* Returns true if the SSRF policy requires redirect chain inspection
|
|
1198
|
+
* (i.e. strict mode where private network is blocked).
|
|
1199
|
+
*/
|
|
1200
|
+
declare function requiresInspectableBrowserNavigationRedirects(ssrfPolicy?: SsrfPolicy): boolean;
|
|
1164
1201
|
|
|
1165
|
-
export { type AriaNode, type AriaSnapshotResult, BrowserClaw, type BrowserNavigationPolicyOptions, type BrowserTab, type ChromeExecutable, type ChromeKind, type ClickOptions, type ColorScheme, type ConnectOptions, type ConsoleMessage, type CookieData, CrawlPage, type DialogOptions, type DownloadResult, type FormField, type FrameEvalResult, type GeolocationOptions, type HttpCredentials, InvalidBrowserNavigationUrlError, type LaunchOptions, type LookupFn, type NetworkRequest, type PageError, type ResponseBodyResult, type RoleRefInfo, type RoleRefs, type ScreenshotOptions, type SnapshotOptions, type SnapshotResult, type SnapshotStats, type SsrfPolicy, type StorageKind, type TraceStartOptions, type TypeOptions, type UntrustedContentMeta, type WaitOptions, assertBrowserNavigationAllowed, isChromeCdpReady, withBrowserNavigationPolicy };
|
|
1202
|
+
export { type AriaNode, type AriaSnapshotResult, BrowserClaw, type BrowserNavigationPolicyOptions, type BrowserNavigationRequestLike, type BrowserTab, type ChromeExecutable, type ChromeKind, type ClickOptions, type ColorScheme, type ConnectOptions, type ConsoleMessage, type CookieData, CrawlPage, type DialogOptions, type DownloadResult, type FormField, type FrameEvalResult, type GeolocationOptions, type HttpCredentials, InvalidBrowserNavigationUrlError, type LaunchOptions, type LookupFn, type NetworkRequest, type PageError, type ResponseBodyResult, type RoleRefInfo, type RoleRefs, type ScreenshotOptions, type SnapshotOptions, type SnapshotResult, type SnapshotStats, type SsrfPolicy, type StorageKind, type TraceStartOptions, type TypeOptions, type UntrustedContentMeta, type WaitOptions, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, getChromeWebSocketUrl, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, requiresInspectableBrowserNavigationRedirects, withBrowserNavigationPolicy };
|
package/dist/index.d.ts
CHANGED
|
@@ -1136,6 +1136,12 @@ declare class BrowserClaw {
|
|
|
1136
1136
|
stop(): Promise<void>;
|
|
1137
1137
|
}
|
|
1138
1138
|
|
|
1139
|
+
/**
|
|
1140
|
+
* Convert a WebSocket CDP URL to an HTTP base URL for `/json/*` endpoints.
|
|
1141
|
+
*/
|
|
1142
|
+
declare function normalizeCdpHttpBaseForJsonEndpoints(cdpUrl: string): string;
|
|
1143
|
+
declare function isChromeReachable(cdpUrl: string, timeoutMs?: number, authToken?: string): Promise<boolean>;
|
|
1144
|
+
declare function getChromeWebSocketUrl(cdpUrl: string, timeoutMs?: number, authToken?: string): Promise<string | null>;
|
|
1139
1145
|
declare function isChromeCdpReady(cdpUrl: string, timeoutMs?: number, handshakeTimeoutMs?: number): Promise<boolean>;
|
|
1140
1146
|
|
|
1141
1147
|
type LookupFn = typeof lookup;
|
|
@@ -1151,6 +1157,11 @@ declare class InvalidBrowserNavigationUrlError extends Error {
|
|
|
1151
1157
|
type BrowserNavigationPolicyOptions = {
|
|
1152
1158
|
ssrfPolicy?: SsrfPolicy;
|
|
1153
1159
|
};
|
|
1160
|
+
/** Playwright-compatible request interface for redirect chain inspection. */
|
|
1161
|
+
type BrowserNavigationRequestLike = {
|
|
1162
|
+
url(): string;
|
|
1163
|
+
redirectedFrom(): BrowserNavigationRequestLike | null;
|
|
1164
|
+
};
|
|
1154
1165
|
/** Build a BrowserNavigationPolicyOptions from an SsrfPolicy. */
|
|
1155
1166
|
declare function withBrowserNavigationPolicy(ssrfPolicy?: SsrfPolicy): BrowserNavigationPolicyOptions;
|
|
1156
1167
|
/**
|
|
@@ -1161,5 +1172,31 @@ declare function assertBrowserNavigationAllowed(opts: {
|
|
|
1161
1172
|
url: string;
|
|
1162
1173
|
lookupFn?: LookupFn;
|
|
1163
1174
|
} & BrowserNavigationPolicyOptions): Promise<void>;
|
|
1175
|
+
/**
|
|
1176
|
+
* Best-effort post-navigation guard for the final page URL.
|
|
1177
|
+
* Only validates http/https URLs and about:blank — swallows errors on
|
|
1178
|
+
* unparseable URLs and non-network protocols (e.g. chrome-error://) to avoid
|
|
1179
|
+
* false positives on browser-internal error pages.
|
|
1180
|
+
*
|
|
1181
|
+
* Call this after `page.goto()` to catch redirect-based SSRF bypasses.
|
|
1182
|
+
*/
|
|
1183
|
+
declare function assertBrowserNavigationResultAllowed(opts: {
|
|
1184
|
+
url: string;
|
|
1185
|
+
lookupFn?: LookupFn;
|
|
1186
|
+
} & BrowserNavigationPolicyOptions): Promise<void>;
|
|
1187
|
+
/**
|
|
1188
|
+
* Walk the full redirect chain and validate each hop against the SSRF policy.
|
|
1189
|
+
* Call this after `page.goto()` with `response?.request()` to catch intermediate
|
|
1190
|
+
* redirects that resolve to private/internal addresses.
|
|
1191
|
+
*/
|
|
1192
|
+
declare function assertBrowserNavigationRedirectChainAllowed(opts: {
|
|
1193
|
+
request?: BrowserNavigationRequestLike | null;
|
|
1194
|
+
lookupFn?: LookupFn;
|
|
1195
|
+
} & BrowserNavigationPolicyOptions): Promise<void>;
|
|
1196
|
+
/**
|
|
1197
|
+
* Returns true if the SSRF policy requires redirect chain inspection
|
|
1198
|
+
* (i.e. strict mode where private network is blocked).
|
|
1199
|
+
*/
|
|
1200
|
+
declare function requiresInspectableBrowserNavigationRedirects(ssrfPolicy?: SsrfPolicy): boolean;
|
|
1164
1201
|
|
|
1165
|
-
export { type AriaNode, type AriaSnapshotResult, BrowserClaw, type BrowserNavigationPolicyOptions, type BrowserTab, type ChromeExecutable, type ChromeKind, type ClickOptions, type ColorScheme, type ConnectOptions, type ConsoleMessage, type CookieData, CrawlPage, type DialogOptions, type DownloadResult, type FormField, type FrameEvalResult, type GeolocationOptions, type HttpCredentials, InvalidBrowserNavigationUrlError, type LaunchOptions, type LookupFn, type NetworkRequest, type PageError, type ResponseBodyResult, type RoleRefInfo, type RoleRefs, type ScreenshotOptions, type SnapshotOptions, type SnapshotResult, type SnapshotStats, type SsrfPolicy, type StorageKind, type TraceStartOptions, type TypeOptions, type UntrustedContentMeta, type WaitOptions, assertBrowserNavigationAllowed, isChromeCdpReady, withBrowserNavigationPolicy };
|
|
1202
|
+
export { type AriaNode, type AriaSnapshotResult, BrowserClaw, type BrowserNavigationPolicyOptions, type BrowserNavigationRequestLike, type BrowserTab, type ChromeExecutable, type ChromeKind, type ClickOptions, type ColorScheme, type ConnectOptions, type ConsoleMessage, type CookieData, CrawlPage, type DialogOptions, type DownloadResult, type FormField, type FrameEvalResult, type GeolocationOptions, type HttpCredentials, InvalidBrowserNavigationUrlError, type LaunchOptions, type LookupFn, type NetworkRequest, type PageError, type ResponseBodyResult, type RoleRefInfo, type RoleRefs, type ScreenshotOptions, type SnapshotOptions, type SnapshotResult, type SnapshotStats, type SsrfPolicy, type StorageKind, type TraceStartOptions, type TypeOptions, type UntrustedContentMeta, type WaitOptions, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, getChromeWebSocketUrl, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, requiresInspectableBrowserNavigationRedirects, withBrowserNavigationPolicy };
|
package/dist/index.js
CHANGED
|
@@ -340,39 +340,109 @@ function resolveUserDataDir(profileName) {
|
|
|
340
340
|
const configDir = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config");
|
|
341
341
|
return path.join(configDir, "browserclaw", "profiles", profileName, "user-data");
|
|
342
342
|
}
|
|
343
|
-
|
|
344
|
-
const ctrl = new AbortController();
|
|
345
|
-
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
343
|
+
function isWebSocketUrl(url) {
|
|
346
344
|
try {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal, headers });
|
|
350
|
-
if (!res.ok) return false;
|
|
351
|
-
const data = await res.json();
|
|
352
|
-
return data != null && typeof data === "object";
|
|
345
|
+
const parsed = new URL(url);
|
|
346
|
+
return parsed.protocol === "ws:" || parsed.protocol === "wss:";
|
|
353
347
|
} catch {
|
|
354
348
|
return false;
|
|
355
|
-
} finally {
|
|
356
|
-
clearTimeout(t);
|
|
357
349
|
}
|
|
358
350
|
}
|
|
359
|
-
|
|
351
|
+
function isLoopbackHost(hostname) {
|
|
352
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
353
|
+
}
|
|
354
|
+
function normalizeCdpWsUrl(wsUrl, cdpUrl) {
|
|
355
|
+
const ws = new URL(wsUrl);
|
|
356
|
+
const cdp = new URL(cdpUrl);
|
|
357
|
+
const isWildcardBind = ws.hostname === "0.0.0.0" || ws.hostname === "[::]";
|
|
358
|
+
if ((isLoopbackHost(ws.hostname) || isWildcardBind) && !isLoopbackHost(cdp.hostname)) {
|
|
359
|
+
ws.hostname = cdp.hostname;
|
|
360
|
+
const cdpPort = cdp.port || (cdp.protocol === "https:" ? "443" : "80");
|
|
361
|
+
ws.port = cdpPort;
|
|
362
|
+
ws.protocol = cdp.protocol === "https:" ? "wss:" : "ws:";
|
|
363
|
+
}
|
|
364
|
+
if (cdp.protocol === "https:" && ws.protocol === "ws:") ws.protocol = "wss:";
|
|
365
|
+
if (!ws.username && !ws.password && (cdp.username || cdp.password)) {
|
|
366
|
+
ws.username = cdp.username;
|
|
367
|
+
ws.password = cdp.password;
|
|
368
|
+
}
|
|
369
|
+
for (const [key, value] of cdp.searchParams.entries()) {
|
|
370
|
+
if (!ws.searchParams.has(key)) ws.searchParams.append(key, value);
|
|
371
|
+
}
|
|
372
|
+
return ws.toString();
|
|
373
|
+
}
|
|
374
|
+
function normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) {
|
|
375
|
+
try {
|
|
376
|
+
const url = new URL(cdpUrl);
|
|
377
|
+
if (url.protocol === "ws:") url.protocol = "http:";
|
|
378
|
+
else if (url.protocol === "wss:") url.protocol = "https:";
|
|
379
|
+
url.pathname = url.pathname.replace(/\/devtools\/browser\/.*$/, "");
|
|
380
|
+
url.pathname = url.pathname.replace(/\/cdp$/, "");
|
|
381
|
+
return url.toString().replace(/\/$/, "");
|
|
382
|
+
} catch {
|
|
383
|
+
return cdpUrl.replace(/^ws:/, "http:").replace(/^wss:/, "https:").replace(/\/devtools\/browser\/.*$/, "").replace(/\/cdp$/, "").replace(/\/$/, "");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function appendCdpPath(cdpUrl, cdpPath) {
|
|
387
|
+
const url = new URL(cdpUrl);
|
|
388
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}${cdpPath.startsWith("/") ? cdpPath : `/${cdpPath}`}`;
|
|
389
|
+
return url.toString();
|
|
390
|
+
}
|
|
391
|
+
async function canOpenWebSocket(url, timeoutMs) {
|
|
392
|
+
return new Promise((resolve2) => {
|
|
393
|
+
let settled = false;
|
|
394
|
+
const finish = (value) => {
|
|
395
|
+
if (settled) return;
|
|
396
|
+
settled = true;
|
|
397
|
+
clearTimeout(timer);
|
|
398
|
+
try {
|
|
399
|
+
ws.close();
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
resolve2(value);
|
|
403
|
+
};
|
|
404
|
+
const timer = setTimeout(() => finish(false), Math.max(50, timeoutMs + 25));
|
|
405
|
+
let ws;
|
|
406
|
+
try {
|
|
407
|
+
ws = new WebSocket(url);
|
|
408
|
+
} catch {
|
|
409
|
+
finish(false);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
ws.onopen = () => finish(true);
|
|
413
|
+
ws.onerror = () => finish(false);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
async function fetchChromeVersion(cdpUrl, timeoutMs = 500, authToken) {
|
|
360
417
|
const ctrl = new AbortController();
|
|
361
418
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
362
419
|
try {
|
|
420
|
+
const httpBase = isWebSocketUrl(cdpUrl) ? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) : cdpUrl;
|
|
363
421
|
const headers = {};
|
|
364
422
|
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
365
|
-
const res = await fetch(
|
|
423
|
+
const res = await fetch(appendCdpPath(httpBase, "/json/version"), { signal: ctrl.signal, headers });
|
|
366
424
|
if (!res.ok) return null;
|
|
367
425
|
const data = await res.json();
|
|
368
426
|
if (!data || typeof data !== "object") return null;
|
|
369
|
-
return
|
|
427
|
+
return data;
|
|
370
428
|
} catch {
|
|
371
429
|
return null;
|
|
372
430
|
} finally {
|
|
373
431
|
clearTimeout(t);
|
|
374
432
|
}
|
|
375
433
|
}
|
|
434
|
+
async function isChromeReachable(cdpUrl, timeoutMs = 500, authToken) {
|
|
435
|
+
if (isWebSocketUrl(cdpUrl)) return await canOpenWebSocket(cdpUrl, timeoutMs);
|
|
436
|
+
const version = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
|
|
437
|
+
return Boolean(version);
|
|
438
|
+
}
|
|
439
|
+
async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
|
|
440
|
+
if (isWebSocketUrl(cdpUrl)) return cdpUrl;
|
|
441
|
+
const version = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
|
|
442
|
+
const wsUrl = String(version?.webSocketDebuggerUrl ?? "").trim();
|
|
443
|
+
if (!wsUrl) return null;
|
|
444
|
+
return normalizeCdpWsUrl(wsUrl, cdpUrl);
|
|
445
|
+
}
|
|
376
446
|
async function isChromeCdpReady(cdpUrl, timeoutMs = 500, handshakeTimeoutMs = 800) {
|
|
377
447
|
const wsUrl = await getChromeWebSocketUrl(cdpUrl, timeoutMs);
|
|
378
448
|
if (!wsUrl) return false;
|
|
@@ -770,7 +840,7 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
770
840
|
}
|
|
771
841
|
if (cdpUrl) {
|
|
772
842
|
try {
|
|
773
|
-
const listUrl = `${cdpUrl
|
|
843
|
+
const listUrl = `${normalizeCdpHttpBaseForJsonEndpoints(cdpUrl)}/json/list`;
|
|
774
844
|
const headers = {};
|
|
775
845
|
if (cached?.authToken) headers["Authorization"] = `Bearer ${cached.authToken}`;
|
|
776
846
|
const response = await fetch(listUrl, { headers });
|
|
@@ -1258,6 +1328,20 @@ function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
|
1258
1328
|
}
|
|
1259
1329
|
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
1260
1330
|
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
1331
|
+
var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
1332
|
+
function isAllowedNonNetworkNavigationUrl(parsed) {
|
|
1333
|
+
return SAFE_NON_NETWORK_URLS.has(parsed.href);
|
|
1334
|
+
}
|
|
1335
|
+
function isPrivateNetworkAllowedByPolicy(policy) {
|
|
1336
|
+
return policy?.dangerouslyAllowPrivateNetwork === true || policy?.allowPrivateNetwork === true;
|
|
1337
|
+
}
|
|
1338
|
+
function hasProxyEnvConfigured(env = process.env) {
|
|
1339
|
+
for (const key of PROXY_ENV_KEYS) {
|
|
1340
|
+
const value = env[key];
|
|
1341
|
+
if (typeof value === "string" && value.trim().length > 0) return true;
|
|
1342
|
+
}
|
|
1343
|
+
return false;
|
|
1344
|
+
}
|
|
1261
1345
|
async function assertBrowserNavigationAllowed(opts) {
|
|
1262
1346
|
const rawUrl = String(opts.url ?? "").trim();
|
|
1263
1347
|
let parsed;
|
|
@@ -1267,9 +1351,14 @@ async function assertBrowserNavigationAllowed(opts) {
|
|
|
1267
1351
|
throw new InvalidBrowserNavigationUrlError(`Invalid URL: "${rawUrl}"`);
|
|
1268
1352
|
}
|
|
1269
1353
|
if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
|
|
1270
|
-
if (
|
|
1354
|
+
if (isAllowedNonNetworkNavigationUrl(parsed)) return;
|
|
1271
1355
|
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
|
|
1272
1356
|
}
|
|
1357
|
+
if (hasProxyEnvConfigured() && !isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy)) {
|
|
1358
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1359
|
+
"Navigation blocked: strict browser SSRF policy cannot be enforced while env proxy variables are set"
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1273
1362
|
const policy = opts.ssrfPolicy;
|
|
1274
1363
|
if (policy?.dangerouslyAllowPrivateNetwork ?? policy?.allowPrivateNetwork ?? true) return;
|
|
1275
1364
|
const allowedHostnames = [
|
|
@@ -1474,10 +1563,24 @@ async function assertBrowserNavigationResultAllowed(opts) {
|
|
|
1474
1563
|
} catch {
|
|
1475
1564
|
return;
|
|
1476
1565
|
}
|
|
1477
|
-
if (NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol) ||
|
|
1566
|
+
if (NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol) || isAllowedNonNetworkNavigationUrl(parsed)) {
|
|
1478
1567
|
await assertBrowserNavigationAllowed(opts);
|
|
1479
1568
|
}
|
|
1480
1569
|
}
|
|
1570
|
+
async function assertBrowserNavigationRedirectChainAllowed(opts) {
|
|
1571
|
+
const chain = [];
|
|
1572
|
+
let current = opts.request ?? null;
|
|
1573
|
+
while (current) {
|
|
1574
|
+
chain.push(current.url());
|
|
1575
|
+
current = current.redirectedFrom();
|
|
1576
|
+
}
|
|
1577
|
+
for (const url of [...chain].reverse()) {
|
|
1578
|
+
await assertBrowserNavigationAllowed({ url, lookupFn: opts.lookupFn, ssrfPolicy: opts.ssrfPolicy });
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
|
|
1582
|
+
return !isPrivateNetworkAllowedByPolicy(ssrfPolicy);
|
|
1583
|
+
}
|
|
1481
1584
|
|
|
1482
1585
|
// src/actions/interaction.ts
|
|
1483
1586
|
async function clickViaPlaywright(opts) {
|
|
@@ -1682,7 +1785,8 @@ async function navigateViaPlaywright(opts) {
|
|
|
1682
1785
|
await assertBrowserNavigationAllowed({ url, ssrfPolicy: policy });
|
|
1683
1786
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1684
1787
|
ensurePageState(page);
|
|
1685
|
-
await page.goto(url, { timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
|
|
1788
|
+
const response = await page.goto(url, { timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
|
|
1789
|
+
await assertBrowserNavigationRedirectChainAllowed({ request: response?.request(), ...withBrowserNavigationPolicy(policy) });
|
|
1686
1790
|
const finalUrl = page.url();
|
|
1687
1791
|
await assertBrowserNavigationResultAllowed({ url: finalUrl, ssrfPolicy: policy });
|
|
1688
1792
|
return { url: finalUrl };
|
|
@@ -1713,7 +1817,9 @@ async function createPageViaPlaywright(opts) {
|
|
|
1713
1817
|
const page = await context.newPage();
|
|
1714
1818
|
ensurePageState(page);
|
|
1715
1819
|
if (targetUrl !== "about:blank") {
|
|
1716
|
-
|
|
1820
|
+
const navigationPolicy = withBrowserNavigationPolicy(policy);
|
|
1821
|
+
const response = await page.goto(targetUrl, { timeout: normalizeTimeoutMs(void 0, 2e4) });
|
|
1822
|
+
await assertBrowserNavigationRedirectChainAllowed({ request: response?.request(), ...navigationPolicy });
|
|
1717
1823
|
await assertBrowserNavigationResultAllowed({ url: page.url(), ssrfPolicy: policy });
|
|
1718
1824
|
}
|
|
1719
1825
|
const tid = await pageTargetId(page).catch(() => null);
|
|
@@ -3239,6 +3345,6 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
3239
3345
|
}
|
|
3240
3346
|
};
|
|
3241
3347
|
|
|
3242
|
-
export { BrowserClaw, CrawlPage, InvalidBrowserNavigationUrlError, assertBrowserNavigationAllowed, isChromeCdpReady, withBrowserNavigationPolicy };
|
|
3348
|
+
export { BrowserClaw, CrawlPage, InvalidBrowserNavigationUrlError, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, getChromeWebSocketUrl, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, requiresInspectableBrowserNavigationRedirects, withBrowserNavigationPolicy };
|
|
3243
3349
|
//# sourceMappingURL=index.js.map
|
|
3244
3350
|
//# sourceMappingURL=index.js.map
|