diff --git a/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj b/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj index e50e8de8..0a2e31b3 100644 --- a/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj +++ b/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj @@ -11,7 +11,7 @@ http://opensource.org/licenses/MIT git https://github.com/stefanprodan/AspNetCoreRateLimit - 8 + 9 3.2.3 true ../../sgKey.snk @@ -28,12 +28,12 @@ - + - - - - + + + + @@ -52,11 +52,11 @@ - + true - + diff --git a/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs b/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs index f09472d7..5ff509c0 100644 --- a/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs +++ b/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs @@ -7,17 +7,22 @@ namespace AspNetCoreRateLimit public class ClientRateLimitProcessor : RateLimitProcessor, IRateLimitProcessor { private readonly ClientRateLimitOptions _options; + private readonly IProcessingStrategy _processingStrategy; private readonly IRateLimitStore _policyStore; + private readonly ICounterKeyBuilder _counterKeyBuilder; public ClientRateLimitProcessor( - ClientRateLimitOptions options, - IRateLimitCounterStore counterStore, - IClientPolicyStore policyStore, - IRateLimitConfiguration config) - : base(options, counterStore, new ClientCounterKeyBuilder(options), config) + ClientRateLimitOptions options, + IRateLimitCounterStore counterStore, + IClientPolicyStore policyStore, + IRateLimitConfiguration config, + IProcessingStrategy processingStrategy) + : base(options) { _options = options; _policyStore = policyStore; + _counterKeyBuilder = new ClientCounterKeyBuilder(options); + _processingStrategy = processingStrategy; } public async Task> GetMatchingRulesAsync(ClientRequestIdentity identity, CancellationToken cancellationToken = default) @@ -26,5 +31,10 @@ public async Task> GetMatchingRulesAsync(ClientReques return GetMatchingRules(identity, policy?.Rules); } + + public async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default) + { + return await _processingStrategy.ProcessRequestAsync(requestIdentity, rule, _counterKeyBuilder, _options, cancellationToken); + } } } \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Core/IRateLimitProcessor.cs b/src/AspNetCoreRateLimit/Core/IRateLimitProcessor.cs index abbb53fd..f87819c9 100644 --- a/src/AspNetCoreRateLimit/Core/IRateLimitProcessor.cs +++ b/src/AspNetCoreRateLimit/Core/IRateLimitProcessor.cs @@ -7,11 +7,8 @@ namespace AspNetCoreRateLimit public interface IRateLimitProcessor { Task> GetMatchingRulesAsync(ClientRequestIdentity identity, CancellationToken cancellationToken = default); - RateLimitHeaders GetRateLimitHeaders(RateLimitCounter? counter, RateLimitRule rule, CancellationToken cancellationToken = default); - Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default); - bool IsWhitelisted(ClientRequestIdentity requestIdentity); } } \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs b/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs index a62258ec..a5191ad1 100644 --- a/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs +++ b/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs @@ -9,18 +9,24 @@ public class IpRateLimitProcessor : RateLimitProcessor, IRateLimitProcessor { private readonly IpRateLimitOptions _options; private readonly IRateLimitStore _policyStore; + private readonly IProcessingStrategy _processingStrategy; + private readonly ICounterKeyBuilder _counterKeyBuilder; public IpRateLimitProcessor( - IpRateLimitOptions options, - IRateLimitCounterStore counterStore, - IIpPolicyStore policyStore, - IRateLimitConfiguration config) - : base(options, counterStore, new IpCounterKeyBuilder(options), config) + IpRateLimitOptions options, + IRateLimitCounterStore counterStore, + IIpPolicyStore policyStore, + IRateLimitConfiguration config, + IProcessingStrategy processingStrategy) + : base(options) { _options = options; _policyStore = policyStore; + _counterKeyBuilder = new IpCounterKeyBuilder(options); + _processingStrategy = processingStrategy; } + public async Task> GetMatchingRulesAsync(ClientRequestIdentity identity, CancellationToken cancellationToken = default) { var policies = await _policyStore.GetAsync($"{_options.IpPolicyPrefix}", cancellationToken); @@ -40,5 +46,10 @@ public async Task> GetMatchingRulesAsync(ClientReques return GetMatchingRules(identity, rules); } + + public async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default) + { + return await _processingStrategy.ProcessRequestAsync(requestIdentity, rule, _counterKeyBuilder, _options, cancellationToken); + } } } \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/AsyncKeyLockProcessingStrategy.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/AsyncKeyLockProcessingStrategy.cs new file mode 100644 index 00000000..6b2b6e0d --- /dev/null +++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/AsyncKeyLockProcessingStrategy.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace AspNetCoreRateLimit +{ + public class AsyncKeyLockProcessingStrategy : ProcessingStrategy + { + private readonly IRateLimitCounterStore _counterStore; + private readonly IRateLimitConfiguration _config; + + public AsyncKeyLockProcessingStrategy(IRateLimitCounterStore counterStore, IRateLimitConfiguration config) + : base(config) + { + _counterStore = counterStore; + _config = config; + } + + /// The key-lock used for limiting requests. + private static readonly AsyncKeyLock AsyncLock = new AsyncKeyLock(); + + public override async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, ICounterKeyBuilder counterKeyBuilder, RateLimitOptions rateLimitOptions, CancellationToken cancellationToken = default) + { + var counter = new RateLimitCounter + { + Timestamp = DateTime.UtcNow, + Count = 1 + }; + + var counterId = BuildCounterKey(requestIdentity, rule, counterKeyBuilder, rateLimitOptions); + + // serial reads and writes on same key + using (await AsyncLock.WriterLockAsync(counterId).ConfigureAwait(false)) + { + var entry = await _counterStore.GetAsync(counterId, cancellationToken); + + if (entry.HasValue) + { + // entry has not expired + if (entry.Value.Timestamp + rule.PeriodTimespan.Value >= DateTime.UtcNow) + { + // increment request count + var totalCount = entry.Value.Count + _config.RateIncrementer?.Invoke() ?? 1; + + // deep copy + counter = new RateLimitCounter + { + Timestamp = entry.Value.Timestamp, + Count = totalCount + }; + } + } + + // stores: id (string) - timestamp (datetime) - total_requests (long) + await _counterStore.SetAsync(counterId, counter, rule.PeriodTimespan.Value, cancellationToken); + } + + return counter; + } + } +} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategy.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategy.cs new file mode 100644 index 00000000..9dcadf24 --- /dev/null +++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategy.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace AspNetCoreRateLimit +{ + public interface IProcessingStrategy + { + Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, ICounterKeyBuilder counterKeyBuilder, RateLimitOptions rateLimitOptions, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategy.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategy.cs new file mode 100644 index 00000000..f366b089 --- /dev/null +++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategy.cs @@ -0,0 +1,37 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace AspNetCoreRateLimit +{ + public abstract class ProcessingStrategy : IProcessingStrategy + { + private readonly IRateLimitConfiguration _config; + + protected ProcessingStrategy(IRateLimitConfiguration config) + { + _config = config; + } + + public abstract Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, ICounterKeyBuilder counterKeyBuilder, RateLimitOptions rateLimitOptions, CancellationToken cancellationToken = default); + + protected virtual string BuildCounterKey(ClientRequestIdentity requestIdentity, RateLimitRule rule, ICounterKeyBuilder counterKeyBuilder, RateLimitOptions rateLimitOptions) + { + var key = counterKeyBuilder.Build(requestIdentity, rule); + + if (rateLimitOptions.EnableEndpointRateLimiting && _config.EndpointCounterKeyBuilder != null) + { + key += _config.EndpointCounterKeyBuilder.Build(requestIdentity, rule); + } + + var bytes = Encoding.UTF8.GetBytes(key); + + using var algorithm = new SHA1Managed(); + var hash = algorithm.ComputeHash(bytes); + + return Convert.ToBase64String(hash); + } + } +} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Core/RateLimitProcessor.cs b/src/AspNetCoreRateLimit/Core/RateLimitProcessor.cs index d22fe5d5..9ce4ff1d 100644 --- a/src/AspNetCoreRateLimit/Core/RateLimitProcessor.cs +++ b/src/AspNetCoreRateLimit/Core/RateLimitProcessor.cs @@ -12,24 +12,12 @@ namespace AspNetCoreRateLimit public abstract class RateLimitProcessor { private readonly RateLimitOptions _options; - private readonly IRateLimitCounterStore _counterStore; - private readonly ICounterKeyBuilder _counterKeyBuilder; - private readonly IRateLimitConfiguration _config; - - protected RateLimitProcessor( - RateLimitOptions options, - IRateLimitCounterStore counterStore, - ICounterKeyBuilder counterKeyBuilder, - IRateLimitConfiguration config) + + protected RateLimitProcessor(RateLimitOptions options) { _options = options; - _counterStore = counterStore; - _counterKeyBuilder = counterKeyBuilder; - _config = config; } - /// The key-lock used for limiting requests. - private static readonly AsyncKeyLock AsyncLock = new AsyncKeyLock(); public virtual bool IsWhitelisted(ClientRequestIdentity requestIdentity) { @@ -55,45 +43,6 @@ public virtual bool IsWhitelisted(ClientRequestIdentity requestIdentity) return false; } - public virtual async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default) - { - var counter = new RateLimitCounter - { - Timestamp = DateTime.UtcNow, - Count = 1 - }; - - var counterId = BuildCounterKey(requestIdentity, rule); - - // serial reads and writes on same key - using (await AsyncLock.WriterLockAsync(counterId).ConfigureAwait(false)) - { - var entry = await _counterStore.GetAsync(counterId, cancellationToken); - - if (entry.HasValue) - { - // entry has not expired - if (entry.Value.Timestamp + rule.PeriodTimespan.Value >= DateTime.UtcNow) - { - // increment request count - var totalCount = entry.Value.Count + _config.RateIncrementer?.Invoke() ?? 1; - - // deep copy - counter = new RateLimitCounter - { - Timestamp = entry.Value.Timestamp, - Count = totalCount - }; - } - } - - // stores: id (string) - timestamp (datetime) - total_requests (long) - await _counterStore.SetAsync(counterId, counter, rule.PeriodTimespan.Value, cancellationToken); - } - - return counter; - } - public virtual RateLimitHeaders GetRateLimitHeaders(RateLimitCounter? counter, RateLimitRule rule, CancellationToken cancellationToken = default) { var headers = new RateLimitHeaders(); @@ -119,23 +68,6 @@ public virtual RateLimitHeaders GetRateLimitHeaders(RateLimitCounter? counter, R return headers; } - protected virtual string BuildCounterKey(ClientRequestIdentity requestIdentity, RateLimitRule rule) - { - var key = _counterKeyBuilder.Build(requestIdentity, rule); - - if (_options.EnableEndpointRateLimiting && _config.EndpointCounterKeyBuilder != null) - { - key += _config.EndpointCounterKeyBuilder.Build(requestIdentity, rule); - } - - var bytes = Encoding.UTF8.GetBytes(key); - - using var algorithm = new SHA1Managed(); - var hash = algorithm.ComputeHash(bytes); - - return Convert.ToBase64String(hash); - } - protected virtual List GetMatchingRules(ClientRequestIdentity identity, List rules = null) { var limits = new List(); diff --git a/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs index d8c1050d..ae52b790 100644 --- a/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs +++ b/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs @@ -9,12 +9,13 @@ public class ClientRateLimitMiddleware : RateLimitMiddleware _logger; public ClientRateLimitMiddleware(RequestDelegate next, + IProcessingStrategy processingStrategy, IOptions options, IRateLimitCounterStore counterStore, IClientPolicyStore policyStore, IRateLimitConfiguration config, ILogger logger) - : base(next, options?.Value, new ClientRateLimitProcessor(options?.Value, counterStore, policyStore, config), config) + : base(next, options?.Value, new ClientRateLimitProcessor(options?.Value, counterStore, policyStore, config, processingStrategy), config) { _logger = logger; } diff --git a/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs b/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs index 4eeced18..86e1bb1e 100644 --- a/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs +++ b/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs @@ -9,13 +9,14 @@ public class IpRateLimitMiddleware : RateLimitMiddleware private readonly ILogger _logger; public IpRateLimitMiddleware(RequestDelegate next, + IProcessingStrategy processingStrategy, IOptions options, IRateLimitCounterStore counterStore, IIpPolicyStore policyStore, IRateLimitConfiguration config, - ILogger logger) - : base(next, options?.Value, new IpRateLimitProcessor(options?.Value, counterStore, policyStore, config), config) - + ILogger logger + ) + : base(next, options?.Value, new IpRateLimitProcessor(options?.Value, counterStore, policyStore, config, processingStrategy), config) { _logger = logger; } diff --git a/src/AspNetCoreRateLimit/Middleware/MiddlewareExtensions.cs b/src/AspNetCoreRateLimit/Middleware/MiddlewareExtensions.cs deleted file mode 100644 index 63a1300c..00000000 --- a/src/AspNetCoreRateLimit/Middleware/MiddlewareExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.AspNetCore.Builder; - -namespace AspNetCoreRateLimit -{ - public static class MiddlewareExtensions - { - public static IApplicationBuilder UseIpRateLimiting(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - - public static IApplicationBuilder UseClientRateLimiting(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - } -} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/StartupExtensions.cs b/src/AspNetCoreRateLimit/StartupExtensions.cs new file mode 100644 index 00000000..bfe493ae --- /dev/null +++ b/src/AspNetCoreRateLimit/StartupExtensions.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace AspNetCoreRateLimit +{ + public static class StartupExtensions + { + public static IServiceCollection AddMemoryCacheStores(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + return services; + } + + public static IServiceCollection AddDistributedCacheStores(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + return services; + } + + public static IApplicationBuilder UseIpRateLimiting(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + public static IApplicationBuilder UseClientRateLimiting(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Store/DistributedCacheClientPolicyStore.cs b/src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheClientPolicyStore.cs similarity index 100% rename from src/AspNetCoreRateLimit/Store/DistributedCacheClientPolicyStore.cs rename to src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheClientPolicyStore.cs diff --git a/src/AspNetCoreRateLimit/Store/DistributedCacheIpPolicyStore.cs b/src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheIpPolicyStore.cs similarity index 100% rename from src/AspNetCoreRateLimit/Store/DistributedCacheIpPolicyStore.cs rename to src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheIpPolicyStore.cs diff --git a/src/AspNetCoreRateLimit/Store/DistributedCacheRateLimitCounterStore.cs b/src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheRateLimitCounterStore.cs similarity index 100% rename from src/AspNetCoreRateLimit/Store/DistributedCacheRateLimitCounterStore.cs rename to src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheRateLimitCounterStore.cs diff --git a/src/AspNetCoreRateLimit/Store/DistributedCacheRateLimitStore.cs b/src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheRateLimitStore.cs similarity index 100% rename from src/AspNetCoreRateLimit/Store/DistributedCacheRateLimitStore.cs rename to src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheRateLimitStore.cs diff --git a/src/AspNetCoreRateLimit/Store/MemoryCacheClientPolicyStore.cs b/src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheClientPolicyStore.cs similarity index 100% rename from src/AspNetCoreRateLimit/Store/MemoryCacheClientPolicyStore.cs rename to src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheClientPolicyStore.cs diff --git a/src/AspNetCoreRateLimit/Store/MemoryCacheIpPolicyStore.cs b/src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheIpPolicyStore.cs similarity index 100% rename from src/AspNetCoreRateLimit/Store/MemoryCacheIpPolicyStore.cs rename to src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheIpPolicyStore.cs diff --git a/src/AspNetCoreRateLimit/Store/MemoryCacheRateLimitCounterStore.cs b/src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheRateLimitCounterStore.cs similarity index 100% rename from src/AspNetCoreRateLimit/Store/MemoryCacheRateLimitCounterStore.cs rename to src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheRateLimitCounterStore.cs diff --git a/src/AspNetCoreRateLimit/Store/MemoryCacheRateLimitStore.cs b/src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheRateLimitStore.cs similarity index 100% rename from src/AspNetCoreRateLimit/Store/MemoryCacheRateLimitStore.cs rename to src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheRateLimitStore.cs diff --git a/test/AspNetCoreRateLimit.Demo/Startup.cs b/test/AspNetCoreRateLimit.Demo/Startup.cs index 77635376..a5359140 100644 --- a/test/AspNetCoreRateLimit.Demo/Startup.cs +++ b/test/AspNetCoreRateLimit.Demo/Startup.cs @@ -26,19 +26,22 @@ public void ConfigureServices(IServiceCollection services) // needed to store rate limit counters and ip rules services.AddMemoryCache(); + // configure ip rate limiting middleware - version 4.0 + + // configure client rate limiting middleware - version 4.0 + // configure ip rate limiting middleware services.Configure(Configuration.GetSection("IpRateLimiting")); services.Configure(Configuration.GetSection("IpRateLimitPolicies")); - services.AddSingleton(); - services.AddSingleton(); // configure client rate limiting middleware services.Configure(Configuration.GetSection("ClientRateLimiting")); services.Configure(Configuration.GetSection("ClientRateLimitPolicies")); - services.AddSingleton(); - //services.AddSingleton(); - services.AddMvc((options) => + // register stores + services.AddMemoryCacheStores(); + + services.AddMvc((options) => { options.EnableEndpointRouting = false;