UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

174 lines (173 loc) • 6.46 kB
import { getLogger } from "@logtape/logtape"; import { SpanKind, SpanStatusCode } from "@opentelemetry/api"; import { domainToASCII } from "node:url"; import { Link as LinkObject } from "../vocab/mod.js"; const logger = getLogger(["fedify", "webfinger", "server"]); /** * Handles a WebFinger request. You would not typically call this function * directly, but instead use {@link Federation.fetch} method. * @param request The WebFinger request to handle. * @param parameters The parameters for handling the request. * @returns The response to the request. */ export async function handleWebFinger(request, options) { if (options.tracer == null) { return await handleWebFingerInternal(request, options); } return await options.tracer.startActiveSpan("webfinger.handle", { kind: SpanKind.SERVER }, async (span) => { try { const response = await handleWebFingerInternal(request, options); span.setStatus({ code: response.ok ? SpanStatusCode.UNSET : SpanStatusCode.ERROR, }); return response; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(error), }); throw error; } finally { span.end(); } }); } async function handleWebFingerInternal(request, { context, host, actorDispatcher, actorHandleMapper, actorAliasMapper, onNotFound, span, }) { if (actorDispatcher == null) return await onNotFound(request); const resource = context.url.searchParams.get("resource"); if (resource == null) { return new Response("Missing resource parameter.", { status: 400 }); } span?.setAttribute("webfinger.resource", resource); let resourceUrl; try { resourceUrl = new URL(resource); } catch (e) { if (e instanceof TypeError) { return new Response("Invalid resource URL.", { status: 400 }); } throw e; } span?.setAttribute("webfinger.resource.scheme", resourceUrl.protocol.replace(/:$/, "")); if (actorDispatcher == null) { logger.error("Actor dispatcher is not set."); return await onNotFound(request); } async function mapUsernameToIdentifier(username) { if (actorHandleMapper == null) { logger.error("No actor handle mapper is set; use the WebFinger username {username}" + " as the actor's internal identifier.", { username }); return username; } const identifier = await actorHandleMapper(context, username); if (identifier == null) { logger.error("Actor {username} not found.", { username }); return null; } return identifier; } let identifier = null; const uriParsed = context.parseUri(resourceUrl); if (uriParsed?.type != "actor") { const match = /^acct:([^@]+)@([^@]+)$/.exec(resource); if (match == null) { const result = await actorAliasMapper?.(context, resourceUrl); if (result == null) return await onNotFound(request); if ("identifier" in result) identifier = result.identifier; else { identifier = await mapUsernameToIdentifier(result.username); } } else { const portMatch = /:\d+$/.exec(match[2]); const normalizedHost = portMatch == null ? domainToASCII(match[2].toLowerCase()) : domainToASCII(match[2].substring(0, portMatch.index).toLowerCase()) + portMatch[0]; if (normalizedHost != context.url.host && normalizedHost != host) { return await onNotFound(request); } else { identifier = await mapUsernameToIdentifier(match[1]); resourceUrl = new URL(`acct:${match[1]}@${normalizedHost}`); } } } else { identifier = uriParsed.identifier; } if (identifier == null) { return await onNotFound(request); } const actor = await actorDispatcher(context, identifier); if (actor == null) { logger.error("Actor {identifier} not found.", { identifier }); return await onNotFound(request); } const links = [ { rel: "self", href: context.getActorUri(identifier).href, type: "application/activity+json", }, ]; for (const url of actor.urls) { if (url instanceof LinkObject && url.href != null) { links.push({ rel: url.rel ?? "http://webfinger.net/rel/profile-page", href: url.href.href, type: url.mediaType == null ? undefined : url.mediaType, }); } else if (url instanceof URL) { links.push({ rel: "http://webfinger.net/rel/profile-page", href: url.href, }); } } for await (const image of actor.getIcons()) { if (image.url?.href == null) continue; const link = { rel: "http://webfinger.net/rel/avatar", href: image.url.href.toString(), }; if (image.mediaType != null) link.type = image.mediaType; links.push(link); } const aliases = []; if (resourceUrl.protocol != "acct:" && actor.preferredUsername != null) { aliases.push(`acct:${actor.preferredUsername}@${host ?? context.url.host}`); if (host != null && host !== context.url.host) { aliases.push(`acct:${actor.preferredUsername}@${context.url.host}`); } } if (resourceUrl.href !== context.getActorUri(identifier).href) { aliases.push(context.getActorUri(identifier).href); } if (resourceUrl.protocol === "acct:" && host != null && host !== context.url.host && !resourceUrl.href.endsWith(`@${host}`)) { const username = resourceUrl.href.replace(/^acct:/, "").replace(/@.*$/, ""); aliases.push(`acct:${username}@${host}`); } const jrd = { subject: resourceUrl.href, aliases, links, }; return new Response(JSON.stringify(jrd), { headers: { "Content-Type": "application/jrd+json", "Access-Control-Allow-Origin": "*", }, }); }