feat(server): harden hostile input and overload behavior (#15)
quality-gate / quality (push) Failing after 1m5s

This commit is contained in:
KyuubiYoru
2026-07-16 12:37:38 +02:00
parent 2ff7cd6d9d
commit 88ef946af5
22 changed files with 2159 additions and 93 deletions
@@ -0,0 +1,80 @@
using FinalFactory.Rendezvous.Contracts;
using Microsoft.AspNetCore.Http.Features;
namespace FinalFactory.Rendezvous.Server.Abuse;
internal sealed class HttpAbuseProtectionMiddleware(
RequestDelegate next,
AbuseProtectionService protection)
{
public async Task InvokeAsync(HttpContext context)
{
IHttpMaxRequestBodySizeFeature? bodySize =
context.Features.Get<IHttpMaxRequestBodySizeFeature>();
if (bodySize is { IsReadOnly: false })
{
bodySize.MaxRequestBodySize = ContractLimits.HttpRequestMaxBytes;
}
string operation = context.GetEndpoint()?.Metadata.GetMetadata<IEndpointNameMetadata>()
?.EndpointName ?? "Unmatched";
bool healthEndpoint = operation is "GetLiveness" or "GetReadiness";
bool acquired = healthEndpoint
? protection.TryAcquireHealthIngress(
context.Connection.RemoteIpAddress,
out AbuseProtectionService.AbuseLease? lease,
out int retryAfterSeconds)
: protection.TryAcquireHttpIngress(
context.Connection.RemoteIpAddress,
operation,
out lease,
out retryAfterSeconds);
if (!acquired)
{
context.Response.Headers.RetryAfter = retryAfterSeconds.ToString(
System.Globalization.CultureInfo.InvariantCulture);
await WriteErrorAsync(
context,
StatusCodes.Status429TooManyRequests,
RendezvousErrorCode.RateLimited,
"The request rate limit was exceeded.",
retryAfterSeconds).ConfigureAwait(false);
return;
}
using (lease)
{
if (context.Request.ContentLength > ContractLimits.HttpRequestMaxBytes)
{
await WriteErrorAsync(
context,
StatusCodes.Status413PayloadTooLarge,
RendezvousErrorCode.InvalidRequest,
"The request body exceeds the supported size.").ConfigureAwait(false);
return;
}
await next(context).ConfigureAwait(false);
}
}
private static Task WriteErrorAsync(
HttpContext context,
int status,
RendezvousErrorCode code,
string message,
int? retryAfterSeconds = null)
{
context.Response.StatusCode = status;
return context.Response.WriteAsJsonAsync(
new ApiError
{
Code = code,
Message = message,
RetryAfterSeconds = retryAfterSeconds,
},
ContractJson.Options,
contentType: "application/json",
cancellationToken: context.RequestAborted);
}
}