mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-28 00:19:10 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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 _);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user