Loading CHANGES.md +1 −0 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ To be released. became `Temporal.DurationLike | false` (was `Temporal.DurationLike`). - The type of `VerifyRequestOptions.timeWindow` property became `Temporal.DurationLike | false` (was `Temporal.DurationLike`). - Added `CreateFederationOptions.skipSignatureVerification` property. - Removed the singular actor key pair dispatcher APIs which were deprecated in version 0.10.0. Loading src/federation/handler.test.ts +129 −1 Original line number Diff line number Diff line import { assert, assertEquals, assertFalse } from "@std/assert"; import { createRequestContext } from "../testing/context.ts"; import { mockDocumentLoader } from "../testing/docloader.ts"; import { rsaPublicKey2 } from "../testing/keys.ts"; import { rsaPrivateKey3, rsaPublicKey2, rsaPublicKey3, } from "../testing/keys.ts"; import { test } from "../testing/mod.ts"; import { type Activity, Loading @@ -21,10 +25,13 @@ import { acceptsJsonLd, handleActor, handleCollection, handleInbox, handleObject, respondWithObject, respondWithObjectIfAcceptable, } from "./handler.ts"; import { MemoryKvStore } from "./kv.ts"; import { signRequest } from "../sig/http.ts"; test("acceptsJsonLd()", () => { assert(acceptsJsonLd( Loading Loading @@ -928,6 +935,127 @@ test("handleCollection()", async () => { assertEquals(onUnauthorizedCalled, null); }); test("handleInbox()", async () => { const activity = new Create({ id: new URL("https://example.com/activities/1"), actor: new URL("https://example.com/person2"), object: new Note({ id: new URL("https://example.com/notes/1"), attribution: new URL("https://example.com/person2"), content: "Hello, world!", }), }); const unsignedRequest = new Request("https://example.com/", { method: "POST", body: JSON.stringify(await activity.toJsonLd()), }); const unsignedContext = createRequestContext({ request: unsignedRequest, url: new URL(unsignedRequest.url), data: undefined, }); let onNotFoundCalled: Request | null = null; const onNotFound = (request: Request) => { onNotFoundCalled = request; return new Response("Not found", { status: 404 }); }; const actorDispatcher: ActorDispatcher<void> = (_ctx, handle) => { if (handle !== "someone") return null; return new Person({ name: "Someone" }); }; const inboxOptions = { kv: new MemoryKvStore(), kvPrefixes: { activityIdempotence: ["_fedify", "activityIdempotence"], publicKey: ["_fedify", "publicKey"], }, actorDispatcher, onNotFound, signatureTimeWindow: { minutes: 5 }, skipSignatureVerification: false, } as const; let response = await handleInbox(unsignedRequest, { handle: null, context: unsignedContext, ...inboxOptions, actorDispatcher: undefined, }); assertEquals(onNotFoundCalled, unsignedRequest); assertEquals(response.status, 404); onNotFoundCalled = null; response = await handleInbox(unsignedRequest, { handle: "nobody", context: unsignedContext, ...inboxOptions, }); assertEquals(onNotFoundCalled, unsignedRequest); assertEquals(response.status, 404); onNotFoundCalled = null; response = await handleInbox(unsignedRequest, { handle: null, context: unsignedContext, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 401); response = await handleInbox(unsignedRequest, { handle: "someone", context: unsignedContext, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 401); onNotFoundCalled = null; const signedRequest = await signRequest( unsignedRequest.clone(), rsaPrivateKey3, rsaPublicKey3.id!, ); const signedContext = createRequestContext({ request: signedRequest, url: new URL(signedRequest.url), data: undefined, documentLoader: mockDocumentLoader, }); response = await handleInbox(signedRequest, { handle: null, context: signedContext, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 202); response = await handleInbox(signedRequest, { handle: "someone", context: signedContext, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 202); response = await handleInbox(unsignedRequest, { handle: null, context: unsignedContext, ...inboxOptions, skipSignatureVerification: true, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 202); response = await handleInbox(unsignedRequest, { handle: "someone", context: unsignedContext, ...inboxOptions, skipSignatureVerification: true, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 202); }); test("respondWithObject()", async () => { const response = await respondWithObject( new Note({ Loading src/federation/handler.ts +20 −13 Original line number Diff line number Diff line Loading @@ -329,6 +329,7 @@ export interface InboxHandlerParameters<TContextData> { inboxErrorHandler?: InboxErrorHandler<TContextData>; onNotFound(request: Request): Response | Promise<Response>; signatureTimeWindow: Temporal.DurationLike | false; skipSignatureVerification: boolean; } export async function handleInbox<TContextData>( Loading @@ -344,6 +345,7 @@ export async function handleInbox<TContextData>( inboxErrorHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, }: InboxHandlerParameters<TContextData>, ): Promise<Response> { const logger = getLogger(["fedify", "federation", "inbox"]); Loading Loading @@ -422,6 +424,7 @@ export async function handleInbox<TContextData>( } let httpSigKey: CryptographicKey | null = null; if (activity == null) { if (!skipSignatureVerification) { const key = await verifyRequest(request, { contextLoader: context.contextLoader, documentLoader: context.documentLoader, Loading @@ -430,13 +433,17 @@ export async function handleInbox<TContextData>( }); if (key == null) { logger.error("Failed to verify the request signature.", { handle }); const response = new Response("Failed to verify the request signature.", { const response = new Response( "Failed to verify the request signature.", { status: 401, headers: { "Content-Type": "text/plain; charset=utf-8" }, }); }, ); return response; } httpSigKey = key; } activity = await Activity.fromJsonLd(json, context); } const cacheKey = activity.id == null Loading src/federation/middleware.ts +12 −0 Original line number Diff line number Diff line Loading @@ -139,6 +139,15 @@ export interface CreateFederationOptions { */ signatureTimeWindow?: Temporal.DurationLike | false; /** * Whether to skip HTTP Signatures verification for incoming activities. * This is useful for testing purposes, but should not be used in production. * * By default, this is `false` (i.e., signatures are verified). * @since 0.13.0 */ skipSignatureVerification?: boolean; /** * The retry policy for sending activities to recipients' inboxes. * By default, this uses an exponential backoff strategy with a maximum of Loading Loading @@ -620,6 +629,7 @@ class FederationImpl<TContextData> implements Federation<TContextData> { authenticatedDocumentLoaderFactory: AuthenticatedDocumentLoaderFactory; onOutboxError?: OutboxErrorHandler; signatureTimeWindow: Temporal.DurationLike | false; skipSignatureVerification: boolean; outboxRetryPolicy: RetryPolicy; inboxRetryPolicy: RetryPolicy; Loading Loading @@ -654,6 +664,7 @@ class FederationImpl<TContextData> implements Federation<TContextData> { getAuthenticatedDocumentLoader; this.onOutboxError = options.onOutboxError; this.signatureTimeWindow = options.signatureTimeWindow ?? { minutes: 1 }; this.skipSignatureVerification = options.skipSignatureVerification ?? false; this.outboxRetryPolicy = options.outboxRetryPolicy ?? createExponentialBackoffPolicy(); this.inboxRetryPolicy = options.inboxRetryPolicy ?? Loading Loading @@ -1831,6 +1842,7 @@ class FederationImpl<TContextData> implements Federation<TContextData> { inboxErrorHandler: this.inboxErrorHandler, onNotFound, signatureTimeWindow: this.signatureTimeWindow, skipSignatureVerification: this.skipSignatureVerification, }); case "following": return await handleCollection(request, { Loading Loading
CHANGES.md +1 −0 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ To be released. became `Temporal.DurationLike | false` (was `Temporal.DurationLike`). - The type of `VerifyRequestOptions.timeWindow` property became `Temporal.DurationLike | false` (was `Temporal.DurationLike`). - Added `CreateFederationOptions.skipSignatureVerification` property. - Removed the singular actor key pair dispatcher APIs which were deprecated in version 0.10.0. Loading
src/federation/handler.test.ts +129 −1 Original line number Diff line number Diff line import { assert, assertEquals, assertFalse } from "@std/assert"; import { createRequestContext } from "../testing/context.ts"; import { mockDocumentLoader } from "../testing/docloader.ts"; import { rsaPublicKey2 } from "../testing/keys.ts"; import { rsaPrivateKey3, rsaPublicKey2, rsaPublicKey3, } from "../testing/keys.ts"; import { test } from "../testing/mod.ts"; import { type Activity, Loading @@ -21,10 +25,13 @@ import { acceptsJsonLd, handleActor, handleCollection, handleInbox, handleObject, respondWithObject, respondWithObjectIfAcceptable, } from "./handler.ts"; import { MemoryKvStore } from "./kv.ts"; import { signRequest } from "../sig/http.ts"; test("acceptsJsonLd()", () => { assert(acceptsJsonLd( Loading Loading @@ -928,6 +935,127 @@ test("handleCollection()", async () => { assertEquals(onUnauthorizedCalled, null); }); test("handleInbox()", async () => { const activity = new Create({ id: new URL("https://example.com/activities/1"), actor: new URL("https://example.com/person2"), object: new Note({ id: new URL("https://example.com/notes/1"), attribution: new URL("https://example.com/person2"), content: "Hello, world!", }), }); const unsignedRequest = new Request("https://example.com/", { method: "POST", body: JSON.stringify(await activity.toJsonLd()), }); const unsignedContext = createRequestContext({ request: unsignedRequest, url: new URL(unsignedRequest.url), data: undefined, }); let onNotFoundCalled: Request | null = null; const onNotFound = (request: Request) => { onNotFoundCalled = request; return new Response("Not found", { status: 404 }); }; const actorDispatcher: ActorDispatcher<void> = (_ctx, handle) => { if (handle !== "someone") return null; return new Person({ name: "Someone" }); }; const inboxOptions = { kv: new MemoryKvStore(), kvPrefixes: { activityIdempotence: ["_fedify", "activityIdempotence"], publicKey: ["_fedify", "publicKey"], }, actorDispatcher, onNotFound, signatureTimeWindow: { minutes: 5 }, skipSignatureVerification: false, } as const; let response = await handleInbox(unsignedRequest, { handle: null, context: unsignedContext, ...inboxOptions, actorDispatcher: undefined, }); assertEquals(onNotFoundCalled, unsignedRequest); assertEquals(response.status, 404); onNotFoundCalled = null; response = await handleInbox(unsignedRequest, { handle: "nobody", context: unsignedContext, ...inboxOptions, }); assertEquals(onNotFoundCalled, unsignedRequest); assertEquals(response.status, 404); onNotFoundCalled = null; response = await handleInbox(unsignedRequest, { handle: null, context: unsignedContext, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 401); response = await handleInbox(unsignedRequest, { handle: "someone", context: unsignedContext, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 401); onNotFoundCalled = null; const signedRequest = await signRequest( unsignedRequest.clone(), rsaPrivateKey3, rsaPublicKey3.id!, ); const signedContext = createRequestContext({ request: signedRequest, url: new URL(signedRequest.url), data: undefined, documentLoader: mockDocumentLoader, }); response = await handleInbox(signedRequest, { handle: null, context: signedContext, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 202); response = await handleInbox(signedRequest, { handle: "someone", context: signedContext, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 202); response = await handleInbox(unsignedRequest, { handle: null, context: unsignedContext, ...inboxOptions, skipSignatureVerification: true, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 202); response = await handleInbox(unsignedRequest, { handle: "someone", context: unsignedContext, ...inboxOptions, skipSignatureVerification: true, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 202); }); test("respondWithObject()", async () => { const response = await respondWithObject( new Note({ Loading
src/federation/handler.ts +20 −13 Original line number Diff line number Diff line Loading @@ -329,6 +329,7 @@ export interface InboxHandlerParameters<TContextData> { inboxErrorHandler?: InboxErrorHandler<TContextData>; onNotFound(request: Request): Response | Promise<Response>; signatureTimeWindow: Temporal.DurationLike | false; skipSignatureVerification: boolean; } export async function handleInbox<TContextData>( Loading @@ -344,6 +345,7 @@ export async function handleInbox<TContextData>( inboxErrorHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, }: InboxHandlerParameters<TContextData>, ): Promise<Response> { const logger = getLogger(["fedify", "federation", "inbox"]); Loading Loading @@ -422,6 +424,7 @@ export async function handleInbox<TContextData>( } let httpSigKey: CryptographicKey | null = null; if (activity == null) { if (!skipSignatureVerification) { const key = await verifyRequest(request, { contextLoader: context.contextLoader, documentLoader: context.documentLoader, Loading @@ -430,13 +433,17 @@ export async function handleInbox<TContextData>( }); if (key == null) { logger.error("Failed to verify the request signature.", { handle }); const response = new Response("Failed to verify the request signature.", { const response = new Response( "Failed to verify the request signature.", { status: 401, headers: { "Content-Type": "text/plain; charset=utf-8" }, }); }, ); return response; } httpSigKey = key; } activity = await Activity.fromJsonLd(json, context); } const cacheKey = activity.id == null Loading
src/federation/middleware.ts +12 −0 Original line number Diff line number Diff line Loading @@ -139,6 +139,15 @@ export interface CreateFederationOptions { */ signatureTimeWindow?: Temporal.DurationLike | false; /** * Whether to skip HTTP Signatures verification for incoming activities. * This is useful for testing purposes, but should not be used in production. * * By default, this is `false` (i.e., signatures are verified). * @since 0.13.0 */ skipSignatureVerification?: boolean; /** * The retry policy for sending activities to recipients' inboxes. * By default, this uses an exponential backoff strategy with a maximum of Loading Loading @@ -620,6 +629,7 @@ class FederationImpl<TContextData> implements Federation<TContextData> { authenticatedDocumentLoaderFactory: AuthenticatedDocumentLoaderFactory; onOutboxError?: OutboxErrorHandler; signatureTimeWindow: Temporal.DurationLike | false; skipSignatureVerification: boolean; outboxRetryPolicy: RetryPolicy; inboxRetryPolicy: RetryPolicy; Loading Loading @@ -654,6 +664,7 @@ class FederationImpl<TContextData> implements Federation<TContextData> { getAuthenticatedDocumentLoader; this.onOutboxError = options.onOutboxError; this.signatureTimeWindow = options.signatureTimeWindow ?? { minutes: 1 }; this.skipSignatureVerification = options.skipSignatureVerification ?? false; this.outboxRetryPolicy = options.outboxRetryPolicy ?? createExponentialBackoffPolicy(); this.inboxRetryPolicy = options.inboxRetryPolicy ?? Loading Loading @@ -1831,6 +1842,7 @@ class FederationImpl<TContextData> implements Federation<TContextData> { inboxErrorHandler: this.inboxErrorHandler, onNotFound, signatureTimeWindow: this.signatureTimeWindow, skipSignatureVerification: this.skipSignatureVerification, }); case "following": return await handleCollection(request, { Loading