browserclaw 0.3.6 → 0.3.8
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 +4 -4
- package/dist/index.cjs +225 -183
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +226 -184
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<h2 align="center">🦞 BrowserClaw — Standalone OpenClaw browser module</
|
|
1
|
+
<h2 align="center">🦞 BrowserClaw — Standalone OpenClaw browser module</h2>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<a href="https://www.npmjs.com/package/browserclaw"><img src="https://img.shields.io/npm/v/browserclaw.svg" alt="npm version" /></a>
|
|
@@ -217,13 +217,13 @@ await page.press('Meta+Shift+p');
|
|
|
217
217
|
|
|
218
218
|
// Fill multiple form fields at once
|
|
219
219
|
await page.fill([
|
|
220
|
-
{ ref: 'e2',
|
|
221
|
-
{ ref: 'e4',
|
|
220
|
+
{ ref: 'e2', value: 'Jane Doe' },
|
|
221
|
+
{ ref: 'e4', value: 'jane@example.com' },
|
|
222
222
|
{ ref: 'e6', type: 'checkbox', value: true },
|
|
223
223
|
]);
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
-
`fill()` field types: `'text'` calls Playwright `fill()` with the string value. `'checkbox'` and `'radio'` call `setChecked()` — truthy values are `true`, `1`, `'1'`, `'true'`. Empty ref
|
|
226
|
+
`fill()` field types: `'text'` (default) calls Playwright `fill()` with the string value. `'checkbox'` and `'radio'` call `setChecked()` — truthy values are `true`, `1`, `'1'`, `'true'`. Type can be omitted and defaults to `'text'`. Empty ref throws.
|
|
227
227
|
|
|
228
228
|
#### Highlight
|
|
229
229
|
|
package/dist/index.cjs
CHANGED
|
@@ -7,6 +7,7 @@ var net = require('net');
|
|
|
7
7
|
var child_process = require('child_process');
|
|
8
8
|
var playwrightCore = require('playwright-core');
|
|
9
9
|
var promises = require('dns/promises');
|
|
10
|
+
var promises$1 = require('fs/promises');
|
|
10
11
|
|
|
11
12
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
13
|
|
|
@@ -1198,6 +1199,222 @@ function formatAriaNodes(nodes, limit) {
|
|
|
1198
1199
|
}
|
|
1199
1200
|
return out;
|
|
1200
1201
|
}
|
|
1202
|
+
var InvalidBrowserNavigationUrlError = class extends Error {
|
|
1203
|
+
constructor(message) {
|
|
1204
|
+
super(message);
|
|
1205
|
+
this.name = "InvalidBrowserNavigationUrlError";
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
1209
|
+
return { ssrfPolicy };
|
|
1210
|
+
}
|
|
1211
|
+
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
1212
|
+
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
1213
|
+
async function assertBrowserNavigationAllowed(opts) {
|
|
1214
|
+
const rawUrl = String(opts.url ?? "").trim();
|
|
1215
|
+
let parsed;
|
|
1216
|
+
try {
|
|
1217
|
+
parsed = new URL(rawUrl);
|
|
1218
|
+
} catch {
|
|
1219
|
+
throw new InvalidBrowserNavigationUrlError(`Invalid URL: "${rawUrl}"`);
|
|
1220
|
+
}
|
|
1221
|
+
if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
|
|
1222
|
+
if (SAFE_NON_NETWORK_URLS.has(parsed.href)) return;
|
|
1223
|
+
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
|
|
1224
|
+
}
|
|
1225
|
+
const policy = opts.ssrfPolicy;
|
|
1226
|
+
if (policy?.dangerouslyAllowPrivateNetwork ?? policy?.allowPrivateNetwork ?? true) return;
|
|
1227
|
+
const allowedHostnames = [
|
|
1228
|
+
...policy?.allowedHostnames ?? [],
|
|
1229
|
+
...policy?.hostnameAllowlist ?? []
|
|
1230
|
+
];
|
|
1231
|
+
if (allowedHostnames.length) {
|
|
1232
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1233
|
+
if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
|
|
1234
|
+
}
|
|
1235
|
+
if (await isInternalUrlResolved(rawUrl, opts.lookupFn)) {
|
|
1236
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1237
|
+
`Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
async function assertSafeOutputPath(path2, allowedRoots) {
|
|
1242
|
+
if (!path2 || typeof path2 !== "string") {
|
|
1243
|
+
throw new Error("Output path is required.");
|
|
1244
|
+
}
|
|
1245
|
+
const normalized = path.normalize(path2);
|
|
1246
|
+
if (normalized.includes("..")) {
|
|
1247
|
+
throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
|
|
1248
|
+
}
|
|
1249
|
+
if (allowedRoots?.length) {
|
|
1250
|
+
const resolved = path.resolve(normalized);
|
|
1251
|
+
let parentReal;
|
|
1252
|
+
try {
|
|
1253
|
+
parentReal = await promises$1.realpath(path.dirname(resolved));
|
|
1254
|
+
} catch {
|
|
1255
|
+
throw new Error(`Unsafe output path: parent directory is inaccessible for "${path2}".`);
|
|
1256
|
+
}
|
|
1257
|
+
try {
|
|
1258
|
+
const targetStat = await promises$1.lstat(resolved);
|
|
1259
|
+
if (targetStat.isSymbolicLink()) {
|
|
1260
|
+
throw new Error(`Unsafe output path: "${path2}" is a symbolic link.`);
|
|
1261
|
+
}
|
|
1262
|
+
} catch (e) {
|
|
1263
|
+
if (e.code !== "ENOENT") throw e;
|
|
1264
|
+
}
|
|
1265
|
+
const results = await Promise.all(
|
|
1266
|
+
allowedRoots.map(async (root) => {
|
|
1267
|
+
try {
|
|
1268
|
+
const rootStat = await promises$1.lstat(path.resolve(root));
|
|
1269
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) return false;
|
|
1270
|
+
const rootReal = await promises$1.realpath(path.resolve(root));
|
|
1271
|
+
return parentReal === rootReal || parentReal.startsWith(rootReal + path.sep);
|
|
1272
|
+
} catch {
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
})
|
|
1276
|
+
);
|
|
1277
|
+
if (!results.some(Boolean)) {
|
|
1278
|
+
throw new Error(`Unsafe output path: "${path2}" is outside allowed directories.`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
function expandIPv6(ip) {
|
|
1283
|
+
let normalized = ip;
|
|
1284
|
+
const v4Match = normalized.match(/^(.+:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
1285
|
+
if (v4Match) {
|
|
1286
|
+
const octets = v4Match[2].split(".").map(Number);
|
|
1287
|
+
if (octets.some((o) => o > 255)) return null;
|
|
1288
|
+
const hexHi = (octets[0] << 8 | octets[1]).toString(16).padStart(4, "0");
|
|
1289
|
+
const hexLo = (octets[2] << 8 | octets[3]).toString(16).padStart(4, "0");
|
|
1290
|
+
normalized = v4Match[1] + hexHi + ":" + hexLo;
|
|
1291
|
+
}
|
|
1292
|
+
const halves = normalized.split("::");
|
|
1293
|
+
if (halves.length > 2) return null;
|
|
1294
|
+
if (halves.length === 2) {
|
|
1295
|
+
const left = halves[0] !== "" ? halves[0].split(":") : [];
|
|
1296
|
+
const right = halves[1] !== "" ? halves[1].split(":") : [];
|
|
1297
|
+
const needed = 8 - left.length - right.length;
|
|
1298
|
+
if (needed < 0) return null;
|
|
1299
|
+
const groups2 = [...left, ...Array(needed).fill("0"), ...right];
|
|
1300
|
+
if (groups2.length !== 8) return null;
|
|
1301
|
+
return groups2.map((g) => g.padStart(4, "0")).join(":");
|
|
1302
|
+
}
|
|
1303
|
+
const groups = normalized.split(":");
|
|
1304
|
+
if (groups.length !== 8) return null;
|
|
1305
|
+
return groups.map((g) => g.padStart(4, "0")).join(":");
|
|
1306
|
+
}
|
|
1307
|
+
function hexToIPv4(hiHex, loHex) {
|
|
1308
|
+
const hi = parseInt(hiHex, 16);
|
|
1309
|
+
const lo = parseInt(loHex, 16);
|
|
1310
|
+
return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
|
|
1311
|
+
}
|
|
1312
|
+
function extractEmbeddedIPv4(lower) {
|
|
1313
|
+
if (lower.startsWith("::ffff:")) {
|
|
1314
|
+
return lower.slice(7);
|
|
1315
|
+
}
|
|
1316
|
+
const expanded = expandIPv6(lower);
|
|
1317
|
+
if (expanded === null) return "";
|
|
1318
|
+
const groups = expanded.split(":");
|
|
1319
|
+
if (groups.length !== 8) return "";
|
|
1320
|
+
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0000" && groups[3] === "0000" && groups[4] === "0000" && groups[5] === "0000") {
|
|
1321
|
+
return hexToIPv4(groups[6], groups[7]);
|
|
1322
|
+
}
|
|
1323
|
+
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0001") {
|
|
1324
|
+
return hexToIPv4(groups[6], groups[7]);
|
|
1325
|
+
}
|
|
1326
|
+
if (groups[0] === "2002") {
|
|
1327
|
+
return hexToIPv4(groups[1], groups[2]);
|
|
1328
|
+
}
|
|
1329
|
+
if (groups[0] === "2001" && groups[1] === "0000") {
|
|
1330
|
+
const hiXored = (parseInt(groups[6], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1331
|
+
const loXored = (parseInt(groups[7], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1332
|
+
return hexToIPv4(hiXored, loXored);
|
|
1333
|
+
}
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
function isStrictDecimalOctet(part) {
|
|
1337
|
+
if (!/^[0-9]+$/.test(part)) return false;
|
|
1338
|
+
const n = parseInt(part, 10);
|
|
1339
|
+
if (n < 0 || n > 255) return false;
|
|
1340
|
+
if (String(n) !== part) return false;
|
|
1341
|
+
return true;
|
|
1342
|
+
}
|
|
1343
|
+
function isUnsupportedIPv4Literal(ip) {
|
|
1344
|
+
if (/^[0-9]+$/.test(ip)) return true;
|
|
1345
|
+
const parts = ip.split(".");
|
|
1346
|
+
if (parts.length !== 4) return true;
|
|
1347
|
+
if (!parts.every(isStrictDecimalOctet)) return true;
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
function isInternalIP(ip) {
|
|
1351
|
+
if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
|
|
1352
|
+
if (/^127\./.test(ip)) return true;
|
|
1353
|
+
if (/^10\./.test(ip)) return true;
|
|
1354
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
1355
|
+
if (/^192\.168\./.test(ip)) return true;
|
|
1356
|
+
if (/^169\.254\./.test(ip)) return true;
|
|
1357
|
+
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
|
|
1358
|
+
if (ip === "0.0.0.0") return true;
|
|
1359
|
+
const lower = ip.toLowerCase();
|
|
1360
|
+
if (lower === "::1") return true;
|
|
1361
|
+
if (lower.startsWith("fe80:")) return true;
|
|
1362
|
+
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
1363
|
+
if (lower.startsWith("ff")) return true;
|
|
1364
|
+
const embedded = extractEmbeddedIPv4(lower);
|
|
1365
|
+
if (embedded !== null) {
|
|
1366
|
+
if (embedded === "") return true;
|
|
1367
|
+
return isInternalIP(embedded);
|
|
1368
|
+
}
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1371
|
+
function isInternalUrl(url) {
|
|
1372
|
+
let parsed;
|
|
1373
|
+
try {
|
|
1374
|
+
parsed = new URL(url);
|
|
1375
|
+
} catch {
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1379
|
+
if (hostname === "localhost") return true;
|
|
1380
|
+
if (isInternalIP(hostname)) return true;
|
|
1381
|
+
if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
|
|
1382
|
+
return true;
|
|
1383
|
+
}
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
async function assertSafeUploadPaths(paths) {
|
|
1387
|
+
for (const filePath of paths) {
|
|
1388
|
+
let stat;
|
|
1389
|
+
try {
|
|
1390
|
+
stat = await promises$1.lstat(filePath);
|
|
1391
|
+
} catch {
|
|
1392
|
+
throw new Error(`Upload path does not exist or is inaccessible: "${filePath}".`);
|
|
1393
|
+
}
|
|
1394
|
+
if (stat.isSymbolicLink()) {
|
|
1395
|
+
throw new Error(`Upload path is a symbolic link: "${filePath}".`);
|
|
1396
|
+
}
|
|
1397
|
+
if (!stat.isFile()) {
|
|
1398
|
+
throw new Error(`Upload path is not a regular file: "${filePath}".`);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
|
|
1403
|
+
if (isInternalUrl(url)) return true;
|
|
1404
|
+
let parsed;
|
|
1405
|
+
try {
|
|
1406
|
+
parsed = new URL(url);
|
|
1407
|
+
} catch {
|
|
1408
|
+
return true;
|
|
1409
|
+
}
|
|
1410
|
+
try {
|
|
1411
|
+
const { address } = await lookupFn(parsed.hostname);
|
|
1412
|
+
if (isInternalIP(address)) return true;
|
|
1413
|
+
} catch {
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
return false;
|
|
1417
|
+
}
|
|
1201
1418
|
|
|
1202
1419
|
// src/actions/interaction.ts
|
|
1203
1420
|
async function clickViaPlaywright(opts) {
|
|
@@ -1280,11 +1497,10 @@ async function fillFormViaPlaywright(opts) {
|
|
|
1280
1497
|
for (let i = 0; i < opts.fields.length; i++) {
|
|
1281
1498
|
const field = opts.fields[i];
|
|
1282
1499
|
const ref = field.ref.trim();
|
|
1283
|
-
const type = field.type.trim();
|
|
1500
|
+
const type = (typeof field.type === "string" ? field.type.trim() : "") || "text";
|
|
1284
1501
|
const rawValue = field.value;
|
|
1285
1502
|
const value = rawValue == null ? "" : String(rawValue);
|
|
1286
1503
|
if (!ref) throw new Error(`fill(): field at index ${i} has empty ref`);
|
|
1287
|
-
if (!type) throw new Error(`fill(): field "${ref}" has empty type`);
|
|
1288
1504
|
const locator = refLocator(page, ref);
|
|
1289
1505
|
if (type === "checkbox" || type === "radio") {
|
|
1290
1506
|
const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
|
|
@@ -1330,6 +1546,7 @@ async function setInputFilesViaPlaywright(opts) {
|
|
|
1330
1546
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1331
1547
|
const locator = opts.ref ? refLocator(page, opts.ref) : opts.element ? page.locator(opts.element).first() : null;
|
|
1332
1548
|
if (!locator) throw new Error("Either ref or element is required for setInputFiles");
|
|
1549
|
+
await assertSafeUploadPaths(opts.paths);
|
|
1333
1550
|
try {
|
|
1334
1551
|
await locator.setInputFiles(opts.paths);
|
|
1335
1552
|
} catch (err) {
|
|
@@ -1373,7 +1590,9 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
1373
1590
|
const handler = async (fc) => {
|
|
1374
1591
|
clearTimeout(timer);
|
|
1375
1592
|
try {
|
|
1376
|
-
|
|
1593
|
+
const paths = opts.paths ?? [];
|
|
1594
|
+
if (paths.length > 0) await assertSafeUploadPaths(paths);
|
|
1595
|
+
await fc.setFiles(paths);
|
|
1377
1596
|
resolve2();
|
|
1378
1597
|
} catch (err) {
|
|
1379
1598
|
reject(err);
|
|
@@ -1391,183 +1610,6 @@ async function pressKeyViaPlaywright(opts) {
|
|
|
1391
1610
|
ensurePageState(page);
|
|
1392
1611
|
await page.keyboard.press(key, { delay: Math.max(0, Math.floor(opts.delayMs ?? 0)) });
|
|
1393
1612
|
}
|
|
1394
|
-
var InvalidBrowserNavigationUrlError = class extends Error {
|
|
1395
|
-
constructor(message) {
|
|
1396
|
-
super(message);
|
|
1397
|
-
this.name = "InvalidBrowserNavigationUrlError";
|
|
1398
|
-
}
|
|
1399
|
-
};
|
|
1400
|
-
function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
1401
|
-
return { ssrfPolicy };
|
|
1402
|
-
}
|
|
1403
|
-
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
1404
|
-
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
1405
|
-
async function assertBrowserNavigationAllowed(opts) {
|
|
1406
|
-
const rawUrl = String(opts.url ?? "").trim();
|
|
1407
|
-
let parsed;
|
|
1408
|
-
try {
|
|
1409
|
-
parsed = new URL(rawUrl);
|
|
1410
|
-
} catch {
|
|
1411
|
-
throw new InvalidBrowserNavigationUrlError(`Invalid URL: "${rawUrl}"`);
|
|
1412
|
-
}
|
|
1413
|
-
if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
|
|
1414
|
-
if (SAFE_NON_NETWORK_URLS.has(parsed.href)) return;
|
|
1415
|
-
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
|
|
1416
|
-
}
|
|
1417
|
-
const policy = opts.ssrfPolicy;
|
|
1418
|
-
if (policy?.dangerouslyAllowPrivateNetwork ?? policy?.allowPrivateNetwork ?? true) return;
|
|
1419
|
-
const allowedHostnames = [
|
|
1420
|
-
...policy?.allowedHostnames ?? [],
|
|
1421
|
-
...policy?.hostnameAllowlist ?? []
|
|
1422
|
-
];
|
|
1423
|
-
if (allowedHostnames.length) {
|
|
1424
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
1425
|
-
if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
|
|
1426
|
-
}
|
|
1427
|
-
if (await isInternalUrlResolved(rawUrl, opts.lookupFn)) {
|
|
1428
|
-
throw new InvalidBrowserNavigationUrlError(
|
|
1429
|
-
`Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1430
|
-
);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
function assertSafeOutputPath(path2, allowedRoots) {
|
|
1434
|
-
if (!path2 || typeof path2 !== "string") {
|
|
1435
|
-
throw new Error("Output path is required.");
|
|
1436
|
-
}
|
|
1437
|
-
const normalized = path.normalize(path2);
|
|
1438
|
-
if (normalized.includes("..")) {
|
|
1439
|
-
throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
|
|
1440
|
-
}
|
|
1441
|
-
if (allowedRoots?.length) {
|
|
1442
|
-
const resolved = path.resolve(normalized);
|
|
1443
|
-
const withinRoot = allowedRoots.some((root) => {
|
|
1444
|
-
const normalizedRoot = path.resolve(root);
|
|
1445
|
-
return resolved === normalizedRoot || resolved.startsWith(normalizedRoot + path.sep);
|
|
1446
|
-
});
|
|
1447
|
-
if (!withinRoot) {
|
|
1448
|
-
throw new Error(`Unsafe output path: "${path2}" is outside allowed directories.`);
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
function expandIPv6(ip) {
|
|
1453
|
-
let normalized = ip;
|
|
1454
|
-
const v4Match = normalized.match(/^(.+:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
1455
|
-
if (v4Match) {
|
|
1456
|
-
const octets = v4Match[2].split(".").map(Number);
|
|
1457
|
-
if (octets.some((o) => o > 255)) return null;
|
|
1458
|
-
const hexHi = (octets[0] << 8 | octets[1]).toString(16).padStart(4, "0");
|
|
1459
|
-
const hexLo = (octets[2] << 8 | octets[3]).toString(16).padStart(4, "0");
|
|
1460
|
-
normalized = v4Match[1] + hexHi + ":" + hexLo;
|
|
1461
|
-
}
|
|
1462
|
-
const halves = normalized.split("::");
|
|
1463
|
-
if (halves.length > 2) return null;
|
|
1464
|
-
if (halves.length === 2) {
|
|
1465
|
-
const left = halves[0] !== "" ? halves[0].split(":") : [];
|
|
1466
|
-
const right = halves[1] !== "" ? halves[1].split(":") : [];
|
|
1467
|
-
const needed = 8 - left.length - right.length;
|
|
1468
|
-
if (needed < 0) return null;
|
|
1469
|
-
const groups2 = [...left, ...Array(needed).fill("0"), ...right];
|
|
1470
|
-
if (groups2.length !== 8) return null;
|
|
1471
|
-
return groups2.map((g) => g.padStart(4, "0")).join(":");
|
|
1472
|
-
}
|
|
1473
|
-
const groups = normalized.split(":");
|
|
1474
|
-
if (groups.length !== 8) return null;
|
|
1475
|
-
return groups.map((g) => g.padStart(4, "0")).join(":");
|
|
1476
|
-
}
|
|
1477
|
-
function hexToIPv4(hiHex, loHex) {
|
|
1478
|
-
const hi = parseInt(hiHex, 16);
|
|
1479
|
-
const lo = parseInt(loHex, 16);
|
|
1480
|
-
return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
|
|
1481
|
-
}
|
|
1482
|
-
function extractEmbeddedIPv4(lower) {
|
|
1483
|
-
if (lower.startsWith("::ffff:")) {
|
|
1484
|
-
return lower.slice(7);
|
|
1485
|
-
}
|
|
1486
|
-
const expanded = expandIPv6(lower);
|
|
1487
|
-
if (expanded === null) return "";
|
|
1488
|
-
const groups = expanded.split(":");
|
|
1489
|
-
if (groups.length !== 8) return "";
|
|
1490
|
-
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0000" && groups[3] === "0000" && groups[4] === "0000" && groups[5] === "0000") {
|
|
1491
|
-
return hexToIPv4(groups[6], groups[7]);
|
|
1492
|
-
}
|
|
1493
|
-
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0001") {
|
|
1494
|
-
return hexToIPv4(groups[6], groups[7]);
|
|
1495
|
-
}
|
|
1496
|
-
if (groups[0] === "2002") {
|
|
1497
|
-
return hexToIPv4(groups[1], groups[2]);
|
|
1498
|
-
}
|
|
1499
|
-
if (groups[0] === "2001" && groups[1] === "0000") {
|
|
1500
|
-
const hiXored = (parseInt(groups[6], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1501
|
-
const loXored = (parseInt(groups[7], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1502
|
-
return hexToIPv4(hiXored, loXored);
|
|
1503
|
-
}
|
|
1504
|
-
return null;
|
|
1505
|
-
}
|
|
1506
|
-
function isStrictDecimalOctet(part) {
|
|
1507
|
-
if (!/^[0-9]+$/.test(part)) return false;
|
|
1508
|
-
const n = parseInt(part, 10);
|
|
1509
|
-
if (n < 0 || n > 255) return false;
|
|
1510
|
-
if (String(n) !== part) return false;
|
|
1511
|
-
return true;
|
|
1512
|
-
}
|
|
1513
|
-
function isUnsupportedIPv4Literal(ip) {
|
|
1514
|
-
if (/^[0-9]+$/.test(ip)) return true;
|
|
1515
|
-
const parts = ip.split(".");
|
|
1516
|
-
if (parts.length !== 4) return true;
|
|
1517
|
-
if (!parts.every(isStrictDecimalOctet)) return true;
|
|
1518
|
-
return false;
|
|
1519
|
-
}
|
|
1520
|
-
function isInternalIP(ip) {
|
|
1521
|
-
if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
|
|
1522
|
-
if (/^127\./.test(ip)) return true;
|
|
1523
|
-
if (/^10\./.test(ip)) return true;
|
|
1524
|
-
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
1525
|
-
if (/^192\.168\./.test(ip)) return true;
|
|
1526
|
-
if (/^169\.254\./.test(ip)) return true;
|
|
1527
|
-
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
|
|
1528
|
-
if (ip === "0.0.0.0") return true;
|
|
1529
|
-
const lower = ip.toLowerCase();
|
|
1530
|
-
if (lower === "::1") return true;
|
|
1531
|
-
if (lower.startsWith("fe80:")) return true;
|
|
1532
|
-
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
1533
|
-
const embedded = extractEmbeddedIPv4(lower);
|
|
1534
|
-
if (embedded !== null) {
|
|
1535
|
-
if (embedded === "") return true;
|
|
1536
|
-
return isInternalIP(embedded);
|
|
1537
|
-
}
|
|
1538
|
-
return false;
|
|
1539
|
-
}
|
|
1540
|
-
function isInternalUrl(url) {
|
|
1541
|
-
let parsed;
|
|
1542
|
-
try {
|
|
1543
|
-
parsed = new URL(url);
|
|
1544
|
-
} catch {
|
|
1545
|
-
return true;
|
|
1546
|
-
}
|
|
1547
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
1548
|
-
if (hostname === "localhost") return true;
|
|
1549
|
-
if (isInternalIP(hostname)) return true;
|
|
1550
|
-
if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
|
|
1551
|
-
return true;
|
|
1552
|
-
}
|
|
1553
|
-
return false;
|
|
1554
|
-
}
|
|
1555
|
-
async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
|
|
1556
|
-
if (isInternalUrl(url)) return true;
|
|
1557
|
-
let parsed;
|
|
1558
|
-
try {
|
|
1559
|
-
parsed = new URL(url);
|
|
1560
|
-
} catch {
|
|
1561
|
-
return true;
|
|
1562
|
-
}
|
|
1563
|
-
try {
|
|
1564
|
-
const { address } = await lookupFn(parsed.hostname);
|
|
1565
|
-
if (isInternalIP(address)) return true;
|
|
1566
|
-
} catch {
|
|
1567
|
-
return true;
|
|
1568
|
-
}
|
|
1569
|
-
return false;
|
|
1570
|
-
}
|
|
1571
1613
|
|
|
1572
1614
|
// src/actions/navigation.ts
|
|
1573
1615
|
async function navigateViaPlaywright(opts) {
|
|
@@ -1759,7 +1801,7 @@ async function evaluateViaPlaywright(opts) {
|
|
|
1759
1801
|
|
|
1760
1802
|
// src/actions/download.ts
|
|
1761
1803
|
async function downloadViaPlaywright(opts) {
|
|
1762
|
-
assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1804
|
+
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1763
1805
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1764
1806
|
ensurePageState(page);
|
|
1765
1807
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
@@ -1786,7 +1828,7 @@ async function waitForDownloadViaPlaywright(opts) {
|
|
|
1786
1828
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
1787
1829
|
const download = await page.waitForEvent("download", { timeout });
|
|
1788
1830
|
const savePath = opts.path ?? download.suggestedFilename();
|
|
1789
|
-
assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
1831
|
+
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
1790
1832
|
await download.saveAs(savePath);
|
|
1791
1833
|
return {
|
|
1792
1834
|
url: download.url(),
|
|
@@ -1970,7 +2012,7 @@ async function traceStartViaPlaywright(opts) {
|
|
|
1970
2012
|
});
|
|
1971
2013
|
}
|
|
1972
2014
|
async function traceStopViaPlaywright(opts) {
|
|
1973
|
-
assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
2015
|
+
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1974
2016
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1975
2017
|
ensurePageState(page);
|
|
1976
2018
|
const context = page.context();
|