diff --git a/src/AspNetCoreRateLimit/Middleware/RateLimitMiddleware.cs b/src/AspNetCoreRateLimit/Middleware/RateLimitMiddleware.cs index 3b6f63fc..33a2bff3 100644 --- a/src/AspNetCoreRateLimit/Middleware/RateLimitMiddleware.cs +++ b/src/AspNetCoreRateLimit/Middleware/RateLimitMiddleware.cs @@ -31,7 +31,7 @@ protected RateLimitMiddleware( public async Task Invoke(HttpContext context) { // check if rate limiting is enabled - if (_options == null) + if (_options == null || _options.RateLimitingEnabled == false) { await _next.Invoke(context); return; diff --git a/src/AspNetCoreRateLimit/Models/RateLimitOptions.cs b/src/AspNetCoreRateLimit/Models/RateLimitOptions.cs index 89d2cf73..29b8a9f0 100644 --- a/src/AspNetCoreRateLimit/Models/RateLimitOptions.cs +++ b/src/AspNetCoreRateLimit/Models/RateLimitOptions.cs @@ -7,6 +7,11 @@ namespace AspNetCoreRateLimit { public class RateLimitOptions { + /// + /// Gets or sets whether Rate Limiting is enabled + /// + public bool RateLimitingEnabled { get; set; } = true; // default for backward compatibility + public List GeneralRules { get; set; } public List EndpointWhitelist { get; set; } diff --git a/test/AspNetCoreRateLimit.Demo/appsettings.EnabledFalse.json b/test/AspNetCoreRateLimit.Demo/appsettings.EnabledFalse.json new file mode 100644 index 00000000..6e7c157b --- /dev/null +++ b/test/AspNetCoreRateLimit.Demo/appsettings.EnabledFalse.json @@ -0,0 +1,207 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + + "IpRateLimiting": { + "RateLimitingEnabled": false, + "EnableEndpointRateLimiting": true, + "EnableRegexRuleMatching": true, + "StackBlockedRequests": false, + "RealIpHeader": "X-Real-IP", + "ClientIdHeader": "X-ClientId", + "HttpStatusCode": 429, + "IpWhitelist": [ "::1/10", "192.168.0.0/24" ], + "EndpointWhitelist": [ "delete:/api/values", ":/api/clients", ":/api/ClientRateLimit", ":/api/IpRateLimit", "get:/" ], + "ClientWhitelist": [ "cl-key-1", "cl-key-2" ], + "QuotaExceededResponse": { + "Content": "{{ \"message\": \"Whoa! Calm down, cowboy!\", \"details\": \"Quota exceeded. Maximum allowed: {0} per {1}. Please try again in {2} second(s).\" }}", + "ContentType": "application/json" + }, + "GeneralRules": [ + { + "Endpoint": ".+", + "Period": "1s", + "Limit": 2 + }, + { + "Endpoint": ".+", + "Period": "1m", + "Limit": 5 + } + ] + }, + + "IpRateLimitPolicies": { + "IpRules": [ + { + "Ip": "84.247.85.224", + "Rules": [ + { + "Endpoint": ".+", + "Period": "1s", + "Limit": 10 + }, + { + "Endpoint": ".+", + "Period": "1m", + "Limit": 2 + }, + { + "Endpoint": "post:/api/values", + "Period": "1m", + "Limit": 5 + } + ] + }, + { + "Ip": "84.247.85.225", + "Rules": [ + { + "Endpoint": ".+", + "Period": "1s", + "Limit": 10 + }, + { + "Endpoint": ".+", + "Period": "1m", + "Limit": 5 + }, + { + "Endpoint": ".+", + "Period": "1h", + "Limit": 2 + } + ] + }, + { + "Ip": "84.247.85.226", + "Rules": [ + { + "Endpoint": ".+", + "Period": "1s", + "Limit": 10 + }, + { + "Endpoint": ".+", + "Period": "1m", + "Limit": 5 + }, + { + "Endpoint": ".+", + "Period": "1d", + "Limit": 2 + } + ] + }, + { + "Ip": "84.247.85.231", + "Rules": [ + { + "Endpoint": ".+", + "Period": "1m", + "Limit": 0 + } + ] + }, + { + "Ip": "84.247.85.232", + "Rules": [ + { + "Endpoint": ".+", + "Period": "1m", + "Limit": 1, + "MonitorMode": true + } + ] + } + ] + }, + + "ClientRateLimiting": { + "RateLimitingEnabled": false, + "EnableEndpointRateLimiting": true, + "EnableRegexRuleMatching": true, + "ClientIdHeader": "X-ClientId", + "HttpStatusCode": 429, + "EndpointWhitelist": [ "((post)|(put)|(get)|(delete)):/api/values", "delete:/api/clients" ], + "ClientWhitelist": [ "cl-key-a", "cl-key-b" ], + "GeneralRules": [ + { + "Endpoint": ".+", + "Period": "1s", + "Limit": 2 + }, + { + "Endpoint": ".+", + "Period": "1m", + "Limit": 5 + }, + { + "Endpoint": "post:/api/clients", + "Period": "5m", + "Limit": 3 + } + ] + }, + + "ClientRateLimitPolicies": { + "ClientRules": [ + { + "ClientId": "cl-key-1", + "Rules": [ + { + "Endpoint": ".+", + "Period": "1s", + "Limit": 10 + }, + { + "Endpoint": "get:/api/clients", + "Period": "1m", + "Limit": 2 + }, + { + "Endpoint": "put:/api/clients", + "Period": "5m", + "Limit": 2 + } + ] + }, + { + "ClientId": "cl-key-2", + "Rules": [ + { + "Endpoint": ".+", + "Period": "1s", + "Limit": 10 + }, + { + "Endpoint": "get:/api/clients", + "Period": "1m", + "Limit": 0 + }, + { + "Endpoint": "post:/api/clients", + "Period": "5m", + "Limit": 50 + } + ] + }, + { + "ClientId": "cl-key-3", + "Rules": [ + { + "Endpoint": "post:/api/clients", + "Period": "1s", + "Limit": 3 + } + ] + } + ] + } +} diff --git a/test/AspNetCoreRateLimit.Demo/appsettings.Regex.json b/test/AspNetCoreRateLimit.Demo/appsettings.Regex.json index 0a6b3597..edd1190c 100644 --- a/test/AspNetCoreRateLimit.Demo/appsettings.Regex.json +++ b/test/AspNetCoreRateLimit.Demo/appsettings.Regex.json @@ -9,6 +9,7 @@ }, "IpRateLimiting": { + "RateLimitingEnabled": true, "EnableEndpointRateLimiting": true, "EnableRegexRuleMatching": true, "StackBlockedRequests": false, @@ -123,6 +124,7 @@ }, "ClientRateLimiting": { + "RateLimitingEnabled": true, "EnableEndpointRateLimiting": true, "EnableRegexRuleMatching": true, "ClientIdHeader": "X-ClientId", diff --git a/test/AspNetCoreRateLimit.Demo/appsettings.json b/test/AspNetCoreRateLimit.Demo/appsettings.json index 8c587347..b07ee6da 100644 --- a/test/AspNetCoreRateLimit.Demo/appsettings.json +++ b/test/AspNetCoreRateLimit.Demo/appsettings.json @@ -9,6 +9,7 @@ }, "IpRateLimiting": { + "RateLimitingEnabled": true, "EnableEndpointRateLimiting": true, "StackBlockedRequests": false, "RealIpHeader": "X-Real-IP", @@ -130,6 +131,7 @@ }, "ClientRateLimiting": { + "RateLimitingEnabled": true, "EnableEndpointRateLimiting": true, "ClientIdHeader": "X-ClientId", "HttpStatusCode": 429, diff --git a/test/AspNetCoreRateLimit.Tests/BaseClassFixture.cs b/test/AspNetCoreRateLimit.Tests/BaseClassFixture.cs index 15dc1afe..8b7893d2 100644 --- a/test/AspNetCoreRateLimit.Tests/BaseClassFixture.cs +++ b/test/AspNetCoreRateLimit.Tests/BaseClassFixture.cs @@ -11,6 +11,7 @@ public abstract class BaseClassFixture : IClassFixture + { + builder.ConfigureAppConfiguration((context, conf) => + { + conf.AddJsonFile("appsettings.EnabledFalse.json"); + }); + }).CreateClient(options: new WebApplicationFactoryClientOptions + { + BaseAddress = new System.Uri("https://localhost:44304"), + + }); } /// @@ -43,9 +56,16 @@ protected HttpClient GetClient(ClientType clientType) return _wildcardClient; case ClientType.Regex: return _regexClient; + case ClientType.EnabledFalse: + return _enabledFalseClient; default: throw new ArgumentOutOfRangeException(nameof(clientType), clientType, "Unexpected client type."); } } + + protected void SetOptions(RateLimitOptions options) + { + + } } } \ No newline at end of file diff --git a/test/AspNetCoreRateLimit.Tests/ClientRateLimitTests.cs b/test/AspNetCoreRateLimit.Tests/ClientRateLimitTests.cs index 0b26c325..a7128347 100644 --- a/test/AspNetCoreRateLimit.Tests/ClientRateLimitTests.cs +++ b/test/AspNetCoreRateLimit.Tests/ClientRateLimitTests.cs @@ -1,5 +1,6 @@ using AspNetCoreRateLimit.Tests.Enums; using System.Net.Http; +using System.Text; using System.Threading.Tasks; using Xunit; @@ -224,5 +225,29 @@ public async Task UpdateOptions(ClientType clientType) // Assert Assert.Contains(keyword, content); } + + [Theory] + [InlineData(ClientType.EnabledFalse)] + public async Task RulesDisabled(ClientType clientType) + { + // Arrange + var clientId = "cl-key-3"; + int responseStatusCode = 0; + + // Act + for (int i = 0; i < 4; i++) + { + var request = new HttpRequestMessage(HttpMethod.Post, apiPath); + request.Headers.Add("X-ClientId", clientId); + request.Headers.Add("X-Real-IP", ip); + request.Content = new StringContent("{}", Encoding.UTF8, "application/json"); + + var response = await GetClient(clientType).SendAsync(request); + responseStatusCode = (int)response.StatusCode; + } + + // Assert + Assert.Equal(200, responseStatusCode); + } } } \ No newline at end of file diff --git a/test/AspNetCoreRateLimit.Tests/Enums/ClientType.cs b/test/AspNetCoreRateLimit.Tests/Enums/ClientType.cs index b95d09ef..7b84cf12 100644 --- a/test/AspNetCoreRateLimit.Tests/Enums/ClientType.cs +++ b/test/AspNetCoreRateLimit.Tests/Enums/ClientType.cs @@ -3,6 +3,7 @@ public enum ClientType { Wildcard, - Regex + Regex, + EnabledFalse } } \ No newline at end of file diff --git a/test/AspNetCoreRateLimit.Tests/IpRateLimitTests.cs b/test/AspNetCoreRateLimit.Tests/IpRateLimitTests.cs index 8400c675..01170c3c 100644 --- a/test/AspNetCoreRateLimit.Tests/IpRateLimitTests.cs +++ b/test/AspNetCoreRateLimit.Tests/IpRateLimitTests.cs @@ -1,5 +1,6 @@ using AspNetCoreRateLimit.Tests.Enums; using System.Net.Http; +using System.Text; using System.Threading.Tasks; using Xunit; @@ -265,5 +266,28 @@ public async Task SpecificIpRuleMonitorActive(ClientType clientType, string ip) // Assert Assert.Equal(200, responseStatusCode); } + + [Theory] + [InlineData(ClientType.EnabledFalse, "84.247.85.232")] + public async Task RulesDisabled(ClientType clientType, string ip) + { + // Arrange + int responseStatusCode = 0; + + // Act + for (int i = 0; i < 2; i++) + { + var request = new HttpRequestMessage(HttpMethod.Get, apiValuesPath); + request.Headers.Add("X-Real-IP", ip); + request.Content = new StringContent("{}", Encoding.UTF8, "application/json"); + + var response = await GetClient(clientType).SendAsync(request); + responseStatusCode = (int)response.StatusCode; + } + + // Assert + Assert.Equal(200, responseStatusCode); + } + } } \ No newline at end of file