using System.Collections.Concurrent; namespace RadicadeServer.Utils { public static class RegistrationGuard { public static readonly TimeSpan Window = TimeSpan.FromMinutes(10); private static int maxCreationsPerIp = 20; private static int maxCreationsPerDevice = 10; private static TimeSpan minimumDeviceInterval = TimeSpan.FromSeconds(10); public static int MaxCreationsPerIp => maxCreationsPerIp; public static int MaxCreationsPerDevice => maxCreationsPerDevice; public static TimeSpan MinimumDeviceInterval => minimumDeviceInterval; public static void Configure(int maxCreationsPerIp, int maxCreationsPerDevice, TimeSpan minimumDeviceInterval) { RegistrationGuard.maxCreationsPerIp = maxCreationsPerIp; RegistrationGuard.maxCreationsPerDevice = maxCreationsPerDevice; RegistrationGuard.minimumDeviceInterval = minimumDeviceInterval; Reset(); } private sealed class Counter { public readonly List Events = []; public DateTime LastCreation = DateTime.MinValue; } private static readonly ConcurrentDictionary ByIp = new(StringComparer.Ordinal); private static readonly ConcurrentDictionary ByDevice = new(StringComparer.Ordinal); private static readonly ConcurrentDictionary IdentityGates = new(StringComparer.Ordinal); public static string? Check(string? ip, string? deviceId) { var now = DateTime.UtcNow; if (!string.IsNullOrWhiteSpace(deviceId)) { var device = ByDevice.GetOrAdd(deviceId, _ => new Counter()); lock (device) { device.Events.RemoveAll(evt => now - evt > Window); if (now - device.LastCreation < MinimumDeviceInterval) return "An account was just created on this device. Please wait a moment and try again."; if (device.Events.Count >= MaxCreationsPerDevice) return "Too many accounts have been created on this device. Please try again later."; } } if (!string.IsNullOrWhiteSpace(ip)) { var source = ByIp.GetOrAdd(ip, _ => new Counter()); lock (source) { source.Events.RemoveAll(evt => now - evt > Window); if (source.Events.Count >= MaxCreationsPerIp) return "Too many accounts have been created from this connection. Please try again later."; } } return null; } public static void Record(string? ip, string? deviceId) { var now = DateTime.UtcNow; if (!string.IsNullOrWhiteSpace(deviceId)) { var device = ByDevice.GetOrAdd(deviceId, _ => new Counter()); lock (device) { device.Events.RemoveAll(evt => now - evt > Window); device.Events.Add(now); device.LastCreation = now; } } if (!string.IsNullOrWhiteSpace(ip)) { var source = ByIp.GetOrAdd(ip, _ => new Counter()); lock (source) { source.Events.RemoveAll(evt => now - evt > Window); source.Events.Add(now); } } } public static string IdentityKey(string platform, string? platformId, string? deviceId) => platform + "|" + (platformId?.Trim() ?? string.Empty) + "|" + (deviceId?.Trim() ?? string.Empty); public static Task LockNameAsync(string name) => LockIdentityAsync("name|" + UsernameRules.ComparisonKey(name)); public static async Task LockIdentityAsync(string key) { var gate = IdentityGates.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); await gate.WaitAsync(); return new Releaser(gate); } private sealed class Releaser(SemaphoreSlim gate) : IDisposable { private int released; public void Dispose() { if (Interlocked.Exchange(ref released, 1) == 0) gate.Release(); } } public static void Reset() { ByIp.Clear(); ByDevice.Clear(); IdentityGates.Clear(); } internal static string Describe(string? ip, string? deviceId) => "ip=" + (ip ?? "") + " device=" + (deviceId ?? ""); } }