@fedify/webfinger 2.2.19 → 2.2.20
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 +440 -109
- package/dist/lookup.test.js +441 -109
- package/dist/mod.cjs +143 -84
- package/dist/mod.d.ts +1 -1
- package/dist/mod.js +144 -85
- package/package.json +3 -3
- package/src/lookup-impl.ts +234 -0
- package/src/lookup.test.ts +469 -36
- package/src/lookup.ts +2 -145
package/dist/lookup.test.cjs
CHANGED
|
@@ -23,11 +23,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
//#endregion
|
|
24
24
|
let _fedify_fixture = require("@fedify/fixture");
|
|
25
25
|
let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
|
|
26
|
+
let _fedify_vocab_runtime_internal = require("@fedify/vocab-runtime/internal");
|
|
26
27
|
let _logtape_logtape = require("@logtape/logtape");
|
|
27
28
|
let es_toolkit = require("es-toolkit");
|
|
28
29
|
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);
|
|
31
30
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
32
31
|
//#endregion
|
|
33
32
|
//#region ../../node_modules/.pnpm/regexparam@3.0.0/node_modules/regexparam/dist/index.mjs
|
|
@@ -1230,7 +1229,7 @@ var esm_default = new class FetchMock {
|
|
|
1230
1229
|
//#endregion
|
|
1231
1230
|
//#region deno.json
|
|
1232
1231
|
var name = "@fedify/webfinger";
|
|
1233
|
-
var version = "2.2.
|
|
1232
|
+
var version = "2.2.20";
|
|
1234
1233
|
//#endregion
|
|
1235
1234
|
//#region src/body.ts
|
|
1236
1235
|
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
@@ -1306,7 +1305,7 @@ async function readBoundedText(message, maxBytes, url) {
|
|
|
1306
1305
|
}
|
|
1307
1306
|
}
|
|
1308
1307
|
//#endregion
|
|
1309
|
-
//#region src/lookup.ts
|
|
1308
|
+
//#region src/lookup-impl.ts
|
|
1310
1309
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
1311
1310
|
"fedify",
|
|
1312
1311
|
"webfinger",
|
|
@@ -1314,36 +1313,16 @@ const logger = (0, _logtape_logtape.getLogger)([
|
|
|
1314
1313
|
]);
|
|
1315
1314
|
const DEFAULT_MAX_REDIRECTION = 5;
|
|
1316
1315
|
/**
|
|
1317
|
-
* Looks up a WebFinger resource.
|
|
1316
|
+
* Looks up a WebFinger resource, opening a fetch session per hop. This is
|
|
1317
|
+
* the implementation of {@link lookupWebFinger}, which is not exported from
|
|
1318
|
+
* the package entry point.
|
|
1318
1319
|
* @param resource The resource URL to look up.
|
|
1319
1320
|
* @param options Extra options for looking up the resource.
|
|
1321
|
+
* @param deps The dependencies.
|
|
1320
1322
|
* @returns The resource descriptor, or `null` if not found.
|
|
1321
|
-
* @
|
|
1323
|
+
* @internal
|
|
1322
1324
|
*/
|
|
1323
|
-
async function
|
|
1324
|
-
return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version).startActiveSpan("webfinger.lookup", {
|
|
1325
|
-
kind: _opentelemetry_api.SpanKind.CLIENT,
|
|
1326
|
-
attributes: {
|
|
1327
|
-
"webfinger.resource": resource.toString(),
|
|
1328
|
-
"webfinger.resource.scheme": typeof resource === "string" ? resource.replace(/:.*$/, "") : resource.protocol.replace(/:$/, "")
|
|
1329
|
-
}
|
|
1330
|
-
}, async (span) => {
|
|
1331
|
-
try {
|
|
1332
|
-
const result = await lookupWebFingerInternal(resource, options);
|
|
1333
|
-
span.setStatus({ code: result === null ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.OK });
|
|
1334
|
-
return result;
|
|
1335
|
-
} catch (error) {
|
|
1336
|
-
span.setStatus({
|
|
1337
|
-
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
1338
|
-
message: String(error)
|
|
1339
|
-
});
|
|
1340
|
-
throw error;
|
|
1341
|
-
} finally {
|
|
1342
|
-
span.end();
|
|
1343
|
-
}
|
|
1344
|
-
});
|
|
1345
|
-
}
|
|
1346
|
-
async function lookupWebFingerInternal(resource, options = {}) {
|
|
1325
|
+
async function lookupWebFingerInternal(resource, options = {}, deps = {}) {
|
|
1347
1326
|
if (typeof resource === "string") resource = new URL(resource);
|
|
1348
1327
|
let protocol = "https:";
|
|
1349
1328
|
let server;
|
|
@@ -1358,84 +1337,175 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1358
1337
|
}
|
|
1359
1338
|
let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
|
|
1360
1339
|
url.searchParams.set("resource", resource.href);
|
|
1340
|
+
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 }));
|
|
1361
1341
|
let redirected = 0;
|
|
1362
1342
|
while (true) {
|
|
1363
1343
|
logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
|
|
1364
|
-
|
|
1365
|
-
if (
|
|
1366
|
-
|
|
1367
|
-
} catch (e) {
|
|
1368
|
-
if (e instanceof _fedify_vocab_runtime.UrlError) {
|
|
1369
|
-
if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
1370
|
-
url: url.href,
|
|
1371
|
-
error: e
|
|
1372
|
-
});
|
|
1373
|
-
else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
|
|
1374
|
-
return null;
|
|
1375
|
-
}
|
|
1376
|
-
throw e;
|
|
1377
|
-
}
|
|
1344
|
+
const session = await openSession(url, openHop, options.signal);
|
|
1345
|
+
if (session == null) return null;
|
|
1346
|
+
let hop;
|
|
1378
1347
|
try {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1348
|
+
hop = await fetchHop(session, url, options);
|
|
1349
|
+
} finally {
|
|
1350
|
+
await session.close();
|
|
1351
|
+
}
|
|
1352
|
+
if ("result" in hop) return hop.result;
|
|
1353
|
+
redirected++;
|
|
1354
|
+
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
1355
|
+
if (redirected >= maxRedirection) {
|
|
1356
|
+
logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
|
|
1357
|
+
return null;
|
|
1358
|
+
}
|
|
1359
|
+
const redirectedUrl = new URL(hop.redirect, url);
|
|
1360
|
+
if (redirectedUrl.protocol !== url.protocol) {
|
|
1361
|
+
logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
|
|
1362
|
+
protocol: url.protocol,
|
|
1363
|
+
redirectedProtocol: redirectedUrl.protocol
|
|
1386
1364
|
});
|
|
1387
|
-
|
|
1388
|
-
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
url = redirectedUrl;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Opens a fetch session for a hop. A disallowed URL, a failed DNS lookup,
|
|
1372
|
+
* or an aborted lookup yields `null` instead of an error.
|
|
1373
|
+
*/
|
|
1374
|
+
async function openSession(url, openHop, signal) {
|
|
1375
|
+
try {
|
|
1376
|
+
return await openHop(url.href);
|
|
1377
|
+
} catch (e) {
|
|
1378
|
+
if (e instanceof _fedify_vocab_runtime.UrlError) {
|
|
1379
|
+
if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
1389
1380
|
url: url.href,
|
|
1390
|
-
error
|
|
1381
|
+
error: e
|
|
1391
1382
|
});
|
|
1383
|
+
else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
|
|
1392
1384
|
return null;
|
|
1393
1385
|
}
|
|
1394
|
-
if (
|
|
1395
|
-
|
|
1396
|
-
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
1397
|
-
if (redirected >= maxRedirection) {
|
|
1398
|
-
logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
|
|
1399
|
-
return null;
|
|
1400
|
-
}
|
|
1401
|
-
const redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
|
|
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 null;
|
|
1408
|
-
}
|
|
1409
|
-
url = redirectedUrl;
|
|
1410
|
-
continue;
|
|
1411
|
-
}
|
|
1412
|
-
if (!response.ok) {
|
|
1413
|
-
logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
|
|
1386
|
+
if (signal?.aborted) {
|
|
1387
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
|
|
1414
1388
|
url: url.href,
|
|
1415
|
-
|
|
1416
|
-
statusText: response.statusText
|
|
1389
|
+
error: e
|
|
1417
1390
|
});
|
|
1418
1391
|
return null;
|
|
1419
1392
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1393
|
+
throw e;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Fetches a hop with the given session, and consumes the response body or
|
|
1398
|
+
* starts cancelling it before returning. The caller closes the session.
|
|
1399
|
+
*/
|
|
1400
|
+
async function fetchHop(session, url, options) {
|
|
1401
|
+
let response;
|
|
1402
|
+
try {
|
|
1403
|
+
response = await session.fetch(url, {
|
|
1404
|
+
headers: {
|
|
1405
|
+
Accept: "application/jrd+json",
|
|
1406
|
+
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : (0, _fedify_vocab_runtime.getUserAgent)(options.userAgent)
|
|
1407
|
+
},
|
|
1408
|
+
redirect: "manual",
|
|
1409
|
+
signal: options.signal
|
|
1410
|
+
});
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
|
|
1413
|
+
url: url.href,
|
|
1414
|
+
error
|
|
1415
|
+
});
|
|
1416
|
+
return { result: null };
|
|
1417
|
+
}
|
|
1418
|
+
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
1419
|
+
cancelBody(response, url);
|
|
1420
|
+
return { redirect: response.headers.get("Location") };
|
|
1421
|
+
}
|
|
1422
|
+
if (!response.ok) {
|
|
1423
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
|
|
1424
|
+
url: url.href,
|
|
1425
|
+
status: response.status,
|
|
1426
|
+
statusText: response.statusText
|
|
1427
|
+
});
|
|
1428
|
+
cancelBody(response, url);
|
|
1429
|
+
return { result: null };
|
|
1430
|
+
}
|
|
1431
|
+
try {
|
|
1432
|
+
return { result: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url)) };
|
|
1433
|
+
} catch (e) {
|
|
1434
|
+
if (e instanceof BodyTooLargeError) return { result: null };
|
|
1435
|
+
if (e instanceof SyntaxError) {
|
|
1436
|
+
logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
|
|
1437
|
+
return { result: null };
|
|
1429
1438
|
}
|
|
1439
|
+
throw e;
|
|
1430
1440
|
}
|
|
1431
1441
|
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Starts cancelling a response body that is not read, without waiting for it
|
|
1444
|
+
* to settle. The cancellation of a branch of a tee (e.g., a body cloned by
|
|
1445
|
+
* a fetch mock) settles only after the other branch is cancelled too, per
|
|
1446
|
+
* the Streams standard, so awaiting it could block forever. A failure is
|
|
1447
|
+
* only logged.
|
|
1448
|
+
*/
|
|
1449
|
+
function cancelBody(response, url) {
|
|
1450
|
+
response.body?.cancel().catch((error) => {
|
|
1451
|
+
logger.debug("Failed to cancel the response body from {url}: {error}", {
|
|
1452
|
+
url: url.href,
|
|
1453
|
+
error
|
|
1454
|
+
});
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
//#endregion
|
|
1458
|
+
//#region src/lookup.ts
|
|
1459
|
+
/**
|
|
1460
|
+
* Looks up a WebFinger resource.
|
|
1461
|
+
* @param resource The resource URL to look up.
|
|
1462
|
+
* @param options Extra options for looking up the resource.
|
|
1463
|
+
* @returns The resource descriptor, or `null` if not found.
|
|
1464
|
+
* @since 0.2.0
|
|
1465
|
+
*/
|
|
1466
|
+
async function lookupWebFinger(resource, options = {}) {
|
|
1467
|
+
return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version).startActiveSpan("webfinger.lookup", {
|
|
1468
|
+
kind: _opentelemetry_api.SpanKind.CLIENT,
|
|
1469
|
+
attributes: {
|
|
1470
|
+
"webfinger.resource": resource.toString(),
|
|
1471
|
+
"webfinger.resource.scheme": typeof resource === "string" ? resource.replace(/:.*$/, "") : resource.protocol.replace(/:$/, "")
|
|
1472
|
+
}
|
|
1473
|
+
}, async (span) => {
|
|
1474
|
+
try {
|
|
1475
|
+
const result = await lookupWebFingerInternal(resource, options);
|
|
1476
|
+
span.setStatus({ code: result === null ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.OK });
|
|
1477
|
+
return result;
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
span.setStatus({
|
|
1480
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
1481
|
+
message: String(error)
|
|
1482
|
+
});
|
|
1483
|
+
throw error;
|
|
1484
|
+
} finally {
|
|
1485
|
+
span.end();
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1432
1489
|
//#endregion
|
|
1433
1490
|
//#region src/lookup.test.ts
|
|
1491
|
+
function lookupDirect(resource, options) {
|
|
1492
|
+
return lookupWebFingerInternal(resource, options, { openHop: _fedify_vocab_runtime_internal.openDirectFetch });
|
|
1493
|
+
}
|
|
1494
|
+
function lookupResolving(addresses) {
|
|
1495
|
+
return () => Promise.resolve(addresses);
|
|
1496
|
+
}
|
|
1497
|
+
function openExampleDirectly(url) {
|
|
1498
|
+
return new URL(url).hostname === "example.com" ? (0, _fedify_vocab_runtime_internal.openDirectFetch)(url) : (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup: lookupResolving([{
|
|
1499
|
+
address: "93.184.215.14",
|
|
1500
|
+
family: 4
|
|
1501
|
+
}]) });
|
|
1502
|
+
}
|
|
1434
1503
|
(0, _fedify_fixture.test)({
|
|
1435
1504
|
name: "lookupWebFinger()",
|
|
1436
1505
|
sanitizeOps: false,
|
|
1437
1506
|
sanitizeResources: false,
|
|
1438
1507
|
async fn(t) {
|
|
1508
|
+
const responseTimers = [];
|
|
1439
1509
|
await t.step("invalid resource", async () => {
|
|
1440
1510
|
(0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe"), null);
|
|
1441
1511
|
(0, node_assert_strict.deepStrictEqual)(await lookupWebFinger(new URL("acct:johndoe")), null);
|
|
@@ -1446,11 +1516,32 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1446
1516
|
(0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@fedify-test.internal"), null);
|
|
1447
1517
|
(0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("https://fedify-test.internal/foo"), null);
|
|
1448
1518
|
});
|
|
1519
|
+
await t.step("fedify-test.internal resolving to a private address", async () => {
|
|
1520
|
+
esm_default.mockGlobal();
|
|
1521
|
+
esm_default.catch(500);
|
|
1522
|
+
try {
|
|
1523
|
+
const hosts = [];
|
|
1524
|
+
const openHop = (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup: (hostname) => {
|
|
1525
|
+
hosts.push(hostname);
|
|
1526
|
+
return Promise.resolve([{
|
|
1527
|
+
address: "10.0.0.1",
|
|
1528
|
+
family: 4
|
|
1529
|
+
}]);
|
|
1530
|
+
} });
|
|
1531
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:johndoe@fedify-test.internal", {}, { openHop }), null);
|
|
1532
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("https://fedify-test.internal/foo", {}, { openHop }), null);
|
|
1533
|
+
(0, node_assert_strict.deepStrictEqual)(hosts, ["fedify-test.internal", "fedify-test.internal"]);
|
|
1534
|
+
(0, node_assert_strict.deepStrictEqual)(esm_default.callHistory.calls().length, 0);
|
|
1535
|
+
} finally {
|
|
1536
|
+
for (const timer of responseTimers) clearTimeout(timer);
|
|
1537
|
+
esm_default.hardReset();
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1449
1540
|
esm_default.spyGlobal();
|
|
1450
1541
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
|
|
1451
1542
|
await t.step("not found", async () => {
|
|
1452
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1453
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1543
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com"), null);
|
|
1544
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupDirect("https://example.com/foo"), null);
|
|
1454
1545
|
});
|
|
1455
1546
|
const expected = {
|
|
1456
1547
|
subject: "acct:johndoe@example.com",
|
|
@@ -1459,7 +1550,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1459
1550
|
esm_default.removeRoutes();
|
|
1460
1551
|
esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
|
|
1461
1552
|
await t.step("acct", async () => {
|
|
1462
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1553
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com"), expected);
|
|
1463
1554
|
});
|
|
1464
1555
|
const expected2 = {
|
|
1465
1556
|
subject: "https://example.com/foo",
|
|
@@ -1468,12 +1559,12 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1468
1559
|
esm_default.removeRoutes();
|
|
1469
1560
|
esm_default.get("https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo", { body: expected2 });
|
|
1470
1561
|
await t.step("https", async () => {
|
|
1471
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1562
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupDirect("https://example.com/foo"), expected2);
|
|
1472
1563
|
});
|
|
1473
1564
|
esm_default.removeRoutes();
|
|
1474
1565
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
|
|
1475
1566
|
await t.step("invalid response", async () => {
|
|
1476
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1567
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com"), null);
|
|
1477
1568
|
});
|
|
1478
1569
|
esm_default.removeRoutes();
|
|
1479
1570
|
esm_default.get("begin:https://localhost/.well-known/webfinger?", {
|
|
@@ -1502,7 +1593,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1502
1593
|
});
|
|
1503
1594
|
esm_default.get("begin:https://example.com/.well-known/webfinger2", { body: expected });
|
|
1504
1595
|
await t.step("redirection", async () => {
|
|
1505
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1596
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com"), expected);
|
|
1506
1597
|
});
|
|
1507
1598
|
esm_default.removeRoutes();
|
|
1508
1599
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", {
|
|
@@ -1510,7 +1601,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1510
1601
|
headers: { Location: "/.well-known/webfinger" }
|
|
1511
1602
|
});
|
|
1512
1603
|
await t.step("infinite redirection", async () => {
|
|
1513
|
-
(0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() =>
|
|
1604
|
+
(0, node_assert_strict.deepStrictEqual)(await (0, es_toolkit.withTimeout)(() => lookupDirect("acct:johndoe@example.com"), 2e3), null);
|
|
1514
1605
|
});
|
|
1515
1606
|
esm_default.removeRoutes();
|
|
1516
1607
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", {
|
|
@@ -1518,7 +1609,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1518
1609
|
headers: { Location: "ftp://example.com/" }
|
|
1519
1610
|
});
|
|
1520
1611
|
await t.step("redirection to different protocol", async () => {
|
|
1521
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1612
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com"), null);
|
|
1522
1613
|
});
|
|
1523
1614
|
esm_default.removeRoutes();
|
|
1524
1615
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", {
|
|
@@ -1526,7 +1617,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1526
1617
|
headers: { Location: "https://localhost/" }
|
|
1527
1618
|
});
|
|
1528
1619
|
await t.step("redirection to private address", async () => {
|
|
1529
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1620
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:johndoe@example.com", {}, { openHop: openExampleDirectly }), null);
|
|
1530
1621
|
});
|
|
1531
1622
|
esm_default.removeRoutes();
|
|
1532
1623
|
let redirectCount = 0;
|
|
@@ -1540,22 +1631,22 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1540
1631
|
});
|
|
1541
1632
|
await t.step("custom maxRedirection", async () => {
|
|
1542
1633
|
redirectCount = 0;
|
|
1543
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1634
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com", { maxRedirection: 2 }), null);
|
|
1544
1635
|
redirectCount = 0;
|
|
1545
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1636
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com", { maxRedirection: 3 }), expected);
|
|
1546
1637
|
redirectCount = 0;
|
|
1547
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1638
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com"), expected);
|
|
1548
1639
|
});
|
|
1549
1640
|
esm_default.removeRoutes();
|
|
1550
1641
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", () => new Promise((resolve) => {
|
|
1551
1642
|
const timeoutId = setTimeout(() => {
|
|
1552
1643
|
resolve({ body: expected });
|
|
1553
1644
|
}, 1e3);
|
|
1554
|
-
|
|
1645
|
+
responseTimers.push(timeoutId);
|
|
1555
1646
|
}));
|
|
1556
1647
|
await t.step("request cancellation", async () => {
|
|
1557
1648
|
const controller = new AbortController();
|
|
1558
|
-
const promise =
|
|
1649
|
+
const promise = lookupDirect("acct:johndoe@example.com", { signal: controller.signal });
|
|
1559
1650
|
controller.abort();
|
|
1560
1651
|
(0, node_assert_strict.deepStrictEqual)(await promise, null);
|
|
1561
1652
|
});
|
|
@@ -1571,12 +1662,12 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1571
1662
|
const timeoutId = setTimeout(() => {
|
|
1572
1663
|
resolve({ body: expected });
|
|
1573
1664
|
}, 1e3);
|
|
1574
|
-
|
|
1665
|
+
responseTimers.push(timeoutId);
|
|
1575
1666
|
});
|
|
1576
1667
|
});
|
|
1577
1668
|
await t.step("cancellation during redirection", async () => {
|
|
1578
1669
|
const controller = new AbortController();
|
|
1579
|
-
const promise =
|
|
1670
|
+
const promise = lookupDirect("acct:johndoe@example.com", { signal: controller.signal });
|
|
1580
1671
|
setTimeout(() => controller.abort(), 100);
|
|
1581
1672
|
(0, node_assert_strict.deepStrictEqual)(await promise, null);
|
|
1582
1673
|
});
|
|
@@ -1585,18 +1676,20 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1585
1676
|
const timeoutId = setTimeout(() => {
|
|
1586
1677
|
resolve({ body: expected });
|
|
1587
1678
|
}, 500);
|
|
1588
|
-
|
|
1679
|
+
responseTimers.push(timeoutId);
|
|
1589
1680
|
}));
|
|
1590
1681
|
await t.step("cancellation with immediate abort", async () => {
|
|
1591
1682
|
const controller = new AbortController();
|
|
1592
1683
|
controller.abort();
|
|
1684
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com", { signal: controller.signal }), null);
|
|
1593
1685
|
(0, node_assert_strict.deepStrictEqual)(await lookupWebFinger("acct:johndoe@example.com", { signal: controller.signal }), null);
|
|
1594
1686
|
});
|
|
1595
1687
|
esm_default.removeRoutes();
|
|
1596
1688
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: expected });
|
|
1597
1689
|
await t.step("successful request with signal", async () => {
|
|
1598
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1690
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupDirect("acct:johndoe@example.com", { signal: new AbortController().signal }), expected);
|
|
1599
1691
|
});
|
|
1692
|
+
for (const timer of responseTimers) clearTimeout(timer);
|
|
1600
1693
|
esm_default.hardReset();
|
|
1601
1694
|
}
|
|
1602
1695
|
});
|
|
@@ -1616,17 +1709,15 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1616
1709
|
}
|
|
1617
1710
|
});
|
|
1618
1711
|
(0, _fedify_fixture.test)("lookupWebFinger() logs DNS failures as such", async (t) => {
|
|
1619
|
-
if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") return;
|
|
1620
1712
|
for (const result of [
|
|
1621
1713
|
"throws",
|
|
1622
1714
|
"empty",
|
|
1623
1715
|
"private"
|
|
1624
1716
|
]) await t.step(result, async () => {
|
|
1625
|
-
const
|
|
1626
|
-
node_dns_promises.default.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
1717
|
+
const lookup = () => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
1627
1718
|
address: "127.0.0.1",
|
|
1628
1719
|
family: 4
|
|
1629
|
-
}])
|
|
1720
|
+
}]);
|
|
1630
1721
|
const records = [];
|
|
1631
1722
|
await (0, _logtape_logtape.configure)({
|
|
1632
1723
|
sinks: { buffer: (record) => records.push(record) },
|
|
@@ -1641,7 +1732,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1641
1732
|
reset: true
|
|
1642
1733
|
});
|
|
1643
1734
|
try {
|
|
1644
|
-
(0, node_assert_strict.deepStrictEqual)(await
|
|
1735
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:alice@dns-failure.invalid", {}, { openHop: (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url, { lookup }) }), null);
|
|
1645
1736
|
const failures = records.filter((r) => r.rawMessage !== "Fetching WebFinger resource descriptor from {url}...");
|
|
1646
1737
|
(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}"]]);
|
|
1647
1738
|
const { error } = failures[0].properties;
|
|
@@ -1649,8 +1740,248 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
1649
1740
|
(0, node_assert_strict.deepStrictEqual)(error.reason, result === "private" ? "disallowed" : "dns");
|
|
1650
1741
|
} finally {
|
|
1651
1742
|
await (0, _logtape_logtape.reset)();
|
|
1652
|
-
node_dns_promises.default.lookup = originalLookup;
|
|
1653
1743
|
}
|
|
1654
1744
|
});
|
|
1655
1745
|
});
|
|
1746
|
+
function recordHops(respond) {
|
|
1747
|
+
const events = [];
|
|
1748
|
+
const unsettled = [];
|
|
1749
|
+
let opens = 0;
|
|
1750
|
+
let closes = 0;
|
|
1751
|
+
let responses = 0;
|
|
1752
|
+
return {
|
|
1753
|
+
events,
|
|
1754
|
+
async openHop(url) {
|
|
1755
|
+
opens++;
|
|
1756
|
+
events.push(`open ${url}`);
|
|
1757
|
+
const states = [];
|
|
1758
|
+
let closed = false;
|
|
1759
|
+
return {
|
|
1760
|
+
url: new URL(url),
|
|
1761
|
+
async fetch(input) {
|
|
1762
|
+
if (closed) throw new TypeError("The fetch session is closed.");
|
|
1763
|
+
const target = new URL(input instanceof Request ? input.url : input);
|
|
1764
|
+
const id = ++responses;
|
|
1765
|
+
events.push(`fetch ${target.href} #${id}`);
|
|
1766
|
+
const fake = respond(target);
|
|
1767
|
+
const state = {
|
|
1768
|
+
id,
|
|
1769
|
+
settled: false
|
|
1770
|
+
};
|
|
1771
|
+
states.push(state);
|
|
1772
|
+
let sent = false;
|
|
1773
|
+
const body = new ReadableStream({
|
|
1774
|
+
pull(controller) {
|
|
1775
|
+
if (!sent && fake.body != null && fake.body !== "") {
|
|
1776
|
+
sent = true;
|
|
1777
|
+
controller.enqueue(new TextEncoder().encode(fake.body));
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
state.settled = true;
|
|
1781
|
+
events.push(`consumed #${id}`);
|
|
1782
|
+
controller.close();
|
|
1783
|
+
},
|
|
1784
|
+
cancel() {
|
|
1785
|
+
state.settled = true;
|
|
1786
|
+
events.push(`cancel #${id}`);
|
|
1787
|
+
if (fake.cancelFails) throw new Error("Cancellation failed");
|
|
1788
|
+
if (fake.cancelNeverSettles) return new Promise(() => {});
|
|
1789
|
+
}
|
|
1790
|
+
}, { highWaterMark: 0 });
|
|
1791
|
+
const response = new Response(body, {
|
|
1792
|
+
status: fake.status ?? 200,
|
|
1793
|
+
headers: fake.headers
|
|
1794
|
+
});
|
|
1795
|
+
if (fake.url != null) Object.defineProperty(response, "url", { value: fake.url });
|
|
1796
|
+
return response;
|
|
1797
|
+
},
|
|
1798
|
+
async close() {
|
|
1799
|
+
if (closed) return;
|
|
1800
|
+
closed = true;
|
|
1801
|
+
closes++;
|
|
1802
|
+
for (const state of states) if (!state.settled) unsettled.push(`#${state.id}`);
|
|
1803
|
+
events.push(`close ${url}`);
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
},
|
|
1807
|
+
assertInvariants() {
|
|
1808
|
+
(0, node_assert_strict.deepStrictEqual)(closes, opens);
|
|
1809
|
+
(0, node_assert_strict.deepStrictEqual)(unsettled, []);
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
(0, _fedify_fixture.test)("lookupWebFinger() closes each hop after cleaning up its body", async (t) => {
|
|
1814
|
+
const descriptor = {
|
|
1815
|
+
subject: "acct:johndoe@example.com",
|
|
1816
|
+
links: []
|
|
1817
|
+
};
|
|
1818
|
+
const first = "https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com";
|
|
1819
|
+
const second = "https://example.com/.well-known/webfinger2";
|
|
1820
|
+
await t.step("redirect", async () => {
|
|
1821
|
+
const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
|
|
1822
|
+
status: 302,
|
|
1823
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1824
|
+
body: "redirecting",
|
|
1825
|
+
url: url.href.replace("example.com", "93.184.215.14")
|
|
1826
|
+
} : { body: JSON.stringify(descriptor) });
|
|
1827
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
|
|
1828
|
+
(0, node_assert_strict.deepStrictEqual)(recorder.events, [
|
|
1829
|
+
`open ${first}`,
|
|
1830
|
+
`fetch ${first} #1`,
|
|
1831
|
+
"cancel #1",
|
|
1832
|
+
`close ${first}`,
|
|
1833
|
+
`open ${second}`,
|
|
1834
|
+
`fetch ${second} #2`,
|
|
1835
|
+
"consumed #2",
|
|
1836
|
+
`close ${second}`
|
|
1837
|
+
]);
|
|
1838
|
+
recorder.assertInvariants();
|
|
1839
|
+
});
|
|
1840
|
+
await t.step("non-2xx response", async () => {
|
|
1841
|
+
const recorder = recordHops(() => ({
|
|
1842
|
+
status: 404,
|
|
1843
|
+
body: "not found"
|
|
1844
|
+
}));
|
|
1845
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), null);
|
|
1846
|
+
(0, node_assert_strict.deepStrictEqual)(recorder.events, [
|
|
1847
|
+
`open ${first}`,
|
|
1848
|
+
`fetch ${first} #1`,
|
|
1849
|
+
"cancel #1",
|
|
1850
|
+
`close ${first}`
|
|
1851
|
+
]);
|
|
1852
|
+
recorder.assertInvariants();
|
|
1853
|
+
});
|
|
1854
|
+
await t.step("too many redirections", async () => {
|
|
1855
|
+
const recorder = recordHops(() => ({
|
|
1856
|
+
status: 302,
|
|
1857
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1858
|
+
body: "redirecting"
|
|
1859
|
+
}));
|
|
1860
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:johndoe@example.com", { maxRedirection: 2 }, { openHop: recorder.openHop }), null);
|
|
1861
|
+
(0, node_assert_strict.deepStrictEqual)(recorder.events, [
|
|
1862
|
+
`open ${first}`,
|
|
1863
|
+
`fetch ${first} #1`,
|
|
1864
|
+
"cancel #1",
|
|
1865
|
+
`close ${first}`,
|
|
1866
|
+
`open ${second}`,
|
|
1867
|
+
`fetch ${second} #2`,
|
|
1868
|
+
"cancel #2",
|
|
1869
|
+
`close ${second}`
|
|
1870
|
+
]);
|
|
1871
|
+
recorder.assertInvariants();
|
|
1872
|
+
});
|
|
1873
|
+
await t.step("failed redirect body cancellation", async () => {
|
|
1874
|
+
const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
|
|
1875
|
+
status: 302,
|
|
1876
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1877
|
+
body: "redirecting",
|
|
1878
|
+
cancelFails: true
|
|
1879
|
+
} : { body: JSON.stringify(descriptor) });
|
|
1880
|
+
const records = [];
|
|
1881
|
+
await (0, _logtape_logtape.configure)({
|
|
1882
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
1883
|
+
loggers: [{
|
|
1884
|
+
category: "fedify",
|
|
1885
|
+
sinks: ["buffer"],
|
|
1886
|
+
lowestLevel: "debug"
|
|
1887
|
+
}, {
|
|
1888
|
+
category: ["logtape", "meta"],
|
|
1889
|
+
sinks: []
|
|
1890
|
+
}],
|
|
1891
|
+
reset: true
|
|
1892
|
+
});
|
|
1893
|
+
try {
|
|
1894
|
+
(0, node_assert_strict.deepStrictEqual)(await lookupWebFingerInternal("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
|
|
1895
|
+
const failures = records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}");
|
|
1896
|
+
(0, node_assert_strict.deepStrictEqual)(failures.map((r) => [r.level, r.properties.url]), [["debug", first]]);
|
|
1897
|
+
const { error } = failures[0].properties;
|
|
1898
|
+
(0, node_assert_strict.ok)(error instanceof Error);
|
|
1899
|
+
(0, node_assert_strict.deepStrictEqual)(error.message, "Cancellation failed");
|
|
1900
|
+
} finally {
|
|
1901
|
+
await (0, _logtape_logtape.reset)();
|
|
1902
|
+
}
|
|
1903
|
+
(0, node_assert_strict.deepStrictEqual)(recorder.events, [
|
|
1904
|
+
`open ${first}`,
|
|
1905
|
+
`fetch ${first} #1`,
|
|
1906
|
+
"cancel #1",
|
|
1907
|
+
`close ${first}`,
|
|
1908
|
+
`open ${second}`,
|
|
1909
|
+
`fetch ${second} #2`,
|
|
1910
|
+
"consumed #2",
|
|
1911
|
+
`close ${second}`
|
|
1912
|
+
]);
|
|
1913
|
+
recorder.assertInvariants();
|
|
1914
|
+
});
|
|
1915
|
+
await t.step("never-settling body cancellation", async () => {
|
|
1916
|
+
const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
|
|
1917
|
+
status: 302,
|
|
1918
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
1919
|
+
body: "redirecting",
|
|
1920
|
+
cancelNeverSettles: true
|
|
1921
|
+
} : {
|
|
1922
|
+
status: 404,
|
|
1923
|
+
body: "not found",
|
|
1924
|
+
cancelNeverSettles: true
|
|
1925
|
+
});
|
|
1926
|
+
(0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupWebFingerInternal("acct:johndoe@example.com", {}, { openHop: recorder.openHop })), null);
|
|
1927
|
+
(0, node_assert_strict.deepStrictEqual)(recorder.events, [
|
|
1928
|
+
`open ${first}`,
|
|
1929
|
+
`fetch ${first} #1`,
|
|
1930
|
+
"cancel #1",
|
|
1931
|
+
`close ${first}`,
|
|
1932
|
+
`open ${second}`,
|
|
1933
|
+
`fetch ${second} #2`,
|
|
1934
|
+
"cancel #2",
|
|
1935
|
+
`close ${second}`
|
|
1936
|
+
]);
|
|
1937
|
+
recorder.assertInvariants();
|
|
1938
|
+
});
|
|
1939
|
+
});
|
|
1940
|
+
/**
|
|
1941
|
+
* Rejects if the given promise does not settle within the timeout, so that
|
|
1942
|
+
* a hang fails the test instead of blocking it.
|
|
1943
|
+
*/
|
|
1944
|
+
async function settleWithin(promise, ms = 5e3) {
|
|
1945
|
+
let timer;
|
|
1946
|
+
try {
|
|
1947
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
1948
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Did not settle within ${ms} ms.`)), ms);
|
|
1949
|
+
})]);
|
|
1950
|
+
} finally {
|
|
1951
|
+
clearTimeout(timer);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
function neverCancellableResponse(init) {
|
|
1955
|
+
return new Response(new ReadableStream({
|
|
1956
|
+
start(controller) {
|
|
1957
|
+
controller.enqueue(new TextEncoder().encode("never cancelled"));
|
|
1958
|
+
},
|
|
1959
|
+
cancel: () => new Promise(() => {})
|
|
1960
|
+
}), init);
|
|
1961
|
+
}
|
|
1962
|
+
(0, _fedify_fixture.test)("lookupWebFinger() does not wait for cancelling response bodies", async (t) => {
|
|
1963
|
+
const descriptor = {
|
|
1964
|
+
subject: "acct:johndoe@example.com",
|
|
1965
|
+
links: []
|
|
1966
|
+
};
|
|
1967
|
+
esm_default.mockGlobal();
|
|
1968
|
+
try {
|
|
1969
|
+
await t.step("redirect", async () => {
|
|
1970
|
+
esm_default.removeRoutes();
|
|
1971
|
+
esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({
|
|
1972
|
+
status: 302,
|
|
1973
|
+
headers: { Location: "/.well-known/webfinger2" }
|
|
1974
|
+
}));
|
|
1975
|
+
esm_default.get("https://example.com/.well-known/webfinger2", () => new Response(JSON.stringify(descriptor), { headers: { "Content-Type": "application/jrd+json" } }));
|
|
1976
|
+
(0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupDirect("acct:johndoe@example.com")), descriptor);
|
|
1977
|
+
});
|
|
1978
|
+
await t.step("non-2xx response", async () => {
|
|
1979
|
+
esm_default.removeRoutes();
|
|
1980
|
+
esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({ status: 404 }));
|
|
1981
|
+
(0, node_assert_strict.deepStrictEqual)(await settleWithin(lookupDirect("acct:johndoe@example.com")), null);
|
|
1982
|
+
});
|
|
1983
|
+
} finally {
|
|
1984
|
+
esm_default.hardReset();
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1656
1987
|
//#endregion
|