From ff78aaa6b40e212e3cde3f89fb48aec223f5a9ed Mon Sep 17 00:00:00 2001 From: LD-Reborn Date: Sat, 18 Jul 2026 17:26:33 +0200 Subject: [PATCH] fix(server): fixes auth errors not logging to elmah --- src/ReverseLlama.Server/AuthRateLimiter.cs | 41 +++++++++++++++++++++- src/ReverseLlama.Server/Program.cs | 10 ++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/ReverseLlama.Server/AuthRateLimiter.cs b/src/ReverseLlama.Server/AuthRateLimiter.cs index 54a6401..3c1e6ca 100644 --- a/src/ReverseLlama.Server/AuthRateLimiter.cs +++ b/src/ReverseLlama.Server/AuthRateLimiter.cs @@ -1,15 +1,20 @@ using System.Collections.Concurrent; +using ElmahCore; namespace ReverseLlama.Server; internal sealed class AuthRateLimiter { + private const int DecayIntervalMinutes = 144; // ~1 step per 2.4 hours + private readonly ConcurrentDictionary _attempts = new(StringComparer.OrdinalIgnoreCase); private readonly ILogger _logger; + private readonly ErrorLog _errorLog; - public AuthRateLimiter(ILogger logger) + public AuthRateLimiter(ILogger logger, ErrorLog errorLog) { _logger = logger; + _errorLog = errorLog; } public void RecordFailure(string ipAddress, string endpoint) @@ -34,6 +39,27 @@ internal sealed class AuthRateLimiter "Failed auth attempt #{Count} from {IpAddress} on {Endpoint}", info.Count, ipAddress, endpoint); } + + _errorLog.Log(new Error(new AuthFailureException(ipAddress, endpoint, info.Count))); + } + } + + public void RecordSuccess(string ipAddress) + { + if (!_attempts.TryGetValue(ipAddress, out var info)) + return; + + lock (info) + { + if (info.Count > 0) + { + var before = info.Count; + info.Count /= 2; + info.LastAttemptUtc = DateTime.UtcNow; + _logger.LogInformation( + "Auth success from {IpAddress}: count reduced from {Before} to {After}", + ipAddress, before, info.Count); + } } } @@ -59,6 +85,16 @@ internal sealed class AuthRateLimiter return (true, null, false); } + if (info.Count > 0) + { + var elapsed = DateTime.UtcNow - info.LastAttemptUtc; + var decayTicks = (int)(elapsed.TotalMinutes / DecayIntervalMinutes); + if (decayTicks > 0) + { + info.Count = Math.Max(0, info.Count - decayTicks); + } + } + var waitTime = CalculateWaitTime(info.Count); if (waitTime is { } wait) { @@ -113,4 +149,7 @@ internal sealed class AuthRateLimiter public DateTime LastAttemptUtc; public DateTime? BlockedUntilUtc; } + + private sealed class AuthFailureException(string ipAddress, string endpoint, int attemptCount) + : Exception($"Failed auth attempt #{attemptCount} from {ipAddress} on {endpoint}"); } diff --git a/src/ReverseLlama.Server/Program.cs b/src/ReverseLlama.Server/Program.cs index 1f3d061..0c552ca 100644 --- a/src/ReverseLlama.Server/Program.cs +++ b/src/ReverseLlama.Server/Program.cs @@ -155,6 +155,16 @@ app.Use(async (context, next) => { rateLimiter.RecordFailure(ip, context.Request.Path); } + else if (location is not null + && location.StartsWith("/admin", StringComparison.OrdinalIgnoreCase)) + { + rateLimiter.RecordSuccess(ip); + } + } + else if (context.Response.StatusCode is >= 200 and < 300 + && context.Request.Path.StartsWithSegments("/api/admin")) + { + rateLimiter.RecordSuccess(ip); } return Task.CompletedTask;