mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 15:39:01 -07:00
132 lines
3.8 KiB
C#
132 lines
3.8 KiB
C#
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<DateTime> Events = [];
|
|
public DateTime LastCreation = DateTime.MinValue;
|
|
}
|
|
|
|
private static readonly ConcurrentDictionary<string, Counter> ByIp = new(StringComparer.Ordinal);
|
|
private static readonly ConcurrentDictionary<string, Counter> ByDevice = new(StringComparer.Ordinal);
|
|
private static readonly ConcurrentDictionary<string, SemaphoreSlim> 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<IDisposable> LockNameAsync(string name) =>
|
|
LockIdentityAsync("name|" + UsernameRules.ComparisonKey(name));
|
|
|
|
public static async Task<IDisposable> 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 ?? "<none>") + " device=" + (deviceId ?? "<none>");
|
|
}
|
|
}
|