diff --git a/src/AspNetCoreRateLimit/Core/RateLimitProcessor.cs b/src/AspNetCoreRateLimit/Core/RateLimitProcessor.cs index 9ce4ff1d..c140aa8b 100644 --- a/src/AspNetCoreRateLimit/Core/RateLimitProcessor.cs +++ b/src/AspNetCoreRateLimit/Core/RateLimitProcessor.cs @@ -129,8 +129,12 @@ protected virtual List GetMatchingRules(ClientRequestIdentity ide foreach (var generalLimit in generalLimits) { - // add general rule if no specific rule is declared for the specified period - if (!limits.Exists(l => l.Period == generalLimit.Period)) + + if (// add general rule if no specific rule is declared when DiscardEndpointGeneralRulesWhenExistsIPOrClienIdRule is enabled + (_options.DiscardEndpointGeneralRulesWhenExistsIPOrClienIdRule && !limits.Any()) + || + // add general rule if no specific rule is declared for the specified period when DiscardEndpointGeneralRulesWhenExistsIPOrClienIdRule is disabled + (!_options.DiscardEndpointGeneralRulesWhenExistsIPOrClienIdRule && !limits.Exists(l => l.Period == generalLimit.Period))) { limits.Add(generalLimit); } diff --git a/src/AspNetCoreRateLimit/Middleware/RateLimitMiddleware.cs b/src/AspNetCoreRateLimit/Middleware/RateLimitMiddleware.cs index 3b6f63fc..32885c66 100644 --- a/src/AspNetCoreRateLimit/Middleware/RateLimitMiddleware.cs +++ b/src/AspNetCoreRateLimit/Middleware/RateLimitMiddleware.cs @@ -183,7 +183,12 @@ public virtual Task ReturnQuotaExceededResponse(HttpContext httpContext, RateLim { httpContext.Response.Headers["Retry-After"] = retryAfter; } - + if (rule.Limit == 0 && _options.HttpStatusCodeForLimitZeroRequests.HasValue) + { + httpContext.Response.StatusCode = _options.HttpStatusCodeForLimitZeroRequests.Value; + httpContext.Response.ContentType = "text/plain"; + return httpContext.Response.WriteAsync(""); + } httpContext.Response.StatusCode = _options.QuotaExceededResponse?.StatusCode ?? _options.HttpStatusCode; httpContext.Response.ContentType = _options.QuotaExceededResponse?.ContentType ?? "text/plain"; diff --git a/src/AspNetCoreRateLimit/Models/RateLimitOptions.cs b/src/AspNetCoreRateLimit/Models/RateLimitOptions.cs index 89d2cf73..58da0464 100644 --- a/src/AspNetCoreRateLimit/Models/RateLimitOptions.cs +++ b/src/AspNetCoreRateLimit/Models/RateLimitOptions.cs @@ -30,6 +30,11 @@ public class RateLimitOptions /// public int HttpStatusCode { get; set; } = 429; + /// + /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is HttpStatusCode value. + /// + public int? HttpStatusCodeForLimitZeroRequests { get; set; } + /// /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message. /// If none specified the default will be: @@ -57,6 +62,12 @@ public class RateLimitOptions /// public bool EnableEndpointRateLimiting { get; set; } + /// + /// Defines if general rules for an endpoint should be discarded if an ip o client id rule is defined for the same endpoint. + /// By default they are only discarded if there is any rule for the same period. + /// + public bool DiscardEndpointGeneralRulesWhenExistsIPOrClienIdRule { get; set; } + /// /// Disables X-Rate-Limit and Retry-After headers /// diff --git a/src/AspNetCoreRateLimit/Net/IPAddressRange.cs b/src/AspNetCoreRateLimit/Net/IPAddressRange.cs index 1d89a52c..21f2184f 100644 --- a/src/AspNetCoreRateLimit/Net/IPAddressRange.cs +++ b/src/AspNetCoreRateLimit/Net/IPAddressRange.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Text.RegularExpressions; @@ -21,10 +22,13 @@ public class IpAddressRange public IPAddress End { get; set; } + public List IPList { get; set; } + public IpAddressRange() { Begin = new IPAddress(0L); End = new IPAddress(0L); + IPList = new List(); } public IpAddressRange(string ipRangeString) @@ -73,14 +77,31 @@ public IpAddressRange(string ipRangeString) return; } + // Pattern 5. List of ips: "169.258.0.0,169.258.0.255" + var m5 = Regex.Match(ipRangeString, @"^([\da-f\.:]+)(,[\da-f\.:]+)*$", RegexOptions.IgnoreCase); + if (m5.Success) + { + IPList = new List(); + IPList.AddRange(ipRangeString.Split(',').Select(ipString=> IPAddress.Parse(ipString))); + return; + } + throw new FormatException("Unknown IP range string."); } public bool Contains(IPAddress ipaddress) { - if (ipaddress.AddressFamily != Begin.AddressFamily) return false; - var adrBytes = ipaddress.GetAddressBytes(); - return Bits.GE(Begin.GetAddressBytes(), adrBytes) && Bits.LE(End.GetAddressBytes(), adrBytes); + if (IPList != null) + { + var adrBytes = ipaddress.GetAddressBytes(); + return IPList.Any(e => ipaddress.AddressFamily == e.AddressFamily && Bits.EQ(e.GetAddressBytes(), adrBytes)); + } + else + { + if (Begin == null || ipaddress.AddressFamily != Begin.AddressFamily) return false; + var adrBytes = ipaddress.GetAddressBytes(); + return Bits.GE(Begin.GetAddressBytes(), adrBytes) && Bits.LE(End.GetAddressBytes(), adrBytes); + } } } @@ -108,6 +129,13 @@ internal static bool GE(byte[] A, byte[] B) .FirstOrDefault() >= 0; } + internal static bool EQ(byte[] A, byte[] B) + { + return A.Zip(B, (a, b) => a == b ? 0 : 1) + .SkipWhile(c => c == 0) + .FirstOrDefault() == 0; + } + internal static bool LE(byte[] A, byte[] B) { return A.Zip(B, (a, b) => a == b ? 0 : a < b ? 1 : -1) diff --git a/src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheRateLimitStore.cs b/src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheRateLimitStore.cs index 46e3ed5c..76ba3217 100644 --- a/src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheRateLimitStore.cs +++ b/src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheRateLimitStore.cs @@ -45,6 +45,10 @@ public Task SetAsync(string id, T entry, TimeSpan? expirationTime = null, Cancel if (expirationTime.HasValue) { + if (expirationTime.Value.TotalMilliseconds == 0) + { + return Task.CompletedTask; + } options.SetAbsoluteExpiration(expirationTime.Value); }