@fedify/webfinger 2.3.15-dev.2482 → 2.3.15
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;
|
|
@@ -1228,7 +1229,7 @@ var esm_default = new class FetchMock {
|
|
|
1228
1229
|
//#endregion
|
|
1229
1230
|
//#region deno.json
|
|
1230
1231
|
var name = "@fedify/webfinger";
|
|
1231
|
-
var version = "2.3.15
|
|
1232
|
+
var version = "2.3.15";
|
|
1232
1233
|
//#endregion
|
|
1233
1234
|
//#region src/body.ts
|
|
1234
1235
|
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
@@ -1304,13 +1305,223 @@ async function readBoundedText(message, maxBytes, url) {
|
|
|
1304
1305
|
}
|
|
1305
1306
|
}
|
|
1306
1307
|
//#endregion
|
|
1307
|
-
//#region src/lookup.ts
|
|
1308
|
+
//#region src/lookup-impl.ts
|
|
1308
1309
|
const logger = getLogger([
|
|
1309
1310
|
"fedify",
|
|
1310
1311
|
"webfinger",
|
|
1311
1312
|
"lookup"
|
|
1312
1313
|
]);
|
|
1313
1314
|
const DEFAULT_MAX_REDIRECTION = 5;
|
|
1315
|
+
/**
|
|
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.
|
|
1319
|
+
* @param resource The resource URL to look up.
|
|
1320
|
+
* @param options Extra options for looking up the resource.
|
|
1321
|
+
* @param deps The dependencies.
|
|
1322
|
+
* @returns The resource descriptor and terminal lookup classification.
|
|
1323
|
+
* @internal
|
|
1324
|
+
*/
|
|
1325
|
+
async function lookupWebFingerInternal(resource, options = {}, deps = {}) {
|
|
1326
|
+
if (typeof resource === "string") resource = new URL(resource);
|
|
1327
|
+
let protocol = "https:";
|
|
1328
|
+
let server;
|
|
1329
|
+
if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
|
|
1330
|
+
const atPos = resource.pathname.lastIndexOf("@");
|
|
1331
|
+
if (atPos < 0) return {
|
|
1332
|
+
resource: null,
|
|
1333
|
+
result: "invalid"
|
|
1334
|
+
};
|
|
1335
|
+
server = resource.pathname.substring(atPos + 1);
|
|
1336
|
+
if (server === "" || /[/?#]/.test(server)) return {
|
|
1337
|
+
resource: null,
|
|
1338
|
+
result: "invalid"
|
|
1339
|
+
};
|
|
1340
|
+
if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
|
|
1341
|
+
resource: null,
|
|
1342
|
+
result: "invalid"
|
|
1343
|
+
};
|
|
1344
|
+
} else {
|
|
1345
|
+
protocol = resource.protocol;
|
|
1346
|
+
server = resource.host;
|
|
1347
|
+
}
|
|
1348
|
+
let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
|
|
1349
|
+
url.searchParams.set("resource", resource.href);
|
|
1350
|
+
const openHop = deps.openHop ?? ((hopUrl) => options.allowPrivateAddress === true ? openDirectFetch(hopUrl) : openPublicFetch(hopUrl, { signal: options.signal }));
|
|
1351
|
+
let redirected = 0;
|
|
1352
|
+
while (true) {
|
|
1353
|
+
logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
|
|
1354
|
+
const remoteHost = url.host;
|
|
1355
|
+
const session = await openSession(url, openHop, options.signal);
|
|
1356
|
+
if (session == null) return {
|
|
1357
|
+
resource: null,
|
|
1358
|
+
result: "network_error",
|
|
1359
|
+
remoteHost
|
|
1360
|
+
};
|
|
1361
|
+
let hop;
|
|
1362
|
+
try {
|
|
1363
|
+
hop = await fetchHop(session, url, options);
|
|
1364
|
+
} finally {
|
|
1365
|
+
await session.close();
|
|
1366
|
+
}
|
|
1367
|
+
if ("outcome" in hop) return hop.outcome;
|
|
1368
|
+
redirected++;
|
|
1369
|
+
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
1370
|
+
if (redirected > maxRedirection) {
|
|
1371
|
+
logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
|
|
1372
|
+
return {
|
|
1373
|
+
resource: null,
|
|
1374
|
+
result: "invalid",
|
|
1375
|
+
statusCode: hop.statusCode,
|
|
1376
|
+
remoteHost
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
let redirectedUrl;
|
|
1380
|
+
try {
|
|
1381
|
+
redirectedUrl = new URL(hop.redirect, url);
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
logger.error("Invalid Location header while following WebFinger redirect: {error}", {
|
|
1384
|
+
url: url.href,
|
|
1385
|
+
error
|
|
1386
|
+
});
|
|
1387
|
+
return {
|
|
1388
|
+
resource: null,
|
|
1389
|
+
result: "invalid",
|
|
1390
|
+
statusCode: hop.statusCode,
|
|
1391
|
+
remoteHost
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
if (redirectedUrl.protocol !== url.protocol) {
|
|
1395
|
+
logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
|
|
1396
|
+
protocol: url.protocol,
|
|
1397
|
+
redirectedProtocol: redirectedUrl.protocol
|
|
1398
|
+
});
|
|
1399
|
+
return {
|
|
1400
|
+
resource: null,
|
|
1401
|
+
result: "invalid",
|
|
1402
|
+
statusCode: hop.statusCode,
|
|
1403
|
+
remoteHost
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
url = redirectedUrl;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Opens a fetch session for a hop. A disallowed URL, a failed DNS lookup,
|
|
1411
|
+
* or an aborted lookup yields `null` instead of an error.
|
|
1412
|
+
*/
|
|
1413
|
+
async function openSession(url, openHop, signal) {
|
|
1414
|
+
try {
|
|
1415
|
+
return await openHop(url.href);
|
|
1416
|
+
} catch (e) {
|
|
1417
|
+
if (e instanceof UrlError) {
|
|
1418
|
+
if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
1419
|
+
url: url.href,
|
|
1420
|
+
error: e
|
|
1421
|
+
});
|
|
1422
|
+
else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
|
|
1423
|
+
return null;
|
|
1424
|
+
}
|
|
1425
|
+
if (signal?.aborted) {
|
|
1426
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
|
|
1427
|
+
url: url.href,
|
|
1428
|
+
error: e
|
|
1429
|
+
});
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
throw e;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Fetches a hop with the given session, and consumes the response body or
|
|
1437
|
+
* starts cancelling it before returning. The caller closes the session.
|
|
1438
|
+
*/
|
|
1439
|
+
async function fetchHop(session, url, options) {
|
|
1440
|
+
let response;
|
|
1441
|
+
try {
|
|
1442
|
+
response = await session.fetch(url, {
|
|
1443
|
+
headers: {
|
|
1444
|
+
Accept: "application/jrd+json",
|
|
1445
|
+
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
1446
|
+
},
|
|
1447
|
+
redirect: "manual",
|
|
1448
|
+
signal: options.signal
|
|
1449
|
+
});
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
|
|
1452
|
+
url: url.href,
|
|
1453
|
+
error
|
|
1454
|
+
});
|
|
1455
|
+
return { outcome: {
|
|
1456
|
+
resource: null,
|
|
1457
|
+
result: "network_error",
|
|
1458
|
+
remoteHost: url.host
|
|
1459
|
+
} };
|
|
1460
|
+
}
|
|
1461
|
+
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
1462
|
+
cancelBody(response, url);
|
|
1463
|
+
return {
|
|
1464
|
+
redirect: response.headers.get("Location"),
|
|
1465
|
+
statusCode: response.status
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
if (!response.ok) {
|
|
1469
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
|
|
1470
|
+
url: url.href,
|
|
1471
|
+
status: response.status,
|
|
1472
|
+
statusText: response.statusText
|
|
1473
|
+
});
|
|
1474
|
+
cancelBody(response, url);
|
|
1475
|
+
return { outcome: {
|
|
1476
|
+
resource: null,
|
|
1477
|
+
result: response.status === 404 || response.status === 410 ? "not_found" : "error",
|
|
1478
|
+
statusCode: response.status,
|
|
1479
|
+
remoteHost: url.host
|
|
1480
|
+
} };
|
|
1481
|
+
}
|
|
1482
|
+
try {
|
|
1483
|
+
return { outcome: {
|
|
1484
|
+
resource: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url)),
|
|
1485
|
+
result: "found",
|
|
1486
|
+
statusCode: response.status,
|
|
1487
|
+
remoteHost: url.host
|
|
1488
|
+
} };
|
|
1489
|
+
} catch (e) {
|
|
1490
|
+
if (e instanceof BodyTooLargeError) return { outcome: {
|
|
1491
|
+
resource: null,
|
|
1492
|
+
result: "invalid",
|
|
1493
|
+
statusCode: response.status,
|
|
1494
|
+
remoteHost: url.host
|
|
1495
|
+
} };
|
|
1496
|
+
if (e instanceof SyntaxError) {
|
|
1497
|
+
logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
|
|
1498
|
+
return { outcome: {
|
|
1499
|
+
resource: null,
|
|
1500
|
+
result: "invalid",
|
|
1501
|
+
statusCode: response.status,
|
|
1502
|
+
remoteHost: url.host
|
|
1503
|
+
} };
|
|
1504
|
+
}
|
|
1505
|
+
throw e;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Starts cancelling a response body that is not read, without waiting for it
|
|
1510
|
+
* to settle. The cancellation of a branch of a tee (e.g., a body cloned by
|
|
1511
|
+
* a fetch mock) settles only after the other branch is cancelled too, per
|
|
1512
|
+
* the Streams standard, so awaiting it could block forever. A failure is
|
|
1513
|
+
* only logged.
|
|
1514
|
+
*/
|
|
1515
|
+
function cancelBody(response, url) {
|
|
1516
|
+
response.body?.cancel().catch((error) => {
|
|
1517
|
+
logger.debug("Failed to cancel the response body from {url}: {error}", {
|
|
1518
|
+
url: url.href,
|
|
1519
|
+
error
|
|
1520
|
+
});
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
//#endregion
|
|
1524
|
+
//#region src/lookup.ts
|
|
1314
1525
|
const WEBFINGER_HISTOGRAM_BUCKETS = [
|
|
1315
1526
|
5,
|
|
1316
1527
|
10,
|
|
@@ -1364,13 +1575,11 @@ function getMetricResourceScheme(scheme) {
|
|
|
1364
1575
|
return WEBFINGER_LOOKUP_SCHEME_WHITELIST.has(scheme) ? scheme : "other";
|
|
1365
1576
|
}
|
|
1366
1577
|
/**
|
|
1367
|
-
*
|
|
1368
|
-
*
|
|
1369
|
-
* @
|
|
1370
|
-
* @returns The resource descriptor, or `null` if not found.
|
|
1371
|
-
* @since 0.2.0
|
|
1578
|
+
* The lookup implementation with injectable hop sessions for tests.
|
|
1579
|
+
* Not exported from the package entry point.
|
|
1580
|
+
* @internal
|
|
1372
1581
|
*/
|
|
1373
|
-
async function
|
|
1582
|
+
async function lookupWebFingerWithDependencies(resource, options = {}, deps = {}) {
|
|
1374
1583
|
const tracer = (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version);
|
|
1375
1584
|
const scheme = getResourceScheme(resource);
|
|
1376
1585
|
return await tracer.startActiveSpan("webfinger.lookup", {
|
|
@@ -1387,7 +1596,7 @@ async function lookupWebFinger(resource, options = {}) {
|
|
|
1387
1596
|
result: "error"
|
|
1388
1597
|
};
|
|
1389
1598
|
try {
|
|
1390
|
-
outcome = await lookupWebFingerInternal(resource, options);
|
|
1599
|
+
outcome = await lookupWebFingerInternal(resource, options, deps);
|
|
1391
1600
|
span.setStatus({ code: outcome.resource === null ? SpanStatusCode.ERROR : SpanStatusCode.OK });
|
|
1392
1601
|
return outcome.resource;
|
|
1393
1602
|
} catch (error) {
|
|
@@ -1413,181 +1622,56 @@ function recordWebFingerLookup(meterProvider, durationMs, scheme, outcome) {
|
|
|
1413
1622
|
instruments.lookup.add(1, attributes);
|
|
1414
1623
|
instruments.lookupDuration.record(durationMs, attributes);
|
|
1415
1624
|
}
|
|
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
|
-
return {
|
|
1540
|
-
resource: JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url)),
|
|
1541
|
-
result: "found",
|
|
1542
|
-
statusCode: response.status,
|
|
1543
|
-
remoteHost
|
|
1544
|
-
};
|
|
1545
|
-
} catch (e) {
|
|
1546
|
-
if (e instanceof BodyTooLargeError) return {
|
|
1547
|
-
resource: null,
|
|
1548
|
-
result: "invalid",
|
|
1549
|
-
statusCode: response.status,
|
|
1550
|
-
remoteHost
|
|
1551
|
-
};
|
|
1552
|
-
if (e instanceof SyntaxError) {
|
|
1553
|
-
logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
|
|
1554
|
-
return {
|
|
1555
|
-
resource: null,
|
|
1556
|
-
result: "invalid",
|
|
1557
|
-
statusCode: response.status,
|
|
1558
|
-
remoteHost
|
|
1559
|
-
};
|
|
1560
|
-
}
|
|
1561
|
-
throw e;
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
1625
|
//#endregion
|
|
1566
1626
|
//#region src/lookup.test.ts
|
|
1627
|
+
function lookupMock(resource, options = {}) {
|
|
1628
|
+
if (resource.toString().includes("fedify-test.internal")) return lookupWebFingerWithDependencies(resource, options, { openHop: (url) => Promise.resolve({
|
|
1629
|
+
url: new URL(url),
|
|
1630
|
+
fetch: () => Promise.reject(/* @__PURE__ */ new TypeError("Connection refused")),
|
|
1631
|
+
close: () => Promise.resolve()
|
|
1632
|
+
}) });
|
|
1633
|
+
return lookupWebFingerWithDependencies(resource, options, { openHop: options.allowPrivateAddress === true ? openDirectFetch : openExampleDirectly });
|
|
1634
|
+
}
|
|
1635
|
+
function lookupDirect(resource, options) {
|
|
1636
|
+
return lookupWebFingerWithDependencies(resource, options, { openHop: openDirectFetch });
|
|
1637
|
+
}
|
|
1638
|
+
function lookupResolving(addresses) {
|
|
1639
|
+
return () => Promise.resolve(addresses);
|
|
1640
|
+
}
|
|
1641
|
+
function openExampleDirectly(url) {
|
|
1642
|
+
return new URL(url).hostname === "example.com" ? openDirectFetch(url) : openPublicFetch(url, { lookup: lookupResolving([{
|
|
1643
|
+
address: "93.184.215.14",
|
|
1644
|
+
family: 4
|
|
1645
|
+
}]) });
|
|
1646
|
+
}
|
|
1647
|
+
async function lookupResourceWithDependencies(resource, options, deps) {
|
|
1648
|
+
return (await lookupWebFingerInternal(resource, options, deps)).resource;
|
|
1649
|
+
}
|
|
1567
1650
|
test({
|
|
1568
1651
|
name: "lookupWebFinger()",
|
|
1569
1652
|
sanitizeOps: false,
|
|
1570
1653
|
sanitizeResources: false,
|
|
1571
1654
|
async fn(t) {
|
|
1655
|
+
const responseTimers = [];
|
|
1572
1656
|
await t.step("invalid resource", async () => {
|
|
1573
|
-
deepStrictEqual(await
|
|
1574
|
-
deepStrictEqual(await
|
|
1575
|
-
deepStrictEqual(await
|
|
1576
|
-
deepStrictEqual(await
|
|
1577
|
-
deepStrictEqual(await
|
|
1578
|
-
deepStrictEqual(await
|
|
1579
|
-
deepStrictEqual(await
|
|
1657
|
+
deepStrictEqual(await lookupMock("acct:johndoe"), null);
|
|
1658
|
+
deepStrictEqual(await lookupMock(new URL("acct:johndoe")), null);
|
|
1659
|
+
deepStrictEqual(await lookupMock("acct:johndoe@"), null);
|
|
1660
|
+
deepStrictEqual(await lookupMock(new URL("acct:johndoe@")), null);
|
|
1661
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com/exploit"), null);
|
|
1662
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com?x=1"), null);
|
|
1663
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com#frag"), null);
|
|
1580
1664
|
});
|
|
1581
1665
|
await t.step("connection refused", async () => {
|
|
1582
|
-
deepStrictEqual(await
|
|
1583
|
-
deepStrictEqual(await
|
|
1666
|
+
deepStrictEqual(await lookupMock("acct:johndoe@fedify-test.internal"), null);
|
|
1667
|
+
deepStrictEqual(await lookupMock("https://fedify-test.internal/foo"), null);
|
|
1584
1668
|
});
|
|
1585
1669
|
esm_default.spyGlobal();
|
|
1586
1670
|
try {
|
|
1587
1671
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
|
|
1588
1672
|
await t.step("not found", async () => {
|
|
1589
|
-
deepStrictEqual(await
|
|
1590
|
-
deepStrictEqual(await
|
|
1673
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), null);
|
|
1674
|
+
deepStrictEqual(await lookupMock("https://example.com/foo"), null);
|
|
1591
1675
|
});
|
|
1592
1676
|
const expected = {
|
|
1593
1677
|
subject: "acct:johndoe@example.com",
|
|
@@ -1596,7 +1680,7 @@ test({
|
|
|
1596
1680
|
esm_default.removeRoutes();
|
|
1597
1681
|
esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
|
|
1598
1682
|
await t.step("acct", async () => {
|
|
1599
|
-
deepStrictEqual(await
|
|
1683
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), expected);
|
|
1600
1684
|
});
|
|
1601
1685
|
const expected2 = {
|
|
1602
1686
|
subject: "https://example.com/foo",
|
|
@@ -1605,7 +1689,7 @@ test({
|
|
|
1605
1689
|
esm_default.removeRoutes();
|
|
1606
1690
|
esm_default.get("https://example.com/.well-known/webfinger?resource=https%3A%2F%2Fexample.com%2Ffoo", { body: expected2 });
|
|
1607
1691
|
await t.step("https", async () => {
|
|
1608
|
-
deepStrictEqual(await
|
|
1692
|
+
deepStrictEqual(await lookupMock("https://example.com/foo"), expected2);
|
|
1609
1693
|
});
|
|
1610
1694
|
const mailtoExpected = {
|
|
1611
1695
|
subject: "mailto:juliet@example.com",
|
|
@@ -1614,7 +1698,7 @@ test({
|
|
|
1614
1698
|
esm_default.removeRoutes();
|
|
1615
1699
|
esm_default.get("https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com", { body: mailtoExpected });
|
|
1616
1700
|
await t.step("mailto", async () => {
|
|
1617
|
-
deepStrictEqual(await
|
|
1701
|
+
deepStrictEqual(await lookupMock("mailto:juliet@example.com"), mailtoExpected);
|
|
1618
1702
|
});
|
|
1619
1703
|
const mailtoQueryExpected = {
|
|
1620
1704
|
subject: "mailto:juliet@example.com?subject=Hi",
|
|
@@ -1623,12 +1707,12 @@ test({
|
|
|
1623
1707
|
esm_default.removeRoutes();
|
|
1624
1708
|
esm_default.get("https://example.com/.well-known/webfinger?resource=mailto%3Ajuliet%40example.com%3Fsubject%3DHi", { body: mailtoQueryExpected });
|
|
1625
1709
|
await t.step("mailto with hfields", async () => {
|
|
1626
|
-
deepStrictEqual(await
|
|
1710
|
+
deepStrictEqual(await lookupMock("mailto:juliet@example.com?subject=Hi"), mailtoQueryExpected);
|
|
1627
1711
|
});
|
|
1628
1712
|
esm_default.removeRoutes();
|
|
1629
1713
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
|
|
1630
1714
|
await t.step("invalid response", async () => {
|
|
1631
|
-
deepStrictEqual(await
|
|
1715
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), null);
|
|
1632
1716
|
});
|
|
1633
1717
|
esm_default.removeRoutes();
|
|
1634
1718
|
esm_default.get("begin:https://localhost/.well-known/webfinger?", {
|
|
@@ -1640,8 +1724,8 @@ test({
|
|
|
1640
1724
|
}]
|
|
1641
1725
|
});
|
|
1642
1726
|
await t.step("private address", async () => {
|
|
1643
|
-
deepStrictEqual(await
|
|
1644
|
-
deepStrictEqual(await
|
|
1727
|
+
deepStrictEqual(await lookupMock("acct:test@localhost"), null);
|
|
1728
|
+
deepStrictEqual(await lookupMock("acct:test@localhost", { allowPrivateAddress: true }), {
|
|
1645
1729
|
subject: "acct:test@localhost",
|
|
1646
1730
|
links: [{
|
|
1647
1731
|
rel: "self",
|
|
@@ -1657,7 +1741,7 @@ test({
|
|
|
1657
1741
|
});
|
|
1658
1742
|
esm_default.get("begin:https://example.com/.well-known/webfinger2", { body: expected });
|
|
1659
1743
|
await t.step("redirection", async () => {
|
|
1660
|
-
deepStrictEqual(await
|
|
1744
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), expected);
|
|
1661
1745
|
});
|
|
1662
1746
|
esm_default.removeRoutes();
|
|
1663
1747
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", {
|
|
@@ -1665,7 +1749,7 @@ test({
|
|
|
1665
1749
|
headers: { Location: "/.well-known/webfinger" }
|
|
1666
1750
|
});
|
|
1667
1751
|
await t.step("infinite redirection", async () => {
|
|
1668
|
-
deepStrictEqual(await withTimeout(() =>
|
|
1752
|
+
deepStrictEqual(await withTimeout(() => lookupMock("acct:johndoe@example.com"), 2e3), null);
|
|
1669
1753
|
});
|
|
1670
1754
|
esm_default.removeRoutes();
|
|
1671
1755
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", {
|
|
@@ -1673,7 +1757,7 @@ test({
|
|
|
1673
1757
|
headers: { Location: "ftp://example.com/" }
|
|
1674
1758
|
});
|
|
1675
1759
|
await t.step("redirection to different protocol", async () => {
|
|
1676
|
-
deepStrictEqual(await
|
|
1760
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), null);
|
|
1677
1761
|
});
|
|
1678
1762
|
esm_default.removeRoutes();
|
|
1679
1763
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", {
|
|
@@ -1681,7 +1765,7 @@ test({
|
|
|
1681
1765
|
headers: { Location: "https://localhost/" }
|
|
1682
1766
|
});
|
|
1683
1767
|
await t.step("redirection to private address", async () => {
|
|
1684
|
-
deepStrictEqual(await
|
|
1768
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), null);
|
|
1685
1769
|
});
|
|
1686
1770
|
esm_default.removeRoutes();
|
|
1687
1771
|
let redirectCount = 0;
|
|
@@ -1695,13 +1779,13 @@ test({
|
|
|
1695
1779
|
});
|
|
1696
1780
|
await t.step("custom maxRedirection", async () => {
|
|
1697
1781
|
redirectCount = 0;
|
|
1698
|
-
deepStrictEqual(await
|
|
1782
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), null);
|
|
1699
1783
|
redirectCount = 0;
|
|
1700
|
-
deepStrictEqual(await
|
|
1784
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { maxRedirection: 2 }), expected);
|
|
1701
1785
|
redirectCount = 0;
|
|
1702
|
-
deepStrictEqual(await
|
|
1786
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { maxRedirection: 3 }), expected);
|
|
1703
1787
|
redirectCount = 0;
|
|
1704
|
-
deepStrictEqual(await
|
|
1788
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com"), expected);
|
|
1705
1789
|
});
|
|
1706
1790
|
await t.step("maxRedirection: 1 follows exactly one redirect", async () => {
|
|
1707
1791
|
esm_default.removeRoutes();
|
|
@@ -1713,7 +1797,7 @@ test({
|
|
|
1713
1797
|
headers: { Location: "/.well-known/webfinger?after=1" }
|
|
1714
1798
|
} : { body: expected };
|
|
1715
1799
|
});
|
|
1716
|
-
deepStrictEqual(await
|
|
1800
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), expected);
|
|
1717
1801
|
esm_default.removeRoutes();
|
|
1718
1802
|
count = 0;
|
|
1719
1803
|
esm_default.get("begin:https://example.com/.well-known/webfinger", () => {
|
|
@@ -1723,18 +1807,18 @@ test({
|
|
|
1723
1807
|
headers: { Location: `/.well-known/webfinger?after=${count}` }
|
|
1724
1808
|
} : { body: expected };
|
|
1725
1809
|
});
|
|
1726
|
-
deepStrictEqual(await
|
|
1810
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { maxRedirection: 1 }), null);
|
|
1727
1811
|
});
|
|
1728
1812
|
esm_default.removeRoutes();
|
|
1729
1813
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", () => new Promise((resolve) => {
|
|
1730
1814
|
const timeoutId = setTimeout(() => {
|
|
1731
1815
|
resolve({ body: expected });
|
|
1732
1816
|
}, 1e3);
|
|
1733
|
-
|
|
1817
|
+
responseTimers.push(timeoutId);
|
|
1734
1818
|
}));
|
|
1735
1819
|
await t.step("request cancellation", async () => {
|
|
1736
1820
|
const controller = new AbortController();
|
|
1737
|
-
const promise =
|
|
1821
|
+
const promise = lookupMock("acct:johndoe@example.com", { signal: controller.signal });
|
|
1738
1822
|
controller.abort();
|
|
1739
1823
|
deepStrictEqual(await promise, null);
|
|
1740
1824
|
});
|
|
@@ -1750,12 +1834,12 @@ test({
|
|
|
1750
1834
|
const timeoutId = setTimeout(() => {
|
|
1751
1835
|
resolve({ body: expected });
|
|
1752
1836
|
}, 1e3);
|
|
1753
|
-
|
|
1837
|
+
responseTimers.push(timeoutId);
|
|
1754
1838
|
});
|
|
1755
1839
|
});
|
|
1756
1840
|
await t.step("cancellation during redirection", async () => {
|
|
1757
1841
|
const controller = new AbortController();
|
|
1758
|
-
const promise =
|
|
1842
|
+
const promise = lookupMock("acct:johndoe@example.com", { signal: controller.signal });
|
|
1759
1843
|
setTimeout(() => controller.abort(), 100);
|
|
1760
1844
|
deepStrictEqual(await promise, null);
|
|
1761
1845
|
});
|
|
@@ -1764,19 +1848,20 @@ test({
|
|
|
1764
1848
|
const timeoutId = setTimeout(() => {
|
|
1765
1849
|
resolve({ body: expected });
|
|
1766
1850
|
}, 500);
|
|
1767
|
-
|
|
1851
|
+
responseTimers.push(timeoutId);
|
|
1768
1852
|
}));
|
|
1769
1853
|
await t.step("cancellation with immediate abort", async () => {
|
|
1770
1854
|
const controller = new AbortController();
|
|
1771
1855
|
controller.abort();
|
|
1772
|
-
deepStrictEqual(await
|
|
1856
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { signal: controller.signal }), null);
|
|
1773
1857
|
});
|
|
1774
1858
|
esm_default.removeRoutes();
|
|
1775
1859
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: expected });
|
|
1776
1860
|
await t.step("successful request with signal", async () => {
|
|
1777
|
-
deepStrictEqual(await
|
|
1861
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { signal: new AbortController().signal }), expected);
|
|
1778
1862
|
});
|
|
1779
1863
|
} finally {
|
|
1864
|
+
for (const timer of responseTimers) clearTimeout(timer);
|
|
1780
1865
|
esm_default.removeRoutes();
|
|
1781
1866
|
esm_default.hardReset();
|
|
1782
1867
|
}
|
|
@@ -1796,7 +1881,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1796
1881
|
esm_default.removeRoutes();
|
|
1797
1882
|
esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
|
|
1798
1883
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1799
|
-
deepStrictEqual(await
|
|
1884
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { meterProvider }), expected);
|
|
1800
1885
|
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
1801
1886
|
deepStrictEqual(counters.length, 1);
|
|
1802
1887
|
deepStrictEqual(counters[0].type, "counter");
|
|
@@ -1819,7 +1904,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1819
1904
|
links: []
|
|
1820
1905
|
} });
|
|
1821
1906
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1822
|
-
await
|
|
1907
|
+
await lookupMock("https://example.com/foo", { meterProvider });
|
|
1823
1908
|
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
1824
1909
|
deepStrictEqual(counters.length, 1);
|
|
1825
1910
|
deepStrictEqual(counters[0].attributes["webfinger.resource.scheme"], "https");
|
|
@@ -1832,7 +1917,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1832
1917
|
links: []
|
|
1833
1918
|
} });
|
|
1834
1919
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1835
|
-
await
|
|
1920
|
+
await lookupMock("https://example.com:8443/foo", { meterProvider });
|
|
1836
1921
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1837
1922
|
ok(counter != null);
|
|
1838
1923
|
deepStrictEqual(counter.attributes["activitypub.remote.host"], "example.com:8443");
|
|
@@ -1841,7 +1926,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1841
1926
|
esm_default.removeRoutes();
|
|
1842
1927
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 404 });
|
|
1843
1928
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1844
|
-
deepStrictEqual(await
|
|
1929
|
+
deepStrictEqual(await lookupMock("acct:johndoe@example.com", { meterProvider }), null);
|
|
1845
1930
|
const counters = recorder.getMeasurements("webfinger.lookup");
|
|
1846
1931
|
deepStrictEqual(counters.length, 1);
|
|
1847
1932
|
deepStrictEqual(counters[0].attributes["webfinger.lookup.result"], "not_found");
|
|
@@ -1855,7 +1940,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1855
1940
|
esm_default.removeRoutes();
|
|
1856
1941
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 410 });
|
|
1857
1942
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1858
|
-
await
|
|
1943
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1859
1944
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1860
1945
|
ok(counter != null);
|
|
1861
1946
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "not_found");
|
|
@@ -1865,7 +1950,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1865
1950
|
esm_default.removeRoutes();
|
|
1866
1951
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { status: 500 });
|
|
1867
1952
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1868
|
-
await
|
|
1953
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1869
1954
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1870
1955
|
ok(counter != null);
|
|
1871
1956
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "error");
|
|
@@ -1875,7 +1960,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1875
1960
|
esm_default.removeRoutes();
|
|
1876
1961
|
esm_default.get("begin:https://example.com/.well-known/webfinger?", { body: "not json" });
|
|
1877
1962
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1878
|
-
await
|
|
1963
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1879
1964
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1880
1965
|
ok(counter != null);
|
|
1881
1966
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "invalid");
|
|
@@ -1884,7 +1969,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1884
1969
|
await t.step("records result=network_error when fetch never reaches the remote", async () => {
|
|
1885
1970
|
esm_default.removeRoutes();
|
|
1886
1971
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1887
|
-
deepStrictEqual(await
|
|
1972
|
+
deepStrictEqual(await lookupMock("acct:johndoe@fedify-test.internal", { meterProvider }), null);
|
|
1888
1973
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1889
1974
|
ok(counter != null);
|
|
1890
1975
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "network_error");
|
|
@@ -1893,7 +1978,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1893
1978
|
});
|
|
1894
1979
|
await t.step("records result=invalid for malformed acct: resources", async () => {
|
|
1895
1980
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1896
|
-
deepStrictEqual(await
|
|
1981
|
+
deepStrictEqual(await lookupMock("acct:johndoe", { meterProvider }), null);
|
|
1897
1982
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1898
1983
|
ok(counter != null);
|
|
1899
1984
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "invalid");
|
|
@@ -1907,7 +1992,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1907
1992
|
headers: { Location: "/.well-known/webfinger" }
|
|
1908
1993
|
});
|
|
1909
1994
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1910
|
-
deepStrictEqual(await withTimeout(() =>
|
|
1995
|
+
deepStrictEqual(await withTimeout(() => lookupMock("acct:johndoe@example.com", {
|
|
1911
1996
|
meterProvider,
|
|
1912
1997
|
maxRedirection: 3
|
|
1913
1998
|
}), 2e3), null);
|
|
@@ -1924,7 +2009,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1924
2009
|
headers: { Location: "ftp://example.com/" }
|
|
1925
2010
|
});
|
|
1926
2011
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1927
|
-
await
|
|
2012
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1928
2013
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1929
2014
|
ok(counter != null);
|
|
1930
2015
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "invalid");
|
|
@@ -1937,7 +2022,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1937
2022
|
headers: { Location: "https://localhost/" }
|
|
1938
2023
|
});
|
|
1939
2024
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1940
|
-
await
|
|
2025
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1941
2026
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1942
2027
|
ok(counter != null);
|
|
1943
2028
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "network_error");
|
|
@@ -1950,7 +2035,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1950
2035
|
headers: { Location: "http://[bad" }
|
|
1951
2036
|
});
|
|
1952
2037
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1953
|
-
await
|
|
2038
|
+
await lookupMock("acct:johndoe@example.com", { meterProvider });
|
|
1954
2039
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1955
2040
|
ok(counter != null);
|
|
1956
2041
|
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "invalid");
|
|
@@ -1960,7 +2045,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1960
2045
|
await t.step("buckets unknown resource schemes as 'other' to keep metric cardinality bounded", async () => {
|
|
1961
2046
|
esm_default.removeRoutes();
|
|
1962
2047
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1963
|
-
await
|
|
2048
|
+
await lookupMock("ssh://example.com/foo", { meterProvider });
|
|
1964
2049
|
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
1965
2050
|
ok(counter != null);
|
|
1966
2051
|
deepStrictEqual(counter.attributes["webfinger.resource.scheme"], "other");
|
|
@@ -1969,7 +2054,7 @@ test("lookupWebFinger() records webfinger.lookup counter and duration", {
|
|
|
1969
2054
|
esm_default.removeRoutes();
|
|
1970
2055
|
esm_default.get("https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com", { body: expected });
|
|
1971
2056
|
const [_unused, recorder] = createTestMeterProvider();
|
|
1972
|
-
await
|
|
2057
|
+
await lookupMock("acct:johndoe@example.com");
|
|
1973
2058
|
deepStrictEqual(recorder.getMeasurements("webfinger.lookup").length, 0);
|
|
1974
2059
|
deepStrictEqual(recorder.getMeasurements("webfinger.lookup.duration").length, 0);
|
|
1975
2060
|
});
|
|
@@ -1986,25 +2071,23 @@ test("lookupWebFinger() bounds resource descriptors", async () => {
|
|
|
1986
2071
|
"Content-Type": "application/jrd+json",
|
|
1987
2072
|
"Content-Length": "1"
|
|
1988
2073
|
} }));
|
|
1989
|
-
deepStrictEqual(await
|
|
2074
|
+
deepStrictEqual(await lookupMock("acct:alice@example.com", { allowPrivateAddress: true }), null);
|
|
1990
2075
|
oversized = false;
|
|
1991
|
-
deepStrictEqual(await
|
|
2076
|
+
deepStrictEqual(await lookupMock("acct:alice@example.com", { allowPrivateAddress: true }), { subject: "acct:alice@example.com" });
|
|
1992
2077
|
} finally {
|
|
1993
2078
|
esm_default.hardReset();
|
|
1994
2079
|
}
|
|
1995
2080
|
});
|
|
1996
2081
|
test("lookupWebFinger() logs DNS failures as such", async (t) => {
|
|
1997
|
-
if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") return;
|
|
1998
2082
|
for (const result of [
|
|
1999
2083
|
"throws",
|
|
2000
2084
|
"empty",
|
|
2001
2085
|
"private"
|
|
2002
2086
|
]) await t.step(result, async () => {
|
|
2003
|
-
const
|
|
2004
|
-
dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
2087
|
+
const lookup = () => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
|
|
2005
2088
|
address: "127.0.0.1",
|
|
2006
2089
|
family: 4
|
|
2007
|
-
}])
|
|
2090
|
+
}]);
|
|
2008
2091
|
const records = [];
|
|
2009
2092
|
await configure({
|
|
2010
2093
|
sinks: { buffer: (record) => records.push(record) },
|
|
@@ -2019,7 +2102,7 @@ test("lookupWebFinger() logs DNS failures as such", async (t) => {
|
|
|
2019
2102
|
reset: true
|
|
2020
2103
|
});
|
|
2021
2104
|
try {
|
|
2022
|
-
deepStrictEqual(await
|
|
2105
|
+
deepStrictEqual(await lookupResourceWithDependencies("acct:alice@dns-failure.invalid", {}, { openHop: (url) => openPublicFetch(url, { lookup }) }), null);
|
|
2023
2106
|
const failures = records.filter((r) => r.rawMessage !== "Fetching WebFinger resource descriptor from {url}...");
|
|
2024
2107
|
deepStrictEqual(failures.map((r) => [r.level, r.rawMessage]), [result === "private" ? ["error", "Invalid URL for WebFinger resource descriptor: {error}"] : ["debug", "DNS lookup failed for {url}"]]);
|
|
2025
2108
|
const { error } = failures[0].properties;
|
|
@@ -2027,9 +2110,267 @@ test("lookupWebFinger() logs DNS failures as such", async (t) => {
|
|
|
2027
2110
|
deepStrictEqual(error.reason, result === "private" ? "disallowed" : "dns");
|
|
2028
2111
|
} finally {
|
|
2029
2112
|
await reset();
|
|
2030
|
-
dns.lookup = originalLookup;
|
|
2031
2113
|
}
|
|
2032
2114
|
});
|
|
2033
2115
|
});
|
|
2116
|
+
function recordHops(respond) {
|
|
2117
|
+
const events = [];
|
|
2118
|
+
const unsettled = [];
|
|
2119
|
+
let opens = 0;
|
|
2120
|
+
let closes = 0;
|
|
2121
|
+
let responses = 0;
|
|
2122
|
+
return {
|
|
2123
|
+
events,
|
|
2124
|
+
async openHop(url) {
|
|
2125
|
+
opens++;
|
|
2126
|
+
events.push(`open ${url}`);
|
|
2127
|
+
const states = [];
|
|
2128
|
+
let closed = false;
|
|
2129
|
+
return {
|
|
2130
|
+
url: new URL(url),
|
|
2131
|
+
async fetch(input) {
|
|
2132
|
+
if (closed) throw new TypeError("The fetch session is closed.");
|
|
2133
|
+
const target = new URL(input instanceof Request ? input.url : input);
|
|
2134
|
+
const id = ++responses;
|
|
2135
|
+
events.push(`fetch ${target.href} #${id}`);
|
|
2136
|
+
const fake = respond(target);
|
|
2137
|
+
const state = {
|
|
2138
|
+
id,
|
|
2139
|
+
settled: false
|
|
2140
|
+
};
|
|
2141
|
+
states.push(state);
|
|
2142
|
+
let sent = false;
|
|
2143
|
+
const body = new ReadableStream({
|
|
2144
|
+
pull(controller) {
|
|
2145
|
+
if (!sent && fake.body != null && fake.body !== "") {
|
|
2146
|
+
sent = true;
|
|
2147
|
+
controller.enqueue(new TextEncoder().encode(fake.body));
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
state.settled = true;
|
|
2151
|
+
events.push(`consumed #${id}`);
|
|
2152
|
+
controller.close();
|
|
2153
|
+
},
|
|
2154
|
+
cancel() {
|
|
2155
|
+
state.settled = true;
|
|
2156
|
+
events.push(`cancel #${id}`);
|
|
2157
|
+
if (fake.cancelFails) throw new Error("Cancellation failed");
|
|
2158
|
+
if (fake.cancelNeverSettles) return new Promise(() => {});
|
|
2159
|
+
}
|
|
2160
|
+
}, { highWaterMark: 0 });
|
|
2161
|
+
const response = new Response(body, {
|
|
2162
|
+
status: fake.status ?? 200,
|
|
2163
|
+
headers: fake.headers
|
|
2164
|
+
});
|
|
2165
|
+
if (fake.url != null) Object.defineProperty(response, "url", { value: fake.url });
|
|
2166
|
+
return response;
|
|
2167
|
+
},
|
|
2168
|
+
async close() {
|
|
2169
|
+
if (closed) return;
|
|
2170
|
+
closed = true;
|
|
2171
|
+
closes++;
|
|
2172
|
+
for (const state of states) if (!state.settled) unsettled.push(`#${state.id}`);
|
|
2173
|
+
events.push(`close ${url}`);
|
|
2174
|
+
}
|
|
2175
|
+
};
|
|
2176
|
+
},
|
|
2177
|
+
assertInvariants() {
|
|
2178
|
+
deepStrictEqual(closes, opens);
|
|
2179
|
+
deepStrictEqual(unsettled, []);
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
test("lookupWebFinger() closes each hop after cleaning up its body", async (t) => {
|
|
2184
|
+
const descriptor = {
|
|
2185
|
+
subject: "acct:johndoe@example.com",
|
|
2186
|
+
links: []
|
|
2187
|
+
};
|
|
2188
|
+
const first = "https://example.com/.well-known/webfinger?resource=acct%3Ajohndoe%40example.com";
|
|
2189
|
+
const second = "https://example.com/.well-known/webfinger2";
|
|
2190
|
+
await t.step("redirect", async () => {
|
|
2191
|
+
const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
|
|
2192
|
+
status: 302,
|
|
2193
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
2194
|
+
body: "redirecting",
|
|
2195
|
+
url: url.href.replace("example.com", "93.184.215.14")
|
|
2196
|
+
} : { body: JSON.stringify(descriptor) });
|
|
2197
|
+
deepStrictEqual(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
|
|
2198
|
+
deepStrictEqual(recorder.events, [
|
|
2199
|
+
`open ${first}`,
|
|
2200
|
+
`fetch ${first} #1`,
|
|
2201
|
+
"cancel #1",
|
|
2202
|
+
`close ${first}`,
|
|
2203
|
+
`open ${second}`,
|
|
2204
|
+
`fetch ${second} #2`,
|
|
2205
|
+
"consumed #2",
|
|
2206
|
+
`close ${second}`
|
|
2207
|
+
]);
|
|
2208
|
+
recorder.assertInvariants();
|
|
2209
|
+
});
|
|
2210
|
+
await t.step("non-2xx response", async () => {
|
|
2211
|
+
const recorder = recordHops(() => ({
|
|
2212
|
+
status: 404,
|
|
2213
|
+
body: "not found"
|
|
2214
|
+
}));
|
|
2215
|
+
deepStrictEqual(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), null);
|
|
2216
|
+
deepStrictEqual(recorder.events, [
|
|
2217
|
+
`open ${first}`,
|
|
2218
|
+
`fetch ${first} #1`,
|
|
2219
|
+
"cancel #1",
|
|
2220
|
+
`close ${first}`
|
|
2221
|
+
]);
|
|
2222
|
+
recorder.assertInvariants();
|
|
2223
|
+
});
|
|
2224
|
+
await t.step("too many redirections", async () => {
|
|
2225
|
+
const recorder = recordHops(() => ({
|
|
2226
|
+
status: 302,
|
|
2227
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
2228
|
+
body: "redirecting"
|
|
2229
|
+
}));
|
|
2230
|
+
deepStrictEqual(await lookupResourceWithDependencies("acct:johndoe@example.com", { maxRedirection: 1 }, { openHop: recorder.openHop }), null);
|
|
2231
|
+
deepStrictEqual(recorder.events, [
|
|
2232
|
+
`open ${first}`,
|
|
2233
|
+
`fetch ${first} #1`,
|
|
2234
|
+
"cancel #1",
|
|
2235
|
+
`close ${first}`,
|
|
2236
|
+
`open ${second}`,
|
|
2237
|
+
`fetch ${second} #2`,
|
|
2238
|
+
"cancel #2",
|
|
2239
|
+
`close ${second}`
|
|
2240
|
+
]);
|
|
2241
|
+
recorder.assertInvariants();
|
|
2242
|
+
});
|
|
2243
|
+
await t.step("failed redirect body cancellation", async () => {
|
|
2244
|
+
const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
|
|
2245
|
+
status: 302,
|
|
2246
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
2247
|
+
body: "redirecting",
|
|
2248
|
+
cancelFails: true
|
|
2249
|
+
} : { body: JSON.stringify(descriptor) });
|
|
2250
|
+
const records = [];
|
|
2251
|
+
await configure({
|
|
2252
|
+
sinks: { buffer: (record) => records.push(record) },
|
|
2253
|
+
loggers: [{
|
|
2254
|
+
category: "fedify",
|
|
2255
|
+
sinks: ["buffer"],
|
|
2256
|
+
lowestLevel: "debug"
|
|
2257
|
+
}, {
|
|
2258
|
+
category: ["logtape", "meta"],
|
|
2259
|
+
sinks: []
|
|
2260
|
+
}],
|
|
2261
|
+
reset: true
|
|
2262
|
+
});
|
|
2263
|
+
try {
|
|
2264
|
+
deepStrictEqual(await lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop }), descriptor);
|
|
2265
|
+
const failures = records.filter((r) => r.rawMessage === "Failed to cancel the response body from {url}: {error}");
|
|
2266
|
+
deepStrictEqual(failures.map((r) => [r.level, r.properties.url]), [["debug", first]]);
|
|
2267
|
+
const { error } = failures[0].properties;
|
|
2268
|
+
ok(error instanceof Error);
|
|
2269
|
+
deepStrictEqual(error.message, "Cancellation failed");
|
|
2270
|
+
} finally {
|
|
2271
|
+
await reset();
|
|
2272
|
+
}
|
|
2273
|
+
deepStrictEqual(recorder.events, [
|
|
2274
|
+
`open ${first}`,
|
|
2275
|
+
`fetch ${first} #1`,
|
|
2276
|
+
"cancel #1",
|
|
2277
|
+
`close ${first}`,
|
|
2278
|
+
`open ${second}`,
|
|
2279
|
+
`fetch ${second} #2`,
|
|
2280
|
+
"consumed #2",
|
|
2281
|
+
`close ${second}`
|
|
2282
|
+
]);
|
|
2283
|
+
recorder.assertInvariants();
|
|
2284
|
+
});
|
|
2285
|
+
await t.step("never-settling body cancellation", async () => {
|
|
2286
|
+
const recorder = recordHops((url) => url.pathname === "/.well-known/webfinger" ? {
|
|
2287
|
+
status: 302,
|
|
2288
|
+
headers: { Location: "/.well-known/webfinger2" },
|
|
2289
|
+
body: "redirecting",
|
|
2290
|
+
cancelNeverSettles: true
|
|
2291
|
+
} : {
|
|
2292
|
+
status: 404,
|
|
2293
|
+
body: "not found",
|
|
2294
|
+
cancelNeverSettles: true
|
|
2295
|
+
});
|
|
2296
|
+
deepStrictEqual(await settleWithin(lookupResourceWithDependencies("acct:johndoe@example.com", {}, { openHop: recorder.openHop })), null);
|
|
2297
|
+
deepStrictEqual(recorder.events, [
|
|
2298
|
+
`open ${first}`,
|
|
2299
|
+
`fetch ${first} #1`,
|
|
2300
|
+
"cancel #1",
|
|
2301
|
+
`close ${first}`,
|
|
2302
|
+
`open ${second}`,
|
|
2303
|
+
`fetch ${second} #2`,
|
|
2304
|
+
"cancel #2",
|
|
2305
|
+
`close ${second}`
|
|
2306
|
+
]);
|
|
2307
|
+
recorder.assertInvariants();
|
|
2308
|
+
});
|
|
2309
|
+
});
|
|
2310
|
+
/**
|
|
2311
|
+
* Rejects if the given promise does not settle within the timeout, so that
|
|
2312
|
+
* a hang fails the test instead of blocking it.
|
|
2313
|
+
*/
|
|
2314
|
+
async function settleWithin(promise, ms = 5e3) {
|
|
2315
|
+
let timer;
|
|
2316
|
+
try {
|
|
2317
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
2318
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Did not settle within ${ms} ms.`)), ms);
|
|
2319
|
+
})]);
|
|
2320
|
+
} finally {
|
|
2321
|
+
clearTimeout(timer);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
function neverCancellableResponse(init) {
|
|
2325
|
+
return new Response(new ReadableStream({
|
|
2326
|
+
start(controller) {
|
|
2327
|
+
controller.enqueue(new TextEncoder().encode("never cancelled"));
|
|
2328
|
+
},
|
|
2329
|
+
cancel: () => new Promise(() => {})
|
|
2330
|
+
}), init);
|
|
2331
|
+
}
|
|
2332
|
+
test("lookupWebFinger() does not wait for cancelling response bodies", async (t) => {
|
|
2333
|
+
const descriptor = {
|
|
2334
|
+
subject: "acct:johndoe@example.com",
|
|
2335
|
+
links: []
|
|
2336
|
+
};
|
|
2337
|
+
esm_default.mockGlobal();
|
|
2338
|
+
try {
|
|
2339
|
+
await t.step("redirect", async () => {
|
|
2340
|
+
esm_default.removeRoutes();
|
|
2341
|
+
esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({
|
|
2342
|
+
status: 302,
|
|
2343
|
+
headers: { Location: "/.well-known/webfinger2" }
|
|
2344
|
+
}));
|
|
2345
|
+
esm_default.get("https://example.com/.well-known/webfinger2", () => new Response(JSON.stringify(descriptor), { headers: { "Content-Type": "application/jrd+json" } }));
|
|
2346
|
+
deepStrictEqual(await settleWithin(lookupDirect("acct:johndoe@example.com")), descriptor);
|
|
2347
|
+
});
|
|
2348
|
+
await t.step("non-2xx response", async () => {
|
|
2349
|
+
esm_default.removeRoutes();
|
|
2350
|
+
esm_default.get("begin:https://example.com/.well-known/webfinger?", () => neverCancellableResponse({ status: 404 }));
|
|
2351
|
+
deepStrictEqual(await settleWithin(lookupDirect("acct:johndoe@example.com")), null);
|
|
2352
|
+
});
|
|
2353
|
+
} finally {
|
|
2354
|
+
esm_default.hardReset();
|
|
2355
|
+
}
|
|
2356
|
+
});
|
|
2357
|
+
test$1("lookupWebFinger() records network_error for rejected hop addresses", async () => {
|
|
2358
|
+
for (const lookup of [
|
|
2359
|
+
() => Promise.reject(/* @__PURE__ */ new Error("DNS unavailable")),
|
|
2360
|
+
() => Promise.resolve([]),
|
|
2361
|
+
() => Promise.resolve([{
|
|
2362
|
+
address: "127.0.0.1",
|
|
2363
|
+
family: 4
|
|
2364
|
+
}])
|
|
2365
|
+
]) {
|
|
2366
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
2367
|
+
deepStrictEqual(await lookupWebFingerWithDependencies("acct:alice@dns-failure.invalid", { meterProvider }, { openHop: (url) => openPublicFetch(url, { lookup }) }), null);
|
|
2368
|
+
const counter = recorder.getMeasurement("webfinger.lookup");
|
|
2369
|
+
ok(counter != null);
|
|
2370
|
+
deepStrictEqual(counter.attributes["webfinger.lookup.result"], "network_error");
|
|
2371
|
+
deepStrictEqual(counter.attributes["activitypub.remote.host"], "dns-failure.invalid");
|
|
2372
|
+
deepStrictEqual("http.response.status_code" in counter.attributes, false);
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2034
2375
|
//#endregion
|
|
2035
2376
|
export {};
|