@fedify/webfinger 2.5.0-dev.2448 → 2.5.0-dev.2492
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/deno.json +1 -1
- package/dist/lookup.test.cjs +599 -227
- package/dist/lookup.test.js +600 -227
- package/dist/mod.cjs +226 -160
- package/dist/mod.d.cts +29 -1
- package/dist/mod.d.ts +30 -2
- package/dist/mod.js +227 -162
- package/package.json +3 -3
- package/src/lookup-impl.ts +338 -0
- package/src/lookup.test.ts +569 -80
- package/src/lookup.ts +34 -231
package/dist/lookup.test.cjs
CHANGED
|
@@ -26,9 +26,9 @@ let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
|
|
|
26
26
|
let _logtape_logtape = require("@logtape/logtape");
|
|
27
27
|
let es_toolkit = require("es-toolkit");
|
|
28
28
|
let node_assert_strict = require("node:assert/strict");
|
|
29
|
-
let node_dns_promises = require("node:dns/promises");
|
|
30
|
-
node_dns_promises = __toESM(node_dns_promises, 1);
|
|
31
29
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
30
|
+
let _fedify_vocab_runtime_internal = require("@fedify/vocab-runtime/internal");
|
|
31
|
+
let node_test = require("node:test");
|
|
32
32
|
//#endregion
|
|
33
33
|
//#region ../../node_modules/.pnpm/regexparam@3.0.0/node_modules/regexparam/dist/index.mjs
|
|
34
34
|
var import_glob_to_regexp = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -1237,7 +1237,7 @@ var esm_default = new class FetchMock {
|
|
|
1237
1237
|
//#endregion
|
|
1238
1238
|
//#region deno.json
|
|
1239
1239
|
var name = "@fedify/webfinger";
|
|
1240
|
-
var version = "2.5.0-dev.
|
|
1240
|
+
var version = "2.5.0-dev.2492+4648c04e";
|
|
1241
1241
|
//#endregion
|
|
1242
1242
|
//#region src/body.ts
|
|
1243
1243
|
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
@@ -1313,13 +1313,230 @@ async function readBoundedText(message, maxBytes, url) {
|
|
|
1313
1313
|
}
|
|
1314
1314
|
}
|
|
1315
1315
|
//#endregion
|
|
1316
|
-
//#region src/lookup.ts
|
|
1316
|
+
//#region src/lookup-impl.ts
|
|
1317
1317
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
1318
1318
|
"fedify",
|
|
1319
1319
|
"webfinger",
|
|
1320
1320
|
"lookup"
|
|
1321
1321
|
]);
|
|
1322
1322
|
const DEFAULT_MAX_REDIRECTION = 5;
|
|
1323
|
+
/**
|
|
1324
|
+
* Looks up a WebFinger resource, opening a fetch session per hop. This is
|
|
1325
|
+
* the implementation of {@link lookupWebFinger}, which is not exported from
|
|
1326
|
+
* the package entry point.
|
|
1327
|
+
* @param resource The resource URL to look up.
|
|
1328
|
+
* @param options Extra options for looking up the resource.
|
|
1329
|
+
* @param deps The dependencies.
|
|
1330
|
+
* @returns The resource descriptor and terminal lookup classification.
|
|
1331
|
+
* @internal
|
|
1332
|
+
*/
|
|
1333
|
+
async function lookupWebFingerInternal(resource, options = {}, deps = {}) {
|
|
1334
|
+
if (typeof resource === "string") resource = new URL(resource);
|
|
1335
|
+
let protocol = "https:";
|
|
1336
|
+
let server;
|
|
1337
|
+
if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
|
|
1338
|
+
const atPos = resource.pathname.lastIndexOf("@");
|
|
1339
|
+
if (atPos < 0) return {
|
|
1340
|
+
resource: null,
|
|
1341
|
+
result: "invalid"
|
|
1342
|
+
};
|
|
1343
|
+
server = resource.pathname.substring(atPos + 1);
|
|
1344
|
+
if (server === "" || /[/?#]/.test(server)) return {
|
|
1345
|
+
resource: null,
|
|
1346
|
+
result: "invalid"
|
|
1347
|
+
};
|
|
1348
|
+
if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
|
|
1349
|
+
resource: null,
|
|
1350
|
+
result: "invalid"
|
|
1351
|
+
};
|
|
1352
|
+
} else {
|
|
1353
|
+
protocol = resource.protocol;
|
|
1354
|
+
server = resource.host;
|
|
1355
|
+
}
|
|
1356
|
+
let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
|
|
1357
|
+
url.searchParams.set("resource", resource.href);
|
|
1358
|
+
const openHop = deps.openHop ?? ((hopUrl) => options.allowPrivateAddress === true ? (0, _fedify_vocab_runtime_internal.openDirectFetch)(hopUrl) : (0, _fedify_vocab_runtime_internal.openPublicFetch)(hopUrl, { signal: options.signal }));
|
|
1359
|
+
let redirected = 0;
|
|
1360
|
+
while (true) {
|
|
1361
|
+
logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
|
|
1362
|
+
const remoteHost = url.host;
|
|
1363
|
+
const session = await openSession(url, openHop, options.signal);
|
|
1364
|
+
if (session == null) return {
|
|
1365
|
+
resource: null,
|
|
1366
|
+
result: "network_error",
|
|
1367
|
+
remoteHost
|
|
1368
|
+
};
|
|
1369
|
+
let hop;
|
|
1370
|
+
try {
|
|
1371
|
+
hop = await fetchHop(session, url, options);
|
|
1372
|
+
} finally {
|
|
1373
|
+
await session.close();
|
|
1374
|
+
}
|
|
1375
|
+
if ("outcome" in hop) return hop.outcome;
|
|
1376
|
+
redirected++;
|
|
1377
|
+
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
1378
|
+
if (redirected > maxRedirection) {
|
|
1379
|
+
logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
|
|
1380
|
+
return {
|
|
1381
|
+
resource: null,
|
|
1382
|
+
result: "invalid",
|
|
1383
|
+
statusCode: hop.statusCode,
|
|
1384
|
+
remoteHost
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
let redirectedUrl;
|
|
1388
|
+
try {
|
|
1389
|
+
redirectedUrl = new URL(hop.redirect, url);
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
logger.error("Invalid Location header while following WebFinger redirect: {error}", {
|
|
1392
|
+
url: url.href,
|
|
1393
|
+
error
|
|
1394
|
+
});
|
|
1395
|
+
return {
|
|
1396
|
+
resource: null,
|
|
1397
|
+
result: "invalid",
|
|
1398
|
+
statusCode: hop.statusCode,
|
|
1399
|
+
remoteHost
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
if (redirectedUrl.protocol !== url.protocol) {
|
|
1403
|
+
logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
|
|
1404
|
+
protocol: url.protocol,
|
|
1405
|
+
redirectedProtocol: redirectedUrl.protocol
|
|
1406
|
+
});
|
|
1407
|
+
return {
|
|
1408
|
+
resource: null,
|
|
1409
|
+
result: "invalid",
|
|
1410
|
+
statusCode: hop.statusCode,
|
|
1411
|
+
remoteHost
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
url = redirectedUrl;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Opens a fetch session for a hop. A disallowed URL, a failed DNS lookup,
|
|
1419
|
+
* or an aborted lookup yields `null` instead of an error.
|
|
1420
|
+
*/
|
|
1421
|
+
async function openSession(url, openHop, signal) {
|
|
1422
|
+
try {
|
|
1423
|
+
return await openHop(url.href);
|
|
1424
|
+
} catch (e) {
|
|
1425
|
+
if (e instanceof _fedify_vocab_runtime.UrlError) {
|
|
1426
|
+
if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
1427
|
+
url: url.href,
|
|
1428
|
+
error: e
|
|
1429
|
+
});
|
|
1430
|
+
else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
|
|
1431
|
+
return null;
|
|
1432
|
+
}
|
|
1433
|
+
if (signal?.aborted) {
|
|
1434
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
|
|
1435
|
+
url: url.href,
|
|
1436
|
+
error: e
|
|
1437
|
+
});
|
|
1438
|
+
return null;
|
|
1439
|
+
}
|
|
1440
|
+
throw e;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Fetches a hop with the given session, and consumes the response body or
|
|
1445
|
+
* starts cancelling it before returning. The caller closes the session.
|
|
1446
|
+
*/
|
|
1447
|
+
async function fetchHop(session, url, options) {
|
|
1448
|
+
let response;
|
|
1449
|
+
try {
|
|
1450
|
+
response = await session.fetch(url, {
|
|
1451
|
+
headers: {
|
|
1452
|
+
Accept: "application/jrd+json",
|
|
1453
|
+
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, _fedify_vocab_runtime.getUserAgent)(options.userAgent)
|
|
1454
|
+
},
|
|
1455
|
+
redirect: "manual",
|
|
1456
|
+
signal: options.signal
|
|
1457
|
+
});
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
|
|
1460
|
+
url: url.href,
|
|
1461
|
+
error
|
|
1462
|
+
});
|
|
1463
|
+
return { outcome: {
|
|
1464
|
+
resource: null,
|
|
1465
|
+
result: "network_error",
|
|
1466
|
+
remoteHost: url.host
|
|
1467
|
+
} };
|
|
1468
|
+
}
|
|
1469
|
+
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
1470
|
+
cancelBody(response, url);
|
|
1471
|
+
return {
|
|
1472
|
+
redirect: response.headers.get("Location"),
|
|
1473
|
+
statusCode: response.status
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
if (!response.ok) {
|
|
1477
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
|
|
1478
|
+
url: url.href,
|
|
1479
|
+
status: response.status,
|
|
1480
|
+
statusText: response.statusText
|
|
1481
|
+
});
|
|
1482
|
+
cancelBody(response, url);
|
|
1483
|
+
return { outcome: {
|
|
1484
|
+
resource: null,
|
|
1485
|
+
result: response.status === 404 || response.status === 410 ? "not_found" : "error",
|
|
1486
|
+
statusCode: response.status,
|
|
1487
|
+
remoteHost: url.host
|
|
1488
|
+
} };
|
|
1489
|
+
}
|
|
1490
|
+
try {
|
|
1491
|
+
const parsed = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url));
|
|
1492
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { outcome: {
|
|
1493
|
+
resource: null,
|
|
1494
|
+
result: "invalid",
|
|
1495
|
+
statusCode: response.status,
|
|
1496
|
+
remoteHost: url.host
|
|
1497
|
+
} };
|
|
1498
|
+
return { outcome: {
|
|
1499
|
+
resource: parsed,
|
|
1500
|
+
result: "found",
|
|
1501
|
+
statusCode: response.status,
|
|
1502
|
+
remoteHost: url.host
|
|
1503
|
+
} };
|
|
1504
|
+
} catch (e) {
|
|
1505
|
+
if (e instanceof BodyTooLargeError) return { outcome: {
|
|
1506
|
+
resource: null,
|
|
1507
|
+
result: "invalid",
|
|
1508
|
+
statusCode: response.status,
|
|
1509
|
+
remoteHost: url.host
|
|
1510
|
+
} };
|
|
1511
|
+
if (e instanceof SyntaxError) {
|
|
1512
|
+
logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
|
|
1513
|
+
return { outcome: {
|
|
1514
|
+
resource: null,
|
|
1515
|
+
result: "invalid",
|
|
1516
|
+
statusCode: response.status,
|
|
1517
|
+
remoteHost: url.host
|
|
1518
|
+
} };
|
|
1519
|
+
}
|
|
1520
|
+
throw e;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Starts cancelling a response body that is not read, without waiting for it
|
|
1525
|
+
* to settle. The cancellation of a branch of a tee (e.g., a body cloned by
|
|
1526
|
+
* a fetch mock) settles only after the other branch is cancelled too, per
|
|
1527
|
+
* the Streams standard, so awaiting it could block forever. A failure is
|
|
1528
|
+
* only logged.
|
|
1529
|
+
*/
|
|
1530
|
+
function cancelBody(response, url) {
|
|
1531
|
+
response.body?.cancel().catch((error) => {
|
|
1532
|
+
logger.debug("Failed to cancel the response body from {url}: {error}", {
|
|
1533
|
+
url: url.href,
|
|
1534
|
+
error
|
|
1535
|
+
});
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
//#endregion
|
|
1539
|
+
//#region src/lookup.ts
|
|
1323
1540
|
const WEBFINGER_HISTOGRAM_BUCKETS = [
|
|
1324
1541
|
5,
|
|
1325
1542
|
10,
|
|
@@ -1373,6 +1590,9 @@ function getMetricResourceScheme(scheme) {
|
|
|
1373
1590
|
return WEBFINGER_LOOKUP_SCHEME_WHITELIST.has(scheme) ? scheme : "other";
|
|
1374
1591
|
}
|
|
1375
1592
|
async function lookupWebFinger(resource, options = {}) {
|
|
1593
|
+
return await lookupWebFingerWithDependencies(resource, options);
|
|
1594
|
+
}
|
|
1595
|
+
async function lookupWebFingerWithDependencies(resource, options = {}, deps = {}) {
|
|
1376
1596
|
const tracer = (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version);
|
|
1377
1597
|
const scheme = getResourceScheme(resource);
|
|
1378
1598
|
return await tracer.startActiveSpan("webfinger.lookup", {
|
|
@@ -1389,7 +1609,7 @@ async function lookupWebFinger(resource, options = {}) {
|
|
|
1389
1609
|
result: "error"
|
|
1390
1610
|
};
|
|
1391
1611
|
try {
|
|
1392
|
-
outcome = await lookupWebFingerInternal(resource, options);
|
|
1612
|
+
outcome = await lookupWebFingerInternal(resource, options, deps);
|
|
1393
1613
|
span.setStatus({ code: outcome.resource === null ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.OK });
|
|
1394
1614
|
return options.withDiagnostics ? outcome : outcome.resource;
|
|
1395
1615
|
} catch (error) {
|
|
@@ -1415,188 +1635,56 @@ function recordWebFingerLookup(meterProvider, durationMs, scheme, outcome) {
|
|
|
1415
1635
|
instruments.lookup.add(1, attributes);
|
|
1416
1636
|
instruments.lookupDuration.record(durationMs, attributes);
|
|
1417
1637
|
}
|
|
1418
|
-
async function lookupWebFingerInternal(resource, options = {}) {
|
|
1419
|
-
if (typeof resource === "string") resource = new URL(resource);
|
|
1420
|
-
let protocol = "https:";
|
|
1421
|
-
let server;
|
|
1422
|
-
if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
|
|
1423
|
-
const atPos = resource.pathname.lastIndexOf("@");
|
|
1424
|
-
if (atPos < 0) return {
|
|
1425
|
-
resource: null,
|
|
1426
|
-
result: "invalid"
|
|
1427
|
-
};
|
|
1428
|
-
server = resource.pathname.substring(atPos + 1);
|
|
1429
|
-
if (server === "" || /[/?#]/.test(server)) return {
|
|
1430
|
-
resource: null,
|
|
1431
|
-
result: "invalid"
|
|
1432
|
-
};
|
|
1433
|
-
if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
|
|
1434
|
-
resource: null,
|
|
1435
|
-
result: "invalid"
|
|
1436
|
-
};
|
|
1437
|
-
} else {
|
|
1438
|
-
protocol = resource.protocol;
|
|
1439
|
-
server = resource.host;
|
|
1440
|
-
}
|
|
1441
|
-
let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
|
|
1442
|
-
url.searchParams.set("resource", resource.href);
|
|
1443
|
-
let redirected = 0;
|
|
1444
|
-
while (true) {
|
|
1445
|
-
const remoteHost = url.host;
|
|
1446
|
-
logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
|
|
1447
|
-
let response;
|
|
1448
|
-
if (options.allowPrivateAddress !== true) try {
|
|
1449
|
-
await (0, _fedify_vocab_runtime.validatePublicUrl)(url.href);
|
|
1450
|
-
} catch (e) {
|
|
1451
|
-
if (e instanceof _fedify_vocab_runtime.UrlError) {
|
|
1452
|
-
if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
1453
|
-
url: url.href,
|
|
1454
|
-
error: e
|
|
1455
|
-
});
|
|
1456
|
-
else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
|
|
1457
|
-
return {
|
|
1458
|
-
resource: null,
|
|
1459
|
-
result: "network_error",
|
|
1460
|
-
remoteHost
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1463
|
-
throw e;
|
|
1464
|
-
}
|
|
1465
|
-
try {
|
|
1466
|
-
response = await fetch(url, {
|
|
1467
|
-
headers: {
|
|
1468
|
-
Accept: "application/jrd+json",
|
|
1469
|
-
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, _fedify_vocab_runtime.getUserAgent)(options.userAgent)
|
|
1470
|
-
},
|
|
1471
|
-
redirect: "manual",
|
|
1472
|
-
signal: options.signal
|
|
1473
|
-
});
|
|
1474
|
-
} catch (error) {
|
|
1475
|
-
logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
|
|
1476
|
-
url: url.href,
|
|
1477
|
-
error
|
|
1478
|
-
});
|
|
1479
|
-
return {
|
|
1480
|
-
resource: null,
|
|
1481
|
-
result: "network_error",
|
|
1482
|
-
remoteHost
|
|
1483
|
-
};
|
|
1484
|
-
}
|
|
1485
|
-
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
1486
|
-
redirected++;
|
|
1487
|
-
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
1488
|
-
if (redirected > maxRedirection) {
|
|
1489
|
-
logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
|
|
1490
|
-
return {
|
|
1491
|
-
resource: null,
|
|
1492
|
-
result: "invalid",
|
|
1493
|
-
statusCode: response.status,
|
|
1494
|
-
remoteHost
|
|
1495
|
-
};
|
|
1496
|
-
}
|
|
1497
|
-
let redirectedUrl;
|
|
1498
|
-
try {
|
|
1499
|
-
redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
|
|
1500
|
-
} catch (e) {
|
|
1501
|
-
logger.error("Invalid Location header while following WebFinger redirect: {error}", {
|
|
1502
|
-
url: url.href,
|
|
1503
|
-
error: e
|
|
1504
|
-
});
|
|
1505
|
-
return {
|
|
1506
|
-
resource: null,
|
|
1507
|
-
result: "invalid",
|
|
1508
|
-
statusCode: response.status,
|
|
1509
|
-
remoteHost
|
|
1510
|
-
};
|
|
1511
|
-
}
|
|
1512
|
-
if (redirectedUrl.protocol !== url.protocol) {
|
|
1513
|
-
logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
|
|
1514
|
-
protocol: url.protocol,
|
|
1515
|
-
redirectedProtocol: redirectedUrl.protocol
|
|
1516
|
-
});
|
|
1517
|
-
return {
|
|
1518
|
-
resource: null,
|
|
1519
|
-
result: "invalid",
|
|
1520
|
-
statusCode: response.status,
|
|
1521
|
-
remoteHost
|
|
1522
|
-
};
|
|
1523
|
-
}
|
|
1524
|
-
url = redirectedUrl;
|
|
1525
|
-
continue;
|
|
1526
|
-
}
|
|
1527
|
-
if (!response.ok) {
|
|
1528
|
-
logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
|
|
1529
|
-
url: url.href,
|
|
1530
|
-
status: response.status,
|
|
1531
|
-
statusText: response.statusText
|
|
1532
|
-
});
|
|
1533
|
-
return {
|
|
1534
|
-
resource: null,
|
|
1535
|
-
result: response.status === 404 || response.status === 410 ? "not_found" : "error",
|
|
1536
|
-
statusCode: response.status,
|
|
1537
|
-
remoteHost
|
|
1538
|
-
};
|
|
1539
|
-
}
|
|
1540
|
-
try {
|
|
1541
|
-
const parsed = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url));
|
|
1542
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {
|
|
1543
|
-
resource: null,
|
|
1544
|
-
result: "invalid",
|
|
1545
|
-
statusCode: response.status,
|
|
1546
|
-
remoteHost
|
|
1547
|
-
};
|
|
1548
|
-
return {
|
|
1549
|
-
resource: parsed,
|
|
1550
|
-
result: "found",
|
|
1551
|
-
statusCode: response.status,
|
|
1552
|
-
remoteHost
|
|
1553
|
-
};
|
|
1554
|
-
} catch (e) {
|
|
1555
|
-
if (e instanceof BodyTooLargeError) return {
|
|
1556
|
-
resource: null,
|
|
1557
|
-
result: "invalid",
|
|
1558
|
-
statusCode: response.status,
|
|
1559
|
-
remoteHost
|
|
1560
|
-
};
|
|
1561
|
-
if (e instanceof SyntaxError) {
|
|
1562
|
-
logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
|
|
1563
|
-
return {
|
|
1564
|
-
resource: null,
|
|
1565
|
-
result: "invalid",
|
|
1566
|
-
statusCode: response.status,
|
|
1567
|
-
remoteHost
|
|
1568
|
-
};
|
|
1569
|
-
}
|
|
1570
|
-
throw e;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
1638
|
//#endregion
|
|
1575
1639
|
//#region src/lookup.test.ts
|
|
1640
|
+
function lookupMock(resource, options = {}) {
|
|
1641
|
+
if (resource.toString().includes("fedify-test.internal")) return lookupWebFingerWithDependencies(resource, options, { openHop: (url) => Promise.resolve({
|
|
1642
|
+
url: new URL(url),
|
|
1643
|
+
fetch: () => Promise.reject(/* @__PURE__ */ new TypeError("Connection refused")),
|
|
1644
|
+
close: () => Promise.resolve()
|
|
1645
|
+
}) });
|
|
1646
|
+
return lookupWebFingerWithDependencies(resource, options, { openHop: options.allowPrivateAddress === true ? _fedify_vocab_runtime_internal.openDirectFetch : openExampleDirectly });
|
|
1647
|
+
}
|
|
1648
|
+
function lookupDirect(resource, options) {
|
|
1649
|
+
return lookupWebFingerWithDependencies(resource, options, { openHop: _fedify_vocab_runtime_internal.openDirectFetch });
|
|
1650
|
+
}
|
|
1651
|
+
function lookupResolving(addresses) {
|
|
1652
|
+
return () => Promise.resolve(addresses);
|
|
1653
|
+
}
|
|
1654
|
+
function openExampleDirectly(url) {
|
|
1655
|
+
return new URL(url).hostname === "example.com" ? (0, _fedify_vocab_runtime_internal.openDirectFetch)(url) : (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup: lookupResolving([{
|
|
1656
|
+
address: "93.184.215.14",
|
|
1657
|
+
family: 4
|
|
1658
|
+
}]) });
|
|
1659
|
+
}
|
|
1660
|
+
async function lookupResourceWithDependencies(resource, options, deps) {
|
|
1661
|
+
return (await lookupWebFingerInternal(resource, options, deps)).resource;
|
|
1662
|
+
}
|
|
1576
1663
|
(0, _fedify_fixture.test)({
|
|
1577
1664
|
name: "lookupWebFinger()",
|
|
1578
1665
|
sanitizeOps: false,
|
|
1579
1666
|
sanitizeResources: false,
|
|
1580
1667
|
async fn(t) {
|
|
1668
|
+
const responseTimers = [];
|
|
1581
1669
|
await t.step("invalid resource", async () => {
|
|
1582
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1583
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1584
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1585
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1586
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1587
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1588
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1670
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe"), null);
|
|
1671
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock(new URL("acct:johndoe")), null);
|
|
1672
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@"), null);
|
|
1673
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock(new URL("acct:johndoe@")), null);
|
|
1674
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com/exploit"), null);
|
|
1675
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com?x=1"), null);
|
|
1676
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com#frag"), null);
|
|
1589
1677
|
});
|
|
1590
1678
|
await t.step("connection refused", async () => {
|
|
1591
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1592
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1679
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@fedify-test.internal"), null);
|
|
1680
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("https://fedify-test.internal/foo"), null);
|
|
1593
1681
|
});
|
|
1594
1682
|
esm_default.spyGlobal();
|
|
1595
1683
|
try {
|
|
1596
1684
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
|
|
1597
1685
|
await t.step("not found", async () => {
|
|
1598
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1599
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1686
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), null);
|
|
1687
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("https://example.com/foo"), null);
|
|
1600
1688
|
});
|
|
1601
1689
|
const expected = {
|
|
1602
1690
|
subject: "acct:johndoe@example.com",
|
|
@@ -1605,7 +1693,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1605
1693
|
esm_default.removeRoutes();
|
|
1606
1694
|
esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
|
|
1607
1695
|
await t.step("acct", async () => {
|
|
1608
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1696
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), expected);
|
|
1609
1697
|
});
|
|
1610
1698
|
const expected2 = {
|
|
1611
1699
|
subject: "https://example.com/foo",
|
|
@@ -1614,7 +1702,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1614
1702
|
esm_default.removeRoutes();
|
|
1615
1703
|
esm_default.get("https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo", { body: expected2 });
|
|
1616
1704
|
await t.step("https", async () => {
|
|
1617
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1705
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("https://example.com/foo"), expected2);
|
|
1618
1706
|
});
|
|
1619
1707
|
const mailtoExpected = {
|
|
1620
1708
|
subject: "mailto:juliet@example.com",
|
|
@@ -1623,7 +1711,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1623
1711
|
esm_default.removeRoutes();
|
|
1624
1712
|
esm_default.get("https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com", { body: mailtoExpected });
|
|
1625
1713
|
await t.step("mailto", async () => {
|
|
1626
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1714
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("mailto:juliet@example.com"), mailtoExpected);
|
|
1627
1715
|
});
|
|
1628
1716
|
const mailtoQueryExpected = {
|
|
1629
1717
|
subject: "mailto:juliet@example.com?subject=Hi",
|
|
@@ -1632,12 +1720,12 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1632
1720
|
esm_default.removeRoutes();
|
|
1633
1721
|
esm_default.get("https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com%3Fsubject%3DHi", { body: mailtoQueryExpected });
|
|
1634
1722
|
await t.step("mailto with hfields", async () => {
|
|
1635
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1723
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("mailto:juliet@example.com?subject=Hi"), mailtoQueryExpected);
|
|
1636
1724
|
});
|
|
1637
1725
|
esm_default.removeRoutes();
|
|
1638
1726
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
|
|
1639
1727
|
await t.step("invalid response", async () => {
|
|
1640
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1728
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), null);
|
|
1641
1729
|
});
|
|
1642
1730
|
esm_default.removeRoutes();
|
|
1643
1731
|
esm_default.get("begin:https://localhost/.well-known/webfinger?", {
|
|
@@ -1649,8 +1737,8 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1649
1737
|
}]
|
|
1650
1738
|
});
|
|
1651
1739
|
await t.step("private address", async () => {
|
|
1652
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1653
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1740
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:test@localhost"), null);
|
|
1741
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:test@localhost", { allowPrivateAddress: true }), {
|
|
1654
1742
|
subject: "acct:test@localhost",
|
|
1655
1743
|
links: [{
|
|
1656
1744
|
rel: "self",
|
|
@@ -1666,7 +1754,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1666
1754
|
});
|
|
1667
1755
|
esm_default.get("begin:https://example.com/.well-known/webfinger2", { body: expected });
|
|
1668
1756
|
await t.step("redirection", async () => {
|
|
1669
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1757
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), expected);
|
|
1670
1758
|
});
|
|
1671
1759
|
esm_default.removeRoutes();
|
|
1672
1760
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", {
|
|
@@ -1674,7 +1762,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1674
1762
|
headers: { Location: "/.well-known/webfinger" }
|
|
1675
1763
|
});
|
|
1676
1764
|
await t.step("infinite redirection", async () => {
|
|
1677
|
-
(0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() =>
|
|
1765
|
+
(0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupMock("acct:johndoe@example.com"), 2e3), null);
|
|
1678
1766
|
});
|
|
1679
1767
|
esm_default.removeRoutes();
|
|
1680
1768
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", {
|
|
@@ -1682,7 +1770,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1682
1770
|
headers: { Location: "ftp://example.com/" }
|
|
1683
1771
|
});
|
|
1684
1772
|
await t.step("redirection to different protocol", async () => {
|
|
1685
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1773
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), null);
|
|
1686
1774
|
});
|
|
1687
1775
|
esm_default.removeRoutes();
|
|
1688
1776
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", {
|
|
@@ -1690,7 +1778,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1690
1778
|
headers: { Location: "https://localhost/" }
|
|
1691
1779
|
});
|
|
1692
1780
|
await t.step("redirection to private address", async () => {
|
|
1693
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1781
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), null);
|
|
1694
1782
|
});
|
|
1695
1783
|
esm_default.removeRoutes();
|
|
1696
1784
|
let redirectCount = 0;
|
|
@@ -1704,13 +1792,13 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1704
1792
|
});
|
|
1705
1793
|
await t.step("custom maxRedirection", async () => {
|
|
1706
1794
|
redirectCount = 0;
|
|
1707
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1795
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), null);
|
|
1708
1796
|
redirectCount = 0;
|
|
1709
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1797
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 2 }), expected);
|
|
1710
1798
|
redirectCount = 0;
|
|
1711
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1799
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 3 }), expected);
|
|
1712
1800
|
redirectCount = 0;
|
|
1713
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1801
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com"), expected);
|
|
1714
1802
|
});
|
|
1715
1803
|
await t.step("maxRedirection: 1 follows exactly one redirect", async () => {
|
|
1716
1804
|
esm_default.removeRoutes();
|
|
@@ -1722,7 +1810,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1722
1810
|
headers: { Location: "/.well-known/webfinger?after=1" }
|
|
1723
1811
|
} : { body: expected };
|
|
1724
1812
|
});
|
|
1725
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1813
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), expected);
|
|
1726
1814
|
esm_default.removeRoutes();
|
|
1727
1815
|
count = 0;
|
|
1728
1816
|
esm_default.get("begin:https://example.com/.well-known/webfinger", () => {
|
|
@@ -1732,18 +1820,18 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1732
1820
|
headers: { Location: `/.well-known/webfinger?after=${count}` }
|
|
1733
1821
|
} : { body: expected };
|
|
1734
1822
|
});
|
|
1735
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1823
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), null);
|
|
1736
1824
|
});
|
|
1737
1825
|
esm_default.removeRoutes();
|
|
1738
1826
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", () => new Promise((resolve) => {
|
|
1739
1827
|
const timeoutId = setTimeout(() => {
|
|
1740
1828
|
resolve({ body: expected });
|
|
1741
1829
|
}, 1e3);
|
|
1742
|
-
|
|
1830
|
+
responseTimers.push(timeoutId);
|
|
1743
1831
|
}));
|
|
1744
1832
|
await t.step("request cancellation", async () => {
|
|
1745
1833
|
const controller = new AbortController();
|
|
1746
|
-
const promise =
|
|
1834
|
+
const promise = lookupMock("acct:johndoe@example.com", { signal: controller.signal });
|
|
1747
1835
|
controller.abort();
|
|
1748
1836
|
(0, node_assert_strict.deepStrictEqual)(await promise, null);
|
|
1749
1837
|
});
|
|
@@ -1759,12 +1847,12 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1759
1847
|
const timeoutId = setTimeout(() => {
|
|
1760
1848
|
resolve({ body: expected });
|
|
1761
1849
|
}, 1e3);
|
|
1762
|
-
|
|
1850
|
+
responseTimers.push(timeoutId);
|
|
1763
1851
|
});
|
|
1764
1852
|
});
|
|
1765
1853
|
await t.step("cancellation during redirection", async () => {
|
|
1766
1854
|
const controller = new AbortController();
|
|
1767
|
-
const promise =
|
|
1855
|
+
const promise = lookupMock("acct:johndoe@example.com", { signal: controller.signal });
|
|
1768
1856
|
setTimeout(() => controller.abort(), 100);
|
|
1769
1857
|
(0, node_assert_strict.deepStrictEqual)(await promise, null);
|
|
1770
1858
|
});
|
|
@@ -1773,19 +1861,20 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1773
1861
|
const timeoutId = setTimeout(() => {
|
|
1774
1862
|
resolve({ body: expected });
|
|
1775
1863
|
}, 500);
|
|
1776
|
-
|
|
1864
|
+
responseTimers.push(timeoutId);
|
|
1777
1865
|
}));
|
|
1778
1866
|
await t.step("cancellation with immediate abort", async () => {
|
|
1779
1867
|
const controller = new AbortController();
|
|
1780
1868
|
controller.abort();
|
|
1781
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1869
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { signal: controller.signal }), null);
|
|
1782
1870
|
});
|
|
1783
1871
|
esm_default.removeRoutes();
|
|
1784
1872
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: expected });
|
|
1785
1873
|
await t.step("successful request with signal", async () => {
|
|
1786
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1874
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { signal: new AbortController().signal }), expected);
|
|
1787
1875
|
});
|
|
1788
1876
|
} finally {
|
|
1877
|
+
for (const timer of responseTimers) clearTimeout(timer);
|
|
1789
1878
|
esm_default.removeRoutes();
|
|
1790
1879
|
esm_default.hardReset();
|
|
1791
1880
|
}
|
|
@@ -1805,7 +1894,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1805
1894
|
esm_default.removeRoutes();
|
|
1806
1895
|
esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
|
|
1807
1896
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1808
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1897
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { meterProvider }), expected);
|
|
1809
1898
|
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
1810
1899
|
(0, node_assert_strict.deepStrictEqual)(counters.length, 1);
|
|
1811
1900
|
(0, node_assert_strict.deepStrictEqual)(counters[0].type, "counter");
|
|
@@ -1828,7 +1917,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1828
1917
|
links: []
|
|
1829
1918
|
} });
|
|
1830
1919
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1831
|
-
await
|
|
1920
|
+
await lookupMock("https://example.com/foo", { meterProvider });
|
|
1832
1921
|
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
1833
1922
|
(0, node_assert_strict.deepStrictEqual)(counters.length, 1);
|
|
1834
1923
|
(0, node_assert_strict.deepStrictEqual)(counters[0].attributes["webfinger.resource.scheme"], "https");
|
|
@@ -1841,7 +1930,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1841
1930
|
links: []
|
|
1842
1931
|
} });
|
|
1843
1932
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1844
|
-
await
|
|
1933
|
+
await lookupMock("https://example.com:8443/foo", { meterProvider });
|
|
1845
1934
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1846
1935
|
(0, node_assert_strict.ok)(counter != null);
|
|
1847
1936
|
(0, node_assert_strict.deepStrictEqual)(counter.attributes["activitypub.remote.host"], "example.com:8443");
|
|
@@ -1850,7 +1939,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1850
1939
|
esm_default.removeRoutes();
|
|
1851
1940
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
|
|
1852
1941
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1853
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1942
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@example.com", { meterProvider }), null);
|
|
1854
1943
|
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
1855
1944
|
(0, node_assert_strict.deepStrictEqual)(counters.length, 1);
|
|
1856
1945
|
(0, node_assert_strict.deepStrictEqual)(counters[0].attributes["webfinger.lookup.result"], "not_found");
|
|
@@ -1864,7 +1953,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1864
1953
|
esm_default.removeRoutes();
|
|
1865
1954
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 410 });
|
|
1866
1955
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1867
|
-
await
|
|
1956
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1868
1957
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1869
1958
|
(0, node_assert_strict.ok)(counter != null);
|
|
1870
1959
|
(0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "not_found");
|
|
@@ -1874,7 +1963,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1874
1963
|
esm_default.removeRoutes();
|
|
1875
1964
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 500 });
|
|
1876
1965
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1877
|
-
await
|
|
1966
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1878
1967
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1879
1968
|
(0, node_assert_strict.ok)(counter != null);
|
|
1880
1969
|
(0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "error");
|
|
@@ -1884,7 +1973,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1884
1973
|
esm_default.removeRoutes();
|
|
1885
1974
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
|
|
1886
1975
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1887
|
-
await
|
|
1976
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1888
1977
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1889
1978
|
(0, node_assert_strict.ok)(counter != null);
|
|
1890
1979
|
(0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
|
|
@@ -1893,7 +1982,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1893
1982
|
await t.step("records result=network_error when fetch never reaches the remote", async () => {
|
|
1894
1983
|
esm_default.removeRoutes();
|
|
1895
1984
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1896
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1985
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe@fedify-test.internal", { meterProvider }), null);
|
|
1897
1986
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1898
1987
|
(0, node_assert_strict.ok)(counter != null);
|
|
1899
1988
|
(0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "network_error");
|
|
@@ -1902,7 +1991,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1902
1991
|
});
|
|
1903
1992
|
await t.step("records result=invalid for malformed acct: resources", async () => {
|
|
1904
1993
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1905
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1994
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:johndoe", { meterProvider }), null);
|
|
1906
1995
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1907
1996
|
(0, node_assert_strict.ok)(counter != null);
|
|
1908
1997
|
(0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
|
|
@@ -1916,7 +2005,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1916
2005
|
headers: { Location: "/.well-known/webfinger" }
|
|
1917
2006
|
});
|
|
1918
2007
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1919
|
-
(0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() =>
|
|
2008
|
+
(0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupMock("acct:johndoe@example.com", {
|
|
1920
2009
|
meterProvider,
|
|
1921
2010
|
maxRedirection: 3
|
|
1922
2011
|
}), 2e3), null);
|
|
@@ -1933,7 +2022,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1933
2022
|
headers: { Location: "ftp://example.com/" }
|
|
1934
2023
|
});
|
|
1935
2024
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1936
|
-
await
|
|
2025
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1937
2026
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1938
2027
|
(0, node_assert_strict.ok)(counter != null);
|
|
1939
2028
|
(0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
|
|
@@ -1946,7 +2035,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1946
2035
|
headers: { Location: "https://localhost/" }
|
|
1947
2036
|
});
|
|
1948
2037
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1949
|
-
await
|
|
2038
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1950
2039
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1951
2040
|
(0, node_assert_strict.ok)(counter != null);
|
|
1952
2041
|
(0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "network_error");
|
|
@@ -1959,7 +2048,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1959
2048
|
headers: { Location: "http://[bad" }
|
|
1960
2049
|
});
|
|
1961
2050
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1962
|
-
await
|
|
2051
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1963
2052
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1964
2053
|
(0, node_assert_strict.ok)(counter != null);
|
|
1965
2054
|
(0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "invalid");
|
|
@@ -1969,7 +2058,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1969
2058
|
await t.step("buckets unknown resource schemes as 'other' to keep metric cardinality bounded", async () => {
|
|
1970
2059
|
esm_default.removeRoutes();
|
|
1971
2060
|
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1972
|
-
await
|
|
2061
|
+
await lookupMock("ssh://example.com/foo", { meterProvider });
|
|
1973
2062
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1974
2063
|
(0, node_assert_strict.ok)(counter != null);
|
|
1975
2064
|
(0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.resource.scheme"], "other");
|
|
@@ -1978,7 +2067,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1978
2067
|
esm_default.removeRoutes();
|
|
1979
2068
|
esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
|
|
1980
2069
|
const [_unused, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
1981
|
-
await
|
|
2070
|
+
await lookupMock("acct:johndoe@example.com");
|
|
1982
2071
|
(0, node_assert_strict.deepStrictEqual)(recorder.getMeasurements("webfinger.lookup").length, 0);
|
|
1983
2072
|
(0, node_assert_strict.deepStrictEqual)(recorder.getMeasurements("webfinger.lookup.duration").length, 0);
|
|
1984
2073
|
});
|
|
@@ -1995,25 +2084,23 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1995
2084
|
"Content-Type": "application/jrd+json",
|
|
1996
2085
|
"Content-Length": "1"
|
|
1997
2086
|
} }));
|
|
1998
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
2087
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:alice@example.com", { allowPrivateAddress: true }), null);
|
|
1999
2088
|
oversized = false;
|
|
2000
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
2089
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupMock("acct:alice@example.com", { allowPrivateAddress: true }), { subject: "acct:alice@example.com" });
|
|
2001
2090
|
} finally {
|
|
2002
2091
|
esm_default.hardReset();
|
|
2003
2092
|
}
|
|
2004
2093
|
});
|
|
2005
2094
|
(0, _fedify_fixture.test)("lookupWebFinger() logs DNS failures as such", async (t) => {
|
|
2006
|
-
if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") return;
|
|
2007
2095
|
for (const result of [
|
|
2008
2096
|
"throws",
|
|
2009
2097
|
"empty",
|
|
2010
2098
|
"private"
|
|
2011
2099
|
]) await t.step(result, async () => {
|
|
2012
|
-
const
|
|
2013
|
-
node_dns_promises.default.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
2100
|
+
const lookup = () => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
2014
2101
|
address: "127.0.0.1",
|
|
2015
2102
|
family: 4
|
|
2016
|
-
}])
|
|
2103
|
+
}]);
|
|
2017
2104
|
const records = [];
|
|
2018
2105
|
await (0, _logtape_logtape.configure)({
|
|
2019
2106
|
sinks: { buffer: (record) => records.push(record) },
|
|
@@ -2028,7 +2115,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
2028
2115
|
reset: true
|
|
2029
2116
|
});
|
|
2030
2117
|
try {
|
|
2031
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
2118
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:alice@dns-failure.invalid", {}, { openHop: (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup }) }), null);
|
|
2032
2119
|
const failures = records.filter((r) => r.rawMessage !== "Fetching WebFinger resource descriptor from {url}...");
|
|
2033
2120
|
(0, node_assert_strict.deepStrictEqual)(failures.map((r) => [r.level, r.rawMessage]), [result === "private" ? ["error", "Invalid URL for WebFinger resource descriptor: {error}"] : ["debug", "DNS lookup failed for {url}"]]);
|
|
2034
2121
|
const { error } = failures[0].properties;
|
|
@@ -2036,7 +2123,6 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
2036
2123
|
(0, node_assert_strict.deepStrictEqual)(error.reason, result === "private" ? "disallowed" : "dns");
|
|
2037
2124
|
} finally {
|
|
2038
2125
|
await (0, _logtape_logtape.reset)();
|
|
2039
|
-
node_dns_promises.default.lookup = originalLookup;
|
|
2040
2126
|
}
|
|
2041
2127
|
});
|
|
2042
2128
|
});
|
|
@@ -2045,7 +2131,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
2045
2131
|
sanitizeResources: false
|
|
2046
2132
|
}, async (t) => {
|
|
2047
2133
|
await t.step("network_error", async () => {
|
|
2048
|
-
const outcome = await
|
|
2134
|
+
const outcome = await lookupMock("acct:johndoe@fedify-test.internal", { withDiagnostics: true });
|
|
2049
2135
|
(0, node_assert_strict.deepStrictEqual)(outcome.resource, null);
|
|
2050
2136
|
(0, node_assert_strict.deepStrictEqual)(outcome.result, "network_error");
|
|
2051
2137
|
(0, node_assert_strict.deepStrictEqual)("statusCode" in outcome, false, "no HTTP response means no status code");
|
|
@@ -2059,7 +2145,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
2059
2145
|
links: []
|
|
2060
2146
|
};
|
|
2061
2147
|
esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
|
|
2062
|
-
const outcome = await
|
|
2148
|
+
const outcome = await lookupMock("acct:johndoe@example.com", { withDiagnostics: true });
|
|
2063
2149
|
(0, node_assert_strict.deepStrictEqual)(outcome.resource, expected);
|
|
2064
2150
|
(0, node_assert_strict.deepStrictEqual)(outcome.result, "found");
|
|
2065
2151
|
(0, node_assert_strict.deepStrictEqual)(outcome.statusCode, 200);
|
|
@@ -2068,7 +2154,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
2068
2154
|
await t.step("not_found", async () => {
|
|
2069
2155
|
esm_default.removeRoutes();
|
|
2070
2156
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
|
|
2071
|
-
const outcome = await
|
|
2157
|
+
const outcome = await lookupMock("acct:johndoe@example.com", { withDiagnostics: true });
|
|
2072
2158
|
(0, node_assert_strict.deepStrictEqual)(outcome.resource, null);
|
|
2073
2159
|
(0, node_assert_strict.deepStrictEqual)(outcome.result, "not_found");
|
|
2074
2160
|
(0, node_assert_strict.deepStrictEqual)(outcome.statusCode, 404);
|
|
@@ -2077,7 +2163,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
2077
2163
|
await t.step("invalid", async () => {
|
|
2078
2164
|
esm_default.removeRoutes();
|
|
2079
2165
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
|
|
2080
|
-
const outcome = await
|
|
2166
|
+
const outcome = await lookupMock("acct:johndoe@example.com", { withDiagnostics: true });
|
|
2081
2167
|
(0, node_assert_strict.deepStrictEqual)(outcome.resource, null);
|
|
2082
2168
|
(0, node_assert_strict.deepStrictEqual)(outcome.result, "invalid");
|
|
2083
2169
|
(0, node_assert_strict.deepStrictEqual)(outcome.statusCode, 200);
|
|
@@ -2086,7 +2172,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
2086
2172
|
await t.step("invalid: a literal JSON null body is not a resource descriptor", async () => {
|
|
2087
2173
|
esm_default.removeRoutes();
|
|
2088
2174
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "null" });
|
|
2089
|
-
const outcome = await
|
|
2175
|
+
const outcome = await lookupMock("acct:johndoe@example.com", { withDiagnostics: true });
|
|
2090
2176
|
(0, node_assert_strict.deepStrictEqual)(outcome.resource, null);
|
|
2091
2177
|
(0, node_assert_strict.deepStrictEqual)(outcome.result, "invalid");
|
|
2092
2178
|
(0, node_assert_strict.deepStrictEqual)(outcome.statusCode, 200);
|
|
@@ -2095,7 +2181,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
2095
2181
|
await t.step("invalid: a JSON array body is not a resource descriptor", async () => {
|
|
2096
2182
|
esm_default.removeRoutes();
|
|
2097
2183
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "[]" });
|
|
2098
|
-
const outcome = await
|
|
2184
|
+
const outcome = await lookupMock("acct:johndoe@example.com", { withDiagnostics: true });
|
|
2099
2185
|
(0, node_assert_strict.deepStrictEqual)(outcome.resource, null);
|
|
2100
2186
|
(0, node_assert_strict.deepStrictEqual)(outcome.result, "invalid");
|
|
2101
2187
|
(0, node_assert_strict.deepStrictEqual)(outcome.statusCode, 200);
|
|
@@ -2104,7 +2190,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
2104
2190
|
await t.step("error", async () => {
|
|
2105
2191
|
esm_default.removeRoutes();
|
|
2106
2192
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 500 });
|
|
2107
|
-
const outcome = await
|
|
2193
|
+
const outcome = await lookupMock("acct:johndoe@example.com", { withDiagnostics: true });
|
|
2108
2194
|
(0, node_assert_strict.deepStrictEqual)(outcome.resource, null);
|
|
2109
2195
|
(0, node_assert_strict.deepStrictEqual)(outcome.result, "error");
|
|
2110
2196
|
(0, node_assert_strict.deepStrictEqual)(outcome.statusCode, 500);
|
|
@@ -2123,10 +2209,296 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
2123
2209
|
links: []
|
|
2124
2210
|
};
|
|
2125
2211
|
esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
|
|
2126
|
-
(0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", {
|
|
2212
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", {
|
|
2213
|
+
userAgent: "test-agent",
|
|
2214
|
+
allowPrivateAddress: true
|
|
2215
|
+
}), expected);
|
|
2127
2216
|
} finally {
|
|
2128
2217
|
esm_default.removeRoutes();
|
|
2129
2218
|
esm_default.hardReset();
|
|
2130
2219
|
}
|
|
2131
2220
|
});
|
|
2221
|
+
function recordHops(respond) {
|
|
2222
|
+
const events = [];
|
|
2223
|
+
const unsettled = [];
|
|
2224
|
+
let opens = 0;
|
|
2225
|
+
let closes = 0;
|
|
2226
|
+
let responses = 0;
|
|
2227
|
+
return {
|
|
2228
|
+
events,
|
|
2229
|
+
async openHop(url) {
|
|
2230
|
+
opens++;
|
|
2231
|
+
events.push(`open ${url}`);
|
|
2232
|
+
const states = [];
|
|
2233
|
+
let closed = false;
|
|
2234
|
+
return {
|
|
2235
|
+
url: new URL(url),
|
|
2236
|
+
async fetch(input) {
|
|
2237
|
+
if (closed) throw new TypeError("The fetch session is closed.");
|
|
2238
|
+
const target = new URL(input instanceof Request ? input.url : input);
|
|
2239
|
+
const id = ++responses;
|
|
2240
|
+
events.push(`fetch ${target.href} #${id}`);
|
|
2241
|
+
const fake = respond(target);
|
|
2242
|
+
const state = {
|
|
2243
|
+
id,
|
|
2244
|
+
settled: false
|
|
2245
|
+
};
|
|
2246
|
+
states.push(state);
|
|
2247
|
+
let sent = false;
|
|
2248
|
+
const body = new ReadableStream({
|
|
2249
|
+
pull(controller) {
|
|
2250
|
+
if (!sent && fake.body != null && fake.body !== "") {
|
|
2251
|
+
sent = true;
|
|
2252
|
+
controller.enqueue(new TextEncoder().encode(fake.body));
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
state.settled = true;
|
|
2256
|
+
events.push(`consumed #${id}`);
|
|
2257
|
+
controller.close();
|
|
2258
|
+
},
|
|
2259
|
+
cancel() {
|
|
2260
|
+
state.settled = true;
|
|
2261
|
+
events.push(`cancel #${id}`);
|
|
2262
|
+
if (fake.cancelFails) throw new Error("Cancellation failed");
|
|
2263
|
+
if (fake.cancelNeverSettles) return new Promise(() => {});
|
|
2264
|
+
}
|
|
2265
|
+
}, { highWaterMark: 0 });
|
|
2266
|
+
const response = new Response(body, {
|
|
2267
|
+
status: fake.status ?? 200,
|
|
2268
|
+
headers: fake.headers
|
|
2269
|
+
});
|
|
2270
|
+
if (fake.url != null) Object.defineProperty(response, "url", { value: fake.url });
|
|
2271
|
+
return response;
|
|
2272
|
+
},
|
|
2273
|
+
async close() {
|
|
2274
|
+
if (closed) return;
|
|
2275
|
+
closed = true;
|
|
2276
|
+
closes++;
|
|
2277
|
+
for (const state of states) if (!state.settled) unsettled.push(`#${state.id}`);
|
|
2278
|
+
events.push(`close ${url}`);
|
|
2279
|
+
}
|
|
2280
|
+
};
|
|
2281
|
+
},
|
|
2282
|
+
assertInvariants() {
|
|
2283
|
+
(0, node_assert_strict.deepStrictEqual)(closes, opens);
|
|
2284
|
+
(0, node_assert_strict.deepStrictEqual)(unsettled, []);
|
|
2285
|
+
}
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
(0, _fedify_fixture.test)("lookupWebFinger() closes each hop after cleaning up its body", async (t) => {
|
|
2289
|
+
const descriptor = {
|
|
2290
|
+
subject: "acct:johndoe@example.com",
|
|
2291
|
+
links: []
|
|
2292
|
+
};
|
|
2293
|
+
const first = "https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com";
|
|
2294
|
+
const second = "https://example.com/.well-known/webfinger2";
|
|
2295
|
+
await t.step("redirect", async () => {
|
|
2296
|
+
const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
|
|
2297
|
+
status: 302,
|
|
2298
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
2299
|
+
body: "redirecting",
|
|
2300
|
+
url: url.href.replace("example.com", "93.184.215.14")
|
|
2301
|
+
} : { body: JSON.stringify(descriptor) });
|
|
2302
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
|
|
2303
|
+
(0, node_assert_strict.deepStrictEqual)(recorder.events, [
|
|
2304
|
+
`open ${first}`,
|
|
2305
|
+
`fetch ${first} #1`,
|
|
2306
|
+
"cancel #1",
|
|
2307
|
+
`close ${first}`,
|
|
2308
|
+
`open ${second}`,
|
|
2309
|
+
`fetch ${second} #2`,
|
|
2310
|
+
"consumed #2",
|
|
2311
|
+
`close ${second}`
|
|
2312
|
+
]);
|
|
2313
|
+
recorder.assertInvariants();
|
|
2314
|
+
});
|
|
2315
|
+
await t.step("non-2xx response", async () => {
|
|
2316
|
+
const recorder = recordHops(() => ({
|
|
2317
|
+
status: 404,
|
|
2318
|
+
body: "not found"
|
|
2319
|
+
}));
|
|
2320
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), null);
|
|
2321
|
+
(0, node_assert_strict.deepStrictEqual)(recorder.events, [
|
|
2322
|
+
`open ${first}`,
|
|
2323
|
+
`fetch ${first} #1`,
|
|
2324
|
+
"cancel #1",
|
|
2325
|
+
`close ${first}`
|
|
2326
|
+
]);
|
|
2327
|
+
recorder.assertInvariants();
|
|
2328
|
+
});
|
|
2329
|
+
await t.step("too many redirections", async () => {
|
|
2330
|
+
const recorder = recordHops(() => ({
|
|
2331
|
+
status: 302,
|
|
2332
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
2333
|
+
body: "redirecting"
|
|
2334
|
+
}));
|
|
2335
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:johndoe@example.com", { maxRedirection: 1 }, { openHop: recorder.openHop }), null);
|
|
2336
|
+
(0, node_assert_strict.deepStrictEqual)(recorder.events, [
|
|
2337
|
+
`open ${first}`,
|
|
2338
|
+
`fetch ${first} #1`,
|
|
2339
|
+
"cancel #1",
|
|
2340
|
+
`close ${first}`,
|
|
2341
|
+
`open ${second}`,
|
|
2342
|
+
`fetch ${second} #2`,
|
|
2343
|
+
"cancel #2",
|
|
2344
|
+
`close ${second}`
|
|
2345
|
+
]);
|
|
2346
|
+
recorder.assertInvariants();
|
|
2347
|
+
});
|
|
2348
|
+
await t.step("failed redirect body cancellation", async () => {
|
|
2349
|
+
const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
|
|
2350
|
+
status: 302,
|
|
2351
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
2352
|
+
body: "redirecting",
|
|
2353
|
+
cancelFails: true
|
|
2354
|
+
} : { body: JSON.stringify(descriptor) });
|
|
2355
|
+
const records = [];
|
|
2356
|
+
await (0, _logtape_logtape.configure)({
|
|
2357
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
2358
|
+
loggers: [{
|
|
2359
|
+
category: "fedify",
|
|
2360
|
+
sinks: ["buffer"],
|
|
2361
|
+
lowestLevel: "debug"
|
|
2362
|
+
}, {
|
|
2363
|
+
category: ["logtape", "meta"],
|
|
2364
|
+
sinks: []
|
|
2365
|
+
}],
|
|
2366
|
+
reset: true
|
|
2367
|
+
});
|
|
2368
|
+
try {
|
|
2369
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
|
|
2370
|
+
const failures = records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}");
|
|
2371
|
+
(0, node_assert_strict.deepStrictEqual)(failures.map((r) => [r.level, r.properties.url]), [["debug", first]]);
|
|
2372
|
+
const { error } = failures[0].properties;
|
|
2373
|
+
(0, node_assert_strict.ok)(error instanceof Error);
|
|
2374
|
+
(0, node_assert_strict.deepStrictEqual)(error.message, "Cancellation failed");
|
|
2375
|
+
} finally {
|
|
2376
|
+
await (0, _logtape_logtape.reset)();
|
|
2377
|
+
}
|
|
2378
|
+
(0, node_assert_strict.deepStrictEqual)(recorder.events, [
|
|
2379
|
+
`open ${first}`,
|
|
2380
|
+
`fetch ${first} #1`,
|
|
2381
|
+
"cancel #1",
|
|
2382
|
+
`close ${first}`,
|
|
2383
|
+
`open ${second}`,
|
|
2384
|
+
`fetch ${second} #2`,
|
|
2385
|
+
"consumed #2",
|
|
2386
|
+
`close ${second}`
|
|
2387
|
+
]);
|
|
2388
|
+
recorder.assertInvariants();
|
|
2389
|
+
});
|
|
2390
|
+
await t.step("never-settling body cancellation", async () => {
|
|
2391
|
+
const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
|
|
2392
|
+
status: 302,
|
|
2393
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
2394
|
+
body: "redirecting",
|
|
2395
|
+
cancelNeverSettles: true
|
|
2396
|
+
} : {
|
|
2397
|
+
status: 404,
|
|
2398
|
+
body: "not found",
|
|
2399
|
+
cancelNeverSettles: true
|
|
2400
|
+
});
|
|
2401
|
+
(0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop })), null);
|
|
2402
|
+
(0, node_assert_strict.deepStrictEqual)(recorder.events, [
|
|
2403
|
+
`open ${first}`,
|
|
2404
|
+
`fetch ${first} #1`,
|
|
2405
|
+
"cancel #1",
|
|
2406
|
+
`close ${first}`,
|
|
2407
|
+
`open ${second}`,
|
|
2408
|
+
`fetch ${second} #2`,
|
|
2409
|
+
"cancel #2",
|
|
2410
|
+
`close ${second}`
|
|
2411
|
+
]);
|
|
2412
|
+
recorder.assertInvariants();
|
|
2413
|
+
});
|
|
2414
|
+
});
|
|
2415
|
+
/**
|
|
2416
|
+
* Rejects if the given promise does not settle within the timeout, so that
|
|
2417
|
+
* a hang fails the test instead of blocking it.
|
|
2418
|
+
*/
|
|
2419
|
+
async function settleWithin(promise, ms = 5e3) {
|
|
2420
|
+
let timer;
|
|
2421
|
+
try {
|
|
2422
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
2423
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Did not settle within ${ms} ms.`)), ms);
|
|
2424
|
+
})]);
|
|
2425
|
+
} finally {
|
|
2426
|
+
clearTimeout(timer);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
function neverCancellableResponse(init) {
|
|
2430
|
+
return new Response(new ReadableStream({
|
|
2431
|
+
start(controller) {
|
|
2432
|
+
controller.enqueue(new TextEncoder().encode("never cancelled"));
|
|
2433
|
+
},
|
|
2434
|
+
cancel: () => new Promise(() => {})
|
|
2435
|
+
}), init);
|
|
2436
|
+
}
|
|
2437
|
+
(0, _fedify_fixture.test)("lookupWebFinger() does not wait for cancelling response bodies", async (t) => {
|
|
2438
|
+
const descriptor = {
|
|
2439
|
+
subject: "acct:johndoe@example.com",
|
|
2440
|
+
links: []
|
|
2441
|
+
};
|
|
2442
|
+
esm_default.mockGlobal();
|
|
2443
|
+
try {
|
|
2444
|
+
await t.step("redirect", async () => {
|
|
2445
|
+
esm_default.removeRoutes();
|
|
2446
|
+
esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({
|
|
2447
|
+
status: 302,
|
|
2448
|
+
headers: { Location: "/.well-known/webfinger2" }
|
|
2449
|
+
}));
|
|
2450
|
+
esm_default.get("https://example.com/.well-known/webfinger2", () => new Response(JSON.stringify(descriptor), { headers: { "Content-Type": "application/jrd+json" } }));
|
|
2451
|
+
(0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupDirect("acct:johndoe@example.com")), descriptor);
|
|
2452
|
+
});
|
|
2453
|
+
await t.step("non-2xx response", async () => {
|
|
2454
|
+
esm_default.removeRoutes();
|
|
2455
|
+
esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({ status: 404 }));
|
|
2456
|
+
(0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupDirect("acct:johndoe@example.com")), null);
|
|
2457
|
+
});
|
|
2458
|
+
} finally {
|
|
2459
|
+
esm_default.hardReset();
|
|
2460
|
+
}
|
|
2461
|
+
});
|
|
2462
|
+
(0, node_test.test)("lookupWebFinger() records network_error for rejected hop addresses", async () => {
|
|
2463
|
+
for (const lookup of [
|
|
2464
|
+
() => Promise.reject(/* @__PURE__ */ new Error("DNS unavailable")),
|
|
2465
|
+
() => Promise.resolve([]),
|
|
2466
|
+
() => Promise.resolve([{
|
|
2467
|
+
address: "127.0.0.1",
|
|
2468
|
+
family: 4
|
|
2469
|
+
}])
|
|
2470
|
+
]) {
|
|
2471
|
+
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
2472
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupWebFingerWithDependencies("acct:alice@dns-failure.invalid", { meterProvider }, { openHop: (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup }) }), null);
|
|
2473
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
2474
|
+
(0, node_assert_strict.ok)(counter != null);
|
|
2475
|
+
(0, node_assert_strict.deepStrictEqual)(counter.attributes["webfinger.lookup.result"], "network_error");
|
|
2476
|
+
(0, node_assert_strict.deepStrictEqual)(counter.attributes["activitypub.remote.host"], "dns-failure.invalid");
|
|
2477
|
+
(0, node_assert_strict.deepStrictEqual)("http.response.status_code" in counter.attributes, false);
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
2480
|
+
(0, node_test.test)("WebFinger diagnostics classify non-object JSON after closing the hop", async () => {
|
|
2481
|
+
for (const body of [
|
|
2482
|
+
"null",
|
|
2483
|
+
"[]",
|
|
2484
|
+
"42",
|
|
2485
|
+
"\"text\"",
|
|
2486
|
+
"true"
|
|
2487
|
+
]) {
|
|
2488
|
+
const hops = recordHops(() => ({ body }));
|
|
2489
|
+
const [meterProvider, recorder] = (0, _fedify_fixture.createTestMeterProvider)();
|
|
2490
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupWebFingerWithDependencies("acct:alice@example.com", {
|
|
2491
|
+
withDiagnostics: true,
|
|
2492
|
+
meterProvider
|
|
2493
|
+
}, { openHop: hops.openHop }), {
|
|
2494
|
+
resource: null,
|
|
2495
|
+
result: "invalid",
|
|
2496
|
+
statusCode: 200,
|
|
2497
|
+
remoteHost: "example.com"
|
|
2498
|
+
});
|
|
2499
|
+
(0, node_assert_strict.deepStrictEqual)(hops.events.at(-1), "close https://example.com/.well-known/webfinger?resource=acct%3Aalice%40example.com");
|
|
2500
|
+
hops.assertInvariants();
|
|
2501
|
+
(0, node_assert_strict.deepStrictEqual)(recorder.getMeasurement("webfinger.lookup")?.attributes["webfinger.lookup.result"], "invalid");
|
|
2502
|
+
}
|
|
2503
|
+
});
|
|
2132
2504
|
//#endregion
|