Communities: - a post addressed to a community (to, cc or audience) is accepted according to its posting policy - followers, anyone, or moderators - and GroupDistributor announces the whole activity with `audience` to the community's followers, plus the object for new posts so Mastodon shows them; updates and deletes of community content are announced too; - top-level posts are Pages with a name (the title, or a headline from the text); /flock counts members, /wardens lists moderators; - a Mastodon client posts into a community by mentioning it, or into a remote group, which sets `audience`; - an Announce of an activity from a remote group a persona follows (Lemmy) is followed through: the object is fetched from its own origin, kept with its AudienceURI, and fanned out to the group's local followers; updates are applied in place and deletes checked against the origin. Circles stop being local-only: an undiscoverable Group actor whose follows are all requests the owner approves; posts addressed to the circle and its /flock and delivered to members' own inboxes, never announced, never public; a remote member's post into the circle is accepted from members only. SignedFetchAuthorizer serves circle posts and collections only to a signed request from a member or a member server's instance actor - 404 for anyone else. Circles never surface in search, lookups, mentions, account ids or profile pages. Federation:SecureMode requires a valid signature on every GET under /peasants except the instance actor. Group forms take a posting policy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
85 lines
3.1 KiB
C#
85 lines
3.1 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Http.Features;
|
|
|
|
using PrivaPub.Federation.Signing;
|
|
|
|
using System.Globalization;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace PrivaPub.Tests.Support
|
|
{
|
|
public sealed class RemoteActor
|
|
{
|
|
readonly RSA _key = RSA.Create(2048);
|
|
|
|
public RemoteActor(Peer peer, string name, string origin = default, string type = "Person")
|
|
{
|
|
Name = $"{name}{Guid.NewGuid():N}"[..20];
|
|
Id = $"{origin ?? peer.A}/users/{Name}";
|
|
Type = type;
|
|
peer.Serve($"/users/{Name}", Document().ToJsonString());
|
|
}
|
|
|
|
public string Type { get; }
|
|
|
|
public string Name { get; }
|
|
public string Id { get; }
|
|
public string KeyId => Id + "#main-key";
|
|
public string SharedInbox => Id.Split("/users/")[0] + "/inbox";
|
|
|
|
public JsonObject Document() => new()
|
|
{
|
|
["id"] = Id,
|
|
["type"] = Type,
|
|
["preferredUsername"] = Name,
|
|
["inbox"] = Id + "/inbox",
|
|
["followers"] = Id + "/followers",
|
|
["publicKey"] = new JsonObject
|
|
{
|
|
["id"] = KeyId,
|
|
["owner"] = Id,
|
|
["publicKeyPem"] = _key.ExportSubjectPublicKeyInfoPem()
|
|
}
|
|
};
|
|
|
|
public HttpRequest Get(string host, string path)
|
|
{
|
|
var date = DateTimeOffset.UtcNow.ToString("r", CultureInfo.InvariantCulture);
|
|
var signingString = $"(request-target): get {path}\nhost: {host}\ndate: {date}";
|
|
var signature = Convert.ToBase64String(_key.SignData(Encoding.UTF8.GetBytes(signingString), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
|
|
var context = new DefaultHttpContext();
|
|
context.Request.Method = "GET";
|
|
context.Request.Host = new HostString(host);
|
|
context.Request.Path = path;
|
|
context.Features.Get<IHttpRequestFeature>().RawTarget = path;
|
|
context.Request.Headers["Date"] = date;
|
|
context.Request.Headers["Signature"] = $"keyId=\"{KeyId}\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date\",signature=\"{signature}\"";
|
|
return context.Request;
|
|
}
|
|
|
|
public HttpRequest Post(string host, string path, JsonNode activity)
|
|
{
|
|
var body = Encoding.UTF8.GetBytes(activity.ToJsonString());
|
|
var date = DateTimeOffset.UtcNow.ToString("r", CultureInfo.InvariantCulture);
|
|
var digest = HttpSignatures.Digest(body);
|
|
var signingString = $"(request-target): post {path}\nhost: {host}\ndate: {date}\ndigest: {digest}";
|
|
var signature = Convert.ToBase64String(_key.SignData(Encoding.UTF8.GetBytes(signingString), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
|
|
|
|
var context = new DefaultHttpContext();
|
|
context.Request.Method = "POST";
|
|
context.Request.Host = new HostString(host);
|
|
context.Request.Path = path;
|
|
context.Features.Get<IHttpRequestFeature>().RawTarget = path;
|
|
context.Request.Headers["Date"] = date;
|
|
context.Request.Headers["Digest"] = digest;
|
|
context.Request.Headers["Content-Type"] = "application/activity+json";
|
|
context.Request.Headers["Signature"] = $"keyId=\"{KeyId}\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"{signature}\"";
|
|
context.Request.Body = new MemoryStream(body);
|
|
context.Request.ContentLength = body.Length;
|
|
return context.Request;
|
|
}
|
|
}
|
|
}
|