namespace Supercell.Laser.Server.SCID { using System.Collections.Concurrent; using System.Net; using Microsoft.AspNetCore.Http; internal static class SCIDRateLimiter { private static readonly ConcurrentDictionary> Requests = new(); private static readonly object Sync = new(); public static bool AllowCodeRequest(HttpRequest request, string identity) { string address = GetAddress(request); bool allowed = Allow($"identity:{identity}", 1, TimeSpan.FromMinutes(1)) && Allow($"identity-hour:{identity}", 5, TimeSpan.FromHours(1)) && Allow($"address-hour:{address}", 20, TimeSpan.FromHours(1)); SCIDStart.Log($"rate limit check address={address} allowed={allowed}"); return allowed; } // A failed delivery must not lock the user out for a minute. Otherwise // every retry is rejected before the mail provider is called and the // client only shows its generic "temporarily unavailable" message. public static void ResetIdentity(string identity) { lock (Sync) { Requests.TryRemove($"identity:{identity}", out _); } } private static bool Allow(string key, int limit, TimeSpan period) { lock (Sync) { DateTime now = DateTime.UtcNow; Queue requests = Requests.GetOrAdd(key, _ => new Queue()); while (requests.Count > 0 && now - requests.Peek() >= period) requests.Dequeue(); if (requests.Count >= limit) return false; requests.Enqueue(now); return true; } } private static string GetAddress(HttpRequest request) { if (request == null) return "unknown"; string forwardedAddress = request.Headers["X-Forwarded-For"].FirstOrDefault()?.Split(',')[0].Trim(); string value = forwardedAddress ?? request.HttpContext.Connection.RemoteIpAddress?.ToString(); return IPAddress.TryParse(value, out IPAddress address) ? address.ToString() : value ?? "unknown"; } } }