fix(server): fixes auth errors not logging to elmah
This commit is contained in:
@@ -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}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user