Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,16 @@ To be released.
collections instead of being treated as unknown routes.
[[#849], [#851] by ChanHaeng Lee]

- Fixed split-origin WebFinger responses for `acct:` aliases on the web
Comment thread
dahlia marked this conversation as resolved.
origin host. When a local actor is queried through the server-origin
`acct:` alias, Fedify now returns the canonical handle-host `acct:` URI as
the JRD `subject` and keeps the queried `acct:` URI in `aliases`.
[[#920], [#921]]

[#849]: https://github.com/fedify-dev/fedify/issues/849
[#851]: https://github.com/fedify-dev/fedify/pull/851
[#920]: https://github.com/fedify-dev/fedify/issues/920
[#921]: https://github.com/fedify-dev/fedify/pull/921

### @fedify/cli

Expand Down
78 changes: 73 additions & 5 deletions packages/fedify/src/federation/webfinger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ test("handleWebFinger()", async (t) => {
? "someone"
: username === "bar"
? "someone2"
: username === "qux"
? "someone2"
: null;
};

Expand Down Expand Up @@ -402,22 +404,88 @@ test("handleWebFinger()", async (t) => {
});

await t.step("handleHost", async () => {
const u = new URL(url);
u.searchParams.set("resource", "acct:someone@example.com");
let u = new URL("https://ap.example.com/.well-known/webfinger");
u.searchParams.set("resource", "acct:someone@ap.example.com");
let context = createContext(u);
let request = context.request;
let response = await handleWebFinger(request, {
context,
host: "handle.example.com",
host: "example.com",
actorDispatcher,
onNotFound,
});
assertEquals(response.status, 200);
assertEquals(await response.json(), {
...expected,
aliases: [...expected.aliases, "acct:someone@handle.example.com"],
subject: "acct:someone@example.com",
aliases: [
"https://ap.example.com/users/someone",
"acct:someone@ap.example.com",
],
links: [
{
href: "https://ap.example.com/users/someone",
rel: "self",
type: "application/activity+json",
},
{
href: "https://ap.example.com/@someone",
rel: "http://webfinger.net/rel/profile-page",
},
{
href: "https://ap.example.com/@someone",
rel: "alternate",
type: "text/html",
},
{
href: "https://ap.example.com/icon.jpg",
rel: "http://webfinger.net/rel/avatar",
type: "image/jpeg",
},
],
});

u.searchParams.set("resource", "acct:qux@ap.example.com");
context = createContext(u);
request = context.request;
response = await handleWebFinger(request, {
context,
host: "example.com",
actorDispatcher,
actorHandleMapper,
onNotFound,
});
assertEquals(response.status, 200);
assertEquals(await response.json(), {
subject: "acct:bar@example.com",
aliases: [
"https://ap.example.com/users/someone2",
"acct:qux@ap.example.com",
"acct:bar@ap.example.com",
],
links: [
{
href: "https://ap.example.com/users/someone2",
rel: "self",
type: "application/activity+json",
},
{
href: "https://ap.example.com/@someone2",
rel: "http://webfinger.net/rel/profile-page",
},
{
href: "https://ap.example.com/@someone2",
rel: "alternate",
type: "text/html",
},
{
href: "https://ap.example.com/icon.jpg",
rel: "http://webfinger.net/rel/avatar",
type: "image/jpeg",
},
],
});

u = new URL(url);
u.searchParams.set("resource", "acct:someone@handle.example.com");
context = createContext(u);
request = context.request;
Expand Down
86 changes: 63 additions & 23 deletions packages/fedify/src/federation/webfinger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link as LinkObject } from "@fedify/vocab";
import { type LanguageString, Link as LinkObject } from "@fedify/vocab";
import type { Link, ResourceDescriptor } from "@fedify/webfinger";
import { getLogger } from "@logtape/logtape";
import type { Span, Tracer } from "@opentelemetry/api";
Expand All @@ -14,6 +14,53 @@ import type { RequestContext } from "./context.ts";

const logger = getLogger(["fedify", "webfinger", "server"]);

interface WebFingerSubjectAndAliasesOptions {
resourceUrl: URL;
actorUri: URL;
preferredUsername: string | LanguageString | null | undefined;
acctUsername: string | null;
host: string | undefined;
contextHost: string;
}

function getWebFingerSubjectAndAliases(
{
resourceUrl,
actorUri,
preferredUsername,
acctUsername,
host,
contextHost,
}: WebFingerSubjectAndAliasesOptions,
): Pick<ResourceDescriptor, "subject" | "aliases"> {
const aliases: string[] = [];
let subject = resourceUrl.href;
if (resourceUrl.protocol != "acct:" && preferredUsername != null) {
aliases.push(`acct:${preferredUsername}@${host ?? contextHost}`);
if (host != null && host !== contextHost) {
aliases.push(`acct:${preferredUsername}@${contextHost}`);
}
}
if (resourceUrl.href !== actorUri.href) {
aliases.push(actorUri.href);
}
if (
resourceUrl.protocol === "acct:" && host != null &&
host !== contextHost &&
!resourceUrl.href.endsWith(`@${host}`)
) {
subject = `acct:${preferredUsername ?? acctUsername}@${host}`;
aliases.push(resourceUrl.href);
if (
preferredUsername != null && preferredUsername !== "" &&
preferredUsername !== acctUsername
) {
aliases.push(`acct:${preferredUsername}@${contextHost}`);
}
}
return { subject, aliases };
}

/**
* Parameters for {@link handleWebFinger}.
*/
Expand Down Expand Up @@ -164,6 +211,7 @@ async function handleWebFingerInternal<TContextData>(
}

let identifier: string | null = null;
let acctUsername: string | null = null;
const uriParsed = context.parseUri(resourceUrl);
if (uriParsed?.type != "actor") {
const match = /^acct:([^@]+)@([^@]+)$/.exec(resource);
Expand All @@ -185,8 +233,9 @@ async function handleWebFingerInternal<TContextData>(
if (normalizedHost != context.url.host && normalizedHost != host) {
return await onNotFound(request);
} else {
identifier = await mapUsernameToIdentifier(match[1]);
resourceUrl = new URL(`acct:${match[1]}@${normalizedHost}`);
acctUsername = match[1];
identifier = await mapUsernameToIdentifier(acctUsername);
resourceUrl = new URL(`acct:${acctUsername}@${normalizedHost}`);
}
}
} else {
Expand All @@ -200,10 +249,11 @@ async function handleWebFingerInternal<TContextData>(
logger.error("Actor {identifier} not found.", { identifier });
return await onNotFound(request);
}
const actorUri = context.getActorUri(identifier);
const links: Link[] = [
{
rel: "self",
href: context.getActorUri(identifier).href,
href: actorUri.href,
type: "application/activity+json",
},
];
Expand Down Expand Up @@ -239,26 +289,16 @@ async function handleWebFingerInternal<TContextData>(
}
}

const aliases: string[] = [];
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 { subject, aliases } = getWebFingerSubjectAndAliases({
resourceUrl,
actorUri,
preferredUsername: actor.preferredUsername,
acctUsername,
host,
contextHost: context.url.host,
});
const jrd: ResourceDescriptor = {
subject: resourceUrl.href,
subject,
aliases,
links,
};
Expand Down
Loading