UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

137 lines (136 loc) 5.18 kB
import { getLogger } from "@logtape/logtape"; import { SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api"; import metadata from "../deno.js"; import { getUserAgent, } from "../runtime/docloader.js"; import { UrlError, validatePublicUrl } from "../runtime/url.js"; const logger = getLogger(["fedify", "webfinger", "lookup"]); const MAX_REDIRECTION = 5; // TODO: Make this configurable. /** * Looks up a WebFinger resource. * @param resource The resource URL to look up. * @param options Extra options for looking up the resource. * @returns The resource descriptor, or `null` if not found. * @since 0.2.0 */ export async function lookupWebFinger(resource, options = {}) { const tracerProvider = options.tracerProvider ?? trace.getTracerProvider(); const tracer = tracerProvider.getTracer(metadata.name, metadata.version); return await tracer.startActiveSpan("webfinger.lookup", { kind: SpanKind.CLIENT, attributes: { "webfinger.resource": resource.toString(), "webfinger.resource.scheme": typeof resource === "string" ? resource.replace(/:.*$/, "") : resource.protocol.replace(/:$/, ""), }, }, async (span) => { try { const result = await lookupWebFingerInternal(resource, options); span.setStatus({ code: result === null ? SpanStatusCode.ERROR : SpanStatusCode.OK, }); return result; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(error), }); throw error; } finally { span.end(); } }); } async function lookupWebFingerInternal(resource, options = {}) { if (typeof resource === "string") resource = new URL(resource); let protocol = "https:"; let server; if (resource.protocol === "acct:") { const atPos = resource.pathname.lastIndexOf("@"); if (atPos < 0) return null; server = resource.pathname.substring(atPos + 1); if (server === "") return null; } else { protocol = resource.protocol; server = resource.host; } let url = new URL(`${protocol}//${server}/.well-known/webfinger`); url.searchParams.set("resource", resource.href); let redirected = 0; while (true) { logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href }); let response; if (options.allowPrivateAddress !== true) { try { await validatePublicUrl(url.href); } catch (e) { if (e instanceof UrlError) { logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e }); return null; } throw e; } } try { response = await fetch(url, { headers: { Accept: "application/jrd+json", "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent), }, redirect: "manual", }); } catch (error) { logger.debug("Failed to fetch WebFinger resource descriptor: {error}", { url: url.href, error }); return null; } if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) { redirected++; if (redirected >= MAX_REDIRECTION) { logger.error("Too many redirections ({redirections}) while fetching WebFinger " + "resource descriptor.", { redirections: redirected }); return null; } const redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url); if (redirectedUrl.protocol !== url.protocol) { logger.error("Redirected to a different protocol ({protocol} to " + "{redirectedProtocol}) while fetching WebFinger resource " + "descriptor.", { protocol: url.protocol, redirectedProtocol: redirectedUrl.protocol, }); return null; } url = redirectedUrl; continue; } if (!response.ok) { logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", { url: url.href, status: response.status, statusText: response.statusText, }); return null; } try { return await response.json(); } catch (e) { if (e instanceof SyntaxError) { logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e }); return null; } throw e; } } }