Sharkey 2025.4.7 is the Misskey peer under another name, image, database, Redis db and home. Its scenario is Misskey's plus edits both ways and the FEP-e232 quote tag: 40 checks pass. Akkoma 3.20.1 publishes no image, so tools/pasture/images/akkoma installs its OTP release, pinned by checksum, and appends Caddy's CA to the CA bundles the release ships. Its scenario has 44 checks, which pass three runs in a row: - follows, posts, CW, followers-only posts and the published time; - replies, likes, boosts and their undos; - EmojiReact both ways and withdrawn; - DMs, polls, quotes, media, edits with history and deletes, both ways; - unfollow, block, unblock and statistics. The two bugs, both fixed: - Followers-only posts arrived as DMs on Pleroma and Akkoma. They call a post private only if an address in `to` contains "/followers" or its cc is not empty. Ours is /groupies, and a post mentioning nobody had an empty cc. The followers collection is now named in cc as well, which tells nobody anything new. - Akkoma's open polls showed as ended and refused votes. Akkoma carries an open poll's end in `closed` and sends no `endTime`. A `closed` in the future is now read as the end. Neither of these is a PrivaPub bug: - Akkoma's Linkify never takes @user@host.test for a mention, so its DM addresses alice with to[]. - Its API never reports a remote blocker as blocked_by, so the block is read from its database. Also in this commit: - The rate limits are configuration (RateLimits: AccountsPerMinute, InboxBurst, InboxPerTenSeconds), with the old values as defaults. The pasture raises the accounts limit, which back-to-back runs from one address had hit. - A new Lemmy never sends what it queued for a server before it started that server's send worker, so the scenario waits for the worker before its first follow. All six peers in one clean pass: GoToSocial 54 (+1 expected), Mastodon 49 (+2), Misskey 35, Sharkey 40, Akkoma 44, and Lemmy 20 (+3) once the worker wait was added. 642 tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
71 lines
2.9 KiB
C#
71 lines
2.9 KiB
C#
using Microsoft.AspNetCore.RateLimiting;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
using PrivaPub.Federation.Objects;
|
|
using PrivaPub.Federation.Signing;
|
|
using PrivaPub.Infrastructure.Statistics;
|
|
using PrivaPub.Models.Statistics;
|
|
|
|
using System.Threading.RateLimiting;
|
|
|
|
namespace PrivaPub.Infrastructure
|
|
{
|
|
public class RateLimitOptions
|
|
{
|
|
public int AccountsPerMinute { get; set; } = 10;//sign-ups, sign-ins and recoveries per client address
|
|
public int InboxBurst { get; set; } = 300;//deliveries a sending origin may make at once
|
|
public int InboxPerTenSeconds { get; set; } = 50;//and the rate it earns them back
|
|
}
|
|
|
|
public static class RateLimiting
|
|
{
|
|
public const string Accounts = "accounts";
|
|
public const string Inbox = "inbox";
|
|
|
|
static RateLimitOptions Limits(HttpContext context) => context.RequestServices.GetRequiredService<IOptions<RateLimitOptions>>().Value;
|
|
|
|
public static IServiceCollection PrivaPubRateLimiting(this IServiceCollection service, IConfiguration configuration) =>
|
|
service.Configure<RateLimitOptions>(configuration.GetSection("RateLimits")).AddRateLimiter(options =>
|
|
{
|
|
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
|
options.OnRejected = (context, _) =>
|
|
{
|
|
var http = context.HttpContext;
|
|
var ledger = http.RequestServices.GetService<IInteractionLedger>();
|
|
var policy = http.GetEndpoint()?.Metadata.GetMetadata<EnableRateLimitingAttribute>()?.PolicyName;
|
|
if (policy == Inbox)
|
|
ledger?.Record(new InteractionEvent
|
|
{
|
|
Channel = Interactions.Receive,
|
|
Host = Interactions.HostOf(SenderOrigin(http.Request)),
|
|
Status = StatusCodes.Status429TooManyRequests,
|
|
Outcome = Interactions.Refused,
|
|
Reason = "rate-limited",
|
|
Inbox = http.Request.Path.Value?.EndsWith("/mouth", StringComparison.Ordinal) == true ? "personal" : "shared"
|
|
}, hostClaimed: true);
|
|
else
|
|
ledger?.CountServer(ServerSections.Client, $"{policy ?? "unknown"}:429");
|
|
return ValueTask.CompletedTask;
|
|
};
|
|
options.AddPolicy(Accounts, context => RateLimitPartition.GetFixedWindowLimiter(
|
|
context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
|
_ => new FixedWindowRateLimiterOptions { PermitLimit = Limits(context).AccountsPerMinute, Window = TimeSpan.FromMinutes(1), QueueLimit = 0 }));
|
|
options.AddPolicy(Inbox, context => RateLimitPartition.GetTokenBucketLimiter(
|
|
SenderOrigin(context.Request) ?? "unsigned:" + context.Connection.RemoteIpAddress,
|
|
_ => new TokenBucketRateLimiterOptions
|
|
{
|
|
TokenLimit = Limits(context).InboxBurst,
|
|
TokensPerPeriod = Limits(context).InboxPerTenSeconds,
|
|
ReplenishmentPeriod = TimeSpan.FromSeconds(10),
|
|
QueueLimit = 0
|
|
}));
|
|
});
|
|
|
|
static string SenderOrigin(HttpRequest request)
|
|
{
|
|
var signature = request.Headers["Signature"].ToString();
|
|
return string.IsNullOrEmpty(signature) ? default : Origin.Of(HttpSignatures.Parse(signature)?.KeyId);
|
|
}
|
|
}
|
|
}
|