fix(server): fixes auth errors not logging to elmah

This commit is contained in:
2026-07-18 17:26:33 +02:00
parent 9a1048936e
commit ff78aaa6b4
2 changed files with 50 additions and 1 deletions
+40 -1
View File
@@ -1,15 +1,20 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using ElmahCore;
namespace ReverseLlama.Server; namespace ReverseLlama.Server;
internal sealed class AuthRateLimiter internal sealed class AuthRateLimiter
{ {
private const int DecayIntervalMinutes = 144; // ~1 step per 2.4 hours
private readonly ConcurrentDictionary<string, AuthAttemptInfo> _attempts = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, AuthAttemptInfo> _attempts = new(StringComparer.OrdinalIgnoreCase);
private readonly ILogger<AuthRateLimiter> _logger; private readonly ILogger<AuthRateLimiter> _logger;
private readonly ErrorLog _errorLog;
public AuthRateLimiter(ILogger<AuthRateLimiter> logger) public AuthRateLimiter(ILogger<AuthRateLimiter> logger, ErrorLog errorLog)
{ {
_logger = logger; _logger = logger;
_errorLog = errorLog;
} }
public void RecordFailure(string ipAddress, string endpoint) public void RecordFailure(string ipAddress, string endpoint)
@@ -34,6 +39,27 @@ internal sealed class AuthRateLimiter
"Failed auth attempt #{Count} from {IpAddress} on {Endpoint}", "Failed auth attempt #{Count} from {IpAddress} on {Endpoint}",
info.Count, ipAddress, 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); 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); var waitTime = CalculateWaitTime(info.Count);
if (waitTime is { } wait) if (waitTime is { } wait)
{ {
@@ -113,4 +149,7 @@ internal sealed class AuthRateLimiter
public DateTime LastAttemptUtc; public DateTime LastAttemptUtc;
public DateTime? BlockedUntilUtc; public DateTime? BlockedUntilUtc;
} }
private sealed class AuthFailureException(string ipAddress, string endpoint, int attemptCount)
: Exception($"Failed auth attempt #{attemptCount} from {ipAddress} on {endpoint}");
} }
+10
View File
@@ -155,6 +155,16 @@ app.Use(async (context, next) =>
{ {
rateLimiter.RecordFailure(ip, context.Request.Path); 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; return Task.CompletedTask;