browserclaw 0.3.5 → 0.3.7
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 +228 -182
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +229 -183
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
|
@@ -355,7 +356,9 @@ async function isChromeReachable(cdpUrl, timeoutMs = 500, authToken) {
|
|
|
355
356
|
const headers = {};
|
|
356
357
|
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
357
358
|
const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal, headers });
|
|
358
|
-
|
|
359
|
+
if (!res.ok) return false;
|
|
360
|
+
const data = await res.json();
|
|
361
|
+
return data != null && typeof data === "object";
|
|
359
362
|
} catch {
|
|
360
363
|
return false;
|
|
361
364
|
} finally {
|
|
@@ -371,6 +374,7 @@ async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
|
|
|
371
374
|
const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal, headers });
|
|
372
375
|
if (!res.ok) return null;
|
|
373
376
|
const data = await res.json();
|
|
377
|
+
if (!data || typeof data !== "object") return null;
|
|
374
378
|
return String(data?.webSocketDebuggerUrl ?? "").trim() || null;
|
|
375
379
|
} catch {
|
|
376
380
|
return null;
|
|
@@ -1195,6 +1199,222 @@ function formatAriaNodes(nodes, limit) {
|
|
|
1195
1199
|
}
|
|
1196
1200
|
return out;
|
|
1197
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
|
+
}
|
|
1198
1418
|
|
|
1199
1419
|
// src/actions/interaction.ts
|
|
1200
1420
|
async function clickViaPlaywright(opts) {
|
|
@@ -1327,6 +1547,7 @@ async function setInputFilesViaPlaywright(opts) {
|
|
|
1327
1547
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1328
1548
|
const locator = opts.ref ? refLocator(page, opts.ref) : opts.element ? page.locator(opts.element).first() : null;
|
|
1329
1549
|
if (!locator) throw new Error("Either ref or element is required for setInputFiles");
|
|
1550
|
+
await assertSafeUploadPaths(opts.paths);
|
|
1330
1551
|
try {
|
|
1331
1552
|
await locator.setInputFiles(opts.paths);
|
|
1332
1553
|
} catch (err) {
|
|
@@ -1370,7 +1591,9 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
1370
1591
|
const handler = async (fc) => {
|
|
1371
1592
|
clearTimeout(timer);
|
|
1372
1593
|
try {
|
|
1373
|
-
|
|
1594
|
+
const paths = opts.paths ?? [];
|
|
1595
|
+
if (paths.length > 0) await assertSafeUploadPaths(paths);
|
|
1596
|
+
await fc.setFiles(paths);
|
|
1374
1597
|
resolve2();
|
|
1375
1598
|
} catch (err) {
|
|
1376
1599
|
reject(err);
|
|
@@ -1388,183 +1611,6 @@ async function pressKeyViaPlaywright(opts) {
|
|
|
1388
1611
|
ensurePageState(page);
|
|
1389
1612
|
await page.keyboard.press(key, { delay: Math.max(0, Math.floor(opts.delayMs ?? 0)) });
|
|
1390
1613
|
}
|
|
1391
|
-
var InvalidBrowserNavigationUrlError = class extends Error {
|
|
1392
|
-
constructor(message) {
|
|
1393
|
-
super(message);
|
|
1394
|
-
this.name = "InvalidBrowserNavigationUrlError";
|
|
1395
|
-
}
|
|
1396
|
-
};
|
|
1397
|
-
function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
1398
|
-
return { ssrfPolicy };
|
|
1399
|
-
}
|
|
1400
|
-
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
1401
|
-
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
1402
|
-
async function assertBrowserNavigationAllowed(opts) {
|
|
1403
|
-
const rawUrl = String(opts.url ?? "").trim();
|
|
1404
|
-
let parsed;
|
|
1405
|
-
try {
|
|
1406
|
-
parsed = new URL(rawUrl);
|
|
1407
|
-
} catch {
|
|
1408
|
-
throw new InvalidBrowserNavigationUrlError(`Invalid URL: "${rawUrl}"`);
|
|
1409
|
-
}
|
|
1410
|
-
if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
|
|
1411
|
-
if (SAFE_NON_NETWORK_URLS.has(parsed.href)) return;
|
|
1412
|
-
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
|
|
1413
|
-
}
|
|
1414
|
-
const policy = opts.ssrfPolicy;
|
|
1415
|
-
if (policy?.dangerouslyAllowPrivateNetwork ?? policy?.allowPrivateNetwork ?? true) return;
|
|
1416
|
-
const allowedHostnames = [
|
|
1417
|
-
...policy?.allowedHostnames ?? [],
|
|
1418
|
-
...policy?.hostnameAllowlist ?? []
|
|
1419
|
-
];
|
|
1420
|
-
if (allowedHostnames.length) {
|
|
1421
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
1422
|
-
if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
|
|
1423
|
-
}
|
|
1424
|
-
if (await isInternalUrlResolved(rawUrl, opts.lookupFn)) {
|
|
1425
|
-
throw new InvalidBrowserNavigationUrlError(
|
|
1426
|
-
`Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1427
|
-
);
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
function assertSafeOutputPath(path2, allowedRoots) {
|
|
1431
|
-
if (!path2 || typeof path2 !== "string") {
|
|
1432
|
-
throw new Error("Output path is required.");
|
|
1433
|
-
}
|
|
1434
|
-
const normalized = path.normalize(path2);
|
|
1435
|
-
if (normalized.includes("..")) {
|
|
1436
|
-
throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
|
|
1437
|
-
}
|
|
1438
|
-
if (allowedRoots?.length) {
|
|
1439
|
-
const resolved = path.resolve(normalized);
|
|
1440
|
-
const withinRoot = allowedRoots.some((root) => {
|
|
1441
|
-
const normalizedRoot = path.resolve(root);
|
|
1442
|
-
return resolved === normalizedRoot || resolved.startsWith(normalizedRoot + path.sep);
|
|
1443
|
-
});
|
|
1444
|
-
if (!withinRoot) {
|
|
1445
|
-
throw new Error(`Unsafe output path: "${path2}" is outside allowed directories.`);
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
function expandIPv6(ip) {
|
|
1450
|
-
let normalized = ip;
|
|
1451
|
-
const v4Match = normalized.match(/^(.+:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
1452
|
-
if (v4Match) {
|
|
1453
|
-
const octets = v4Match[2].split(".").map(Number);
|
|
1454
|
-
if (octets.some((o) => o > 255)) return null;
|
|
1455
|
-
const hexHi = (octets[0] << 8 | octets[1]).toString(16).padStart(4, "0");
|
|
1456
|
-
const hexLo = (octets[2] << 8 | octets[3]).toString(16).padStart(4, "0");
|
|
1457
|
-
normalized = v4Match[1] + hexHi + ":" + hexLo;
|
|
1458
|
-
}
|
|
1459
|
-
const halves = normalized.split("::");
|
|
1460
|
-
if (halves.length > 2) return null;
|
|
1461
|
-
if (halves.length === 2) {
|
|
1462
|
-
const left = halves[0] !== "" ? halves[0].split(":") : [];
|
|
1463
|
-
const right = halves[1] !== "" ? halves[1].split(":") : [];
|
|
1464
|
-
const needed = 8 - left.length - right.length;
|
|
1465
|
-
if (needed < 0) return null;
|
|
1466
|
-
const groups2 = [...left, ...Array(needed).fill("0"), ...right];
|
|
1467
|
-
if (groups2.length !== 8) return null;
|
|
1468
|
-
return groups2.map((g) => g.padStart(4, "0")).join(":");
|
|
1469
|
-
}
|
|
1470
|
-
const groups = normalized.split(":");
|
|
1471
|
-
if (groups.length !== 8) return null;
|
|
1472
|
-
return groups.map((g) => g.padStart(4, "0")).join(":");
|
|
1473
|
-
}
|
|
1474
|
-
function hexToIPv4(hiHex, loHex) {
|
|
1475
|
-
const hi = parseInt(hiHex, 16);
|
|
1476
|
-
const lo = parseInt(loHex, 16);
|
|
1477
|
-
return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
|
|
1478
|
-
}
|
|
1479
|
-
function extractEmbeddedIPv4(lower) {
|
|
1480
|
-
if (lower.startsWith("::ffff:")) {
|
|
1481
|
-
return lower.slice(7);
|
|
1482
|
-
}
|
|
1483
|
-
const expanded = expandIPv6(lower);
|
|
1484
|
-
if (expanded === null) return "";
|
|
1485
|
-
const groups = expanded.split(":");
|
|
1486
|
-
if (groups.length !== 8) return "";
|
|
1487
|
-
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0000" && groups[3] === "0000" && groups[4] === "0000" && groups[5] === "0000") {
|
|
1488
|
-
return hexToIPv4(groups[6], groups[7]);
|
|
1489
|
-
}
|
|
1490
|
-
if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0001") {
|
|
1491
|
-
return hexToIPv4(groups[6], groups[7]);
|
|
1492
|
-
}
|
|
1493
|
-
if (groups[0] === "2002") {
|
|
1494
|
-
return hexToIPv4(groups[1], groups[2]);
|
|
1495
|
-
}
|
|
1496
|
-
if (groups[0] === "2001" && groups[1] === "0000") {
|
|
1497
|
-
const hiXored = (parseInt(groups[6], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1498
|
-
const loXored = (parseInt(groups[7], 16) ^ 65535).toString(16).padStart(4, "0");
|
|
1499
|
-
return hexToIPv4(hiXored, loXored);
|
|
1500
|
-
}
|
|
1501
|
-
return null;
|
|
1502
|
-
}
|
|
1503
|
-
function isStrictDecimalOctet(part) {
|
|
1504
|
-
if (!/^[0-9]+$/.test(part)) return false;
|
|
1505
|
-
const n = parseInt(part, 10);
|
|
1506
|
-
if (n < 0 || n > 255) return false;
|
|
1507
|
-
if (String(n) !== part) return false;
|
|
1508
|
-
return true;
|
|
1509
|
-
}
|
|
1510
|
-
function isUnsupportedIPv4Literal(ip) {
|
|
1511
|
-
if (/^[0-9]+$/.test(ip)) return true;
|
|
1512
|
-
const parts = ip.split(".");
|
|
1513
|
-
if (parts.length !== 4) return true;
|
|
1514
|
-
if (!parts.every(isStrictDecimalOctet)) return true;
|
|
1515
|
-
return false;
|
|
1516
|
-
}
|
|
1517
|
-
function isInternalIP(ip) {
|
|
1518
|
-
if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
|
|
1519
|
-
if (/^127\./.test(ip)) return true;
|
|
1520
|
-
if (/^10\./.test(ip)) return true;
|
|
1521
|
-
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
1522
|
-
if (/^192\.168\./.test(ip)) return true;
|
|
1523
|
-
if (/^169\.254\./.test(ip)) return true;
|
|
1524
|
-
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
|
|
1525
|
-
if (ip === "0.0.0.0") return true;
|
|
1526
|
-
const lower = ip.toLowerCase();
|
|
1527
|
-
if (lower === "::1") return true;
|
|
1528
|
-
if (lower.startsWith("fe80:")) return true;
|
|
1529
|
-
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
1530
|
-
const embedded = extractEmbeddedIPv4(lower);
|
|
1531
|
-
if (embedded !== null) {
|
|
1532
|
-
if (embedded === "") return true;
|
|
1533
|
-
return isInternalIP(embedded);
|
|
1534
|
-
}
|
|
1535
|
-
return false;
|
|
1536
|
-
}
|
|
1537
|
-
function isInternalUrl(url) {
|
|
1538
|
-
let parsed;
|
|
1539
|
-
try {
|
|
1540
|
-
parsed = new URL(url);
|
|
1541
|
-
} catch {
|
|
1542
|
-
return true;
|
|
1543
|
-
}
|
|
1544
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
1545
|
-
if (hostname === "localhost") return true;
|
|
1546
|
-
if (isInternalIP(hostname)) return true;
|
|
1547
|
-
if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
|
|
1548
|
-
return true;
|
|
1549
|
-
}
|
|
1550
|
-
return false;
|
|
1551
|
-
}
|
|
1552
|
-
async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
|
|
1553
|
-
if (isInternalUrl(url)) return true;
|
|
1554
|
-
let parsed;
|
|
1555
|
-
try {
|
|
1556
|
-
parsed = new URL(url);
|
|
1557
|
-
} catch {
|
|
1558
|
-
return true;
|
|
1559
|
-
}
|
|
1560
|
-
try {
|
|
1561
|
-
const { address } = await lookupFn(parsed.hostname);
|
|
1562
|
-
if (isInternalIP(address)) return true;
|
|
1563
|
-
} catch {
|
|
1564
|
-
return true;
|
|
1565
|
-
}
|
|
1566
|
-
return false;
|
|
1567
|
-
}
|
|
1568
1614
|
|
|
1569
1615
|
// src/actions/navigation.ts
|
|
1570
1616
|
async function navigateViaPlaywright(opts) {
|
|
@@ -1756,7 +1802,7 @@ async function evaluateViaPlaywright(opts) {
|
|
|
1756
1802
|
|
|
1757
1803
|
// src/actions/download.ts
|
|
1758
1804
|
async function downloadViaPlaywright(opts) {
|
|
1759
|
-
assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1805
|
+
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1760
1806
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1761
1807
|
ensurePageState(page);
|
|
1762
1808
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
@@ -1783,7 +1829,7 @@ async function waitForDownloadViaPlaywright(opts) {
|
|
|
1783
1829
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
1784
1830
|
const download = await page.waitForEvent("download", { timeout });
|
|
1785
1831
|
const savePath = opts.path ?? download.suggestedFilename();
|
|
1786
|
-
assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
1832
|
+
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
1787
1833
|
await download.saveAs(savePath);
|
|
1788
1834
|
return {
|
|
1789
1835
|
url: download.url(),
|
|
@@ -1967,7 +2013,7 @@ async function traceStartViaPlaywright(opts) {
|
|
|
1967
2013
|
});
|
|
1968
2014
|
}
|
|
1969
2015
|
async function traceStopViaPlaywright(opts) {
|
|
1970
|
-
assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
2016
|
+
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1971
2017
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1972
2018
|
ensurePageState(page);
|
|
1973
2019
|
const context = page.context();
|