From 60e6ebc043f95f2b7e75b1a351de66716c3aa2e4 Mon Sep 17 00:00:00 2001 From: Nick Cromwell Date: Thu, 17 Dec 2020 18:49:15 -0500 Subject: [PATCH 1/2] Add StackExchangeRedis atomic Lua script support and refactor to support differing processing strategies --- .../AspNetCoreRateLimit.csproj | 19 ++--- .../Core/ClientRateLimitProcessor.cs | 18 +++-- .../Core/IpRateLimitProcessor.cs | 19 +++-- .../AsyncKeyLockProcessingStrategy.cs | 66 +++++++++++++++++ .../IProcessingStrategy.cs | 10 +++ .../IProcessingStrategyFactory.cs | 7 ++ .../ProcessingStrategy.cs | 42 +++++++++++ .../ProcessingStrategyFactory.cs | 27 +++++++ .../StackExchangeRedisProcessingStrategy.cs | 44 ++++++++++++ .../Core/RateLimitProcessor.cs | 72 +------------------ .../Middleware/ClientRateLimitMiddleware.cs | 3 +- .../Middleware/IpRateLimitMiddleware.cs | 3 +- .../Middleware/MiddlewareExtensions.cs | 17 ----- src/AspNetCoreRateLimit/StartupExtensions.cs | 44 ++++++++++++ .../DistributedCacheClientPolicyStore.cs | 0 .../DistributedCacheIpPolicyStore.cs | 0 .../DistributedCacheRateLimitCounterStore.cs | 0 .../DistributedCacheRateLimitStore.cs | 0 .../MemoryCacheClientPolicyStore.cs | 0 .../MemoryCacheIpPolicyStore.cs | 0 .../MemoryCacheRateLimitCounterStore.cs | 0 .../MemoryCacheRateLimitStore.cs | 0 .../StackExchangeRedisClientPolicyStore.cs | 34 +++++++++ .../StackExchangeRedisIpPolicyStore.cs | 31 ++++++++ ...StackExchangeRedisRateLimitCounterStore.cs | 12 ++++ .../StackExchangeRedisRateLimitStore.cs | 50 +++++++++++++ test/AspNetCoreRateLimit.Demo/Startup.cs | 13 ++-- 27 files changed, 419 insertions(+), 112 deletions(-) create mode 100644 src/AspNetCoreRateLimit/Core/ProcessingStrategies/AsyncKeyLockProcessingStrategy.cs create mode 100644 src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategy.cs create mode 100644 src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategyFactory.cs create mode 100644 src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategy.cs create mode 100644 src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategyFactory.cs create mode 100644 src/AspNetCoreRateLimit/Core/ProcessingStrategies/StackExchangeRedisProcessingStrategy.cs delete mode 100644 src/AspNetCoreRateLimit/Middleware/MiddlewareExtensions.cs create mode 100644 src/AspNetCoreRateLimit/StartupExtensions.cs rename src/AspNetCoreRateLimit/Store/{ => DistributedCache}/DistributedCacheClientPolicyStore.cs (100%) rename src/AspNetCoreRateLimit/Store/{ => DistributedCache}/DistributedCacheIpPolicyStore.cs (100%) rename src/AspNetCoreRateLimit/Store/{ => DistributedCache}/DistributedCacheRateLimitCounterStore.cs (100%) rename src/AspNetCoreRateLimit/Store/{ => DistributedCache}/DistributedCacheRateLimitStore.cs (100%) rename src/AspNetCoreRateLimit/Store/{ => MemoryCache}/MemoryCacheClientPolicyStore.cs (100%) rename src/AspNetCoreRateLimit/Store/{ => MemoryCache}/MemoryCacheIpPolicyStore.cs (100%) rename src/AspNetCoreRateLimit/Store/{ => MemoryCache}/MemoryCacheRateLimitCounterStore.cs (100%) rename src/AspNetCoreRateLimit/Store/{ => MemoryCache}/MemoryCacheRateLimitStore.cs (100%) create mode 100644 src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisClientPolicyStore.cs create mode 100644 src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisIpPolicyStore.cs create mode 100644 src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitCounterStore.cs create mode 100644 src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitStore.cs diff --git a/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj b/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj index e50e8de8..580a5d12 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 @@ -27,13 +27,15 @@ + - + - - - - + + + + + @@ -41,6 +43,7 @@ + @@ -52,11 +55,11 @@ - + true - + diff --git a/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs b/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs index f09472d7..61f4bc43 100644 --- a/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs +++ b/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs @@ -7,17 +7,20 @@ namespace AspNetCoreRateLimit public class ClientRateLimitProcessor : RateLimitProcessor, IRateLimitProcessor { private readonly ClientRateLimitOptions _options; + private readonly IProcessingStrategy _processingStrategy; private readonly IRateLimitStore _policyStore; public ClientRateLimitProcessor( - ClientRateLimitOptions options, - IRateLimitCounterStore counterStore, - IClientPolicyStore policyStore, - IRateLimitConfiguration config) - : base(options, counterStore, new ClientCounterKeyBuilder(options), config) + IProcessingStrategyFactory processingStrategyFactory, + ClientRateLimitOptions options, + IRateLimitCounterStore counterStore, + IClientPolicyStore policyStore, + IRateLimitConfiguration config) + : base(options) { _options = options; _policyStore = policyStore; + _processingStrategy = processingStrategyFactory.CreateProcessingStrategy(counterStore, new ClientCounterKeyBuilder(options), config, options); } public async Task> GetMatchingRulesAsync(ClientRequestIdentity identity, CancellationToken cancellationToken = default) @@ -26,5 +29,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, cancellationToken); + } } } \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs b/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs index a62258ec..9abbfc95 100644 --- a/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs +++ b/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs @@ -9,18 +9,22 @@ public class IpRateLimitProcessor : RateLimitProcessor, IRateLimitProcessor { private readonly IpRateLimitOptions _options; private readonly IRateLimitStore _policyStore; + private readonly IProcessingStrategy _processingStrategy; public IpRateLimitProcessor( - IpRateLimitOptions options, - IRateLimitCounterStore counterStore, - IIpPolicyStore policyStore, - IRateLimitConfiguration config) - : base(options, counterStore, new IpCounterKeyBuilder(options), config) + IProcessingStrategyFactory processingStrategyFactory, + IpRateLimitOptions options, + IRateLimitCounterStore counterStore, + IIpPolicyStore policyStore, + IRateLimitConfiguration config) + : base(options) { _options = options; _policyStore = policyStore; + _processingStrategy = processingStrategyFactory.CreateProcessingStrategy(counterStore, new IpCounterKeyBuilder(options), config, options); } + public async Task> GetMatchingRulesAsync(ClientRequestIdentity identity, CancellationToken cancellationToken = default) { var policies = await _policyStore.GetAsync($"{_options.IpPolicyPrefix}", cancellationToken); @@ -40,5 +44,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, 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..9cc31465 --- /dev/null +++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/AsyncKeyLockProcessingStrategy.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace AspNetCoreRateLimit +{ + public class AsyncKeyLockProcessingStrategy : ProcessingStrategy + { + private readonly RateLimitOptions _options; + private readonly IRateLimitCounterStore _counterStore; + private readonly ICounterKeyBuilder _counterKeyBuilder; + private readonly IRateLimitConfiguration _config; + + public AsyncKeyLockProcessingStrategy(IRateLimitCounterStore counterStore, ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options) + : base(counterKeyBuilder, config, options) + { + _counterStore = counterStore; + _counterKeyBuilder = counterKeyBuilder; + _config = config; + _options = options; + } + + + /// The key-lock used for limiting requests. + private static readonly AsyncKeyLock AsyncLock = new AsyncKeyLock(); + + public override 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; + } + } +} \ 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..e15b3b2f --- /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, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategyFactory.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategyFactory.cs new file mode 100644 index 00000000..75c506a9 --- /dev/null +++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategyFactory.cs @@ -0,0 +1,7 @@ +namespace AspNetCoreRateLimit +{ + public interface IProcessingStrategyFactory + { + ProcessingStrategy CreateProcessingStrategy(IRateLimitCounterStore counterStore, ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options); + } +} \ 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..2cd32efe --- /dev/null +++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategy.cs @@ -0,0 +1,42 @@ +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 RateLimitOptions _options; + private readonly ICounterKeyBuilder _counterKeyBuilder; + private readonly IRateLimitConfiguration _config; + + public ProcessingStrategy(ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options) + : base() + { + _counterKeyBuilder = counterKeyBuilder; + _config = config; + _options = options; + } + + 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); + } + + public abstract Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategyFactory.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategyFactory.cs new file mode 100644 index 00000000..a8b7952d --- /dev/null +++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategyFactory.cs @@ -0,0 +1,27 @@ +using System; +using StackExchange.Redis; + +namespace AspNetCoreRateLimit +{ + + public class ProcessingStrategyFactory : IProcessingStrategyFactory + { + private readonly IConnectionMultiplexer _connectionMultiplexer; + + public ProcessingStrategyFactory(IConnectionMultiplexer connectionMultiplexer = null) + { + _connectionMultiplexer = connectionMultiplexer; + } + + public ProcessingStrategy CreateProcessingStrategy(IRateLimitCounterStore counterStore, ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options) + { + return counterStore switch + { + MemoryCacheRateLimitCounterStore => new AsyncKeyLockProcessingStrategy(counterStore, counterKeyBuilder, config, options), + DistributedCacheRateLimitCounterStore => new AsyncKeyLockProcessingStrategy(counterStore, counterKeyBuilder, config, options), + StackExchangeRedisRateLimitCounterStore => new StackExchangeRedisProcessingStrategy(_connectionMultiplexer, counterStore, counterKeyBuilder, config, options), + _ => throw new ArgumentException("Unsupported instance of IRateLimitCounterStore provided") + }; + } + } +} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/StackExchangeRedisProcessingStrategy.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/StackExchangeRedisProcessingStrategy.cs new file mode 100644 index 00000000..ecb73215 --- /dev/null +++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/StackExchangeRedisProcessingStrategy.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using StackExchange.Redis; + +namespace AspNetCoreRateLimit +{ + public class StackExchangeRedisProcessingStrategy : ProcessingStrategy + { + private readonly IConnectionMultiplexer _connectionMultiplexer; + private readonly IRateLimitConfiguration _config; + + public StackExchangeRedisProcessingStrategy(IConnectionMultiplexer connectionMultiplexer, IRateLimitCounterStore counterStore, ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options) + : base(counterKeyBuilder, config, options) + { + _connectionMultiplexer = connectionMultiplexer ?? throw new ArgumentException("IConnectionMultiplexer was null. Ensure StackExchange.Redis was successfully registered"); + _config = config; + } + + + static private readonly LuaScript _atomicIncrement = LuaScript.Prepare("local count count = redis.call(\"INCRBYFLOAT\", @key, tonumber(@delta)) if tonumber(count) == @delta then redis.call(\"EXPIRE\", @key, @timeout) end return count"); + + public override async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default) + { + var counterId = BuildCounterKey(requestIdentity, rule); + return await IncrementAsync(counterId, rule.PeriodTimespan.Value, _config.RateIncrementer); + } + + public async Task IncrementAsync(string counterId, TimeSpan interval, Func RateIncrementer = null) + { + var now = DateTime.UtcNow; + var numberOfIntervals = now.Ticks / interval.Ticks; + var intervalStart = new DateTime(numberOfIntervals * interval.Ticks, DateTimeKind.Utc); + + // Call the Lua script + var count = await _connectionMultiplexer.GetDatabase().ScriptEvaluateAsync(_atomicIncrement, new { key = counterId, timeout = interval.TotalSeconds, delta = RateIncrementer?.Invoke() ?? 1D }); + return new RateLimitCounter + { + Count = (double)count, + Timestamp = intervalStart + }; + } + } +} \ 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..f11781c9 100644 --- a/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs +++ b/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs @@ -10,11 +10,12 @@ public class ClientRateLimitMiddleware : RateLimitMiddleware options, + IProcessingStrategyFactory processingStrategyFactory, 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(processingStrategyFactory, options?.Value, counterStore, policyStore, config), config) { _logger = logger; } diff --git a/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs b/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs index 4eeced18..7b6a338a 100644 --- a/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs +++ b/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs @@ -9,12 +9,13 @@ public class IpRateLimitMiddleware : RateLimitMiddleware private readonly ILogger _logger; public IpRateLimitMiddleware(RequestDelegate next, + IProcessingStrategyFactory processingStrategyFactory, IOptions options, IRateLimitCounterStore counterStore, IIpPolicyStore policyStore, IRateLimitConfiguration config, ILogger logger) - : base(next, options?.Value, new IpRateLimitProcessor(options?.Value, counterStore, policyStore, config), config) + : base(next, options?.Value, new IpRateLimitProcessor(processingStrategyFactory, options?.Value, counterStore, policyStore, config), 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..1cc9615c --- /dev/null +++ b/src/AspNetCoreRateLimit/StartupExtensions.cs @@ -0,0 +1,44 @@ +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(); + return services; + } + + public static IServiceCollection AddStackExchangeRedisStores(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/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisClientPolicyStore.cs b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisClientPolicyStore.cs new file mode 100644 index 00000000..992d503e --- /dev/null +++ b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisClientPolicyStore.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using StackExchange.Redis; + +namespace AspNetCoreRateLimit +{ + public class StackExchangeRedisClientPolicyStore : StackExchangeRedisRateLimitStore, IClientPolicyStore + { + private readonly ClientRateLimitOptions _options; + private readonly ClientRateLimitPolicies _policies; + + public StackExchangeRedisClientPolicyStore( + IConnectionMultiplexer redis, + IOptions options = null, + IOptions policies = null) : base(redis) + { + _options = options?.Value; + _policies = policies?.Value; + } + + public async Task SeedAsync() + { + // on startup, save the IP rules defined in appsettings + if (_options != null && _policies?.ClientRules != null) + { + foreach (var rule in _policies.ClientRules) + { + await SetAsync($"{_options.ClientPolicyPrefix}_{rule.ClientId}", new ClientRateLimitPolicy { ClientId = rule.ClientId, Rules = rule.Rules }).ConfigureAwait(false); + } + } + } + } +} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisIpPolicyStore.cs b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisIpPolicyStore.cs new file mode 100644 index 00000000..8fa36a4d --- /dev/null +++ b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisIpPolicyStore.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using StackExchange.Redis; + +namespace AspNetCoreRateLimit +{ + public class StackExchangeRedisIpPolicyStore : StackExchangeRedisRateLimitStore, IIpPolicyStore + { + private readonly IpRateLimitOptions _options; + private readonly IpRateLimitPolicies _policies; + + public StackExchangeRedisIpPolicyStore( + IConnectionMultiplexer redis, + IOptions options = null, + IOptions policies = null) : base(redis) + { + _options = options?.Value; + _policies = policies?.Value; + } + + public async Task SeedAsync() + { + // on startup, save the IP rules defined in appsettings + if (_options != null && _policies != null) + { + await SetAsync($"{_options.IpPolicyPrefix}", _policies).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitCounterStore.cs b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitCounterStore.cs new file mode 100644 index 00000000..ab32322f --- /dev/null +++ b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitCounterStore.cs @@ -0,0 +1,12 @@ +using StackExchange.Redis; + +namespace AspNetCoreRateLimit +{ + public class StackExchangeRedisRateLimitCounterStore : StackExchangeRedisRateLimitStore, IRateLimitCounterStore + { + public StackExchangeRedisRateLimitCounterStore(IConnectionMultiplexer connectionMultiplexer) + : base(connectionMultiplexer) + { + } + } +} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitStore.cs b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitStore.cs new file mode 100644 index 00000000..569f4a57 --- /dev/null +++ b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitStore.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; +using StackExchange.Redis; +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace AspNetCoreRateLimit +{ + public class StackExchangeRedisRateLimitStore : IRateLimitStore + { + private readonly IConnectionMultiplexer _redis; + + public StackExchangeRedisRateLimitStore(IConnectionMultiplexer redis) + { + _redis = redis ?? throw new ArgumentNullException(nameof(redis)); + } + + public async Task SetAsync(string id, T entry, TimeSpan? expirationTime = null, CancellationToken cancellationToken = default) + { + // Throw an exception if the key could not be set + if (!await _redis.GetDatabase().StringSetAsync(id, JsonConvert.SerializeObject(entry), expirationTime)) + { + throw new ExternalException($"Failed to set key {id}"); + } + } + + public Task ExistsAsync(string id, CancellationToken cancellationToken = default) + { + return _redis.GetDatabase().KeyExistsAsync(id); + } + + public async Task GetAsync(string id, CancellationToken cancellationToken = default) + { + var stored = await _redis.GetDatabase().StringGetAsync(id); + if (stored.HasValue) + { + return JsonConvert.DeserializeObject(stored.ToString()); + } + + return default; + } + + public Task RemoveAsync(string id, CancellationToken cancellationToken = default) + { + // Don't throw an exception if the key doesn't exist + return _redis.GetDatabase().KeyDeleteAsync(id); + } + } +} \ No newline at end of file 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; From a88e84a335289145e493a0f438ca489c103d7d56 Mon Sep 17 00:00:00 2001 From: Nick Cromwell Date: Sat, 19 Dec 2020 14:07:23 -0500 Subject: [PATCH 2/2] Remove need for ProcessingFactory in favor of injectible ProcessingStrategy; remove StackExchange code and references --- .../AspNetCoreRateLimit.csproj | 3 -- .../Core/ClientRateLimitProcessor.cs | 10 ++-- .../Core/IRateLimitProcessor.cs | 3 -- .../Core/IpRateLimitProcessor.cs | 10 ++-- .../AsyncKeyLockProcessingStrategy.cs | 13 ++--- .../IProcessingStrategy.cs | 2 +- .../IProcessingStrategyFactory.cs | 7 --- .../ProcessingStrategy.cs | 17 +++---- .../ProcessingStrategyFactory.cs | 27 ---------- .../StackExchangeRedisProcessingStrategy.cs | 44 ---------------- .../Middleware/ClientRateLimitMiddleware.cs | 4 +- .../Middleware/IpRateLimitMiddleware.cs | 8 +-- src/AspNetCoreRateLimit/StartupExtensions.cs | 12 +---- .../StackExchangeRedisClientPolicyStore.cs | 34 ------------- .../StackExchangeRedisIpPolicyStore.cs | 31 ------------ ...StackExchangeRedisRateLimitCounterStore.cs | 12 ----- .../StackExchangeRedisRateLimitStore.cs | 50 ------------------- 17 files changed, 31 insertions(+), 256 deletions(-) delete mode 100644 src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategyFactory.cs delete mode 100644 src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategyFactory.cs delete mode 100644 src/AspNetCoreRateLimit/Core/ProcessingStrategies/StackExchangeRedisProcessingStrategy.cs delete mode 100644 src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisClientPolicyStore.cs delete mode 100644 src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisIpPolicyStore.cs delete mode 100644 src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitCounterStore.cs delete mode 100644 src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitStore.cs diff --git a/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj b/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj index 580a5d12..0a2e31b3 100644 --- a/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj +++ b/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj @@ -27,7 +27,6 @@ - @@ -35,7 +34,6 @@ - @@ -43,7 +41,6 @@ - diff --git a/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs b/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs index 61f4bc43..5ff509c0 100644 --- a/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs +++ b/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs @@ -9,18 +9,20 @@ public class ClientRateLimitProcessor : RateLimitProcessor, IRateLimitProcessor private readonly ClientRateLimitOptions _options; private readonly IProcessingStrategy _processingStrategy; private readonly IRateLimitStore _policyStore; + private readonly ICounterKeyBuilder _counterKeyBuilder; public ClientRateLimitProcessor( - IProcessingStrategyFactory processingStrategyFactory, ClientRateLimitOptions options, IRateLimitCounterStore counterStore, IClientPolicyStore policyStore, - IRateLimitConfiguration config) + IRateLimitConfiguration config, + IProcessingStrategy processingStrategy) : base(options) { _options = options; _policyStore = policyStore; - _processingStrategy = processingStrategyFactory.CreateProcessingStrategy(counterStore, new ClientCounterKeyBuilder(options), config, options); + _counterKeyBuilder = new ClientCounterKeyBuilder(options); + _processingStrategy = processingStrategy; } public async Task> GetMatchingRulesAsync(ClientRequestIdentity identity, CancellationToken cancellationToken = default) @@ -32,7 +34,7 @@ public async Task> GetMatchingRulesAsync(ClientReques public async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default) { - return await _processingStrategy.ProcessRequestAsync(requestIdentity, rule, cancellationToken); + 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 9abbfc95..a5191ad1 100644 --- a/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs +++ b/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs @@ -10,18 +10,20 @@ public class IpRateLimitProcessor : RateLimitProcessor, IRateLimitProcessor private readonly IpRateLimitOptions _options; private readonly IRateLimitStore _policyStore; private readonly IProcessingStrategy _processingStrategy; + private readonly ICounterKeyBuilder _counterKeyBuilder; public IpRateLimitProcessor( - IProcessingStrategyFactory processingStrategyFactory, IpRateLimitOptions options, IRateLimitCounterStore counterStore, IIpPolicyStore policyStore, - IRateLimitConfiguration config) + IRateLimitConfiguration config, + IProcessingStrategy processingStrategy) : base(options) { _options = options; _policyStore = policyStore; - _processingStrategy = processingStrategyFactory.CreateProcessingStrategy(counterStore, new IpCounterKeyBuilder(options), config, options); + _counterKeyBuilder = new IpCounterKeyBuilder(options); + _processingStrategy = processingStrategy; } @@ -47,7 +49,7 @@ public async Task> GetMatchingRulesAsync(ClientReques public async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default) { - return await _processingStrategy.ProcessRequestAsync(requestIdentity, rule, cancellationToken); + 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 index 9cc31465..6b2b6e0d 100644 --- a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/AsyncKeyLockProcessingStrategy.cs +++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/AsyncKeyLockProcessingStrategy.cs @@ -6,25 +6,20 @@ namespace AspNetCoreRateLimit { public class AsyncKeyLockProcessingStrategy : ProcessingStrategy { - private readonly RateLimitOptions _options; private readonly IRateLimitCounterStore _counterStore; - private readonly ICounterKeyBuilder _counterKeyBuilder; private readonly IRateLimitConfiguration _config; - public AsyncKeyLockProcessingStrategy(IRateLimitCounterStore counterStore, ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options) - : base(counterKeyBuilder, config, options) + public AsyncKeyLockProcessingStrategy(IRateLimitCounterStore counterStore, IRateLimitConfiguration config) + : base(config) { _counterStore = counterStore; - _counterKeyBuilder = counterKeyBuilder; _config = config; - _options = options; } - /// The key-lock used for limiting requests. private static readonly AsyncKeyLock AsyncLock = new AsyncKeyLock(); - public override async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default) + public override async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, ICounterKeyBuilder counterKeyBuilder, RateLimitOptions rateLimitOptions, CancellationToken cancellationToken = default) { var counter = new RateLimitCounter { @@ -32,7 +27,7 @@ public override async Task ProcessRequestAsync(ClientRequestId Count = 1 }; - var counterId = BuildCounterKey(requestIdentity, rule); + var counterId = BuildCounterKey(requestIdentity, rule, counterKeyBuilder, rateLimitOptions); // serial reads and writes on same key using (await AsyncLock.WriterLockAsync(counterId).ConfigureAwait(false)) diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategy.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategy.cs index e15b3b2f..9dcadf24 100644 --- a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategy.cs +++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategy.cs @@ -5,6 +5,6 @@ namespace AspNetCoreRateLimit { public interface IProcessingStrategy { - Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default); + 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/IProcessingStrategyFactory.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategyFactory.cs deleted file mode 100644 index 75c506a9..00000000 --- a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategyFactory.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace AspNetCoreRateLimit -{ - public interface IProcessingStrategyFactory - { - ProcessingStrategy CreateProcessingStrategy(IRateLimitCounterStore counterStore, ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options); - } -} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategy.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategy.cs index 2cd32efe..f366b089 100644 --- a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategy.cs +++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategy.cs @@ -8,23 +8,20 @@ namespace AspNetCoreRateLimit { public abstract class ProcessingStrategy : IProcessingStrategy { - private readonly RateLimitOptions _options; - private readonly ICounterKeyBuilder _counterKeyBuilder; private readonly IRateLimitConfiguration _config; - public ProcessingStrategy(ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options) - : base() + protected ProcessingStrategy(IRateLimitConfiguration config) { - _counterKeyBuilder = counterKeyBuilder; _config = config; - _options = options; } - protected virtual string BuildCounterKey(ClientRequestIdentity requestIdentity, RateLimitRule rule) + 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); + var key = counterKeyBuilder.Build(requestIdentity, rule); - if (_options.EnableEndpointRateLimiting && _config.EndpointCounterKeyBuilder != null) + if (rateLimitOptions.EnableEndpointRateLimiting && _config.EndpointCounterKeyBuilder != null) { key += _config.EndpointCounterKeyBuilder.Build(requestIdentity, rule); } @@ -36,7 +33,5 @@ protected virtual string BuildCounterKey(ClientRequestIdentity requestIdentity, return Convert.ToBase64String(hash); } - - public abstract Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default); } } \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategyFactory.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategyFactory.cs deleted file mode 100644 index a8b7952d..00000000 --- a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategyFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using StackExchange.Redis; - -namespace AspNetCoreRateLimit -{ - - public class ProcessingStrategyFactory : IProcessingStrategyFactory - { - private readonly IConnectionMultiplexer _connectionMultiplexer; - - public ProcessingStrategyFactory(IConnectionMultiplexer connectionMultiplexer = null) - { - _connectionMultiplexer = connectionMultiplexer; - } - - public ProcessingStrategy CreateProcessingStrategy(IRateLimitCounterStore counterStore, ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options) - { - return counterStore switch - { - MemoryCacheRateLimitCounterStore => new AsyncKeyLockProcessingStrategy(counterStore, counterKeyBuilder, config, options), - DistributedCacheRateLimitCounterStore => new AsyncKeyLockProcessingStrategy(counterStore, counterKeyBuilder, config, options), - StackExchangeRedisRateLimitCounterStore => new StackExchangeRedisProcessingStrategy(_connectionMultiplexer, counterStore, counterKeyBuilder, config, options), - _ => throw new ArgumentException("Unsupported instance of IRateLimitCounterStore provided") - }; - } - } -} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/StackExchangeRedisProcessingStrategy.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/StackExchangeRedisProcessingStrategy.cs deleted file mode 100644 index ecb73215..00000000 --- a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/StackExchangeRedisProcessingStrategy.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using StackExchange.Redis; - -namespace AspNetCoreRateLimit -{ - public class StackExchangeRedisProcessingStrategy : ProcessingStrategy - { - private readonly IConnectionMultiplexer _connectionMultiplexer; - private readonly IRateLimitConfiguration _config; - - public StackExchangeRedisProcessingStrategy(IConnectionMultiplexer connectionMultiplexer, IRateLimitCounterStore counterStore, ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options) - : base(counterKeyBuilder, config, options) - { - _connectionMultiplexer = connectionMultiplexer ?? throw new ArgumentException("IConnectionMultiplexer was null. Ensure StackExchange.Redis was successfully registered"); - _config = config; - } - - - static private readonly LuaScript _atomicIncrement = LuaScript.Prepare("local count count = redis.call(\"INCRBYFLOAT\", @key, tonumber(@delta)) if tonumber(count) == @delta then redis.call(\"EXPIRE\", @key, @timeout) end return count"); - - public override async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default) - { - var counterId = BuildCounterKey(requestIdentity, rule); - return await IncrementAsync(counterId, rule.PeriodTimespan.Value, _config.RateIncrementer); - } - - public async Task IncrementAsync(string counterId, TimeSpan interval, Func RateIncrementer = null) - { - var now = DateTime.UtcNow; - var numberOfIntervals = now.Ticks / interval.Ticks; - var intervalStart = new DateTime(numberOfIntervals * interval.Ticks, DateTimeKind.Utc); - - // Call the Lua script - var count = await _connectionMultiplexer.GetDatabase().ScriptEvaluateAsync(_atomicIncrement, new { key = counterId, timeout = interval.TotalSeconds, delta = RateIncrementer?.Invoke() ?? 1D }); - return new RateLimitCounter - { - Count = (double)count, - Timestamp = intervalStart - }; - } - } -} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs index f11781c9..ae52b790 100644 --- a/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs +++ b/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs @@ -9,13 +9,13 @@ public class ClientRateLimitMiddleware : RateLimitMiddleware _logger; public ClientRateLimitMiddleware(RequestDelegate next, + IProcessingStrategy processingStrategy, IOptions options, - IProcessingStrategyFactory processingStrategyFactory, IRateLimitCounterStore counterStore, IClientPolicyStore policyStore, IRateLimitConfiguration config, ILogger logger) - : base(next, options?.Value, new ClientRateLimitProcessor(processingStrategyFactory, 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 7b6a338a..86e1bb1e 100644 --- a/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs +++ b/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs @@ -9,14 +9,14 @@ public class IpRateLimitMiddleware : RateLimitMiddleware private readonly ILogger _logger; public IpRateLimitMiddleware(RequestDelegate next, - IProcessingStrategyFactory processingStrategyFactory, + IProcessingStrategy processingStrategy, IOptions options, IRateLimitCounterStore counterStore, IIpPolicyStore policyStore, IRateLimitConfiguration config, - ILogger logger) - : base(next, options?.Value, new IpRateLimitProcessor(processingStrategyFactory, 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/StartupExtensions.cs b/src/AspNetCoreRateLimit/StartupExtensions.cs index 1cc9615c..bfe493ae 100644 --- a/src/AspNetCoreRateLimit/StartupExtensions.cs +++ b/src/AspNetCoreRateLimit/StartupExtensions.cs @@ -10,7 +10,7 @@ public static IServiceCollection AddMemoryCacheStores(this IServiceCollection se services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); return services; } @@ -19,15 +19,7 @@ public static IServiceCollection AddDistributedCacheStores(this IServiceCollecti services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - return services; - } - - public static IServiceCollection AddStackExchangeRedisStores(this IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisClientPolicyStore.cs b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisClientPolicyStore.cs deleted file mode 100644 index 992d503e..00000000 --- a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisClientPolicyStore.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Options; -using StackExchange.Redis; - -namespace AspNetCoreRateLimit -{ - public class StackExchangeRedisClientPolicyStore : StackExchangeRedisRateLimitStore, IClientPolicyStore - { - private readonly ClientRateLimitOptions _options; - private readonly ClientRateLimitPolicies _policies; - - public StackExchangeRedisClientPolicyStore( - IConnectionMultiplexer redis, - IOptions options = null, - IOptions policies = null) : base(redis) - { - _options = options?.Value; - _policies = policies?.Value; - } - - public async Task SeedAsync() - { - // on startup, save the IP rules defined in appsettings - if (_options != null && _policies?.ClientRules != null) - { - foreach (var rule in _policies.ClientRules) - { - await SetAsync($"{_options.ClientPolicyPrefix}_{rule.ClientId}", new ClientRateLimitPolicy { ClientId = rule.ClientId, Rules = rule.Rules }).ConfigureAwait(false); - } - } - } - } -} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisIpPolicyStore.cs b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisIpPolicyStore.cs deleted file mode 100644 index 8fa36a4d..00000000 --- a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisIpPolicyStore.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Options; -using StackExchange.Redis; - -namespace AspNetCoreRateLimit -{ - public class StackExchangeRedisIpPolicyStore : StackExchangeRedisRateLimitStore, IIpPolicyStore - { - private readonly IpRateLimitOptions _options; - private readonly IpRateLimitPolicies _policies; - - public StackExchangeRedisIpPolicyStore( - IConnectionMultiplexer redis, - IOptions options = null, - IOptions policies = null) : base(redis) - { - _options = options?.Value; - _policies = policies?.Value; - } - - public async Task SeedAsync() - { - // on startup, save the IP rules defined in appsettings - if (_options != null && _policies != null) - { - await SetAsync($"{_options.IpPolicyPrefix}", _policies).ConfigureAwait(false); - } - } - } -} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitCounterStore.cs b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitCounterStore.cs deleted file mode 100644 index ab32322f..00000000 --- a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitCounterStore.cs +++ /dev/null @@ -1,12 +0,0 @@ -using StackExchange.Redis; - -namespace AspNetCoreRateLimit -{ - public class StackExchangeRedisRateLimitCounterStore : StackExchangeRedisRateLimitStore, IRateLimitCounterStore - { - public StackExchangeRedisRateLimitCounterStore(IConnectionMultiplexer connectionMultiplexer) - : base(connectionMultiplexer) - { - } - } -} \ No newline at end of file diff --git a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitStore.cs b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitStore.cs deleted file mode 100644 index 569f4a57..00000000 --- a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitStore.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Newtonsoft.Json; -using StackExchange.Redis; -using System; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -namespace AspNetCoreRateLimit -{ - public class StackExchangeRedisRateLimitStore : IRateLimitStore - { - private readonly IConnectionMultiplexer _redis; - - public StackExchangeRedisRateLimitStore(IConnectionMultiplexer redis) - { - _redis = redis ?? throw new ArgumentNullException(nameof(redis)); - } - - public async Task SetAsync(string id, T entry, TimeSpan? expirationTime = null, CancellationToken cancellationToken = default) - { - // Throw an exception if the key could not be set - if (!await _redis.GetDatabase().StringSetAsync(id, JsonConvert.SerializeObject(entry), expirationTime)) - { - throw new ExternalException($"Failed to set key {id}"); - } - } - - public Task ExistsAsync(string id, CancellationToken cancellationToken = default) - { - return _redis.GetDatabase().KeyExistsAsync(id); - } - - public async Task GetAsync(string id, CancellationToken cancellationToken = default) - { - var stored = await _redis.GetDatabase().StringGetAsync(id); - if (stored.HasValue) - { - return JsonConvert.DeserializeObject(stored.ToString()); - } - - return default; - } - - public Task RemoveAsync(string id, CancellationToken cancellationToken = default) - { - // Don't throw an exception if the key doesn't exist - return _redis.GetDatabase().KeyDeleteAsync(id); - } - } -} \ No newline at end of file