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