@fedify/fedify
Version:
An ActivityPub server framework
131 lines (127 loc) • 4.27 kB
JavaScript
import { Temporal } from "@js-temporal/polyfill";
import { URLPattern } from "urlpattern-polyfill";
globalThis.addEventListener = () => {};
import { deno_default, getUserAgent } from "./docloader-C8mmLN-Y.js";
import { UrlError, validatePublicUrl } from "./url-kTAI6_KP.js";
import { getLogger } from "@logtape/logtape";
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
//#region webfinger/lookup.ts
const logger = getLogger([
"fedify",
"webfinger",
"lookup"
]);
const MAX_REDIRECTION = 5;
/**
* 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
*/
async function lookupWebFinger(resource, options = {}) {
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
const tracer = tracerProvider.getTracer(deno_default.name, deno_default.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;
}
}
}
//#endregion
export { lookupWebFinger };