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;