diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs
index 382cf374..2e442701 100644
--- a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs
+++ b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs
@@ -29,10 +29,12 @@ namespace Ocelot.Configuration.Builder
private ServiceProviderConfiguraion _serviceProviderConfiguraion;
private bool _useQos;
private QoSOptions _qosOptions;
+ public bool _enableRateLimiting;
+ public RateLimitOptions _rateLimitOptions;
public ReRouteBuilder WithLoadBalancer(string loadBalancer)
{
- _loadBalancer = loadBalancer;
+ _loadBalancer = loadBalancer;
return this;
}
@@ -161,6 +163,19 @@ namespace Ocelot.Configuration.Builder
return this;
}
+ public ReRouteBuilder WithEnableRateLimiting(bool input)
+ {
+ _enableRateLimiting = input;
+ return this;
+ }
+
+ public ReRouteBuilder WithRateLimitOptions(RateLimitOptions input)
+ {
+ _rateLimitOptions = input;
+ return this;
+ }
+
+
public ReRoute Build()
{
return new ReRoute(
@@ -185,7 +200,9 @@ namespace Ocelot.Configuration.Builder
_loadBalancerKey,
_serviceProviderConfiguraion,
_useQos,
- _qosOptions);
+ _qosOptions,
+ _enableRateLimiting,
+ _rateLimitOptions);
}
}
}
diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs
index 380e193f..55f31bfa 100644
--- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs
+++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs
@@ -109,13 +109,17 @@ namespace Ocelot.Configuration.Creator
var authOptionsForRoute = BuildAuthenticationOptions(fileReRoute);
var claimsToHeaders = BuildAddThingsToRequest(fileReRoute.AddHeadersToRequest);
-
+
var claimsToClaims = BuildAddThingsToRequest(fileReRoute.AddClaimsToRequest);
var claimsToQueries = BuildAddThingsToRequest(fileReRoute.AddQueriesToRequest);
var qosOptions = BuildQoSOptions(fileReRoute);
+ var enableRateLimiting = IsEnableRateLimiting(fileReRoute);
+
+ var rateLimitOption = BuildRateLimitOptions(fileReRoute, globalConfiguration, enableRateLimiting);
+
var reRoute = new ReRouteBuilder()
.WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate)
.WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate)
@@ -139,13 +143,34 @@ namespace Ocelot.Configuration.Creator
.WithServiceProviderConfiguraion(serviceProviderConfiguration)
.WithIsQos(isQos)
.WithQosOptions(qosOptions)
- .Build();
-
+ .WithEnableRateLimiting(enableRateLimiting)
+ .WithRateLimitOptions(rateLimitOption)
+ .Build();
await SetupLoadBalancer(reRoute);
SetupQosProvider(reRoute);
return reRoute;
}
+ private static RateLimitOptions BuildRateLimitOptions(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration, bool enableRateLimiting)
+ {
+ RateLimitOptions rateLimitOption = null;
+ if (enableRateLimiting)
+ {
+ rateLimitOption = new RateLimitOptions(enableRateLimiting, globalConfiguration.RateLimitOptions.ClientIdHeader,
+ fileReRoute.RateLimitOptions.ClientWhitelist, globalConfiguration.RateLimitOptions.DisableRateLimitHeaders,
+ globalConfiguration.RateLimitOptions.QuotaExceededMessage, globalConfiguration.RateLimitOptions.RateLimitCounterPrefix,
+ new RateLimitRule(fileReRoute.RateLimitOptions.Period, TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan), fileReRoute.RateLimitOptions.Limit)
+ , globalConfiguration.RateLimitOptions.HttpStatusCode);
+ }
+
+ return rateLimitOption;
+ }
+
+ private static bool IsEnableRateLimiting(FileReRoute fileReRoute)
+ {
+ return (fileReRoute.RateLimitOptions != null && fileReRoute.RateLimitOptions.EnableRateLimiting) ? true : false;
+ }
+
private QoSOptions BuildQoSOptions(FileReRoute fileReRoute)
{
return new QoSOptionsBuilder()
diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs
index f414bc83..2264bee9 100644
--- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs
+++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs
@@ -5,8 +5,12 @@
public FileGlobalConfiguration()
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider();
+ RateLimitOptions = new FileRateLimitOptions();
}
public string RequestIdKey { get; set; }
+
public FileServiceDiscoveryProvider ServiceDiscoveryProvider {get;set;}
+
+ public FileRateLimitOptions RateLimitOptions { get; set; }
}
}
diff --git a/src/Ocelot/Configuration/File/FileRateLimitOptions.cs b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs
new file mode 100644
index 00000000..afb3fab5
--- /dev/null
+++ b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Ocelot.Configuration.File
+{
+ public class FileRateLimitOptions
+ {
+ ///
+ /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId
+ ///
+ public string ClientIdHeader { get; set; } = "ClientId";
+
+ ///
+ /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message.
+ /// If none specified the default will be:
+ /// API calls quota exceeded! maximum admitted {0} per {1}
+ ///
+ public string QuotaExceededMessage { get; set; }
+
+ ///
+ /// Gets or sets the counter prefix, used to compose the rate limit counter cache key
+ ///
+ public string RateLimitCounterPrefix { get; set; } = "ocelot";
+
+ ///
+ /// Disables X-Rate-Limit and Rety-After headers
+ ///
+ public bool DisableRateLimitHeaders { get; set; }
+
+ ///
+ /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests)
+ ///
+ public int HttpStatusCode { get; set; } = 429;
+ }
+
+
+}
diff --git a/src/Ocelot/Configuration/File/FileRateLimitRule.cs b/src/Ocelot/Configuration/File/FileRateLimitRule.cs
new file mode 100644
index 00000000..727a9e82
--- /dev/null
+++ b/src/Ocelot/Configuration/File/FileRateLimitRule.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Ocelot.Configuration.File
+{
+
+ public class FileRateLimitRule
+ {
+ public FileRateLimitRule()
+ {
+ ClientWhitelist = new List();
+ }
+
+ public List ClientWhitelist { get; set; }
+
+ ///
+ /// Enables endpoint rate limiting based URL path and HTTP verb
+ ///
+ public bool EnableRateLimiting { get; set; }
+
+ ///
+ /// Rate limit period as in 1s, 1m, 1h
+ ///
+ public string Period { get; set; }
+
+ public double PeriodTimespan { get; set; }
+ ///
+ /// Maximum number of requests that a client can make in a defined period
+ ///
+ public long Limit { get; set; }
+ }
+}
diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs
index bdb8f87f..07562653 100644
--- a/src/Ocelot/Configuration/File/FileReRoute.cs
+++ b/src/Ocelot/Configuration/File/FileReRoute.cs
@@ -13,6 +13,7 @@ namespace Ocelot.Configuration.File
AuthenticationOptions = new FileAuthenticationOptions();
FileCacheOptions = new FileCacheOptions();
QoSOptions = new FileQoSOptions();
+ RateLimitOptions = new FileRateLimitRule();
}
public string DownstreamPathTemplate { get; set; }
@@ -32,5 +33,6 @@ namespace Ocelot.Configuration.File
public int DownstreamPort { get; set; }
public FileQoSOptions QoSOptions { get; set; }
public string LoadBalancer {get;set;}
+ public FileRateLimitRule RateLimitOptions { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Ocelot/Configuration/RateLimitOptions.cs b/src/Ocelot/Configuration/RateLimitOptions.cs
new file mode 100644
index 00000000..85a3bf78
--- /dev/null
+++ b/src/Ocelot/Configuration/RateLimitOptions.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Ocelot.Configuration
+{
+ ///
+ /// RateLimit Options
+ ///
+ public class RateLimitOptions
+ {
+ public RateLimitOptions(bool enbleRateLimiting, string clientIdHeader, List clientWhitelist,bool disableRateLimitHeaders,
+ string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode)
+ {
+ EnableRateLimiting = enbleRateLimiting;
+ ClientIdHeader = clientIdHeader;
+ ClientWhitelist = clientWhitelist?? new List();
+ DisableRateLimitHeaders = disableRateLimitHeaders;
+ QuotaExceededMessage = quotaExceededMessage;
+ RateLimitCounterPrefix = rateLimitCounterPrefix;
+ RateLimitRule = rateLimitRule;
+ HttpStatusCode = httpStatusCode;
+ }
+
+ public RateLimitRule RateLimitRule { get; private set; }
+
+ public List ClientWhitelist { get; private set; }
+
+ ///
+ /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId
+ ///
+ public string ClientIdHeader { get; private set; }
+
+ ///
+ /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests)
+ ///
+ public int HttpStatusCode { get; private set; }
+
+ ///
+ /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message.
+ /// If none specified the default will be:
+ /// API calls quota exceeded! maximum admitted {0} per {1}
+ ///
+ public string QuotaExceededMessage { get; private set; }
+
+ ///
+ /// Gets or sets the counter prefix, used to compose the rate limit counter cache key
+ ///
+ public string RateLimitCounterPrefix { get; private set; }
+
+ ///
+ /// Enables endpoint rate limiting based URL path and HTTP verb
+ ///
+ public bool EnableRateLimiting { get; private set; }
+
+ ///
+ /// Disables X-Rate-Limit and Rety-After headers
+ ///
+ public bool DisableRateLimitHeaders { get; private set; }
+ }
+
+ public class RateLimitRule
+ {
+ public RateLimitRule(string period, TimeSpan periodTimespan, long limit)
+ {
+ Period = period;
+ PeriodTimespan = periodTimespan;
+ Limit = limit;
+ }
+
+ ///
+ /// Rate limit period as in 1s, 1m, 1h,1d
+ ///
+ public string Period { get; private set; }
+
+ public TimeSpan PeriodTimespan { get; private set; }
+ ///
+ /// Maximum number of requests that a client can make in a defined period
+ ///
+ public long Limit { get; private set; }
+ }
+}
diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs
index ab9405d0..bfce7c76 100644
--- a/src/Ocelot/Configuration/ReRoute.cs
+++ b/src/Ocelot/Configuration/ReRoute.cs
@@ -28,7 +28,9 @@ namespace Ocelot.Configuration
string reRouteKey,
ServiceProviderConfiguraion serviceProviderConfiguraion,
bool isQos,
- QoSOptions qos)
+ QoSOptions qos,
+ bool enableRateLimit,
+ RateLimitOptions ratelimitOptions)
{
ReRouteKey = reRouteKey;
ServiceProviderConfiguraion = serviceProviderConfiguraion;
@@ -55,6 +57,8 @@ namespace Ocelot.Configuration
DownstreamScheme = downstreamScheme;
IsQos = isQos;
QosOptions = qos;
+ EnableEndpointRateLimiting = enableRateLimit;
+ RateLimitOptions = ratelimitOptions;
}
public string ReRouteKey {get;private set;}
@@ -79,5 +83,7 @@ namespace Ocelot.Configuration
public string DownstreamHost { get; private set; }
public int DownstreamPort { get; private set; }
public ServiceProviderConfiguraion ServiceProviderConfiguraion { get; private set; }
+ public bool EnableEndpointRateLimiting { get; private set; }
+ public RateLimitOptions RateLimitOptions { get; private set; }
}
}
\ No newline at end of file
diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs
index e9be9841..9fb8c1c9 100644
--- a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs
@@ -31,6 +31,7 @@ using Ocelot.Requester;
using Ocelot.Requester.QoS;
using Ocelot.Responder;
using Ocelot.ServiceDiscovery;
+using Ocelot.RateLimit;
namespace Ocelot.DependencyInjection
{
@@ -87,12 +88,13 @@ namespace Ocelot.DependencyInjection
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
// see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc
// could maybe use a scoped data repository
services.AddSingleton();
services.AddScoped();
-
+ services.AddMemoryCache();
return services;
}
}
diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs
index 352aa501..4d9f643c 100644
--- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs
+++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs
@@ -11,6 +11,7 @@ using Ocelot.Request.Middleware;
using Ocelot.Requester.Middleware;
using Ocelot.RequestId.Middleware;
using Ocelot.Responder.Middleware;
+using Ocelot.RateLimit.Middleware;
namespace Ocelot.Middleware
{
@@ -57,6 +58,9 @@ namespace Ocelot.Middleware
// Then we get the downstream route information
builder.UseDownstreamRouteFinderMiddleware();
+ // We check whether the request is ratelimit, and if there is no continue processing
+ builder.UseRateLimiting();
+
// Now we can look for the requestId
builder.UseRequestIdMiddleware();
diff --git a/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs b/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs
new file mode 100644
index 00000000..a2ee1202
--- /dev/null
+++ b/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs
@@ -0,0 +1,37 @@
+using Microsoft.AspNetCore.Http;
+using Ocelot.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Ocelot.RateLimit
+{
+ public class ClientRateLimitProcessor
+ {
+ private readonly IRateLimitCounterHandler _counterHandler;
+ private readonly RateLimitCore _core;
+
+ public ClientRateLimitProcessor(IRateLimitCounterHandler counterHandler)
+ {
+ _counterHandler = counterHandler;
+ _core = new RateLimitCore(_counterHandler);
+ }
+
+ public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option)
+ {
+ return _core.ProcessRequest(requestIdentity, option);
+ }
+
+ public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
+ {
+ return _core.RetryAfterFrom(timestamp, rule);
+ }
+
+ public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option)
+ {
+ return _core.GetRateLimitHeaders(context, requestIdentity, option);
+ }
+
+ }
+}
diff --git a/src/Ocelot/RateLimit/ClientRequestIdentity.cs b/src/Ocelot/RateLimit/ClientRequestIdentity.cs
new file mode 100644
index 00000000..a27bc994
--- /dev/null
+++ b/src/Ocelot/RateLimit/ClientRequestIdentity.cs
@@ -0,0 +1,18 @@
+namespace Ocelot.RateLimit
+{
+ public class ClientRequestIdentity
+ {
+ public ClientRequestIdentity(string clientId, string path, string httpverb)
+ {
+ ClientId = clientId;
+ Path = path;
+ HttpVerb = httpverb;
+ }
+
+ public string ClientId { get; private set; }
+
+ public string Path { get; private set; }
+
+ public string HttpVerb { get; private set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs b/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs
new file mode 100644
index 00000000..1db8f334
--- /dev/null
+++ b/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs
@@ -0,0 +1,45 @@
+using Microsoft.Extensions.Caching.Distributed;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Ocelot.RateLimit
+{
+ public class DistributedCacheRateLimitCounterHanlder : IRateLimitCounterHandler
+ {
+ private readonly IDistributedCache _memoryCache;
+
+ public DistributedCacheRateLimitCounterHanlder(IDistributedCache memoryCache)
+ {
+ _memoryCache = memoryCache;
+ }
+
+ public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime)
+ {
+ _memoryCache.SetString(id, JsonConvert.SerializeObject(counter), new DistributedCacheEntryOptions().SetAbsoluteExpiration(expirationTime));
+ }
+
+ public bool Exists(string id)
+ {
+ var stored = _memoryCache.GetString(id);
+ return !string.IsNullOrEmpty(stored);
+ }
+
+ public RateLimitCounter? Get(string id)
+ {
+ var stored = _memoryCache.GetString(id);
+ if (!string.IsNullOrEmpty(stored))
+ {
+ return JsonConvert.DeserializeObject(stored);
+ }
+ return null;
+ }
+
+ public void Remove(string id)
+ {
+ _memoryCache.Remove(id);
+ }
+ }
+}
diff --git a/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs
new file mode 100644
index 00000000..cb745a44
--- /dev/null
+++ b/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Ocelot.RateLimit
+{
+ public interface IRateLimitCounterHandler
+ {
+ bool Exists(string id);
+ RateLimitCounter? Get(string id);
+ void Remove(string id);
+ void Set(string id, RateLimitCounter counter, TimeSpan expirationTime);
+ }
+}
diff --git a/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs
new file mode 100644
index 00000000..9756f2ae
--- /dev/null
+++ b/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs
@@ -0,0 +1,45 @@
+using Microsoft.Extensions.Caching.Memory;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Ocelot.RateLimit
+{
+ public class MemoryCacheRateLimitCounterHandler : IRateLimitCounterHandler
+ {
+ private readonly IMemoryCache _memoryCache;
+
+ public MemoryCacheRateLimitCounterHandler(IMemoryCache memoryCache)
+ {
+ _memoryCache = memoryCache;
+ }
+
+ public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime)
+ {
+ _memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime));
+ }
+
+ public bool Exists(string id)
+ {
+ RateLimitCounter counter;
+ return _memoryCache.TryGetValue(id, out counter);
+ }
+
+ public RateLimitCounter? Get(string id)
+ {
+ RateLimitCounter counter;
+ if (_memoryCache.TryGetValue(id, out counter))
+ {
+ return counter;
+ }
+
+ return null;
+ }
+
+ public void Remove(string id)
+ {
+ _memoryCache.Remove(id);
+ }
+ }
+}
diff --git a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs
new file mode 100644
index 00000000..1b3efee1
--- /dev/null
+++ b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs
@@ -0,0 +1,138 @@
+using Ocelot.Middleware;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Ocelot.Infrastructure.RequestData;
+using Microsoft.AspNetCore.Http;
+using Ocelot.Logging;
+using Ocelot.Configuration;
+
+namespace Ocelot.RateLimit.Middleware
+{
+ public class ClientRateLimitMiddleware : OcelotMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly IOcelotLogger _logger;
+ private readonly IRateLimitCounterHandler _counterHandler;
+ private readonly ClientRateLimitProcessor _processor;
+
+ public ClientRateLimitMiddleware(RequestDelegate next,
+ IOcelotLoggerFactory loggerFactory,
+ IRequestScopedDataRepository requestScopedDataRepository,
+ IRateLimitCounterHandler counterHandler)
+ : base(requestScopedDataRepository)
+ {
+ _next = next;
+ _logger = loggerFactory.CreateLogger();
+ _counterHandler = counterHandler;
+ _processor = new ClientRateLimitProcessor(counterHandler);
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ _logger.LogDebug("started calling RateLimit middleware");
+ var options = DownstreamRoute.ReRoute.RateLimitOptions;
+ // check if rate limiting is enabled
+ if (!DownstreamRoute.ReRoute.EnableEndpointRateLimiting)
+ {
+ await _next.Invoke(context);
+ return;
+ }
+ // compute identity from request
+ var identity = SetIdentity(context, options);
+
+ // check white list
+ if (IsWhitelisted(identity, options))
+ {
+ await _next.Invoke(context);
+ return;
+ }
+
+ var rule = options.RateLimitRule;
+ if (rule.Limit > 0)
+ {
+ // increment counter
+ var counter = _processor.ProcessRequest(identity, options);
+
+ // check if limit is reached
+ if (counter.TotalRequests > rule.Limit)
+ {
+ //compute retry after value
+ var retryAfter = _processor.RetryAfterFrom(counter.Timestamp, rule);
+
+ // log blocked request
+ LogBlockedRequest(context, identity, counter, rule);
+
+ // break execution
+ await ReturnQuotaExceededResponse(context, options, retryAfter);
+ return;
+ }
+ }
+ //set X-Rate-Limit headers for the longest period
+ if (!options.DisableRateLimitHeaders)
+ {
+ var headers = _processor.GetRateLimitHeaders( context,identity, options);
+ context.Response.OnStarting(SetRateLimitHeaders, state: headers);
+ }
+
+ await _next.Invoke(context);
+ }
+
+ public virtual ClientRequestIdentity SetIdentity(HttpContext httpContext, RateLimitOptions option)
+ {
+ var clientId = "client";
+ if (httpContext.Request.Headers.Keys.Contains(option.ClientIdHeader))
+ {
+ clientId = httpContext.Request.Headers[option.ClientIdHeader].First();
+ }
+
+ return new ClientRequestIdentity(
+ clientId,
+ httpContext.Request.Path.ToString().ToLowerInvariant(),
+ httpContext.Request.Method.ToLowerInvariant()
+ );
+ }
+
+ public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOptions option)
+ {
+ if (option.ClientWhitelist.Contains(requestIdentity.ClientId))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ public virtual void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitRule rule)
+ {
+ _logger.LogDebug($"Request {identity.HttpVerb}:{identity.Path} from ClientId {identity.ClientId} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.TotalRequests}. Blocked by rule { DownstreamRoute.ReRoute.UpstreamPathTemplate }, TraceIdentifier {httpContext.TraceIdentifier}.");
+ }
+
+ public virtual Task ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitOptions option, string retryAfter)
+ {
+ var message = string.IsNullOrEmpty(option.QuotaExceededMessage) ? $"API calls quota exceeded! maximum admitted {option.RateLimitRule.Limit} per {option.RateLimitRule.Period}." : option.QuotaExceededMessage;
+
+ if (!option.DisableRateLimitHeaders)
+ {
+ httpContext.Response.Headers["Retry-After"] = retryAfter;
+ }
+
+ httpContext.Response.StatusCode = option.HttpStatusCode;
+ return httpContext.Response.WriteAsync(message);
+ }
+
+ private Task SetRateLimitHeaders(object rateLimitHeaders)
+ {
+ var headers = (RateLimitHeaders)rateLimitHeaders;
+
+ headers.Context.Response.Headers["X-Rate-Limit-Limit"] = headers.Limit;
+ headers.Context.Response.Headers["X-Rate-Limit-Remaining"] = headers.Remaining;
+ headers.Context.Response.Headers["X-Rate-Limit-Reset"] = headers.Reset;
+
+ return Task.CompletedTask;
+ }
+
+ }
+}
+
+
diff --git a/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs b/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs
new file mode 100644
index 00000000..b27d6d9d
--- /dev/null
+++ b/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Builder;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Ocelot.RateLimit.Middleware
+{
+ public static class RateLimitMiddlewareExtensions
+ {
+ public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder)
+ {
+ return builder.UseMiddleware();
+ }
+ }
+}
diff --git a/src/Ocelot/RateLimit/RateLimitCore.cs b/src/Ocelot/RateLimit/RateLimitCore.cs
new file mode 100644
index 00000000..07627f8d
--- /dev/null
+++ b/src/Ocelot/RateLimit/RateLimitCore.cs
@@ -0,0 +1,125 @@
+using Microsoft.AspNetCore.Http;
+using Ocelot.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Ocelot.RateLimit
+{
+ public class RateLimitCore
+ {
+ private readonly IRateLimitCounterHandler _counterHandler;
+ private static readonly object _processLocker = new object();
+
+ public RateLimitCore(IRateLimitCounterHandler counterStore)
+ {
+ _counterHandler = counterStore;
+ }
+
+ public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option)
+ {
+ RateLimitCounter counter = new RateLimitCounter(DateTime.UtcNow, 1);
+ var rule = option.RateLimitRule;
+
+ var counterId = ComputeCounterKey(requestIdentity, option);
+
+ // serial reads and writes
+ lock (_processLocker)
+ {
+ var entry = _counterHandler.Get(counterId);
+ if (entry.HasValue)
+ {
+ // entry has not expired
+ if (entry.Value.Timestamp + rule.PeriodTimespan >= DateTime.UtcNow)
+ {
+ // increment request count
+ var totalRequests = entry.Value.TotalRequests + 1;
+
+ // deep copy
+ counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests);
+
+ }
+ }
+ // stores: id (string) - timestamp (datetime) - total_requests (long)
+ _counterHandler.Set(counterId, counter, rule.PeriodTimespan);
+ }
+
+ return counter;
+ }
+
+ public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option)
+ {
+ var rule = option.RateLimitRule;
+ RateLimitHeaders headers = null;
+ var counterId = ComputeCounterKey(requestIdentity, option);
+ var entry = _counterHandler.Get(counterId);
+ if (entry.HasValue)
+ {
+ headers = new RateLimitHeaders(context, rule.Period,
+ (rule.Limit - entry.Value.TotalRequests).ToString(),
+ (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)
+ );
+ }
+ else
+ {
+ headers = new RateLimitHeaders(context,
+ rule.Period,
+ rule.Limit.ToString(),
+ (DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo));
+
+ }
+
+ return headers;
+ }
+
+ public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option)
+ {
+ var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}";
+
+ var idBytes = Encoding.UTF8.GetBytes(key);
+
+ byte[] hashBytes;
+
+ using (var algorithm = SHA1.Create())
+ {
+ hashBytes = algorithm.ComputeHash(idBytes);
+ }
+
+ return BitConverter.ToString(hashBytes).Replace("-", string.Empty);
+ }
+
+ public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule)
+ {
+ var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds);
+ var retryAfter = Convert.ToInt32(rule.PeriodTimespan.TotalSeconds);
+ retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1;
+ return retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture);
+ }
+
+ public TimeSpan ConvertToTimeSpan(string timeSpan)
+ {
+ var l = timeSpan.Length - 1;
+ var value = timeSpan.Substring(0, l);
+ var type = timeSpan.Substring(l, 1);
+
+ switch (type)
+ {
+ case "d":
+ return TimeSpan.FromDays(double.Parse(value));
+ case "h":
+ return TimeSpan.FromHours(double.Parse(value));
+ case "m":
+ return TimeSpan.FromMinutes(double.Parse(value));
+ case "s":
+ return TimeSpan.FromSeconds(double.Parse(value));
+ default:
+ throw new FormatException($"{timeSpan} can't be converted to TimeSpan, unknown type {type}");
+ }
+ }
+
+ }
+}
diff --git a/src/Ocelot/RateLimit/RateLimitCounter.cs b/src/Ocelot/RateLimit/RateLimitCounter.cs
new file mode 100644
index 00000000..42dd03b7
--- /dev/null
+++ b/src/Ocelot/RateLimit/RateLimitCounter.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Ocelot.RateLimit
+{
+ ///
+ /// Stores the initial access time and the numbers of calls made from that point
+ ///
+ public struct RateLimitCounter
+ {
+ public RateLimitCounter(DateTime timestamp, long totalRequest)
+ {
+ Timestamp = timestamp;
+ TotalRequests = totalRequest;
+ }
+
+ public DateTime Timestamp { get; private set; }
+
+ public long TotalRequests { get; private set; }
+ }
+}
diff --git a/src/Ocelot/RateLimit/RateLimitHeaders.cs b/src/Ocelot/RateLimit/RateLimitHeaders.cs
new file mode 100644
index 00000000..909f656d
--- /dev/null
+++ b/src/Ocelot/RateLimit/RateLimitHeaders.cs
@@ -0,0 +1,27 @@
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Ocelot.RateLimit
+{
+ public class RateLimitHeaders
+ {
+ public RateLimitHeaders(HttpContext context, string limit, string remaining, string reset)
+ {
+ Context = context;
+ Limit = limit;
+ Remaining = remaining;
+ Reset = reset;
+ }
+
+ public HttpContext Context { get; private set; }
+
+ public string Limit { get; private set; }
+
+ public string Remaining { get; private set; }
+
+ public string Reset { get; private set; }
+ }
+}
diff --git a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs
new file mode 100644
index 00000000..959777c6
--- /dev/null
+++ b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs
@@ -0,0 +1,165 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Ocelot.Configuration.File;
+using Shouldly;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using TestStack.BDDfy;
+using Xunit;
+
+namespace Ocelot.AcceptanceTests
+{
+ public class ClientRateLimitTests : IDisposable
+ {
+ private IWebHost _builder;
+ private readonly Steps _steps;
+ private int _counterOne;
+
+
+ public ClientRateLimitTests()
+ {
+ _steps = new Steps();
+ }
+
+
+ public void Dispose()
+ {
+ _builder?.Dispose();
+ _steps.Dispose();
+ }
+
+ [Fact]
+ public void should_call_withratelimiting()
+ {
+ var configuration = new FileConfiguration
+ {
+ ReRoutes = new List
+ {
+ new FileReRoute
+ {
+ DownstreamPathTemplate = "/api/ClientRateLimit",
+ DownstreamPort = 51879,
+ DownstreamScheme = "http",
+ DownstreamHost = "localhost",
+ UpstreamPathTemplate = "/api/ClientRateLimit",
+ UpstreamHttpMethod = "Get",
+ RequestIdKey = _steps.RequestIdKey,
+
+ RateLimitOptions = new FileRateLimitRule()
+ {
+ EnableRateLimiting = true,
+ ClientWhitelist = new List(),
+ Limit = 3,
+ Period = "1s",
+ PeriodTimespan = 1000
+ }
+ }
+ },
+ GlobalConfiguration = new FileGlobalConfiguration()
+ {
+ RateLimitOptions = new FileRateLimitOptions()
+ {
+ ClientIdHeader = "ClientId",
+ DisableRateLimitHeaders = false,
+ QuotaExceededMessage = "",
+ RateLimitCounterPrefix = "",
+ HttpStatusCode = 428
+
+ },
+ RequestIdKey ="oceclientrequest"
+ }
+ };
+
+ this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/ClientRateLimit"))
+ .And(x => _steps.GivenThereIsAConfiguration(configuration))
+ .And(x => _steps.GivenOcelotIsRunning())
+ .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1))
+ .Then(x => _steps.ThenTheStatusCodeShouldBe(200))
+ .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2))
+ .Then(x => _steps.ThenTheStatusCodeShouldBe(200))
+ .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1))
+ .Then(x => _steps.ThenTheStatusCodeShouldBe(428))
+ .BDDfy();
+ }
+
+
+ [Fact]
+ public void should_call_middleware_withWhitelistClient()
+ {
+ var configuration = new FileConfiguration
+ {
+ ReRoutes = new List
+ {
+ new FileReRoute
+ {
+ DownstreamPathTemplate = "/api/ClientRateLimit",
+ DownstreamPort = 51879,
+ DownstreamScheme = "http",
+ DownstreamHost = "localhost",
+ UpstreamPathTemplate = "/api/ClientRateLimit",
+ UpstreamHttpMethod = "Get",
+ RequestIdKey = _steps.RequestIdKey,
+
+ RateLimitOptions = new FileRateLimitRule()
+ {
+ EnableRateLimiting = true,
+ ClientWhitelist = new List() { "ocelotclient1"},
+ Limit = 3,
+ Period = "1s",
+ PeriodTimespan = 100
+ }
+ }
+ },
+ GlobalConfiguration = new FileGlobalConfiguration()
+ {
+ RateLimitOptions = new FileRateLimitOptions()
+ {
+ ClientIdHeader = "ClientId",
+ DisableRateLimitHeaders = false,
+ QuotaExceededMessage = "",
+ RateLimitCounterPrefix = ""
+ },
+ RequestIdKey = "oceclientrequest"
+ }
+ };
+
+ this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/ClientRateLimit"))
+ .And(x => _steps.GivenThereIsAConfiguration(configuration))
+ .And(x => _steps.GivenOcelotIsRunning())
+ .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 4))
+ .Then(x => _steps.ThenTheStatusCodeShouldBe(200))
+ .BDDfy();
+ }
+
+
+ private void GivenThereIsAServiceRunningOn(string url)
+ {
+ _builder = new WebHostBuilder()
+ .UseUrls(url)
+ .UseKestrel()
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseUrls(url)
+ .Configure(app =>
+ {
+ app.Run(context =>
+ {
+ _counterOne++;
+ context.Response.StatusCode = 200;
+ context.Response.WriteAsync(_counterOne.ToString());
+ return Task.CompletedTask;
+ });
+ })
+ .Build();
+
+ _builder.Start();
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs
index 9b5faa04..ebced901 100644
--- a/test/Ocelot.AcceptanceTests/Steps.cs
+++ b/test/Ocelot.AcceptanceTests/Steps.cs
@@ -11,6 +11,7 @@ using CacheManager.Core;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Ocelot.Configuration.File;
@@ -100,7 +101,6 @@ namespace Ocelot.AcceptanceTests
})
.WithDictionaryHandle();
};
-
s.AddOcelotOutputCaching(settings);
s.AddOcelotFileConfiguration(configuration);
s.AddOcelot();
@@ -183,6 +183,17 @@ namespace Ocelot.AcceptanceTests
count.ShouldBeGreaterThan(0);
}
+ public void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url, int times)
+ {
+ for (int i = 0; i < times; i++)
+ {
+ var clientId = "ocelotclient1";
+ var request = new HttpRequestMessage(new HttpMethod("GET"), url);
+ request.Headers.Add("ClientId", clientId);
+ _response = _ocelotClient.SendAsync(request).Result;
+ }
+ }
+
public void WhenIGetUrlOnTheApiGateway(string url, string requestId)
{
_ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId);
@@ -210,6 +221,13 @@ namespace Ocelot.AcceptanceTests
_response.StatusCode.ShouldBe(expectedHttpStatusCode);
}
+
+ public void ThenTheStatusCodeShouldBe(int expectedHttpStatusCode)
+ {
+ var responseStatusCode = (int)_response.StatusCode;
+ responseStatusCode.ShouldBe(expectedHttpStatusCode);
+ }
+
public void Dispose()
{
_ocelotClient?.Dispose();
diff --git a/test/Ocelot.AcceptanceTests/configuration.json b/test/Ocelot.AcceptanceTests/configuration.json
new file mode 100755
index 00000000..aba4cdf9
--- /dev/null
+++ b/test/Ocelot.AcceptanceTests/configuration.json
@@ -0,0 +1 @@
+{"ReRoutes":[{"DownstreamPathTemplate":"41879/","UpstreamPathTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false,"ServiceName":null,"DownstreamScheme":"http","DownstreamHost":"localhost","DownstreamPort":41879,"QoSOptions":{"ExceptionsAllowedBeforeBreaking":0,"DurationOfBreak":0,"TimeoutValue":0},"LoadBalancer":null,"RateLimitOptions":{"ClientWhitelist":[],"EnableRateLimiting":false,"Period":null,"PeriodTimespan":0.0,"Limit":0}}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"RateLimitOptions":{"ClientIdHeader":"ClientId","QuotaExceededMessage":null,"RateLimitCounterPrefix":"ocelot","DisableRateLimitHeaders":false,"HttpStatusCode":429}}}
\ No newline at end of file
diff --git a/test/Ocelot.AcceptanceTests/project.json b/test/Ocelot.AcceptanceTests/project.json
index f1aa378b..2dd3094e 100644
--- a/test/Ocelot.AcceptanceTests/project.json
+++ b/test/Ocelot.AcceptanceTests/project.json
@@ -23,7 +23,6 @@
"Microsoft.AspNetCore.Http": "1.1.0",
"Microsoft.DotNet.InternalAbstractions": "1.0.0",
"Ocelot": "0.0.0-dev",
- "xunit": "2.2.0-beta2-build3300",
"dotnet-test-xunit": "2.2.0-preview2-build1029",
"Ocelot.ManualTest": "0.0.0-dev",
"Microsoft.AspNetCore.TestHost": "1.1.0",
@@ -33,7 +32,9 @@
"Microsoft.NETCore.App": "1.1.0",
"Shouldly": "2.8.2",
"TestStack.BDDfy": "4.3.2",
- "Consul": "0.7.2.1"
+ "Consul": "0.7.2.1",
+ "Microsoft.Extensions.Caching.Memory": "1.1.0",
+ "xunit": "2.2.0-rc1-build3507"
},
"runtimes": {
"win10-x64": {},
diff --git a/test/Ocelot.ManualTest/Startup.cs b/test/Ocelot.ManualTest/Startup.cs
index 70448fcf..767aed43 100644
--- a/test/Ocelot.ManualTest/Startup.cs
+++ b/test/Ocelot.ManualTest/Startup.cs
@@ -37,7 +37,6 @@ namespace Ocelot.ManualTest
})
.WithDictionaryHandle();
};
-
services.AddOcelotOutputCaching(settings);
services.AddOcelotFileConfiguration(Configuration);
services.AddOcelot();
diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs
new file mode 100644
index 00000000..8b9b40ee
--- /dev/null
+++ b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs
@@ -0,0 +1,149 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using Ocelot.Infrastructure.RequestData;
+using Ocelot.RateLimit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Ocelot.Logging;
+using System.IO;
+using Ocelot.RateLimit.Middleware;
+using Ocelot.DownstreamRouteFinder;
+using Ocelot.Responses;
+using Xunit;
+using TestStack.BDDfy;
+using Ocelot.Configuration.Builder;
+using Shouldly;
+using Ocelot.Configuration;
+
+namespace Ocelot.UnitTests.RateLimit
+{
+ public class ClientRateLimitMiddlewareTests
+ {
+ private readonly Mock _scopedRepository;
+ private readonly string _url;
+ private readonly TestServer _server;
+ private readonly HttpClient _client;
+ private OkResponse _downstreamRoute;
+ private int responseStatusCode;
+
+ public ClientRateLimitMiddlewareTests()
+ {
+ _url = "http://localhost:51879/api/ClientRateLimit";
+ _scopedRepository = new Mock();
+ var builder = new WebHostBuilder()
+ .ConfigureServices(x =>
+ {
+ x.AddSingleton();
+ x.AddLogging();
+ x.AddMemoryCache();
+ x.AddSingleton();
+ x.AddSingleton(_scopedRepository.Object);
+ })
+ .UseUrls(_url)
+ .UseKestrel()
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseUrls(_url)
+ .Configure(app =>
+ {
+ app.UseRateLimiting();
+ app.Run(async context =>
+ {
+ context.Response.StatusCode = 200;
+ await context.Response.WriteAsync("This is ratelimit test");
+ });
+ });
+
+ _server = new TestServer(builder);
+ _client = _server.CreateClient();
+ }
+
+
+ [Fact]
+ public void should_call_middleware_and_ratelimiting()
+ {
+ var downstreamRoute = new DownstreamRoute(new List(),
+ new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions(
+ new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List(), false, "", "", new Ocelot.Configuration.RateLimitRule("1s", TimeSpan.FromSeconds(100), 3), 429))
+ .WithUpstreamHttpMethod("Get")
+ .Build());
+
+ this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute))
+ .When(x => x.WhenICallTheMiddlewareMultipleTime(2))
+ .Then(x => x.ThenresponseStatusCodeIs200())
+ .When(x => x.WhenICallTheMiddlewareMultipleTime(2))
+ .Then(x => x.ThenresponseStatusCodeIs429())
+ .BDDfy();
+ }
+
+ [Fact]
+ public void should_call_middleware_withWhitelistClient()
+ {
+ var downstreamRoute = new DownstreamRoute(new List(),
+ new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions(
+ new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List() { "ocelotclient2" }, false, "", "", new RateLimitRule( "1s", TimeSpan.FromSeconds(100),3),429))
+ .WithUpstreamHttpMethod("Get")
+ .Build());
+
+ this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute))
+ .When(x => x.WhenICallTheMiddlewareWithWhiteClient())
+ .Then(x => x.ThenresponseStatusCodeIs200())
+ .BDDfy();
+ }
+
+
+ private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute)
+ {
+ _downstreamRoute = new OkResponse(downstreamRoute);
+ _scopedRepository
+ .Setup(x => x.Get(It.IsAny()))
+ .Returns(_downstreamRoute);
+ }
+
+ private void WhenICallTheMiddlewareMultipleTime(int times)
+ {
+ var clientId = "ocelotclient1";
+ // Act
+ for (int i = 0; i < times; i++)
+ {
+ var request = new HttpRequestMessage(new HttpMethod("GET"), _url);
+ request.Headers.Add("ClientId", clientId);
+
+ var response = _client.SendAsync(request);
+ responseStatusCode = (int)response.Result.StatusCode;
+ }
+
+ }
+
+ private void WhenICallTheMiddlewareWithWhiteClient()
+ {
+ var clientId = "ocelotclient2";
+ // Act
+ for (int i = 0; i < 10; i++)
+ {
+ var request = new HttpRequestMessage(new HttpMethod("GET"), _url);
+ request.Headers.Add("ClientId", clientId);
+
+ var response = _client.SendAsync(request);
+ responseStatusCode = (int)response.Result.StatusCode;
+ }
+ }
+
+ private void ThenresponseStatusCodeIs429()
+ {
+ responseStatusCode.ShouldBe(429);
+ }
+
+ private void ThenresponseStatusCodeIs200()
+ {
+ responseStatusCode.ShouldBe(200);
+ }
+ }
+}
diff --git a/test/Ocelot.UnitTests/project.json b/test/Ocelot.UnitTests/project.json
index 3151ac57..2f42c283 100644
--- a/test/Ocelot.UnitTests/project.json
+++ b/test/Ocelot.UnitTests/project.json
@@ -14,7 +14,6 @@
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"Microsoft.AspNetCore.Http": "1.1.0",
"Ocelot": "0.0.0-dev",
- "xunit": "2.2.0-beta2-build3300",
"dotnet-test-xunit": "2.2.0-preview2-build1029",
"Moq": "4.6.38-alpha",
"Microsoft.AspNetCore.TestHost": "1.1.0",
@@ -24,7 +23,8 @@
"Shouldly": "2.8.2",
"TestStack.BDDfy": "4.3.2",
"Microsoft.AspNetCore.Authentication.OAuth": "1.1.0",
- "Microsoft.DotNet.InternalAbstractions": "1.0.0"
+ "Microsoft.DotNet.InternalAbstractions": "1.0.0",
+ "xunit": "2.2.0-rc1-build3507"
},
"runtimes": {
"win10-x64": {},