mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
109 lines
2.4 KiB
C#
109 lines
2.4 KiB
C#
using System.Collections.Concurrent;
|
|
|
|
namespace RadicadeServer.Auth;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static class AdminLoginThrottle
|
|
{
|
|
|
|
public const int MaxFailedAttempts = 5;
|
|
|
|
|
|
public static readonly TimeSpan LockoutDuration = TimeSpan.FromMinutes(15);
|
|
|
|
private sealed record Entry(int Failures, DateTime FirstFailureAt, DateTime LockedUntil);
|
|
|
|
private static readonly ConcurrentDictionary<string, Entry> Attempts = new();
|
|
|
|
|
|
|
|
public static bool IsLocked(string username, string ip, out TimeSpan remaining)
|
|
{
|
|
remaining = TimeSpan.Zero;
|
|
var byUser = LockedRemaining("u:" + username);
|
|
var byIp = LockedRemaining("ip:" + ip);
|
|
|
|
if (byUser == null && byIp == null) return false;
|
|
remaining = Max(byUser, byIp)!.Value;
|
|
return true;
|
|
}
|
|
|
|
public static void RecordFailure(string username, string ip)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
foreach (var key in Keys(username, ip))
|
|
{
|
|
Attempts.AddOrUpdate(key,
|
|
_ => new Entry(1, now, DateTime.MinValue),
|
|
(_, existing) =>
|
|
{
|
|
|
|
|
|
if (now - existing.FirstFailureAt > LockoutDuration)
|
|
return new Entry(1, now, DateTime.MinValue);
|
|
|
|
var failures = existing.Failures + 1;
|
|
|
|
|
|
var lockedUntil = failures >= MaxFailedAttempts ? now + LockoutDuration : DateTime.MinValue;
|
|
return new Entry(failures, existing.FirstFailureAt, lockedUntil);
|
|
});
|
|
}
|
|
Prune(now);
|
|
}
|
|
|
|
public static void Clear(string username, string ip)
|
|
{
|
|
foreach (var key in Keys(username, ip))
|
|
Attempts.TryRemove(key, out _);
|
|
}
|
|
|
|
private static TimeSpan? LockedRemaining(string key)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(key)) return null;
|
|
if (!Attempts.TryGetValue(key, out var entry)) return null;
|
|
|
|
|
|
if (entry.Failures < MaxFailedAttempts) return null;
|
|
|
|
var now = DateTime.UtcNow;
|
|
if (now >= entry.LockedUntil)
|
|
{
|
|
Attempts.TryRemove(key, out _);
|
|
return null;
|
|
}
|
|
return entry.LockedUntil - now;
|
|
}
|
|
|
|
private static IEnumerable<string> Keys(string username, string ip)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(username)) yield return "u:" + username;
|
|
if (!string.IsNullOrWhiteSpace(ip)) yield return "ip:" + ip;
|
|
}
|
|
|
|
private static TimeSpan? Max(TimeSpan? a, TimeSpan? b)
|
|
{
|
|
if (a == null) return b;
|
|
if (b == null) return a;
|
|
return a > b ? a : b;
|
|
}
|
|
|
|
private static void Prune(DateTime now)
|
|
{
|
|
|
|
foreach (var kv in Attempts)
|
|
{
|
|
var e = kv.Value;
|
|
var lockActive = e.Failures >= MaxFailedAttempts && now < e.LockedUntil;
|
|
if (!lockActive && now - e.FirstFailureAt > LockoutDuration)
|
|
Attempts.TryRemove(kv.Key, out _);
|
|
}
|
|
}
|
|
}
|