Domain blocks: suspend, silence, reject media
DomainBlock (domain, severity, reject-media, public and private comment)
covers the domain and its subdomains. Admins manage them under
/clientapi/admin/domainblocks/{list,insert,delete}; the set is kept in
memory, reloaded on every change and at most five minutes stale.
A suspended domain is refused by FederationHttp.IsAllowed, so nothing is
fetched from it and no job delivers to it, and the inbox drops its
activities with a 202 before fetching any key. Reject-media strips the
attachments of posts from that domain. Silence is recorded for the
timelines and notifications that arrive in P1.2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
9ec1f9930b
commit
e6a362c0b8
13 files changed
+318
-13
No files matched your search
@@ -2,6 +2,7 @@ using MongoDB.Driver;
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Federation.Moderation;
|
||||
using PrivaPub.Federation.Objects;
|
||||
using PrivaPub.Federation.Outbox;
|
||||
using PrivaPub.Federation.Rendering;
|
||||
@@ -26,13 +27,16 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
readonly ILocalActorService _localActors;
|
||||
readonly IRemoteActorService _remoteActors;
|
||||
readonly IDeliveryService _delivery;
|
||||
readonly IDomainBlocks _domainBlocks;
|
||||
|
||||
public CreateHandler(DbEntities dbEntities, ILocalActorService localActors, IRemoteActorService remoteActors, IDeliveryService delivery)
|
||||
public CreateHandler(DbEntities dbEntities, ILocalActorService localActors, IRemoteActorService remoteActors, IDeliveryService delivery,
|
||||
IDomainBlocks domainBlocks)
|
||||
{
|
||||
_dbEntities = dbEntities;
|
||||
_localActors = localActors;
|
||||
_remoteActors = remoteActors;
|
||||
_delivery = delivery;
|
||||
_domainBlocks = domainBlocks;
|
||||
}
|
||||
|
||||
public string Type => "Create";
|
||||
@@ -130,7 +134,7 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
Language = note.Language,
|
||||
Mentions = mentions,
|
||||
Tags = note.Tags.ToList(),
|
||||
Media = note.Attachments.ToList(),
|
||||
Media = _domainBlocks.Find(new Uri(author.ActorURI).Host)?.RejectMedia == true ? new() : note.Attachments.ToList(),
|
||||
InReplyToURI = note.InReplyTo,
|
||||
AnsweringToPostId = parent?.ID,
|
||||
InReplyToAccountId = parent?.AuthorAccountId ?? parent?.GroupUserId,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Federation.Moderation;
|
||||
using PrivaPub.Federation.Objects;
|
||||
using PrivaPub.Federation.Signing;
|
||||
using PrivaPub.Infrastructure.Jobs;
|
||||
@@ -28,13 +29,16 @@ namespace PrivaPub.Federation.Inbox
|
||||
readonly ILocalActorService _localActors;
|
||||
readonly IRemoteActorService _remoteActors;
|
||||
readonly IJobQueue _queue;
|
||||
readonly IDomainBlocks _domainBlocks;
|
||||
readonly ILogger<InboxReceiver> _logger;
|
||||
|
||||
public InboxReceiver(ILocalActorService localActors, IRemoteActorService remoteActors, IJobQueue queue, ILogger<InboxReceiver> logger)
|
||||
public InboxReceiver(ILocalActorService localActors, IRemoteActorService remoteActors, IJobQueue queue, IDomainBlocks domainBlocks,
|
||||
ILogger<InboxReceiver> logger)
|
||||
{
|
||||
_localActors = localActors;
|
||||
_remoteActors = remoteActors;
|
||||
_queue = queue;
|
||||
_domainBlocks = domainBlocks;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -69,6 +73,8 @@ namespace PrivaPub.Federation.Inbox
|
||||
var parameters = HttpSignatures.Parse(request.Headers["Signature"].ToString());
|
||||
if (parameters == default)
|
||||
return new(StatusCodes.Status401Unauthorized, "missing or unreadable Signature header");
|
||||
if (_domainBlocks.IsSuspended(HostOf(parameters.KeyId)) || _domainBlocks.IsSuspended(HostOf(actorUri)))
|
||||
return new(StatusCodes.Status202Accepted);
|
||||
|
||||
var requestProblem = HttpSignatures.CheckRequest(request, parameters, body);
|
||||
if (requestProblem != default)
|
||||
@@ -99,6 +105,8 @@ namespace PrivaPub.Federation.Inbox
|
||||
return new(StatusCodes.Status202Accepted);
|
||||
}
|
||||
|
||||
static string HostOf(string uri) => Uri.TryCreate(uri, UriKind.Absolute, out var parsed) ? parsed.Host : default;
|
||||
|
||||
async Task<InboxResult> ShapeProblem(string type, JsonNode activity, string actorUri, CancellationToken token)
|
||||
{
|
||||
var activityId = Id(activity);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Models.Federation;
|
||||
|
||||
namespace PrivaPub.Federation.Moderation
|
||||
{
|
||||
public interface IDomainBlocks
|
||||
{
|
||||
DomainBlock Find(string host);
|
||||
bool IsSuspended(string host);
|
||||
Task Reload(CancellationToken token);
|
||||
}
|
||||
|
||||
public class DomainBlocks : IDomainBlocks
|
||||
{
|
||||
static readonly TimeSpan Staleness = TimeSpan.FromMinutes(5);
|
||||
|
||||
readonly ILogger<DomainBlocks> _logger;
|
||||
IReadOnlyDictionary<string, DomainBlock> _blocks = new Dictionary<string, DomainBlock>();
|
||||
DateTime _loadedAt = DateTime.MinValue;
|
||||
int _reloading;
|
||||
|
||||
public DomainBlocks(ILogger<DomainBlocks> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public static string Normalise(string domain) => domain?.Trim().Trim('.').ToLowerInvariant();
|
||||
|
||||
public DomainBlock Find(string host)
|
||||
{
|
||||
RefreshIfStale();
|
||||
host = Normalise(host);
|
||||
var blocks = _blocks;
|
||||
while (!string.IsNullOrEmpty(host))
|
||||
{
|
||||
if (blocks.TryGetValue(host, out var block))
|
||||
return block;
|
||||
var dot = host.IndexOf('.');
|
||||
host = dot < 0 ? default : host[(dot + 1)..];
|
||||
}
|
||||
return default;
|
||||
}
|
||||
|
||||
public bool IsSuspended(string host) => Find(host)?.Severity == DomainBlockSeverity.Suspend;
|
||||
|
||||
public async Task Reload(CancellationToken token)
|
||||
{
|
||||
var blocks = await DB.Default.Find<DomainBlock>().ExecuteAsync(token);
|
||||
_blocks = blocks.Where(b => !string.IsNullOrEmpty(b.Domain))
|
||||
.GroupBy(b => Normalise(b.Domain))
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
_loadedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
void RefreshIfStale()
|
||||
{
|
||||
if (DateTime.UtcNow - _loadedAt < Staleness || Interlocked.Exchange(ref _reloading, 1) == 1)
|
||||
return;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Reload(CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Domain blocks could not be reloaded");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _reloading, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user