@fedify/fedify
Version:
An ActivityPub server framework
137 lines (136 loc) • 5.18 kB
JavaScript
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;
}
}
}