@fedify/webfinger 2.4.5-dev.2453 → 2.4.5
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 +558 -218
- package/dist/lookup.test.js +559 -218
- package/dist/mod.cjs +224 -153
- package/dist/mod.d.cts +27 -1
- package/dist/mod.d.ts +28 -2
- package/dist/mod.js +225 -155
- package/package.json +3 -3
- package/src/lookup-impl.ts +324 -0
- package/src/lookup.test.ts +515 -69
- package/src/lookup.ts +20 -219
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.4.5
|
|
1239
|
+
var version = "2.4.5";
|
|
1239
1240
|
//#endregion
|
|
1240
1241
|
//#region src/body.ts
|
|
1241
1242
|
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
@@ -1311,13 +1312,223 @@ 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
|
+
return { outcome: {
|
|
1491
|
+
resource: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url)),
|
|
1492
|
+
result: "found",
|
|
1493
|
+
statusCode: response.status,
|
|
1494
|
+
remoteHost: url.host
|
|
1495
|
+
} };
|
|
1496
|
+
} catch (e) {
|
|
1497
|
+
if (e instanceof BodyTooLargeError) return { outcome: {
|
|
1498
|
+
resource: null,
|
|
1499
|
+
result: "invalid",
|
|
1500
|
+
statusCode: response.status,
|
|
1501
|
+
remoteHost: url.host
|
|
1502
|
+
} };
|
|
1503
|
+
if (e instanceof SyntaxError) {
|
|
1504
|
+
logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
|
|
1505
|
+
return { outcome: {
|
|
1506
|
+
resource: null,
|
|
1507
|
+
result: "invalid",
|
|
1508
|
+
statusCode: response.status,
|
|
1509
|
+
remoteHost: url.host
|
|
1510
|
+
} };
|
|
1511
|
+
}
|
|
1512
|
+
throw e;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Starts cancelling a response body that is not read, without waiting for it
|
|
1517
|
+
* to settle. The cancellation of a branch of a tee (e.g., a body cloned by
|
|
1518
|
+
* a fetch mock) settles only after the other branch is cancelled too, per
|
|
1519
|
+
* the Streams standard, so awaiting it could block forever. A failure is
|
|
1520
|
+
* only logged.
|
|
1521
|
+
*/
|
|
1522
|
+
function cancelBody(response, url) {
|
|
1523
|
+
response.body?.cancel().catch((error) => {
|
|
1524
|
+
logger.debug("Failed to cancel the response body from {url}: {error}", {
|
|
1525
|
+
url: url.href,
|
|
1526
|
+
error
|
|
1527
|
+
});
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
//#endregion
|
|
1531
|
+
//#region src/lookup.ts
|
|
1321
1532
|
const WEBFINGER_HISTOGRAM_BUCKETS = [
|
|
1322
1533
|
5,
|
|
1323
1534
|
10,
|
|
@@ -1371,13 +1582,11 @@ function getMetricResourceScheme(scheme) {
|
|
|
1371
1582
|
return WEBFINGER_LOOKUP_SCHEME_WHITELIST.has(scheme) ? scheme : "other";
|
|
1372
1583
|
}
|
|
1373
1584
|
/**
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
* @
|
|
1377
|
-
* @returns The resource descriptor, or `null` if not found.
|
|
1378
|
-
* @since 0.2.0
|
|
1585
|
+
* The lookup implementation with injectable hop sessions for tests.
|
|
1586
|
+
* Not exported from the package entry point.
|
|
1587
|
+
* @internal
|
|
1379
1588
|
*/
|
|
1380
|
-
async function
|
|
1589
|
+
async function lookupWebFingerWithDependencies(resource, options = {}, deps = {}) {
|
|
1381
1590
|
const tracer = (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version);
|
|
1382
1591
|
const scheme = getResourceScheme(resource);
|
|
1383
1592
|
return await tracer.startActiveSpan("webfinger.lookup", {
|
|
@@ -1394,7 +1603,7 @@ async function lookupWebFinger(resource, options = {}) {
|
|
|
1394
1603
|
result: "error"
|
|
1395
1604
|
};
|
|
1396
1605
|
try {
|
|
1397
|
-
outcome = await lookupWebFingerInternal(resource, options);
|
|
1606
|
+
outcome = await lookupWebFingerInternal(resource, options, deps);
|
|
1398
1607
|
span.setStatus({ code: outcome.resource === null ? SpanStatusCode.ERROR : SpanStatusCode.OK });
|
|
1399
1608
|
return outcome.resource;
|
|
1400
1609
|
} catch (error) {
|
|
@@ -1420,181 +1629,56 @@ function recordWebFingerLookup(meterProvider, durationMs, scheme, outcome) {
|
|
|
1420
1629
|
instruments.lookup.add(1, attributes);
|
|
1421
1630
|
instruments.lookupDuration.record(durationMs, attributes);
|
|
1422
1631
|
}
|
|
1423
|
-
async function lookupWebFingerInternal(resource, options = {}) {
|
|
1424
|
-
if (typeof resource === "string") resource = new URL(resource);
|
|
1425
|
-
let protocol = "https:";
|
|
1426
|
-
let server;
|
|
1427
|
-
if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
|
|
1428
|
-
const atPos = resource.pathname.lastIndexOf("@");
|
|
1429
|
-
if (atPos < 0) return {
|
|
1430
|
-
resource: null,
|
|
1431
|
-
result: "invalid"
|
|
1432
|
-
};
|
|
1433
|
-
server = resource.pathname.substring(atPos + 1);
|
|
1434
|
-
if (server === "" || /[/?#]/.test(server)) return {
|
|
1435
|
-
resource: null,
|
|
1436
|
-
result: "invalid"
|
|
1437
|
-
};
|
|
1438
|
-
if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
|
|
1439
|
-
resource: null,
|
|
1440
|
-
result: "invalid"
|
|
1441
|
-
};
|
|
1442
|
-
} else {
|
|
1443
|
-
protocol = resource.protocol;
|
|
1444
|
-
server = resource.host;
|
|
1445
|
-
}
|
|
1446
|
-
let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
|
|
1447
|
-
url.searchParams.set("resource", resource.href);
|
|
1448
|
-
let redirected = 0;
|
|
1449
|
-
while (true) {
|
|
1450
|
-
const remoteHost = url.host;
|
|
1451
|
-
logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
|
|
1452
|
-
let response;
|
|
1453
|
-
if (options.allowPrivateAddress !== true) try {
|
|
1454
|
-
await validatePublicUrl(url.href);
|
|
1455
|
-
} catch (e) {
|
|
1456
|
-
if (e instanceof UrlError) {
|
|
1457
|
-
if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
1458
|
-
url: url.href,
|
|
1459
|
-
error: e
|
|
1460
|
-
});
|
|
1461
|
-
else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
|
|
1462
|
-
return {
|
|
1463
|
-
resource: null,
|
|
1464
|
-
result: "network_error",
|
|
1465
|
-
remoteHost
|
|
1466
|
-
};
|
|
1467
|
-
}
|
|
1468
|
-
throw e;
|
|
1469
|
-
}
|
|
1470
|
-
try {
|
|
1471
|
-
response = await fetch(url, {
|
|
1472
|
-
headers: {
|
|
1473
|
-
Accept: "application/jrd+json",
|
|
1474
|
-
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
1475
|
-
},
|
|
1476
|
-
redirect: "manual",
|
|
1477
|
-
signal: options.signal
|
|
1478
|
-
});
|
|
1479
|
-
} catch (error) {
|
|
1480
|
-
logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
|
|
1481
|
-
url: url.href,
|
|
1482
|
-
error
|
|
1483
|
-
});
|
|
1484
|
-
return {
|
|
1485
|
-
resource: null,
|
|
1486
|
-
result: "network_error",
|
|
1487
|
-
remoteHost
|
|
1488
|
-
};
|
|
1489
|
-
}
|
|
1490
|
-
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
1491
|
-
redirected++;
|
|
1492
|
-
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
1493
|
-
if (redirected > maxRedirection) {
|
|
1494
|
-
logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
|
|
1495
|
-
return {
|
|
1496
|
-
resource: null,
|
|
1497
|
-
result: "invalid",
|
|
1498
|
-
statusCode: response.status,
|
|
1499
|
-
remoteHost
|
|
1500
|
-
};
|
|
1501
|
-
}
|
|
1502
|
-
let redirectedUrl;
|
|
1503
|
-
try {
|
|
1504
|
-
redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
|
|
1505
|
-
} catch (e) {
|
|
1506
|
-
logger.error("Invalid Location header while following WebFinger redirect: {error}", {
|
|
1507
|
-
url: url.href,
|
|
1508
|
-
error: e
|
|
1509
|
-
});
|
|
1510
|
-
return {
|
|
1511
|
-
resource: null,
|
|
1512
|
-
result: "invalid",
|
|
1513
|
-
statusCode: response.status,
|
|
1514
|
-
remoteHost
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
if (redirectedUrl.protocol !== url.protocol) {
|
|
1518
|
-
logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
|
|
1519
|
-
protocol: url.protocol,
|
|
1520
|
-
redirectedProtocol: redirectedUrl.protocol
|
|
1521
|
-
});
|
|
1522
|
-
return {
|
|
1523
|
-
resource: null,
|
|
1524
|
-
result: "invalid",
|
|
1525
|
-
statusCode: response.status,
|
|
1526
|
-
remoteHost
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
|
-
url = redirectedUrl;
|
|
1530
|
-
continue;
|
|
1531
|
-
}
|
|
1532
|
-
if (!response.ok) {
|
|
1533
|
-
logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
|
|
1534
|
-
url: url.href,
|
|
1535
|
-
status: response.status,
|
|
1536
|
-
statusText: response.statusText
|
|
1537
|
-
});
|
|
1538
|
-
return {
|
|
1539
|
-
resource: null,
|
|
1540
|
-
result: response.status === 404 || response.status === 410 ? "not_found" : "error",
|
|
1541
|
-
statusCode: response.status,
|
|
1542
|
-
remoteHost
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
try {
|
|
1546
|
-
return {
|
|
1547
|
-
resource: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url)),
|
|
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
1632
|
//#endregion
|
|
1573
1633
|
//#region src/lookup.test.ts
|
|
1634
|
+
function lookupMock(resource, options = {}) {
|
|
1635
|
+
if (resource.toString().includes("fedify-test.internal")) return lookupWebFingerWithDependencies(resource, options, { openHop: (url) => Promise.resolve({
|
|
1636
|
+
url: new URL(url),
|
|
1637
|
+
fetch: () => Promise.reject(/* @__PURE__ */ new TypeError("Connection refused")),
|
|
1638
|
+
close: () => Promise.resolve()
|
|
1639
|
+
}) });
|
|
1640
|
+
return lookupWebFingerWithDependencies(resource, options, { openHop: options.allowPrivateAddress === true ? openDirectFetch : openExampleDirectly });
|
|
1641
|
+
}
|
|
1642
|
+
function lookupDirect(resource, options) {
|
|
1643
|
+
return lookupWebFingerWithDependencies(resource, options, { openHop: openDirectFetch });
|
|
1644
|
+
}
|
|
1645
|
+
function lookupResolving(addresses) {
|
|
1646
|
+
return () => Promise.resolve(addresses);
|
|
1647
|
+
}
|
|
1648
|
+
function openExampleDirectly(url) {
|
|
1649
|
+
return new URL(url).hostname === "example.com" ? openDirectFetch(url) : openPublicFetch(url, { lookup: lookupResolving([{
|
|
1650
|
+
address: "93.184.215.14",
|
|
1651
|
+
family: 4
|
|
1652
|
+
}]) });
|
|
1653
|
+
}
|
|
1654
|
+
async function lookupResourceWithDependencies(resource, options, deps) {
|
|
1655
|
+
return (await lookupWebFingerInternal(resource, options, deps)).resource;
|
|
1656
|
+
}
|
|
1574
1657
|
test({
|
|
1575
1658
|
name: "lookupWebFinger()",
|
|
1576
1659
|
sanitizeOps: false,
|
|
1577
1660
|
sanitizeResources: false,
|
|
1578
1661
|
async fn(t) {
|
|
1662
|
+
const responseTimers = [];
|
|
1579
1663
|
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
|
|
1664
|
+
deepStrictEqual(await lookupMock("acct:johndoe"), null);
|
|
1665
|
+
deepStrictEqual(await lookupMock(new URL("acct:johndoe")), null);
|
|
1666
|
+
deepStrictEqual(await lookupMock("acct:johndoe@"), null);
|
|
1667
|
+
deepStrictEqual(await lookupMock(new URL("acct:johndoe@")), null);
|
|
1668
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com/exploit"), null);
|
|
1669
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com?x=1"), null);
|
|
1670
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com#frag"), null);
|
|
1587
1671
|
});
|
|
1588
1672
|
await t.step("connection refused", async () => {
|
|
1589
|
-
deepStrictEqual(await
|
|
1590
|
-
deepStrictEqual(await
|
|
1673
|
+
deepStrictEqual(await lookupMock("acct:johndoe@fedify-test.internal"), null);
|
|
1674
|
+
deepStrictEqual(await lookupMock("https://fedify-test.internal/foo"), null);
|
|
1591
1675
|
});
|
|
1592
1676
|
esm_default.spyGlobal();
|
|
1593
1677
|
try {
|
|
1594
1678
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
|
|
1595
1679
|
await t.step("not found", async () => {
|
|
1596
|
-
deepStrictEqual(await
|
|
1597
|
-
deepStrictEqual(await
|
|
1680
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), null);
|
|
1681
|
+
deepStrictEqual(await lookupMock("https://example.com/foo"), null);
|
|
1598
1682
|
});
|
|
1599
1683
|
const expected = {
|
|
1600
1684
|
subject: "acct:johndoe@example.com",
|
|
@@ -1603,7 +1687,7 @@ test({
|
|
|
1603
1687
|
esm_default.removeRoutes();
|
|
1604
1688
|
esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
|
|
1605
1689
|
await t.step("acct", async () => {
|
|
1606
|
-
deepStrictEqual(await
|
|
1690
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), expected);
|
|
1607
1691
|
});
|
|
1608
1692
|
const expected2 = {
|
|
1609
1693
|
subject: "https://example.com/foo",
|
|
@@ -1612,7 +1696,7 @@ test({
|
|
|
1612
1696
|
esm_default.removeRoutes();
|
|
1613
1697
|
esm_default.get("https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo", { body: expected2 });
|
|
1614
1698
|
await t.step("https", async () => {
|
|
1615
|
-
deepStrictEqual(await
|
|
1699
|
+
deepStrictEqual(await lookupMock("https://example.com/foo"), expected2);
|
|
1616
1700
|
});
|
|
1617
1701
|
const mailtoExpected = {
|
|
1618
1702
|
subject: "mailto:juliet@example.com",
|
|
@@ -1621,7 +1705,7 @@ test({
|
|
|
1621
1705
|
esm_default.removeRoutes();
|
|
1622
1706
|
esm_default.get("https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com", { body: mailtoExpected });
|
|
1623
1707
|
await t.step("mailto", async () => {
|
|
1624
|
-
deepStrictEqual(await
|
|
1708
|
+
deepStrictEqual(await lookupMock("mailto:juliet@example.com"), mailtoExpected);
|
|
1625
1709
|
});
|
|
1626
1710
|
const mailtoQueryExpected = {
|
|
1627
1711
|
subject: "mailto:juliet@example.com?subject=Hi",
|
|
@@ -1630,12 +1714,12 @@ test({
|
|
|
1630
1714
|
esm_default.removeRoutes();
|
|
1631
1715
|
esm_default.get("https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com%3Fsubject%3DHi", { body: mailtoQueryExpected });
|
|
1632
1716
|
await t.step("mailto with hfields", async () => {
|
|
1633
|
-
deepStrictEqual(await
|
|
1717
|
+
deepStrictEqual(await lookupMock("mailto:juliet@example.com?subject=Hi"), mailtoQueryExpected);
|
|
1634
1718
|
});
|
|
1635
1719
|
esm_default.removeRoutes();
|
|
1636
1720
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
|
|
1637
1721
|
await t.step("invalid response", async () => {
|
|
1638
|
-
deepStrictEqual(await
|
|
1722
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), null);
|
|
1639
1723
|
});
|
|
1640
1724
|
esm_default.removeRoutes();
|
|
1641
1725
|
esm_default.get("begin:https://localhost/.well-known/webfinger?", {
|
|
@@ -1647,8 +1731,8 @@ test({
|
|
|
1647
1731
|
}]
|
|
1648
1732
|
});
|
|
1649
1733
|
await t.step("private address", async () => {
|
|
1650
|
-
deepStrictEqual(await
|
|
1651
|
-
deepStrictEqual(await
|
|
1734
|
+
deepStrictEqual(await lookupMock("acct:test@localhost"), null);
|
|
1735
|
+
deepStrictEqual(await lookupMock("acct:test@localhost", { allowPrivateAddress: true }), {
|
|
1652
1736
|
subject: "acct:test@localhost",
|
|
1653
1737
|
links: [{
|
|
1654
1738
|
rel: "self",
|
|
@@ -1664,7 +1748,7 @@ test({
|
|
|
1664
1748
|
});
|
|
1665
1749
|
esm_default.get("begin:https://example.com/.well-known/webfinger2", { body: expected });
|
|
1666
1750
|
await t.step("redirection", async () => {
|
|
1667
|
-
deepStrictEqual(await
|
|
1751
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), expected);
|
|
1668
1752
|
});
|
|
1669
1753
|
esm_default.removeRoutes();
|
|
1670
1754
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", {
|
|
@@ -1672,7 +1756,7 @@ test({
|
|
|
1672
1756
|
headers: { Location: "/.well-known/webfinger" }
|
|
1673
1757
|
});
|
|
1674
1758
|
await t.step("infinite redirection", async () => {
|
|
1675
|
-
deepStrictEqual(await withTimeout(() =>
|
|
1759
|
+
deepStrictEqual(await withTimeout(() => lookupMock("acct:johndoe@example.com"), 2e3), null);
|
|
1676
1760
|
});
|
|
1677
1761
|
esm_default.removeRoutes();
|
|
1678
1762
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", {
|
|
@@ -1680,7 +1764,7 @@ test({
|
|
|
1680
1764
|
headers: { Location: "ftp://example.com/" }
|
|
1681
1765
|
});
|
|
1682
1766
|
await t.step("redirection to different protocol", async () => {
|
|
1683
|
-
deepStrictEqual(await
|
|
1767
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), null);
|
|
1684
1768
|
});
|
|
1685
1769
|
esm_default.removeRoutes();
|
|
1686
1770
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", {
|
|
@@ -1688,7 +1772,7 @@ test({
|
|
|
1688
1772
|
headers: { Location: "https://localhost/" }
|
|
1689
1773
|
});
|
|
1690
1774
|
await t.step("redirection to private address", async () => {
|
|
1691
|
-
deepStrictEqual(await
|
|
1775
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), null);
|
|
1692
1776
|
});
|
|
1693
1777
|
esm_default.removeRoutes();
|
|
1694
1778
|
let redirectCount = 0;
|
|
@@ -1702,13 +1786,13 @@ test({
|
|
|
1702
1786
|
});
|
|
1703
1787
|
await t.step("custom maxRedirection", async () => {
|
|
1704
1788
|
redirectCount = 0;
|
|
1705
|
-
deepStrictEqual(await
|
|
1789
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), null);
|
|
1706
1790
|
redirectCount = 0;
|
|
1707
|
-
deepStrictEqual(await
|
|
1791
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { maxRedirection: 2 }), expected);
|
|
1708
1792
|
redirectCount = 0;
|
|
1709
|
-
deepStrictEqual(await
|
|
1793
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { maxRedirection: 3 }), expected);
|
|
1710
1794
|
redirectCount = 0;
|
|
1711
|
-
deepStrictEqual(await
|
|
1795
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), expected);
|
|
1712
1796
|
});
|
|
1713
1797
|
await t.step("maxRedirection: 1 follows exactly one redirect", async () => {
|
|
1714
1798
|
esm_default.removeRoutes();
|
|
@@ -1720,7 +1804,7 @@ test({
|
|
|
1720
1804
|
headers: { Location: "/.well-known/webfinger?after=1" }
|
|
1721
1805
|
} : { body: expected };
|
|
1722
1806
|
});
|
|
1723
|
-
deepStrictEqual(await
|
|
1807
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), expected);
|
|
1724
1808
|
esm_default.removeRoutes();
|
|
1725
1809
|
count = 0;
|
|
1726
1810
|
esm_default.get("begin:https://example.com/.well-known/webfinger", () => {
|
|
@@ -1730,18 +1814,18 @@ test({
|
|
|
1730
1814
|
headers: { Location: `/.well-known/webfinger?after=${count}` }
|
|
1731
1815
|
} : { body: expected };
|
|
1732
1816
|
});
|
|
1733
|
-
deepStrictEqual(await
|
|
1817
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), null);
|
|
1734
1818
|
});
|
|
1735
1819
|
esm_default.removeRoutes();
|
|
1736
1820
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", () => new Promise((resolve) => {
|
|
1737
1821
|
const timeoutId = setTimeout(() => {
|
|
1738
1822
|
resolve({ body: expected });
|
|
1739
1823
|
}, 1e3);
|
|
1740
|
-
|
|
1824
|
+
responseTimers.push(timeoutId);
|
|
1741
1825
|
}));
|
|
1742
1826
|
await t.step("request cancellation", async () => {
|
|
1743
1827
|
const controller = new AbortController();
|
|
1744
|
-
const promise =
|
|
1828
|
+
const promise = lookupMock("acct:johndoe@example.com", { signal: controller.signal });
|
|
1745
1829
|
controller.abort();
|
|
1746
1830
|
deepStrictEqual(await promise, null);
|
|
1747
1831
|
});
|
|
@@ -1757,12 +1841,12 @@ test({
|
|
|
1757
1841
|
const timeoutId = setTimeout(() => {
|
|
1758
1842
|
resolve({ body: expected });
|
|
1759
1843
|
}, 1e3);
|
|
1760
|
-
|
|
1844
|
+
responseTimers.push(timeoutId);
|
|
1761
1845
|
});
|
|
1762
1846
|
});
|
|
1763
1847
|
await t.step("cancellation during redirection", async () => {
|
|
1764
1848
|
const controller = new AbortController();
|
|
1765
|
-
const promise =
|
|
1849
|
+
const promise = lookupMock("acct:johndoe@example.com", { signal: controller.signal });
|
|
1766
1850
|
setTimeout(() => controller.abort(), 100);
|
|
1767
1851
|
deepStrictEqual(await promise, null);
|
|
1768
1852
|
});
|
|
@@ -1771,19 +1855,20 @@ test({
|
|
|
1771
1855
|
const timeoutId = setTimeout(() => {
|
|
1772
1856
|
resolve({ body: expected });
|
|
1773
1857
|
}, 500);
|
|
1774
|
-
|
|
1858
|
+
responseTimers.push(timeoutId);
|
|
1775
1859
|
}));
|
|
1776
1860
|
await t.step("cancellation with immediate abort", async () => {
|
|
1777
1861
|
const controller = new AbortController();
|
|
1778
1862
|
controller.abort();
|
|
1779
|
-
deepStrictEqual(await
|
|
1863
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { signal: controller.signal }), null);
|
|
1780
1864
|
});
|
|
1781
1865
|
esm_default.removeRoutes();
|
|
1782
1866
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: expected });
|
|
1783
1867
|
await t.step("successful request with signal", async () => {
|
|
1784
|
-
deepStrictEqual(await
|
|
1868
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { signal: new AbortController().signal }), expected);
|
|
1785
1869
|
});
|
|
1786
1870
|
} finally {
|
|
1871
|
+
for (const timer of responseTimers) clearTimeout(timer);
|
|
1787
1872
|
esm_default.removeRoutes();
|
|
1788
1873
|
esm_default.hardReset();
|
|
1789
1874
|
}
|
|
@@ -1803,7 +1888,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1803
1888
|
esm_default.removeRoutes();
|
|
1804
1889
|
esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
|
|
1805
1890
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1806
|
-
deepStrictEqual(await
|
|
1891
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { meterProvider }), expected);
|
|
1807
1892
|
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
1808
1893
|
deepStrictEqual(counters.length, 1);
|
|
1809
1894
|
deepStrictEqual(counters[0].type, "counter");
|
|
@@ -1826,7 +1911,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1826
1911
|
links: []
|
|
1827
1912
|
} });
|
|
1828
1913
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1829
|
-
await
|
|
1914
|
+
await lookupMock("https://example.com/foo", { meterProvider });
|
|
1830
1915
|
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
1831
1916
|
deepStrictEqual(counters.length, 1);
|
|
1832
1917
|
deepStrictEqual(counters[0].attributes["webfinger.resource.scheme"], "https");
|
|
@@ -1839,7 +1924,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1839
1924
|
links: []
|
|
1840
1925
|
} });
|
|
1841
1926
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1842
|
-
await
|
|
1927
|
+
await lookupMock("https://example.com:8443/foo", { meterProvider });
|
|
1843
1928
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1844
1929
|
ok(counter != null);
|
|
1845
1930
|
deepStrictEqual(counter.attributes["activitypub.remote.host"], "example.com:8443");
|
|
@@ -1848,7 +1933,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1848
1933
|
esm_default.removeRoutes();
|
|
1849
1934
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
|
|
1850
1935
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1851
|
-
deepStrictEqual(await
|
|
1936
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { meterProvider }), null);
|
|
1852
1937
|
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
1853
1938
|
deepStrictEqual(counters.length, 1);
|
|
1854
1939
|
deepStrictEqual(counters[0].attributes["webfinger.lookup.result"], "not_found");
|
|
@@ -1862,7 +1947,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1862
1947
|
esm_default.removeRoutes();
|
|
1863
1948
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 410 });
|
|
1864
1949
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1865
|
-
await
|
|
1950
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1866
1951
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1867
1952
|
ok(counter != null);
|
|
1868
1953
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "not_found");
|
|
@@ -1872,7 +1957,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1872
1957
|
esm_default.removeRoutes();
|
|
1873
1958
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 500 });
|
|
1874
1959
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1875
|
-
await
|
|
1960
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1876
1961
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1877
1962
|
ok(counter != null);
|
|
1878
1963
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "error");
|
|
@@ -1882,7 +1967,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1882
1967
|
esm_default.removeRoutes();
|
|
1883
1968
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
|
|
1884
1969
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1885
|
-
await
|
|
1970
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1886
1971
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1887
1972
|
ok(counter != null);
|
|
1888
1973
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "invalid");
|
|
@@ -1891,7 +1976,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1891
1976
|
await t.step("records result=network_error when fetch never reaches the remote", async () => {
|
|
1892
1977
|
esm_default.removeRoutes();
|
|
1893
1978
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1894
|
-
deepStrictEqual(await
|
|
1979
|
+
deepStrictEqual(await lookupMock("acct:johndoe@fedify-test.internal", { meterProvider }), null);
|
|
1895
1980
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1896
1981
|
ok(counter != null);
|
|
1897
1982
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "network_error");
|
|
@@ -1900,7 +1985,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1900
1985
|
});
|
|
1901
1986
|
await t.step("records result=invalid for malformed acct: resources", async () => {
|
|
1902
1987
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1903
|
-
deepStrictEqual(await
|
|
1988
|
+
deepStrictEqual(await lookupMock("acct:johndoe", { meterProvider }), null);
|
|
1904
1989
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1905
1990
|
ok(counter != null);
|
|
1906
1991
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "invalid");
|
|
@@ -1914,7 +1999,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1914
1999
|
headers: { Location: "/.well-known/webfinger" }
|
|
1915
2000
|
});
|
|
1916
2001
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1917
|
-
deepStrictEqual(await withTimeout(() =>
|
|
2002
|
+
deepStrictEqual(await withTimeout(() => lookupMock("acct:johndoe@example.com", {
|
|
1918
2003
|
meterProvider,
|
|
1919
2004
|
maxRedirection: 3
|
|
1920
2005
|
}), 2e3), null);
|
|
@@ -1931,7 +2016,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1931
2016
|
headers: { Location: "ftp://example.com/" }
|
|
1932
2017
|
});
|
|
1933
2018
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1934
|
-
await
|
|
2019
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1935
2020
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1936
2021
|
ok(counter != null);
|
|
1937
2022
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "invalid");
|
|
@@ -1944,7 +2029,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1944
2029
|
headers: { Location: "https://localhost/" }
|
|
1945
2030
|
});
|
|
1946
2031
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1947
|
-
await
|
|
2032
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1948
2033
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1949
2034
|
ok(counter != null);
|
|
1950
2035
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "network_error");
|
|
@@ -1957,7 +2042,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1957
2042
|
headers: { Location: "http://[bad" }
|
|
1958
2043
|
});
|
|
1959
2044
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1960
|
-
await
|
|
2045
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1961
2046
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1962
2047
|
ok(counter != null);
|
|
1963
2048
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "invalid");
|
|
@@ -1967,7 +2052,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1967
2052
|
await t.step("buckets unknown resource schemes as 'other' to keep metric cardinality bounded", async () => {
|
|
1968
2053
|
esm_default.removeRoutes();
|
|
1969
2054
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1970
|
-
await
|
|
2055
|
+
await lookupMock("ssh://example.com/foo", { meterProvider });
|
|
1971
2056
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1972
2057
|
ok(counter != null);
|
|
1973
2058
|
deepStrictEqual(counter.attributes["webfinger.resource.scheme"], "other");
|
|
@@ -1976,7 +2061,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1976
2061
|
esm_default.removeRoutes();
|
|
1977
2062
|
esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
|
|
1978
2063
|
const [_unused, recorder] = createTestMeterProvider();
|
|
1979
|
-
await
|
|
2064
|
+
await lookupMock("acct:johndoe@example.com");
|
|
1980
2065
|
deepStrictEqual(recorder.getMeasurements("webfinger.lookup").length, 0);
|
|
1981
2066
|
deepStrictEqual(recorder.getMeasurements("webfinger.lookup.duration").length, 0);
|
|
1982
2067
|
});
|
|
@@ -1993,25 +2078,23 @@ test("lookupWebFinger() bounds resource descriptors", async () => {
|
|
|
1993
2078
|
"Content-Type": "application/jrd+json",
|
|
1994
2079
|
"Content-Length": "1"
|
|
1995
2080
|
} }));
|
|
1996
|
-
deepStrictEqual(await
|
|
2081
|
+
deepStrictEqual(await lookupMock("acct:alice@example.com", { allowPrivateAddress: true }), null);
|
|
1997
2082
|
oversized = false;
|
|
1998
|
-
deepStrictEqual(await
|
|
2083
|
+
deepStrictEqual(await lookupMock("acct:alice@example.com", { allowPrivateAddress: true }), { subject: "acct:alice@example.com" });
|
|
1999
2084
|
} finally {
|
|
2000
2085
|
esm_default.hardReset();
|
|
2001
2086
|
}
|
|
2002
2087
|
});
|
|
2003
2088
|
test("lookupWebFinger() logs DNS failures as such", async (t) => {
|
|
2004
|
-
if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") return;
|
|
2005
2089
|
for (const result of [
|
|
2006
2090
|
"throws",
|
|
2007
2091
|
"empty",
|
|
2008
2092
|
"private"
|
|
2009
2093
|
]) await t.step(result, async () => {
|
|
2010
|
-
const
|
|
2011
|
-
dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
2094
|
+
const lookup = () => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
2012
2095
|
address: "127.0.0.1",
|
|
2013
2096
|
family: 4
|
|
2014
|
-
}])
|
|
2097
|
+
}]);
|
|
2015
2098
|
const records = [];
|
|
2016
2099
|
await configure({
|
|
2017
2100
|
sinks: { buffer: (record) => records.push(record) },
|
|
@@ -2026,7 +2109,7 @@ test("lookupWebFinger() logs DNS failures as such", async (t) => {
|
|
|
2026
2109
|
reset: true
|
|
2027
2110
|
});
|
|
2028
2111
|
try {
|
|
2029
|
-
deepStrictEqual(await
|
|
2112
|
+
deepStrictEqual(await lookupResourceWithDependencies("acct:alice@dns-failure.invalid", {}, { openHop: (url) => openPublicFetch(url, { lookup }) }), null);
|
|
2030
2113
|
const failures = records.filter((r) => r.rawMessage !== "Fetching WebFinger resource descriptor from {url}...");
|
|
2031
2114
|
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
2115
|
const { error } = failures[0].properties;
|
|
@@ -2034,9 +2117,267 @@ test("lookupWebFinger() logs DNS failures as such", async (t) => {
|
|
|
2034
2117
|
deepStrictEqual(error.reason, result === "private" ? "disallowed" : "dns");
|
|
2035
2118
|
} finally {
|
|
2036
2119
|
await reset();
|
|
2037
|
-
dns.lookup = originalLookup;
|
|
2038
2120
|
}
|
|
2039
2121
|
});
|
|
2040
2122
|
});
|
|
2123
|
+
function recordHops(respond) {
|
|
2124
|
+
const events = [];
|
|
2125
|
+
const unsettled = [];
|
|
2126
|
+
let opens = 0;
|
|
2127
|
+
let closes = 0;
|
|
2128
|
+
let responses = 0;
|
|
2129
|
+
return {
|
|
2130
|
+
events,
|
|
2131
|
+
async openHop(url) {
|
|
2132
|
+
opens++;
|
|
2133
|
+
events.push(`open ${url}`);
|
|
2134
|
+
const states = [];
|
|
2135
|
+
let closed = false;
|
|
2136
|
+
return {
|
|
2137
|
+
url: new URL(url),
|
|
2138
|
+
async fetch(input) {
|
|
2139
|
+
if (closed) throw new TypeError("The fetch session is closed.");
|
|
2140
|
+
const target = new URL(input instanceof Request ? input.url : input);
|
|
2141
|
+
const id = ++responses;
|
|
2142
|
+
events.push(`fetch ${target.href} #${id}`);
|
|
2143
|
+
const fake = respond(target);
|
|
2144
|
+
const state = {
|
|
2145
|
+
id,
|
|
2146
|
+
settled: false
|
|
2147
|
+
};
|
|
2148
|
+
states.push(state);
|
|
2149
|
+
let sent = false;
|
|
2150
|
+
const body = new ReadableStream({
|
|
2151
|
+
pull(controller) {
|
|
2152
|
+
if (!sent && fake.body != null && fake.body !== "") {
|
|
2153
|
+
sent = true;
|
|
2154
|
+
controller.enqueue(new TextEncoder().encode(fake.body));
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
state.settled = true;
|
|
2158
|
+
events.push(`consumed #${id}`);
|
|
2159
|
+
controller.close();
|
|
2160
|
+
},
|
|
2161
|
+
cancel() {
|
|
2162
|
+
state.settled = true;
|
|
2163
|
+
events.push(`cancel #${id}`);
|
|
2164
|
+
if (fake.cancelFails) throw new Error("Cancellation failed");
|
|
2165
|
+
if (fake.cancelNeverSettles) return new Promise(() => {});
|
|
2166
|
+
}
|
|
2167
|
+
}, { highWaterMark: 0 });
|
|
2168
|
+
const response = new Response(body, {
|
|
2169
|
+
status: fake.status ?? 200,
|
|
2170
|
+
headers: fake.headers
|
|
2171
|
+
});
|
|
2172
|
+
if (fake.url != null) Object.defineProperty(response, "url", { value: fake.url });
|
|
2173
|
+
return response;
|
|
2174
|
+
},
|
|
2175
|
+
async close() {
|
|
2176
|
+
if (closed) return;
|
|
2177
|
+
closed = true;
|
|
2178
|
+
closes++;
|
|
2179
|
+
for (const state of states) if (!state.settled) unsettled.push(`#${state.id}`);
|
|
2180
|
+
events.push(`close ${url}`);
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
},
|
|
2184
|
+
assertInvariants() {
|
|
2185
|
+
deepStrictEqual(closes, opens);
|
|
2186
|
+
deepStrictEqual(unsettled, []);
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
test("lookupWebFinger() closes each hop after cleaning up its body", async (t) => {
|
|
2191
|
+
const descriptor = {
|
|
2192
|
+
subject: "acct:johndoe@example.com",
|
|
2193
|
+
links: []
|
|
2194
|
+
};
|
|
2195
|
+
const first = "https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com";
|
|
2196
|
+
const second = "https://example.com/.well-known/webfinger2";
|
|
2197
|
+
await t.step("redirect", async () => {
|
|
2198
|
+
const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
|
|
2199
|
+
status: 302,
|
|
2200
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
2201
|
+
body: "redirecting",
|
|
2202
|
+
url: url.href.replace("example.com", "93.184.215.14")
|
|
2203
|
+
} : { body: JSON.stringify(descriptor) });
|
|
2204
|
+
deepStrictEqual(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
|
|
2205
|
+
deepStrictEqual(recorder.events, [
|
|
2206
|
+
`open ${first}`,
|
|
2207
|
+
`fetch ${first} #1`,
|
|
2208
|
+
"cancel #1",
|
|
2209
|
+
`close ${first}`,
|
|
2210
|
+
`open ${second}`,
|
|
2211
|
+
`fetch ${second} #2`,
|
|
2212
|
+
"consumed #2",
|
|
2213
|
+
`close ${second}`
|
|
2214
|
+
]);
|
|
2215
|
+
recorder.assertInvariants();
|
|
2216
|
+
});
|
|
2217
|
+
await t.step("non-2xx response", async () => {
|
|
2218
|
+
const recorder = recordHops(() => ({
|
|
2219
|
+
status: 404,
|
|
2220
|
+
body: "not found"
|
|
2221
|
+
}));
|
|
2222
|
+
deepStrictEqual(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), null);
|
|
2223
|
+
deepStrictEqual(recorder.events, [
|
|
2224
|
+
`open ${first}`,
|
|
2225
|
+
`fetch ${first} #1`,
|
|
2226
|
+
"cancel #1",
|
|
2227
|
+
`close ${first}`
|
|
2228
|
+
]);
|
|
2229
|
+
recorder.assertInvariants();
|
|
2230
|
+
});
|
|
2231
|
+
await t.step("too many redirections", async () => {
|
|
2232
|
+
const recorder = recordHops(() => ({
|
|
2233
|
+
status: 302,
|
|
2234
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
2235
|
+
body: "redirecting"
|
|
2236
|
+
}));
|
|
2237
|
+
deepStrictEqual(await lookupResourceWithDependencies("acct:johndoe@example.com", { maxRedirection: 1 }, { openHop: recorder.openHop }), null);
|
|
2238
|
+
deepStrictEqual(recorder.events, [
|
|
2239
|
+
`open ${first}`,
|
|
2240
|
+
`fetch ${first} #1`,
|
|
2241
|
+
"cancel #1",
|
|
2242
|
+
`close ${first}`,
|
|
2243
|
+
`open ${second}`,
|
|
2244
|
+
`fetch ${second} #2`,
|
|
2245
|
+
"cancel #2",
|
|
2246
|
+
`close ${second}`
|
|
2247
|
+
]);
|
|
2248
|
+
recorder.assertInvariants();
|
|
2249
|
+
});
|
|
2250
|
+
await t.step("failed redirect body cancellation", async () => {
|
|
2251
|
+
const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
|
|
2252
|
+
status: 302,
|
|
2253
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
2254
|
+
body: "redirecting",
|
|
2255
|
+
cancelFails: true
|
|
2256
|
+
} : { body: JSON.stringify(descriptor) });
|
|
2257
|
+
const records = [];
|
|
2258
|
+
await configure({
|
|
2259
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
2260
|
+
loggers: [{
|
|
2261
|
+
category: "fedify",
|
|
2262
|
+
sinks: ["buffer"],
|
|
2263
|
+
lowestLevel: "debug"
|
|
2264
|
+
}, {
|
|
2265
|
+
category: ["logtape", "meta"],
|
|
2266
|
+
sinks: []
|
|
2267
|
+
}],
|
|
2268
|
+
reset: true
|
|
2269
|
+
});
|
|
2270
|
+
try {
|
|
2271
|
+
deepStrictEqual(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
|
|
2272
|
+
const failures = records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}");
|
|
2273
|
+
deepStrictEqual(failures.map((r) => [r.level, r.properties.url]), [["debug", first]]);
|
|
2274
|
+
const { error } = failures[0].properties;
|
|
2275
|
+
ok(error instanceof Error);
|
|
2276
|
+
deepStrictEqual(error.message, "Cancellation failed");
|
|
2277
|
+
} finally {
|
|
2278
|
+
await reset();
|
|
2279
|
+
}
|
|
2280
|
+
deepStrictEqual(recorder.events, [
|
|
2281
|
+
`open ${first}`,
|
|
2282
|
+
`fetch ${first} #1`,
|
|
2283
|
+
"cancel #1",
|
|
2284
|
+
`close ${first}`,
|
|
2285
|
+
`open ${second}`,
|
|
2286
|
+
`fetch ${second} #2`,
|
|
2287
|
+
"consumed #2",
|
|
2288
|
+
`close ${second}`
|
|
2289
|
+
]);
|
|
2290
|
+
recorder.assertInvariants();
|
|
2291
|
+
});
|
|
2292
|
+
await t.step("never-settling body cancellation", async () => {
|
|
2293
|
+
const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
|
|
2294
|
+
status: 302,
|
|
2295
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
2296
|
+
body: "redirecting",
|
|
2297
|
+
cancelNeverSettles: true
|
|
2298
|
+
} : {
|
|
2299
|
+
status: 404,
|
|
2300
|
+
body: "not found",
|
|
2301
|
+
cancelNeverSettles: true
|
|
2302
|
+
});
|
|
2303
|
+
deepStrictEqual(await settleWithin(lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop })), null);
|
|
2304
|
+
deepStrictEqual(recorder.events, [
|
|
2305
|
+
`open ${first}`,
|
|
2306
|
+
`fetch ${first} #1`,
|
|
2307
|
+
"cancel #1",
|
|
2308
|
+
`close ${first}`,
|
|
2309
|
+
`open ${second}`,
|
|
2310
|
+
`fetch ${second} #2`,
|
|
2311
|
+
"cancel #2",
|
|
2312
|
+
`close ${second}`
|
|
2313
|
+
]);
|
|
2314
|
+
recorder.assertInvariants();
|
|
2315
|
+
});
|
|
2316
|
+
});
|
|
2317
|
+
/**
|
|
2318
|
+
* Rejects if the given promise does not settle within the timeout, so that
|
|
2319
|
+
* a hang fails the test instead of blocking it.
|
|
2320
|
+
*/
|
|
2321
|
+
async function settleWithin(promise, ms = 5e3) {
|
|
2322
|
+
let timer;
|
|
2323
|
+
try {
|
|
2324
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
2325
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Did not settle within ${ms} ms.`)), ms);
|
|
2326
|
+
})]);
|
|
2327
|
+
} finally {
|
|
2328
|
+
clearTimeout(timer);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
function neverCancellableResponse(init) {
|
|
2332
|
+
return new Response(new ReadableStream({
|
|
2333
|
+
start(controller) {
|
|
2334
|
+
controller.enqueue(new TextEncoder().encode("never cancelled"));
|
|
2335
|
+
},
|
|
2336
|
+
cancel: () => new Promise(() => {})
|
|
2337
|
+
}), init);
|
|
2338
|
+
}
|
|
2339
|
+
test("lookupWebFinger() does not wait for cancelling response bodies", async (t) => {
|
|
2340
|
+
const descriptor = {
|
|
2341
|
+
subject: "acct:johndoe@example.com",
|
|
2342
|
+
links: []
|
|
2343
|
+
};
|
|
2344
|
+
esm_default.mockGlobal();
|
|
2345
|
+
try {
|
|
2346
|
+
await t.step("redirect", async () => {
|
|
2347
|
+
esm_default.removeRoutes();
|
|
2348
|
+
esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({
|
|
2349
|
+
status: 302,
|
|
2350
|
+
headers: { Location: "/.well-known/webfinger2" }
|
|
2351
|
+
}));
|
|
2352
|
+
esm_default.get("https://example.com/.well-known/webfinger2", () => new Response(JSON.stringify(descriptor), { headers: { "Content-Type": "application/jrd+json" } }));
|
|
2353
|
+
deepStrictEqual(await settleWithin(lookupDirect("acct:johndoe@example.com")), descriptor);
|
|
2354
|
+
});
|
|
2355
|
+
await t.step("non-2xx response", async () => {
|
|
2356
|
+
esm_default.removeRoutes();
|
|
2357
|
+
esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({ status: 404 }));
|
|
2358
|
+
deepStrictEqual(await settleWithin(lookupDirect("acct:johndoe@example.com")), null);
|
|
2359
|
+
});
|
|
2360
|
+
} finally {
|
|
2361
|
+
esm_default.hardReset();
|
|
2362
|
+
}
|
|
2363
|
+
});
|
|
2364
|
+
test$1("lookupWebFinger() records network_error for rejected hop addresses", async () => {
|
|
2365
|
+
for (const lookup of [
|
|
2366
|
+
() => Promise.reject(/* @__PURE__ */ new Error("DNS unavailable")),
|
|
2367
|
+
() => Promise.resolve([]),
|
|
2368
|
+
() => Promise.resolve([{
|
|
2369
|
+
address: "127.0.0.1",
|
|
2370
|
+
family: 4
|
|
2371
|
+
}])
|
|
2372
|
+
]) {
|
|
2373
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
2374
|
+
deepStrictEqual(await lookupWebFingerWithDependencies("acct:alice@dns-failure.invalid", { meterProvider }, { openHop: (url) => openPublicFetch(url, { lookup }) }), null);
|
|
2375
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
2376
|
+
ok(counter != null);
|
|
2377
|
+
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "network_error");
|
|
2378
|
+
deepStrictEqual(counter.attributes["activitypub.remote.host"], "dns-failure.invalid");
|
|
2379
|
+
deepStrictEqual("http.response.status_code" in counter.attributes, false);
|
|
2380
|
+
}
|
|
2381
|
+
});
|
|
2041
2382
|
//#endregion
|
|
2042
2383
|
export {};
|