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;