UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

537 lines (536 loc) • 16.4 kB
import { Temporal } from "@js-temporal/polyfill"; import "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { r as createRequestContext } from "../context-Dk_tacqz.mjs"; import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs"; import "../std__assert-CRDpx_HF.mjs"; import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs"; import { o as createFederation, s as handleWebFinger } from "../middleware-D9k0Knum.mjs"; import { test } from "@fedify/fixture"; import { Image, Link, Person, Tombstone } from "@fedify/vocab"; //#region src/federation/webfinger.test.ts test("handleWebFinger()", async (t) => { const url = new URL("https://example.com/.well-known/webfinger"); function createContext(url) { const context = createRequestContext({ federation: createFederation({ kv: new MemoryKvStore() }), url, data: void 0, getActorUri(identifier) { return new URL(`${url.origin}/users/${identifier}`); }, async getActor(handle) { const actor = await actorDispatcher(context, handle); return actor instanceof Tombstone ? null : actor; }, parseUri(uri) { if (uri == null) return null; if (uri.protocol === "acct:") return null; if (!uri.pathname.startsWith("/users/")) return null; const paths = uri.pathname.split("/"); return { type: "actor", identifier: paths[paths.length - 1] }; } }); return context; } const actorDispatcher = (ctx, identifier) => { if (identifier === "gone") return new Tombstone({ id: ctx.getActorUri(identifier), deleted: Temporal.Instant.from("2024-01-15T00:00:00Z") }); if (identifier !== "someone" && identifier !== "someone2") return null; const actorUri = ctx.getActorUri(identifier); return new Person({ id: actorUri, name: identifier === "someone" ? "Someone" : "Someone 2", preferredUsername: identifier === "someone" ? null : identifier === "someone2" ? "bar" : null, icon: new Image({ url: new URL(`${actorUri.origin}/icon.jpg`), mediaType: "image/jpeg" }), urls: [new URL(`${actorUri.origin}/@${identifier}`), new Link({ href: new URL(`${actorUri.origin}/@${identifier}`), rel: "alternate", mediaType: "text/html" })] }); }; let onNotFoundCalled = null; const onNotFound = (request) => { onNotFoundCalled = request; return new Response("Not found", { status: 404 }); }; await t.step("no actor dispatcher", async () => { const context = createContext(url); const request = context.request; assertEquals((await handleWebFinger(request, { context, onNotFound })).status, 404); assertEquals(onNotFoundCalled, request); }); onNotFoundCalled = null; await t.step("no resource", async () => { const context = createContext(url); const request = context.request; const response = await handleWebFinger(request, { context, actorDispatcher, onNotFound }); assertEquals(response.status, 400); assertEquals(await response.text(), "Missing resource parameter."); assertEquals(onNotFoundCalled, null); }); await t.step("invalid resource", async () => { const u = new URL(url); u.searchParams.set("resource", " invalid "); const context = createContext(u); const response = await handleWebFinger(new Request(u), { context, actorDispatcher, onNotFound }); assertEquals(response.status, 400); assertEquals(await response.text(), "Invalid resource URL."); assertEquals(onNotFoundCalled, null); }); const expected = { subject: "acct:someone@example.com", aliases: ["https://example.com/users/someone"], links: [ { href: "https://example.com/users/someone", rel: "self", type: "application/activity+json" }, { href: "https://example.com/@someone", rel: "http://webfinger.net/rel/profile-page" }, { href: "https://example.com/@someone", rel: "alternate", type: "text/html" }, { href: "https://example.com/icon.jpg", rel: "http://webfinger.net/rel/avatar", type: "image/jpeg" } ] }; await t.step("ok: resource=acct:...", async () => { const u = new URL(url); u.searchParams.set("resource", "acct:someone@example.com"); const context = createContext(u); const request = context.request; const response = await handleWebFinger(request, { context, actorDispatcher, onNotFound }); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/jrd+json"); assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*"); assertEquals(await response.json(), expected); }); const expected2 = { subject: "https://example.com/users/someone2", aliases: ["acct:bar@example.com"], links: [ { href: "https://example.com/users/someone2", rel: "self", type: "application/activity+json" }, { href: "https://example.com/@someone2", rel: "http://webfinger.net/rel/profile-page" }, { href: "https://example.com/@someone2", rel: "alternate", type: "text/html" }, { href: "https://example.com/icon.jpg", rel: "http://webfinger.net/rel/avatar", type: "image/jpeg" } ] }; await t.step("ok: resource=https:...", async () => { const u = new URL(url); u.searchParams.set("resource", "https://example.com/users/someone"); let context = createContext(u); let request = context.request; let response = await handleWebFinger(request, { context, actorDispatcher, onNotFound }); assertEquals(response.status, 200); assertEquals(await response.json(), { ...expected, aliases: [], subject: "https://example.com/users/someone" }); u.searchParams.set("resource", "https://example.com/users/someone2"); context = createContext(u); request = context.request; response = await handleWebFinger(request, { context, actorDispatcher, onNotFound }); assertEquals(response.status, 200); assertEquals(await response.json(), expected2); }); await t.step("gone: resource=acct:...", async () => { const u = new URL(url); u.searchParams.set("resource", "acct:gone@example.com"); const context = createContext(u); const request = context.request; const response = await handleWebFinger(request, { context, actorDispatcher, onNotFound }); assertEquals(response.status, 410); assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*"); assertEquals(onNotFoundCalled, null); }); await t.step("gone: resource=https:...", async () => { const u = new URL(url); u.searchParams.set("resource", "https://example.com/users/gone"); const context = createContext(u); const request = context.request; const response = await handleWebFinger(request, { context, actorDispatcher, onNotFound }); assertEquals(response.status, 410); assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*"); assertEquals(onNotFoundCalled, null); }); await t.step("not found: resource=acct:...", async () => { const u = new URL(url); u.searchParams.set("resource", "acct:no-one@example.com"); const context = createContext(u); const request = context.request; assertEquals((await handleWebFinger(request, { context, actorDispatcher, onNotFound })).status, 404); assertEquals(onNotFoundCalled, request); }); onNotFoundCalled = null; await t.step("not found: resource=http:...", async () => { const u = new URL(url); u.searchParams.set("resource", "https://example.com/users/no-one"); let context = createContext(u); let request = context.request; let response = await handleWebFinger(request, { context, actorDispatcher, onNotFound }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, request); onNotFoundCalled = null; u.searchParams.set("resource", "https://google.com/"); context = createContext(u); request = context.request; response = await handleWebFinger(request, { context, actorDispatcher, onNotFound }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, request); }); onNotFoundCalled = null; const actorHandleMapper = (_ctx, username) => { return username === "foo" ? "someone" : username === "bar" ? "someone2" : null; }; await t.step("handle mapper", async () => { const u = new URL(url); u.searchParams.set("resource", "acct:foo@example.com"); let context = createContext(u); let request = context.request; let response = await handleWebFinger(request, { context, actorDispatcher, actorHandleMapper, onNotFound }); assertEquals(response.status, 200); assertEquals(await response.json(), { ...expected, aliases: ["https://example.com/users/someone"], subject: "acct:foo@example.com" }); u.searchParams.set("resource", "acct:bar@example.com"); context = createContext(u); request = context.request; response = await handleWebFinger(request, { context, actorDispatcher, actorHandleMapper, onNotFound }); assertEquals(response.status, 200); assertEquals(await response.json(), { ...expected2, aliases: ["https://example.com/users/someone2"], subject: "acct:bar@example.com" }); u.searchParams.set("resource", "https://example.com/users/someone"); context = createContext(u); request = context.request; response = await handleWebFinger(request, { context, actorDispatcher, actorHandleMapper, onNotFound }); assertEquals(response.status, 200); assertEquals(await response.json(), { ...expected, aliases: [], subject: "https://example.com/users/someone" }); u.searchParams.set("resource", "acct:baz@example.com"); context = createContext(u); request = context.request; response = await handleWebFinger(request, { context, actorDispatcher, actorHandleMapper, onNotFound }); assertEquals(response.status, 404); }); const actorAliasMapper = (_ctx, resource) => { if (resource.protocol !== "https:") return null; if (resource.host !== "example.com") return null; const m = /^\/@(\w+)$/.exec(resource.pathname); if (m == null) return null; return { username: m[1] }; }; await t.step("alias mapper", async () => { const u = new URL(url); u.searchParams.set("resource", "https://example.com/@someone"); let context = createContext(u); let request = context.request; let response = await handleWebFinger(request, { context, actorDispatcher, actorAliasMapper, onNotFound }); assertEquals(response.status, 200); assertEquals(await response.json(), { ...expected, aliases: ["https://example.com/users/someone"], subject: "https://example.com/@someone" }); u.searchParams.set("resource", "https://example.com/@bar"); context = createContext(u); request = context.request; response = await handleWebFinger(request, { context, actorDispatcher, actorHandleMapper, actorAliasMapper, onNotFound }); assertEquals(response.status, 200); assertEquals(await response.json(), { ...expected2, aliases: ["acct:bar@example.com", "https://example.com/users/someone2"], subject: "https://example.com/@bar" }); u.searchParams.set("resource", "https://example.com/@no-one"); context = createContext(u); request = context.request; response = await handleWebFinger(request, { context, actorDispatcher, actorAliasMapper, onNotFound }); assertEquals(response.status, 404); u.searchParams.set("resource", "https://example.com/@no-one"); context = createContext(u); request = context.request; response = await handleWebFinger(request, { context, actorDispatcher, actorHandleMapper, actorAliasMapper, onNotFound }); assertEquals(response.status, 404); }); await t.step("handleHost", async () => { const u = new URL(url); u.searchParams.set("resource", "acct:someone@example.com"); let context = createContext(u); let request = context.request; let response = await handleWebFinger(request, { context, host: "handle.example.com", actorDispatcher, onNotFound }); assertEquals(response.status, 200); assertEquals(await response.json(), { ...expected, aliases: [...expected.aliases, "acct:someone@handle.example.com"] }); u.searchParams.set("resource", "acct:someone@handle.example.com"); context = createContext(u); request = context.request; response = await handleWebFinger(request, { context, host: "handle.example.com", actorDispatcher, onNotFound }); assertEquals(response.status, 200); assertEquals(await response.json(), { ...expected, subject: "acct:someone@handle.example.com" }); u.searchParams.set("resource", "https://example.com/users/someone2"); context = createContext(u); request = context.request; response = await handleWebFinger(request, { context, host: "handle.example.com", actorDispatcher, onNotFound }); assertEquals(response.status, 200); assertEquals(await response.json(), { ...expected2, aliases: ["acct:bar@handle.example.com", "acct:bar@example.com"], subject: "https://example.com/users/someone2" }); }); const expectedForLocalhostWithPort = { subject: "acct:someone@localhost:8000", aliases: ["https://localhost:8000/users/someone"], links: [ { href: "https://localhost:8000/users/someone", rel: "self", type: "application/activity+json" }, { href: "https://localhost:8000/@someone", rel: "http://webfinger.net/rel/profile-page" }, { href: "https://localhost:8000/@someone", rel: "alternate", type: "text/html" }, { href: "https://localhost:8000/icon.jpg", rel: "http://webfinger.net/rel/avatar", type: "image/jpeg" } ] }; await t.step("on localhost with port, ok: resource=acct:...", async () => { const u = new URL("https://localhost:8000/.well-known/webfinger"); u.searchParams.set("resource", "acct:someone@localhost:8000"); const context = createContext(u); const request = context.request; const response = await handleWebFinger(request, { context, actorDispatcher, onNotFound }); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/jrd+json"); assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*"); assertEquals(await response.json(), expectedForLocalhostWithPort); }); const expectedForHostnameWithPort = { subject: "acct:someone@example.com:8000", aliases: ["http://example.com:8000/users/someone"], links: [ { href: "http://example.com:8000/users/someone", rel: "self", type: "application/activity+json" }, { href: "http://example.com:8000/@someone", rel: "http://webfinger.net/rel/profile-page" }, { href: "http://example.com:8000/@someone", rel: "alternate", type: "text/html" }, { href: "http://example.com:8000/icon.jpg", rel: "http://webfinger.net/rel/avatar", type: "image/jpeg" } ] }; await t.step("on hostname with port, ok: resource=acct:...", async () => { const u = new URL("http://example.com:8000/.well-known/webfinger"); u.searchParams.set("resource", "acct:someone@example.com:8000"); const context = createContext(u); const request = context.request; const response = await handleWebFinger(request, { context, actorDispatcher, onNotFound }); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/jrd+json"); assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*"); assertEquals(await response.json(), expectedForHostnameWithPort); }); await t.step("webFingerLinksDispatcher", async () => { const webFingerLinksDispatcher = (_ctx) => { return [{ rel: "http://ostatus.org/schema/1.0/subscribe", template: "https://example.com/follow?acct={uri}" }]; }; const u = new URL(url); u.searchParams.set("resource", "acct:someone@example.com"); const context = createContext(u); const request = context.request; const response = await handleWebFinger(request, { context, actorDispatcher, webFingerLinksDispatcher, onNotFound }); assertEquals(response.status, 200); assertEquals(await response.json(), { ...expected, links: [...expected.links, { rel: "http://ostatus.org/schema/1.0/subscribe", template: "https://example.com/follow?acct={uri}" }] }); }); }); //#endregion export {};