diff --git a/global.json b/global.json
index ff8d898e..616b2c4e 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"projects": [ "src", "test" ],
"sdk": {
- "version": "1.0.0-preview2-003133"
+ "version": "1.0.0-preview2-003131"
}
}
diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs
index ae8cc047..c5eababe 100644
--- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs
+++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs
@@ -122,7 +122,7 @@ namespace Ocelot.Configuration.Creator
Limit = fileReRoute.RateLimitOptions.Limit,
Period = fileReRoute.RateLimitOptions.Period,
PeriodTimespan = TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan)
- });
+ }, globalConfiguration.RateLimitOptions.HttpStatusCode);
}
var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0;
diff --git a/src/Ocelot/Configuration/File/FileRateLimitOptions.cs b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs
index e685ee56..655b2442 100644
--- a/src/Ocelot/Configuration/File/FileRateLimitOptions.cs
+++ b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs
@@ -7,7 +7,6 @@ namespace Ocelot.Configuration.File
{
public class FileRateLimitOptions
{
-
///
/// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId
///
@@ -29,6 +28,11 @@ namespace Ocelot.Configuration.File
/// 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; private set; } = 429;
}
diff --git a/src/Ocelot/Configuration/RateLimitOptions.cs b/src/Ocelot/Configuration/RateLimitOptions.cs
index 7d4d0f4a..d5c68d23 100644
--- a/src/Ocelot/Configuration/RateLimitOptions.cs
+++ b/src/Ocelot/Configuration/RateLimitOptions.cs
@@ -11,7 +11,7 @@ namespace Ocelot.Configuration
public class RateLimitOptions
{
public RateLimitOptions(bool enbleRateLimiting, string clientIdHeader, List clientWhitelist,bool disableRateLimitHeaders,
- string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule)
+ string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode)
{
EnableRateLimiting = enbleRateLimiting;
ClientIdHeader = clientIdHeader;
@@ -20,6 +20,7 @@ namespace Ocelot.Configuration
QuotaExceededMessage = quotaExceededMessage;
RateLimitCounterPrefix = rateLimitCounterPrefix;
RateLimitRule = rateLimitRule;
+ HttpStatusCode = httpStatusCode;
}
public RateLimitRule RateLimitRule { get; private set; }
@@ -29,12 +30,12 @@ namespace Ocelot.Configuration
///
/// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId
///
- public string ClientIdHeader { get; private set; } = "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; } = 429;
+ public int HttpStatusCode { get; private set; }
///
/// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message.
@@ -46,7 +47,7 @@ namespace Ocelot.Configuration
///
/// Gets or sets the counter prefix, used to compose the rate limit counter cache key
///
- public string RateLimitCounterPrefix { get; private set; } = "ocelot";
+ public string RateLimitCounterPrefix { get; private set; }
///
/// Enables endpoint rate limiting based URL path and HTTP verb
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/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs
new file mode 100644
index 00000000..2e8c3c90
--- /dev/null
+++ b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs
@@ -0,0 +1,208 @@
+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",
+ UpstreamTemplate = "/api/ClientRateLimit",
+ UpstreamHttpMethod = "Get",
+ RequestIdKey = _steps.RequestIdKey,
+
+ RateLimitOptions = new FileRateLimitRule()
+ {
+ EnableRateLimiting = true,
+ ClientWhitelist = new List(),
+ 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", 5))
+ .Then(x => _steps.ThenTheStatusCodeShouldBe(429))
+ .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",
+ UpstreamTemplate = "/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();
+ }
+
+ //private void GetApiRateLimait(string url)
+ //{
+ // var clientId = "ocelotclient1";
+ // var request = new HttpRequestMessage(new HttpMethod("GET"), url);
+ // request.Headers.Add("ClientId", clientId);
+
+ // var response = _client.SendAsync(request);
+ // responseStatusCode = (int)response.Result.StatusCode;
+ // }
+
+ //}
+
+ //public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times)
+ //{
+ // var clientId = "ocelotclient1";
+ // var tasks = new Task[times];
+
+ // for (int i = 0; i < times; i++)
+ // {
+ // var urlCopy = url;
+ // tasks[i] = GetForServiceDiscoveryTest(urlCopy);
+ // Thread.Sleep(_random.Next(40, 60));
+ // }
+
+ // Task.WaitAll(tasks);
+ //}
+
+ //private void WhenICallTheMiddlewareWithWhiteClient()
+ //{
+ // var clientId = "ocelotclient2";
+ // // Act
+ // for (int i = 0; i < 2; i++)
+ // {
+ // var request = new HttpRequestMessage(new HttpMethod("GET"), apiRateLimitPath);
+ // 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);
+ //}
+ }
+}
\ No newline at end of file
diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs
index 92ab6daf..5981c343 100644
--- a/test/Ocelot.AcceptanceTests/Steps.cs
+++ b/test/Ocelot.AcceptanceTests/Steps.cs
@@ -184,6 +184,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);
@@ -211,6 +222,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
index 8626f7c1..af289aab 100755
--- a/test/Ocelot.AcceptanceTests/configuration.json
+++ b/test/Ocelot.AcceptanceTests/configuration.json
@@ -1 +1 @@
-{"ReRoutes":[{"DownstreamPathTemplate":"41879/","UpstreamTemplate":"/","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,"Limit":0}}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"RateLimitOptions":{"ClientIdHeader":"ClientId","QuotaExceededMessage":null,"RateLimitCounterPrefix":"ocelot","DisableRateLimitHeaders":false}}}
\ No newline at end of file
+{"ReRoutes":[{"DownstreamPathTemplate":"41879/","UpstreamTemplate":"/","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,"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.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs
index dd1dc0e0..11bca113 100644
--- a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs
+++ b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs
@@ -29,7 +29,6 @@ namespace Ocelot.UnitTests.RateLimit
private readonly string _url;
private readonly TestServer _server;
private readonly HttpClient _client;
- private HttpResponseMessage _result;
private OkResponse _downstreamRoute;
private int responseStatusCode;
@@ -71,7 +70,7 @@ namespace Ocelot.UnitTests.RateLimit
{
var downstreamRoute = new DownstreamRoute(new List(),
new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions(
- new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List(), false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) }))
+ new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List(), false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) },429))
.Build());
this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute))
@@ -85,7 +84,7 @@ namespace Ocelot.UnitTests.RateLimit
{
var downstreamRoute = new DownstreamRoute(new List(),
new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions(
- new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List() { "ocelotclient2" }, false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) }))
+ new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List() { "ocelotclient2" }, false, "", "", new Ocelot.Configuration.RateLimitRule() { Limit = 3, Period = "1s", PeriodTimespan = TimeSpan.FromSeconds(100) },429))
.Build());
this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute))