Skip to content

Commit cd4fc72

Browse files
committed
Refactor: move federation inside runInbox to avoid double signature verification
1 parent 19279a9 commit cd4fc72

1 file changed

Lines changed: 170 additions & 160 deletions

File tree

packages/cli/src/inbox.tsx

Lines changed: 170 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import {
44
type Context,
55
createFederation,
6+
type Federation,
67
generateCryptoKeyPair,
78
MemoryKvStore,
89
verifyRequest,
910
} from "@fedify/fedify";
11+
import type { DocumentLoader } from "@fedify/vocab-runtime";
1012
import {
1113
Accept,
1214
Activity,
@@ -110,7 +112,7 @@ export const inboxCommand = command(
110112
"--authorized-fetch",
111113
{
112114
description:
113-
message`Enable authorized fetch mode. Incoming requests without valid HTTP signatures with 401 Unauthorized will be rejected.`,
115+
message`Enable authorized fetch mode. Incoming requests without valid HTTP signatures will be rejected with 401 Unauthorized.`,
114116
},
115117
),
116118
}),
@@ -123,23 +125,177 @@ export const inboxCommand = command(
123125
},
124126
);
125127

128+
// Module-level state
129+
const activities: ActivityEntry[] = [];
130+
const acceptFollows: string[] = [];
131+
const peers: Record<string, Actor> = {};
132+
const followers: Record<string, Actor> = {};
133+
126134
export async function runInbox(
127135
command: InferValue<typeof inboxCommand>,
128136
) {
129-
const fetch = createFetchHandler({
130-
actorName: command.actorName,
131-
actorSummary: command.actorSummary,
132-
}, command.authorizedFetch);
133-
const sendDeleteToPeers = createSendDeleteToPeers({
134-
actorName: command.actorName,
135-
actorSummary: command.actorSummary,
136-
});
137-
138137
// Enable Debug mode if requested
139138
if (command.debug) {
140139
await configureLogging();
141140
}
142141

142+
// Create federation inside runInbox to configure skipSignatureVerification
143+
const federationDocumentLoader = await getDocumentLoader();
144+
const authorizedFetchEnabled = command.authorizedFetch ?? false;
145+
146+
const federation = createFederation<ContextData>({
147+
kv: new MemoryKvStore(),
148+
documentLoaderFactory: () => federationDocumentLoader,
149+
// When authorizedFetch is enabled, we verify all requests manually,
150+
// so skip federation's inbox signature verification to avoid double verification
151+
skipSignatureVerification: authorizedFetchEnabled,
152+
});
153+
154+
const time = Temporal.Now.instant();
155+
let actorKeyPairs: CryptoKeyPair[] | undefined = undefined;
156+
157+
// Set up actor dispatcher
158+
federation
159+
.setActorDispatcher("/{identifier}", async (ctx, identifier) => {
160+
if (identifier !== "i") return null;
161+
return new Application({
162+
id: ctx.getActorUri(identifier),
163+
preferredUsername: identifier,
164+
name: ctx.data.actorName,
165+
summary: ctx.data.actorSummary,
166+
inbox: ctx.getInboxUri(identifier),
167+
endpoints: new Endpoints({
168+
sharedInbox: ctx.getInboxUri(),
169+
}),
170+
followers: ctx.getFollowersUri(identifier),
171+
following: ctx.getFollowingUri(identifier),
172+
outbox: ctx.getOutboxUri(identifier),
173+
manuallyApprovesFollowers: true,
174+
published: time,
175+
icon: new Image({
176+
url: new URL("https://fedify.dev/logo.png"),
177+
mediaType: "image/png",
178+
}),
179+
publicKey: (await ctx.getActorKeyPairs(identifier))[0].cryptographicKey,
180+
assertionMethods: (await ctx.getActorKeyPairs(identifier))
181+
.map((pair) => pair.multikey),
182+
url: ctx.getActorUri(identifier),
183+
});
184+
})
185+
.setKeyPairsDispatcher(async (_ctxData, identifier) => {
186+
if (identifier !== "i") return [];
187+
if (actorKeyPairs == null) {
188+
actorKeyPairs = [
189+
await generateCryptoKeyPair("RSASSA-PKCS1-v1_5"),
190+
await generateCryptoKeyPair("Ed25519"),
191+
];
192+
}
193+
return actorKeyPairs;
194+
});
195+
196+
// Set up inbox listeners
197+
federation
198+
.setInboxListeners("/{identifier}/inbox", "/inbox")
199+
.setSharedKeyDispatcher((_) => ({ identifier: "i" }))
200+
.on(Activity, async (ctx, activity) => {
201+
activities[ctx.data.activityIndex].activity = activity;
202+
for await (const actor of activity.getActors()) {
203+
if (actor.id != null) peers[actor.id.href] = actor;
204+
}
205+
for await (const actor of activity.getAttributions()) {
206+
if (actor.id != null) peers[actor.id.href] = actor;
207+
}
208+
if (activity instanceof Follow) {
209+
if (acceptFollows.length < 1) return;
210+
const objectId = activity.objectId;
211+
if (objectId == null) return;
212+
const parsed = ctx.parseUri(objectId);
213+
if (parsed?.type !== "actor" || parsed.identifier !== "i") return;
214+
const { identifier } = parsed;
215+
const follower = await activity.getActor();
216+
if (!isActor(follower)) return;
217+
const accepts = await matchesActor(follower, acceptFollows);
218+
if (!accepts || activity.id == null) {
219+
logger.debug("Does not accept follow from {actor}.", {
220+
actor: follower.id?.href,
221+
});
222+
return;
223+
}
224+
logger.debug("Accepting follow from {actor}.", {
225+
actor: follower.id?.href,
226+
});
227+
followers[activity.id.href] = follower;
228+
await ctx.sendActivity(
229+
{ identifier },
230+
follower,
231+
new Accept({
232+
id: new URL(`#accepts/${follower.id?.href}`, ctx.getActorUri("i")),
233+
actor: ctx.getActorUri(identifier),
234+
object: activity.id,
235+
}),
236+
);
237+
}
238+
});
239+
240+
// Set up collection dispatchers
241+
federation
242+
.setFollowersDispatcher("/{identifier}/followers", (_ctx, identifier) => {
243+
if (identifier !== "i") return null;
244+
const items: Recipient[] = [];
245+
for (const follower of Object.values(followers)) {
246+
if (follower.id == null) continue;
247+
items.push(follower);
248+
}
249+
return { items };
250+
})
251+
.setCounter((_ctx, identifier) => {
252+
if (identifier !== "i") return null;
253+
return Object.keys(followers).length;
254+
});
255+
256+
federation
257+
.setFollowingDispatcher(
258+
"/{identifier}/following",
259+
(_ctx, _identifier) => null,
260+
)
261+
.setCounter((_ctx, _identifier) => 0);
262+
263+
federation
264+
.setOutboxDispatcher("/{identifier}/outbox", (_ctx, _identifier) => null)
265+
.setCounter((_ctx, _identifier) => 0);
266+
267+
federation.setNodeInfoDispatcher("/nodeinfo/2.1", (_ctx) => {
268+
return {
269+
software: {
270+
name: "fedify-cli",
271+
version: metadata.version,
272+
repository: new URL("https://github.com/fedify-dev/fedify"),
273+
},
274+
protocols: ["activitypub"],
275+
usage: {
276+
users: {
277+
total: 1,
278+
activeMonth: 1,
279+
activeHalfyear: 1,
280+
},
281+
localComments: 0,
282+
localPosts: 0,
283+
},
284+
};
285+
});
286+
287+
// Create handlers with the configured federation
288+
const fetch = createFetchHandler(
289+
federation,
290+
federationDocumentLoader,
291+
{ actorName: command.actorName, actorSummary: command.actorSummary },
292+
authorizedFetchEnabled,
293+
);
294+
const sendDeleteToPeers = createSendDeleteToPeers(
295+
federation,
296+
{ actorName: command.actorName, actorSummary: command.actorSummary },
297+
);
298+
143299
const spinner = ora({
144300
text: "Spinning up an ephemeral ActivityPub server...",
145301
discardStdin: false,
@@ -212,63 +368,8 @@ export async function runInbox(
212368
printServerInfo(fedCtx);
213369
}
214370

215-
const federationDocumentLoader = await getDocumentLoader();
216-
217-
const federation = createFederation<ContextData>({
218-
kv: new MemoryKvStore(),
219-
documentLoaderFactory: () => {
220-
return federationDocumentLoader;
221-
},
222-
});
223-
224-
const time = Temporal.Now.instant();
225-
let actorKeyPairs: CryptoKeyPair[] | undefined = undefined;
226-
227-
federation
228-
.setActorDispatcher("/{identifier}", async (ctx, identifier) => {
229-
if (identifier !== "i") return null;
230-
return new Application({
231-
id: ctx.getActorUri(identifier),
232-
preferredUsername: identifier,
233-
name: ctx.data.actorName,
234-
summary: ctx.data.actorSummary,
235-
inbox: ctx.getInboxUri(identifier),
236-
endpoints: new Endpoints({
237-
sharedInbox: ctx.getInboxUri(),
238-
}),
239-
followers: ctx.getFollowersUri(identifier),
240-
following: ctx.getFollowingUri(identifier),
241-
outbox: ctx.getOutboxUri(identifier),
242-
manuallyApprovesFollowers: true,
243-
published: time,
244-
icon: new Image({
245-
url: new URL("https://fedify.dev/logo.png"),
246-
mediaType: "image/png",
247-
}),
248-
publicKey: (await ctx.getActorKeyPairs(identifier))[0].cryptographicKey,
249-
assertionMethods: (await ctx.getActorKeyPairs(identifier))
250-
.map((pair) => pair.multikey),
251-
url: ctx.getActorUri(identifier),
252-
});
253-
})
254-
.setKeyPairsDispatcher(async (_ctxData, identifier) => {
255-
if (identifier !== "i") return [];
256-
if (actorKeyPairs == null) {
257-
actorKeyPairs = [
258-
await generateCryptoKeyPair("RSASSA-PKCS1-v1_5"),
259-
await generateCryptoKeyPair("Ed25519"),
260-
];
261-
}
262-
return actorKeyPairs;
263-
});
264-
265-
const activities: ActivityEntry[] = [];
266-
267-
const acceptFollows: string[] = [];
268-
269-
const peers: Record<string, Actor> = {};
270-
271371
function createSendDeleteToPeers(
372+
federation: Federation<ContextData>,
272373
actorOptions: { actorName: string; actorSummary: string },
273374
): (server: TemporaryServer) => Promise<void> {
274375
return async function sendDeleteToPeers(
@@ -301,97 +402,6 @@ function createSendDeleteToPeers(
301402
};
302403
}
303404

304-
const followers: Record<string, Actor> = {};
305-
306-
federation
307-
.setInboxListeners("/{identifier}/inbox", "/inbox")
308-
.setSharedKeyDispatcher((_) => ({ identifier: "i" }))
309-
.on(Activity, async (ctx, activity) => {
310-
activities[ctx.data.activityIndex].activity = activity;
311-
for await (const actor of activity.getActors()) {
312-
if (actor.id != null) peers[actor.id.href] = actor;
313-
}
314-
for await (const actor of activity.getAttributions()) {
315-
if (actor.id != null) peers[actor.id.href] = actor;
316-
}
317-
if (activity instanceof Follow) {
318-
if (acceptFollows.length < 1) return;
319-
const objectId = activity.objectId;
320-
if (objectId == null) return;
321-
const parsed = ctx.parseUri(objectId);
322-
if (parsed?.type !== "actor" || parsed.identifier !== "i") return;
323-
const { identifier } = parsed;
324-
const follower = await activity.getActor();
325-
if (!isActor(follower)) return;
326-
const accepts = await matchesActor(follower, acceptFollows);
327-
if (!accepts || activity.id == null) {
328-
logger.debug("Does not accept follow from {actor}.", {
329-
actor: follower.id?.href,
330-
});
331-
return;
332-
}
333-
logger.debug("Accepting follow from {actor}.", {
334-
actor: follower.id?.href,
335-
});
336-
followers[activity.id.href] = follower;
337-
await ctx.sendActivity(
338-
{ identifier },
339-
follower,
340-
new Accept({
341-
id: new URL(`#accepts/${follower.id?.href}`, ctx.getActorUri("i")),
342-
actor: ctx.getActorUri(identifier),
343-
object: activity.id,
344-
}),
345-
);
346-
}
347-
});
348-
349-
federation
350-
.setFollowersDispatcher("/{identifier}/followers", (_ctx, identifier) => {
351-
if (identifier !== "i") return null;
352-
const items: Recipient[] = [];
353-
for (const follower of Object.values(followers)) {
354-
if (follower.id == null) continue;
355-
items.push(follower);
356-
}
357-
return { items };
358-
})
359-
.setCounter((_ctx, identifier) => {
360-
if (identifier !== "i") return null;
361-
return Object.keys(followers).length;
362-
});
363-
364-
federation
365-
.setFollowingDispatcher(
366-
"/{identifier}/following",
367-
(_ctx, _identifier) => null,
368-
)
369-
.setCounter((_ctx, _identifier) => 0);
370-
371-
federation
372-
.setOutboxDispatcher("/{identifier}/outbox", (_ctx, _identifier) => null)
373-
.setCounter((_ctx, _identifier) => 0);
374-
375-
federation.setNodeInfoDispatcher("/nodeinfo/2.1", (_ctx) => {
376-
return {
377-
software: {
378-
name: "fedify-cli",
379-
version: metadata.version,
380-
repository: new URL("https://github.com/fedify-dev/fedify"),
381-
},
382-
protocols: ["activitypub"],
383-
usage: {
384-
users: {
385-
total: 1,
386-
activeMonth: 1,
387-
activeHalfyear: 1,
388-
},
389-
localComments: 0,
390-
localPosts: 0,
391-
},
392-
};
393-
});
394-
395405
function printServerInfo(fedCtx: Context<ContextData>): void {
396406
const table = new Table({
397407
chars: tableStyle,
@@ -493,6 +503,8 @@ app.get("/r/:idx{[0-9]+}", (c) => {
493503
});
494504

495505
function createFetchHandler(
506+
federation: Federation<ContextData>,
507+
documentLoader: DocumentLoader,
496508
actorOptions: { actorName: string; actorSummary: string },
497509
authorizedFetchEnabled: boolean,
498510
): (request: Request) => Promise<Response> {
@@ -505,9 +517,7 @@ function createFetchHandler(
505517
}
506518

507519
if (authorizedFetchEnabled) {
508-
const key = await verifyRequest(request, {
509-
documentLoader: federationDocumentLoader,
510-
});
520+
const key = await verifyRequest(request, { documentLoader });
511521
if (key == null) {
512522
logger.error(
513523
"Unauthorized request: HTTP Signature verification failed for {method} {path}",

0 commit comments

Comments
 (0)