Skip to content

Commit 261e0dc

Browse files
authored
Merge pull request #19 from aluitink/refactor/activitypub-client-factory
Refactor/activitypub client factory
2 parents 3ae4f67 + 4dab541 commit 261e0dc

18 files changed

Lines changed: 382 additions & 366 deletions

src/Broca.ActivityPub.Client/Extensions/ServiceCollectionExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public static IServiceCollection AddActivityPubClient(
5656
services.TryAddScoped<ActivityPubClient>();
5757
services.TryAddScoped<IActivityPubClient>(sp => sp.GetRequiredService<ActivityPubClient>());
5858

59+
// Register the factory for creating per-actor clients
60+
services.TryAddSingleton<IActivityPubClientFactory, ActivityPubClientFactory>();
61+
5962
return services;
6063
}
6164

src/Broca.ActivityPub.Client/Services/ActivityBuilder.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,13 @@ public INoteBuilder ToFollowers()
443443
return this;
444444
}
445445

446+
public INoteBuilder CcFollowers()
447+
{
448+
var followersUrl = $"{_actorId}/followers";
449+
_cc.Add(new Link { Href = new Uri(followersUrl) });
450+
return this;
451+
}
452+
446453
public INoteBuilder WithMention(string actorId, string name)
447454
{
448455
// Add to recipients
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using Broca.ActivityPub.Core.Interfaces;
2+
using Broca.ActivityPub.Core.Models;
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.Extensions.Options;
5+
6+
namespace Broca.ActivityPub.Client.Services;
7+
8+
public class ActivityPubClientFactory : IActivityPubClientFactory
9+
{
10+
private readonly IHttpClientFactory _httpClientFactory;
11+
private readonly IWebFingerService _webFingerService;
12+
private readonly HttpSignatureService _signatureService;
13+
private readonly ILogger<ActivityPubClient> _clientLogger;
14+
15+
public ActivityPubClientFactory(
16+
IHttpClientFactory httpClientFactory,
17+
IWebFingerService webFingerService,
18+
HttpSignatureService signatureService,
19+
ILogger<ActivityPubClient> clientLogger)
20+
{
21+
_httpClientFactory = httpClientFactory;
22+
_webFingerService = webFingerService;
23+
_signatureService = signatureService;
24+
_clientLogger = clientLogger;
25+
}
26+
27+
public IActivityPubClient CreateAnonymous()
28+
=> new ActivityPubClient(
29+
_httpClientFactory,
30+
_webFingerService,
31+
_signatureService,
32+
Options.Create(new ActivityPubClientOptions()),
33+
_clientLogger);
34+
35+
public IActivityPubClient CreateForActor(string actorId, string publicKeyId, string privateKeyPem)
36+
=> new ActivityPubClient(
37+
_httpClientFactory,
38+
_webFingerService,
39+
_signatureService,
40+
Options.Create(new ActivityPubClientOptions
41+
{
42+
ActorId = actorId,
43+
PublicKeyId = publicKeyId,
44+
PrivateKeyPem = privateKeyPem
45+
}),
46+
_clientLogger);
47+
}

src/Broca.ActivityPub.Components/PostComposer.razor

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,10 +343,10 @@
343343
switch (visibility)
344344
{
345345
case "public":
346-
builder.ToPublic();
346+
builder.ToPublic().CcFollowers();
347347
break;
348348
case "unlisted":
349-
builder.ToPublic(); // Unlisted is still public but not in public timelines
349+
builder.CcFollowers();
350350
break;
351351
case "followers":
352352
builder.ToFollowers();
@@ -370,6 +370,18 @@
370370
}
371371

372372
var activity = builder.Build();
373+
374+
// Debug: Log the activity before sending
375+
var activityJson = System.Text.Json.JsonSerializer.Serialize(activity, new System.Text.Json.JsonSerializerOptions
376+
{
377+
WriteIndented = true,
378+
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
379+
});
380+
Console.WriteLine($"[PostComposer] Activity being sent:\n{activityJson}");
381+
Console.WriteLine($"[PostComposer] Visibility: {visibility}");
382+
Console.WriteLine($"[PostComposer] Activity.To count: {(activity.To?.Count() ?? 0)}");
383+
Console.WriteLine($"[PostComposer] Activity.Cc count: {(activity.Cc?.Count() ?? 0)}");
384+
373385
await Client.PostToOutboxAsync(activity);
374386

375387
successMessage = "Posted successfully!";

src/Broca.ActivityPub.Core/Interfaces/IActivityBuilder.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ public interface INoteBuilder
169169
/// </summary>
170170
INoteBuilder ToFollowers();
171171

172+
/// <summary>
173+
/// Adds the actor's followers to the Cc field
174+
/// </summary>
175+
INoteBuilder CcFollowers();
176+
172177
/// <summary>
173178
/// Adds a tag or mention
174179
/// </summary>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Broca.ActivityPub.Core.Interfaces;
2+
3+
public interface IActivityPubClientFactory
4+
{
5+
IActivityPubClient CreateAnonymous();
6+
IActivityPubClient CreateForActor(string actorId, string publicKeyId, string privateKeyPem);
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Broca.ActivityPub.Core.Interfaces;
2+
3+
public interface IHttpSignatureVerifier
4+
{
5+
Task<bool> VerifyAsync(
6+
IDictionary<string, string> headers,
7+
string publicKeyPem,
8+
CancellationToken cancellationToken = default);
9+
10+
bool VerifyDigest(byte[] bodyBytes, string digestHeader);
11+
12+
string GetSignatureKeyId(string signatureHeader);
13+
}

src/Broca.ActivityPub.Server/Controllers/InboxController.cs

Lines changed: 15 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
using System.Net.Http.Headers;
2-
using System.Net.Http.Json;
31
using System.Text.Json;
4-
using Broca.ActivityPub.Client.Services;
52
using Broca.ActivityPub.Core.Interfaces;
63
using Broca.ActivityPub.Core.Models;
74
using Broca.ActivityPub.Server.Services;
@@ -21,10 +18,8 @@ public class InboxController : ActivityPubControllerBase
2118
private readonly IInboxHandler _inboxHandler;
2219
private readonly IActivityRepository _activityRepository;
2320
private readonly IActorRepository _actorRepository;
24-
private readonly HttpSignatureService _signatureService;
25-
private readonly IActivityPubClient _activityPubClient;
26-
private readonly ISystemIdentityService _systemIdentityService;
27-
private readonly IHttpClientFactory _httpClientFactory;
21+
private readonly IHttpSignatureVerifier _signatureVerifier;
22+
private readonly SignedClientProvider _signedClientProvider;
2823
private readonly AttachmentProcessingService _attachmentProcessingService;
2924
private readonly ObjectEnrichmentService _enrichmentService;
3025
private readonly IMemoryCache _cache;
@@ -37,10 +32,8 @@ public InboxController(
3732
IInboxHandler inboxHandler,
3833
IActivityRepository activityRepository,
3934
IActorRepository actorRepository,
40-
HttpSignatureService signatureService,
41-
IActivityPubClient activityPubClient,
42-
ISystemIdentityService systemIdentityService,
43-
IHttpClientFactory httpClientFactory,
35+
IHttpSignatureVerifier signatureVerifier,
36+
SignedClientProvider signedClientProvider,
4437
AttachmentProcessingService attachmentProcessingService,
4538
ObjectEnrichmentService enrichmentService,
4639
IMemoryCache cache,
@@ -50,10 +43,8 @@ public InboxController(
5043
_inboxHandler = inboxHandler;
5144
_activityRepository = activityRepository;
5245
_actorRepository = actorRepository;
53-
_signatureService = signatureService;
54-
_activityPubClient = activityPubClient;
55-
_systemIdentityService = systemIdentityService;
56-
_httpClientFactory = httpClientFactory;
46+
_signatureVerifier = signatureVerifier;
47+
_signedClientProvider = signedClientProvider;
5748
_attachmentProcessingService = attachmentProcessingService;
5849
_enrichmentService = enrichmentService;
5950
_cache = cache;
@@ -233,7 +224,6 @@ private async Task<bool> VerifySignatureAsync(string body, CancellationToken can
233224
_logger.LogDebug("Starting signature verification. Request headers: {Headers}",
234225
string.Join(", ", Request.Headers.Keys));
235226

236-
// Get Signature header
237227
if (!Request.Headers.TryGetValue("Signature", out var signatureHeader) || string.IsNullOrEmpty(signatureHeader))
238228
{
239229
_logger.LogWarning("Signature header is missing from request");
@@ -242,20 +232,11 @@ private async Task<bool> VerifySignatureAsync(string body, CancellationToken can
242232

243233
_logger.LogDebug("Signature header found: {SignatureHeader}", signatureHeader!);
244234

245-
// Parse the signature to see what headers it expects
246-
var signatureParts = _signatureService.ParseSignatureParts(signatureHeader!);
247-
if (signatureParts.TryGetValue("headers", out var headersInSignature))
248-
{
249-
_logger.LogInformation("Signature expects these headers to be signed: {SignedHeaders}", headersInSignature);
250-
}
251-
252-
// Extract keyId from signature
253-
var keyId = _signatureService.GetSignatureKeyId(signatureHeader!);
235+
var keyId = _signatureVerifier.GetSignatureKeyId(signatureHeader!);
254236
_logger.LogInformation("Extracted keyId from signature: {KeyId}", keyId);
255237

256238
ValidateRequestClockSkew(Request);
257239

258-
// Fetch the actor's public key
259240
var publicKeyPem = await FetchActorPublicKeyAsync(keyId, cancellationToken);
260241

261242
if (string.IsNullOrWhiteSpace(publicKeyPem))
@@ -266,62 +247,39 @@ private async Task<bool> VerifySignatureAsync(string body, CancellationToken can
266247

267248
_logger.LogDebug("Successfully fetched public key for keyId: {KeyId}", keyId);
268249

269-
// Build headers dictionary for verification
270250
var headers = new Dictionary<string, string>();
271-
272-
// Add the Signature header itself (required for verification)
273251
headers["signature"] = signatureHeader!;
274-
275-
// Add (request-target) pseudo-header
276-
var requestTarget = $"{Request.Method.ToLower()} {Request.Path}";
277-
headers["(request-target)"] = requestTarget;
252+
headers["(request-target)"] = $"{Request.Method.ToLower()} {Request.Path}";
278253

279-
// Add all headers from the request (lowercase keys)
280-
// The verification service will use only the ones that are part of the signature
281254
foreach (var header in Request.Headers)
282255
{
283256
var headerName = header.Key.ToLower();
284-
// Don't duplicate the signature header
285257
if (headerName != "signature")
286-
{
287258
headers[headerName] = header.Value.ToString();
288-
}
289259
}
290260

291261
_logger.LogDebug("Headers being verified: {Headers}",
292262
string.Join(", ", headers.Select(h => $"\"{h.Key}\"")));
293263

294-
// Validate Digest header for POST requests (required per ActivityPub spec)
295264
if (Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase))
296265
{
297266
if (!Request.Headers.TryGetValue("Digest", out var digestHeader))
298267
{
299268
_logger.LogWarning("POST request missing Digest header");
300-
// Some implementations may not send Digest, log but don't fail
301269
}
302270
else
303271
{
304-
// Verify the digest matches the body
305272
var bodyBytes = System.Text.Encoding.UTF8.GetBytes(body);
306-
var expectedDigest = _signatureService.ComputeContentDigestHash(bodyBytes);
307-
var digestValue = digestHeader.ToString();
308-
309-
if (digestValue.StartsWith("SHA-256="))
273+
if (!_signatureVerifier.VerifyDigest(bodyBytes, digestHeader.ToString()))
310274
{
311-
var providedDigest = digestValue.Substring(8);
312-
if (providedDigest != expectedDigest)
313-
{
314-
_logger.LogWarning("Digest header mismatch. Expected: {Expected}, Got: {Got}",
315-
expectedDigest, providedDigest);
316-
throw new InvalidOperationException("Digest header does not match request body");
317-
}
275+
_logger.LogWarning("Digest header mismatch for inbox request to keyId: {KeyId}", keyId);
276+
throw new InvalidOperationException("Digest header does not match request body");
318277
}
319278
}
320279
}
321280

322-
// Verify the signature
323-
_logger.LogDebug("Calling HttpSignatureService.VerifyHttpSignatureAsync with {HeaderCount} headers", headers.Count);
324-
var result = await _signatureService.VerifyHttpSignatureAsync(headers, publicKeyPem, cancellationToken);
281+
_logger.LogDebug("Calling IHttpSignatureVerifier.VerifyAsync with {HeaderCount} headers", headers.Count);
282+
var result = await _signatureVerifier.VerifyAsync(headers, publicKeyPem, cancellationToken);
325283
_logger.LogDebug("Signature verification result: {Result}", result);
326284
return result;
327285
}
@@ -422,21 +380,8 @@ private async Task<bool> VerifySignatureAsync(string body, CancellationToken can
422380
{
423381
try
424382
{
425-
var systemActor = await _systemIdentityService.GetSystemActorAsync(cancellationToken);
426-
var privateKey = await _systemIdentityService.GetSystemPrivateKeyAsync(cancellationToken);
427-
var publicKeyId = $"{systemActor.Id}#main-key";
428-
429-
using var httpClient = _httpClientFactory.CreateClient("ActivityPub");
430-
using var response = await _signatureService.SendSignedGetAsync(
431-
httpClient, new Uri(actorUrl), publicKeyId, privateKey, cancellationToken);
432-
433-
if (!response.IsSuccessStatusCode)
434-
{
435-
_logger.LogWarning("Signed GET for actor {ActorUrl} failed with status {StatusCode}", actorUrl, response.StatusCode);
436-
return null;
437-
}
438-
439-
return await response.Content.ReadFromJsonAsync<Actor>(_jsonOptions, cancellationToken);
383+
var client = await _signedClientProvider.CreateForSystemActorAsync(cancellationToken);
384+
return await client.GetActorAsync(new Uri(actorUrl), cancellationToken);
440385
}
441386
catch (Exception ex)
442387
{

src/Broca.ActivityPub.Server/Controllers/OutboxController.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Text.Json;
2-
using Broca.ActivityPub.Client.Services;
32
using Broca.ActivityPub.Core.Interfaces;
43
using Broca.ActivityPub.Core.Models;
54
using Broca.ActivityPub.Server.Services;
@@ -20,7 +19,7 @@ public class OutboxController : ActivityPubControllerBase
2019
private readonly OutboxProcessor _outboxProcessor;
2120
private readonly AttachmentProcessingService _attachmentProcessingService;
2221
private readonly ObjectEnrichmentService _enrichmentService;
23-
private readonly HttpSignatureService _signatureService;
22+
private readonly IHttpSignatureVerifier _signatureVerifier;
2423
private readonly IMemoryCache _cache;
2524
private readonly ActivityPubServerOptions _options;
2625
private readonly ILogger<OutboxController> _logger;
@@ -33,7 +32,7 @@ public OutboxController(
3332
OutboxProcessor outboxProcessor,
3433
AttachmentProcessingService attachmentProcessingService,
3534
ObjectEnrichmentService enrichmentService,
36-
HttpSignatureService signatureService,
35+
IHttpSignatureVerifier signatureVerifier,
3736
IMemoryCache cache,
3837
IOptions<ActivityPubServerOptions> options,
3938
ILogger<OutboxController> logger)
@@ -43,7 +42,7 @@ public OutboxController(
4342
_outboxProcessor = outboxProcessor;
4443
_attachmentProcessingService = attachmentProcessingService;
4544
_enrichmentService = enrichmentService;
46-
_signatureService = signatureService;
45+
_signatureVerifier = signatureVerifier;
4746
_cache = cache;
4847
_options = options.Value;
4948
_logger = logger;
@@ -190,7 +189,7 @@ public async Task<IActionResult> Post(string username)
190189
if (!Request.Headers.TryGetValue("Signature", out var signatureHeader) || string.IsNullOrEmpty(signatureHeader))
191190
return Unauthorized(new { error = "Signature header is missing" });
192191

193-
var keyId = _signatureService.GetSignatureKeyId(signatureHeader!);
192+
var keyId = _signatureVerifier.GetSignatureKeyId(signatureHeader!);
194193

195194
ValidateRequestClockSkew(Request);
196195

@@ -215,13 +214,11 @@ public async Task<IActionResult> Post(string username)
215214
&& Request.Headers.TryGetValue("Digest", out var digestHeader))
216215
{
217216
var bodyBytes = System.Text.Encoding.UTF8.GetBytes(body);
218-
var expectedDigest = _signatureService.ComputeContentDigestHash(bodyBytes);
219-
var digestValue = digestHeader.ToString();
220-
if (digestValue.StartsWith("SHA-256=") && digestValue.Substring(8) != expectedDigest)
217+
if (!_signatureVerifier.VerifyDigest(bodyBytes, digestHeader.ToString()))
221218
throw new InvalidOperationException("Digest header does not match request body");
222219
}
223220

224-
var isValid = await _signatureService.VerifyHttpSignatureAsync(headers, publicKeyPem, cancellationToken);
221+
var isValid = await _signatureVerifier.VerifyAsync(headers, publicKeyPem, cancellationToken);
225222
if (!isValid)
226223
return Unauthorized(new { error = "Invalid signature" });
227224

0 commit comments

Comments
 (0)