Forwarded activities are believed as far as their origin vouches
A thread's server passes on what happens in it, signed with its own key: Mastodon forwards the replies to its accounts' posts and their deletions, Friendica every activity in its threads. PrivaPub answered them 401, which also tells a sender its signature failed. Now they get 202 and nothing in them is believed: a forwarded Create or Update is taken as its object reads at the actor's origin, a Delete of a public or unlisted copy once that origin answers 404 or 410 (RemoteActorService.IsGone; FederationHttp remembers the status of a refusal), anything else is let go, and our own activities coming back are ignored. A forwarded copy has its own dedupe key, so one that failed never hides the author's own delivery. A reply in the thread of someone followed here is kept, as Mastodon keeps them. Mastodon delivers a reply to the followers of the account it answers; PrivaPub dropped those as unaddressed, which the pasture showed: the outsider's reply its Mastodon scenario said was never delivered had been, and was thrown away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
01808fa644
commit
7eb7c017a5
12 files changed
+334
-21
No files matched your search
@@ -18,8 +18,9 @@ namespace PrivaPub.Federation.Inbox
|
||||
{
|
||||
public sealed record InboxResult(int StatusCode, string Error = default, int? RetryAfterSeconds = default, string Reason = default);
|
||||
|
||||
// ForwardedBy: the server that passed the activity on, signing with its own key (Forwarded)
|
||||
public sealed record InboxPayload(string ActorURI, string Activity, string Inbox = default, string KeyId = default, string Algorithm = default,
|
||||
string[] SignedHeaders = default, DateTime? ReceivedAt = default);
|
||||
string[] SignedHeaders = default, DateTime? ReceivedAt = default, string ForwardedBy = default);
|
||||
|
||||
public interface IInboxReceiver
|
||||
{
|
||||
@@ -100,7 +101,8 @@ namespace PrivaPub.Federation.Inbox
|
||||
Activity = receipt.Type,
|
||||
Object = receipt.ObjectType,
|
||||
Status = result.StatusCode,
|
||||
Outcome = result.StatusCode == StatusCodes.Status202Accepted ? Interactions.Queued : Interactions.Refused,
|
||||
Outcome = result.StatusCode != StatusCodes.Status202Accepted ? Interactions.Refused
|
||||
: result.Reason == "forwarded-ignored" ? Interactions.Dropped : Interactions.Queued,
|
||||
Reason = result.Reason,
|
||||
LatencyMs = latencyMs,
|
||||
Bytes = receipt.Bytes,
|
||||
@@ -171,9 +173,13 @@ namespace PrivaPub.Federation.Inbox
|
||||
return new(StatusCodes.Status401Unauthorized, "the signature does not verify", Reason: "signature-invalid");
|
||||
}
|
||||
|
||||
if (!string.Equals(keyOwner.ActorURI, actorUri, StringComparison.Ordinal))
|
||||
return new(StatusCodes.Status401Unauthorized, "the activity's actor is not the key's owner", Reason: "actor-not-key-owner");
|
||||
receipt.VerifiedActor = actorUri;
|
||||
// signed by someone else: passed on by a server that holds the thread, its signature good (a 401 would tell it
|
||||
// otherwise, and make a double-knocking sender try its other scheme)
|
||||
receipt.VerifiedActor = keyOwner.ActorURI;
|
||||
var forwardedBy = string.Equals(keyOwner.ActorURI, actorUri, StringComparison.Ordinal) ? default : keyOwner.ActorURI;
|
||||
// (ours come back too, Friendica forwarding our comments in its threads: we know them already)
|
||||
if (forwardedBy != default && (!Forwarded.Takeable(type, activity, actorUri) || Origin.Same(actorUri, _localActors.BaseAddress)))
|
||||
return new(StatusCodes.Status202Accepted, Reason: "forwarded-ignored");
|
||||
|
||||
var shapeProblem = await ShapeProblem(type, activity, actorUri, token);
|
||||
if (shapeProblem != default)
|
||||
@@ -181,11 +187,14 @@ namespace PrivaPub.Federation.Inbox
|
||||
|
||||
var activityId = Id(activity);
|
||||
var payload = new InboxPayload(actorUri, activity.ToJsonString(), recipient == default ? "shared" : "personal", signature.KeyId,
|
||||
signature.Algorithm, signature.Covered, DateTime.UtcNow);
|
||||
signature.Algorithm, signature.Covered, DateTime.UtcNow, forwardedBy);
|
||||
// a forwarded copy never stands in for the author's own delivery, which may carry what the copy could not
|
||||
// (a followers-only reply our instance actor cannot read): each is kept once, apart
|
||||
var dedupe = activityId == default ? default : (forwardedBy == default ? "inbox|" : "inbox|forwarded|") + activityId;
|
||||
var queued = await _queue.Enqueue(JobKind.ProcessInbox, JsonSerializer.Serialize(payload),
|
||||
new Uri(actorUri).Host.ToLowerInvariant(), activityId == default ? default : "inbox|" + activityId, token);
|
||||
new Uri(actorUri).Host.ToLowerInvariant(), dedupe, token);
|
||||
_logger.LogInformation("Inbox {Recipient}: {Type} from {Actor} queued", recipient?.Handle ?? "shared", type, actorUri);
|
||||
return new(StatusCodes.Status202Accepted, Reason: queued ? "queued" : "duplicate");
|
||||
return new(StatusCodes.Status202Accepted, Reason: !queued ? "duplicate" : forwardedBy != default ? "forwarded" : "queued");
|
||||
}
|
||||
|
||||
static string HostOf(string uri) => Uri.TryCreate(uri, UriKind.Absolute, out var parsed) ? parsed.Host.ToLowerInvariant() : default;
|
||||
|
||||
Reference in new issue
Block a user