@fedify/webfinger 2.5.0-dev.2446 → 2.5.0-dev.2492
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist/lookup.test.cjs +599 -227
- package/dist/lookup.test.js +600 -227
- package/dist/mod.cjs +226 -160
- package/dist/mod.d.cts +29 -1
- package/dist/mod.d.ts +30 -2
- package/dist/mod.js +227 -162
- package/package.json +3 -3
- package/src/lookup-impl.ts +338 -0
- package/src/lookup.test.ts +569 -80
- package/src/lookup.ts +34 -231
package/src/lookup.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getUserAgent,
|
|
3
|
-
type GetUserAgentOptions,
|
|
4
|
-
UrlError,
|
|
5
|
-
validatePublicUrl,
|
|
6
|
-
} from "@fedify/vocab-runtime";
|
|
7
|
-
import { getLogger } from "@logtape/logtape";
|
|
1
|
+
import type { GetUserAgentOptions } from "@fedify/vocab-runtime";
|
|
8
2
|
import {
|
|
9
3
|
type Attributes,
|
|
10
4
|
type Counter,
|
|
@@ -16,12 +10,12 @@ import {
|
|
|
16
10
|
type TracerProvider,
|
|
17
11
|
} from "@opentelemetry/api";
|
|
18
12
|
import metadata from "../deno.json" with { type: "json" };
|
|
19
|
-
import { BodyTooLargeError, MAX_BODY_SIZE, readBoundedText } from "./body.ts";
|
|
20
13
|
import type { ResourceDescriptor } from "./jrd.ts";
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
import {
|
|
16
|
+
type LookupWebFingerDependencies,
|
|
17
|
+
lookupWebFingerInternal,
|
|
18
|
+
} from "./lookup-impl.ts";
|
|
25
19
|
|
|
26
20
|
/**
|
|
27
21
|
* The terminal classification of an outgoing {@link lookupWebFinger} call,
|
|
@@ -251,6 +245,34 @@ export async function lookupWebFinger(
|
|
|
251
245
|
export async function lookupWebFinger(
|
|
252
246
|
resource: URL | string,
|
|
253
247
|
options: LookupWebFingerOptions & { withDiagnostics?: boolean } = {},
|
|
248
|
+
): Promise<ResourceDescriptor | null | WebFingerLookupOutcome> {
|
|
249
|
+
return await lookupWebFingerWithDependencies(resource, options);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The lookup implementation with injectable hop sessions for tests.
|
|
254
|
+
* Not exported from the package entry point.
|
|
255
|
+
* @internal
|
|
256
|
+
*/
|
|
257
|
+
export function lookupWebFingerWithDependencies(
|
|
258
|
+
resource: URL | string,
|
|
259
|
+
options?: LookupWebFingerOptions & { withDiagnostics?: false },
|
|
260
|
+
deps?: LookupWebFingerDependencies,
|
|
261
|
+
): Promise<ResourceDescriptor | null>;
|
|
262
|
+
export function lookupWebFingerWithDependencies(
|
|
263
|
+
resource: URL | string,
|
|
264
|
+
options: LookupWebFingerOptions & { withDiagnostics: true },
|
|
265
|
+
deps?: LookupWebFingerDependencies,
|
|
266
|
+
): Promise<WebFingerLookupOutcome>;
|
|
267
|
+
export function lookupWebFingerWithDependencies(
|
|
268
|
+
resource: URL | string,
|
|
269
|
+
options: LookupWebFingerOptions & { withDiagnostics?: boolean },
|
|
270
|
+
deps?: LookupWebFingerDependencies,
|
|
271
|
+
): Promise<ResourceDescriptor | null | WebFingerLookupOutcome>;
|
|
272
|
+
export async function lookupWebFingerWithDependencies(
|
|
273
|
+
resource: URL | string,
|
|
274
|
+
options: LookupWebFingerOptions & { withDiagnostics?: boolean } = {},
|
|
275
|
+
deps: LookupWebFingerDependencies = {},
|
|
254
276
|
): Promise<ResourceDescriptor | null | WebFingerLookupOutcome> {
|
|
255
277
|
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
|
|
256
278
|
const tracer = tracerProvider.getTracer(
|
|
@@ -279,7 +301,7 @@ export async function lookupWebFinger(
|
|
|
279
301
|
result: "error",
|
|
280
302
|
};
|
|
281
303
|
try {
|
|
282
|
-
outcome = await lookupWebFingerInternal(resource, options);
|
|
304
|
+
outcome = await lookupWebFingerInternal(resource, options, deps);
|
|
283
305
|
span.setStatus({
|
|
284
306
|
code: outcome.resource === null
|
|
285
307
|
? SpanStatusCode.ERROR
|
|
@@ -323,222 +345,3 @@ function recordWebFingerLookup(
|
|
|
323
345
|
instruments.lookup.add(1, attributes);
|
|
324
346
|
instruments.lookupDuration.record(durationMs, attributes);
|
|
325
347
|
}
|
|
326
|
-
|
|
327
|
-
async function lookupWebFingerInternal(
|
|
328
|
-
resource: URL | string,
|
|
329
|
-
options: LookupWebFingerOptions = {},
|
|
330
|
-
): Promise<WebFingerLookupOutcome> {
|
|
331
|
-
if (typeof resource === "string") resource = new URL(resource);
|
|
332
|
-
let protocol = "https:";
|
|
333
|
-
let server: string;
|
|
334
|
-
if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
|
|
335
|
-
// `acct:` (RFC 7565) and `mailto:` (RFC 6068, used as a WebFinger
|
|
336
|
-
// resource per RFC 7033 §4.5) are opaque-path schemes: their
|
|
337
|
-
// `user@host` authority lives in `pathname`, not in `host`. The
|
|
338
|
-
// WebFinger host is extracted from the substring after the last
|
|
339
|
-
// `@`, and the lookup always goes to https on that host.
|
|
340
|
-
const atPos = resource.pathname.lastIndexOf("@");
|
|
341
|
-
if (atPos < 0) return { resource: null, result: "invalid" };
|
|
342
|
-
server = resource.pathname.substring(atPos + 1);
|
|
343
|
-
// The authority part of both schemes must be a bare host: no
|
|
344
|
-
// path, query, or fragment characters embedded in it. The
|
|
345
|
-
// WHATWG URL parser routes anything after the first `?` or `#`
|
|
346
|
-
// into `search` / `hash`, so by the time we read `pathname` the
|
|
347
|
-
// only stray characters that can land in `server` are slashes.
|
|
348
|
-
// Reject those (along with an empty authority) for both schemes.
|
|
349
|
-
if (server === "" || /[/?#]/.test(server)) {
|
|
350
|
-
return { resource: null, result: "invalid" };
|
|
351
|
-
}
|
|
352
|
-
// `acct:` (RFC 7565 §3) is bare authority only: no `search` or
|
|
353
|
-
// `hash` allowed. `mailto:` (RFC 6068 §2) explicitly permits
|
|
354
|
-
// `?hfields=…` header fields and fragment identifiers, so we
|
|
355
|
-
// forward those to the remote WebFinger lookup unchanged and
|
|
356
|
-
// only enforce the stricter shape for `acct:`.
|
|
357
|
-
if (
|
|
358
|
-
resource.protocol === "acct:" &&
|
|
359
|
-
(resource.search !== "" || resource.hash !== "")
|
|
360
|
-
) {
|
|
361
|
-
return { resource: null, result: "invalid" };
|
|
362
|
-
}
|
|
363
|
-
} else {
|
|
364
|
-
protocol = resource.protocol;
|
|
365
|
-
server = resource.host;
|
|
366
|
-
}
|
|
367
|
-
let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
|
|
368
|
-
url.searchParams.set("resource", resource.href);
|
|
369
|
-
let redirected = 0;
|
|
370
|
-
while (true) {
|
|
371
|
-
const remoteHost = url.host;
|
|
372
|
-
logger.debug(
|
|
373
|
-
"Fetching WebFinger resource descriptor from {url}...",
|
|
374
|
-
{ url: url.href },
|
|
375
|
-
);
|
|
376
|
-
let response: Response;
|
|
377
|
-
if (options.allowPrivateAddress !== true) {
|
|
378
|
-
try {
|
|
379
|
-
await validatePublicUrl(url.href);
|
|
380
|
-
} catch (e) {
|
|
381
|
-
if (e instanceof UrlError) {
|
|
382
|
-
if (e.reason === "dns") {
|
|
383
|
-
logger.debug("DNS lookup failed for {url}", {
|
|
384
|
-
url: url.href,
|
|
385
|
-
error: e,
|
|
386
|
-
});
|
|
387
|
-
} else {
|
|
388
|
-
logger.error(
|
|
389
|
-
"Invalid URL for WebFinger resource descriptor: {error}",
|
|
390
|
-
{ error: e },
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
return { resource: null, result: "network_error", remoteHost };
|
|
394
|
-
}
|
|
395
|
-
throw e;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
try {
|
|
399
|
-
response = await fetch(url, {
|
|
400
|
-
headers: {
|
|
401
|
-
Accept: "application/jrd+json",
|
|
402
|
-
"User-Agent": typeof options.userAgent === "string"
|
|
403
|
-
? options.userAgent
|
|
404
|
-
: getUserAgent(options.userAgent),
|
|
405
|
-
},
|
|
406
|
-
redirect: "manual",
|
|
407
|
-
signal: options.signal,
|
|
408
|
-
});
|
|
409
|
-
} catch (error) {
|
|
410
|
-
logger.debug(
|
|
411
|
-
"Failed to fetch WebFinger resource descriptor: {error}",
|
|
412
|
-
{ url: url.href, error },
|
|
413
|
-
);
|
|
414
|
-
return { resource: null, result: "network_error", remoteHost };
|
|
415
|
-
}
|
|
416
|
-
if (
|
|
417
|
-
response.status >= 300 && response.status < 400 &&
|
|
418
|
-
response.headers.has("Location")
|
|
419
|
-
) {
|
|
420
|
-
redirected++;
|
|
421
|
-
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
422
|
-
// `maxRedirection: N` is documented as "the maximum number of
|
|
423
|
-
// redirections to follow", so the Nth redirect must still be
|
|
424
|
-
// followed and the (N+1)th rejected. An earlier version used
|
|
425
|
-
// `>=` here, which drifted by one from the documented semantics
|
|
426
|
-
// and from the sibling code in @fedify/vocab-runtime's document
|
|
427
|
-
// loader.
|
|
428
|
-
if (redirected > maxRedirection) {
|
|
429
|
-
logger.error(
|
|
430
|
-
"Too many redirections ({redirections}) while fetching WebFinger " +
|
|
431
|
-
"resource descriptor.",
|
|
432
|
-
{ redirections: redirected },
|
|
433
|
-
);
|
|
434
|
-
return {
|
|
435
|
-
resource: null,
|
|
436
|
-
result: "invalid",
|
|
437
|
-
statusCode: response.status,
|
|
438
|
-
remoteHost,
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
let redirectedUrl: URL;
|
|
442
|
-
try {
|
|
443
|
-
redirectedUrl = new URL(
|
|
444
|
-
response.headers.get("Location")!,
|
|
445
|
-
response.url == null || response.url === "" ? url : response.url,
|
|
446
|
-
);
|
|
447
|
-
} catch (e) {
|
|
448
|
-
logger.error(
|
|
449
|
-
"Invalid Location header while following WebFinger redirect: " +
|
|
450
|
-
"{error}",
|
|
451
|
-
{ url: url.href, error: e },
|
|
452
|
-
);
|
|
453
|
-
return {
|
|
454
|
-
resource: null,
|
|
455
|
-
result: "invalid",
|
|
456
|
-
statusCode: response.status,
|
|
457
|
-
remoteHost,
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
if (redirectedUrl.protocol !== url.protocol) {
|
|
461
|
-
logger.error(
|
|
462
|
-
"Redirected to a different protocol ({protocol} to " +
|
|
463
|
-
"{redirectedProtocol}) while fetching WebFinger resource " +
|
|
464
|
-
"descriptor.",
|
|
465
|
-
{
|
|
466
|
-
protocol: url.protocol,
|
|
467
|
-
redirectedProtocol: redirectedUrl.protocol,
|
|
468
|
-
},
|
|
469
|
-
);
|
|
470
|
-
return {
|
|
471
|
-
resource: null,
|
|
472
|
-
result: "invalid",
|
|
473
|
-
statusCode: response.status,
|
|
474
|
-
remoteHost,
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
url = redirectedUrl;
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
if (!response.ok) {
|
|
481
|
-
logger.debug(
|
|
482
|
-
"Failed to fetch WebFinger resource descriptor: {status} {statusText}.",
|
|
483
|
-
{
|
|
484
|
-
url: url.href,
|
|
485
|
-
status: response.status,
|
|
486
|
-
statusText: response.statusText,
|
|
487
|
-
},
|
|
488
|
-
);
|
|
489
|
-
const isNotFound = response.status === 404 || response.status === 410;
|
|
490
|
-
return {
|
|
491
|
-
resource: null,
|
|
492
|
-
result: isNotFound ? "not_found" : "error",
|
|
493
|
-
statusCode: response.status,
|
|
494
|
-
remoteHost,
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
try {
|
|
498
|
-
const parsed: unknown = JSON.parse(
|
|
499
|
-
await readBoundedText(response, MAX_BODY_SIZE, url),
|
|
500
|
-
);
|
|
501
|
-
// A body that parses to anything other than a JSON object—such as
|
|
502
|
-
// a literal `null`, a number, or an array—is not a resource
|
|
503
|
-
// descriptor, even though JSON.parse() accepts it without error.
|
|
504
|
-
if (
|
|
505
|
-
typeof parsed !== "object" || parsed === null || Array.isArray(parsed)
|
|
506
|
-
) {
|
|
507
|
-
return {
|
|
508
|
-
resource: null,
|
|
509
|
-
result: "invalid",
|
|
510
|
-
statusCode: response.status,
|
|
511
|
-
remoteHost,
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
return {
|
|
515
|
-
resource: parsed as ResourceDescriptor,
|
|
516
|
-
result: "found",
|
|
517
|
-
statusCode: response.status,
|
|
518
|
-
remoteHost,
|
|
519
|
-
};
|
|
520
|
-
} catch (e) {
|
|
521
|
-
if (e instanceof BodyTooLargeError) {
|
|
522
|
-
return {
|
|
523
|
-
resource: null,
|
|
524
|
-
result: "invalid",
|
|
525
|
-
statusCode: response.status,
|
|
526
|
-
remoteHost,
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
if (e instanceof SyntaxError) {
|
|
530
|
-
logger.debug(
|
|
531
|
-
"Failed to parse WebFinger resource descriptor as JSON: {error}",
|
|
532
|
-
{ error: e },
|
|
533
|
-
);
|
|
534
|
-
return {
|
|
535
|
-
resource: null,
|
|
536
|
-
result: "invalid",
|
|
537
|
-
statusCode: response.status,
|
|
538
|
-
remoteHost,
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
throw e;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|