diff --git a/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs new file mode 100644 index 00000000..532d990b --- /dev/null +++ b/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; + +namespace Ocelot.Configuration.Builder +{ + public class RateLimitOptionsBuilder + { + private bool _enableRateLimiting; + private string _clientIdHeader; + private List _clientWhitelist; + private bool _disableRateLimitHeaders; + private string _quotaExceededMessage; + private string _rateLimitCounterPrefix; + private RateLimitRule _rateLimitRule; + private int _httpStatusCode; + + public RateLimitOptionsBuilder WithEnableRateLimiting(bool enableRateLimiting) + { + _enableRateLimiting = enableRateLimiting; + return this; + } + + public RateLimitOptionsBuilder WithClientIdHeader(string clientIdheader) + { + _clientIdHeader = clientIdheader; + return this; + } + + public RateLimitOptionsBuilder WithClientWhiteList(List clientWhitelist) + { + _clientWhitelist = clientWhitelist; + return this; + } + + public RateLimitOptionsBuilder WithDisableRateLimitHeaders(bool disableRateLimitHeaders) + { + _disableRateLimitHeaders = disableRateLimitHeaders; + return this; + } + + public RateLimitOptionsBuilder WithQuotaExceededMessage(string quotaExceededMessage) + { + _quotaExceededMessage = quotaExceededMessage; + return this; + } + + public RateLimitOptionsBuilder WithRateLimitCounterPrefix(string rateLimitCounterPrefix) + { + _rateLimitCounterPrefix = rateLimitCounterPrefix; + return this; + } + + public RateLimitOptionsBuilder WithRateLimitRule(RateLimitRule rateLimitRule) + { + _rateLimitRule = rateLimitRule; + return this; + } + + public RateLimitOptionsBuilder WithHttpStatusCode(int httpStatusCode) + { + _httpStatusCode = httpStatusCode; + return this; + } + + public RateLimitOptions Build() + { + return new RateLimitOptions(_enableRateLimiting, _clientIdHeader, _clientWhitelist, + _disableRateLimitHeaders, _quotaExceededMessage, _rateLimitCounterPrefix, + _rateLimitRule, _httpStatusCode); + } + } +} diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index d6caf6a0..7147b40a 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -34,6 +34,7 @@ namespace Ocelot.Configuration.Creator private IServiceProviderConfigurationCreator _serviceProviderConfigCreator; private IQoSOptionsCreator _qosOptionsCreator; private IReRouteOptionsCreator _fileReRouteOptionsCreator; + private IRateLimitOptionsCreator _rateLimitOptionsCreator; public FileOcelotConfigurationCreator( IOptions options, @@ -49,9 +50,11 @@ namespace Ocelot.Configuration.Creator IRequestIdKeyCreator requestIdKeyCreator, IServiceProviderConfigurationCreator serviceProviderConfigCreator, IQoSOptionsCreator qosOptionsCreator, - IReRouteOptionsCreator fileReRouteOptionsCreator + IReRouteOptionsCreator fileReRouteOptionsCreator, + IRateLimitOptionsCreator rateLimitOptionsCreator ) { + _rateLimitOptionsCreator = rateLimitOptionsCreator; _requestIdKeyCreator = requestIdKeyCreator; _upstreamTemplatePatternCreator = upstreamTemplatePatternCreator; _authOptionsCreator = authOptionsCreator; @@ -131,7 +134,7 @@ namespace Ocelot.Configuration.Creator var qosOptions = _qosOptionsCreator.Create(fileReRoute); - var rateLimitOption = BuildRateLimitOptions(fileReRoute, globalConfiguration, fileReRouteOptions.EnableRateLimiting); + var rateLimitOption = _rateLimitOptionsCreator.Create(fileReRoute, globalConfiguration, fileReRouteOptions.EnableRateLimiting); var reRoute = new ReRouteBuilder() .WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate) @@ -165,21 +168,6 @@ namespace Ocelot.Configuration.Creator 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 string CreateReRouteKey(FileReRoute fileReRoute) { //note - not sure if this is the correct key, but this is probably the only unique key i can think of given my poor brain diff --git a/src/Ocelot/Configuration/Creator/IRateLimitOptionsCreator.cs b/src/Ocelot/Configuration/Creator/IRateLimitOptionsCreator.cs new file mode 100644 index 00000000..42f03a96 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IRateLimitOptionsCreator.cs @@ -0,0 +1,9 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public interface IRateLimitOptionsCreator + { + RateLimitOptions Create(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration, bool enableRateLimiting); + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs b/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs new file mode 100644 index 00000000..68bf2a33 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs @@ -0,0 +1,32 @@ +using System; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public class RateLimitOptionsCreator : IRateLimitOptionsCreator + { + public RateLimitOptions Create(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration, bool enableRateLimiting) + { + RateLimitOptions rateLimitOption = null; + + if (enableRateLimiting) + { + rateLimitOption = new RateLimitOptionsBuilder() + .WithClientIdHeader(globalConfiguration.RateLimitOptions.ClientIdHeader) + .WithClientWhiteList(fileReRoute.RateLimitOptions.ClientWhitelist) + .WithDisableRateLimitHeaders(globalConfiguration.RateLimitOptions.DisableRateLimitHeaders) + .WithEnableRateLimiting(fileReRoute.RateLimitOptions.EnableRateLimiting) + .WithHttpStatusCode(globalConfiguration.RateLimitOptions.HttpStatusCode) + .WithQuotaExceededMessage(globalConfiguration.RateLimitOptions.QuotaExceededMessage) + .WithRateLimitCounterPrefix(globalConfiguration.RateLimitOptions.RateLimitCounterPrefix) + .WithRateLimitRule(new RateLimitRule(fileReRoute.RateLimitOptions.Period, + TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan), + fileReRoute.RateLimitOptions.Limit)) + .Build(); + } + + return rateLimitOption; + } + } +} diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs index d69e7741..e99b2490 100644 --- a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -67,6 +67,7 @@ namespace Ocelot.DependencyInjection services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); var identityServerConfiguration = IdentityServerConfigurationCreator.GetIdentityServerConfiguration(); diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index 8a64b654..e3293a90 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -37,6 +37,7 @@ namespace Ocelot.UnitTests.Configuration private Mock _serviceProviderConfigCreator; private Mock _qosOptionsCreator; private Mock _fileReRouteOptionsCreator; + private Mock _rateLimitOptions; public FileConfigurationCreatorTests() { @@ -56,13 +57,41 @@ namespace Ocelot.UnitTests.Configuration _serviceProviderConfigCreator = new Mock(); _qosOptionsCreator = new Mock(); _fileReRouteOptionsCreator = new Mock(); + _rateLimitOptions = new Mock(); _ocelotConfigurationCreator = new FileOcelotConfigurationCreator( _fileConfig.Object, _validator.Object, _logger.Object, _loadBalancerFactory.Object, _loadBalancerHouse.Object, _qosProviderFactory.Object, _qosProviderHouse.Object, _claimsToThingCreator.Object, _authOptionsCreator.Object, _upstreamTemplatePatternCreator.Object, _requestIdKeyCreator.Object, - _serviceProviderConfigCreator.Object, _qosOptionsCreator.Object, _fileReRouteOptionsCreator.Object); + _serviceProviderConfigCreator.Object, _qosOptionsCreator.Object, _fileReRouteOptionsCreator.Object, + _rateLimitOptions.Object); + } + + [Fact] + public void should_call_rate_limit_options_creator() + { + var reRouteOptions = new ReRouteOptionsBuilder() + .Build(); + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHost = "127.0.0.1", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheRateLimitOptionsCreatorIsCalledCorrectly()) + .BDDfy(); } [Fact] @@ -431,8 +460,6 @@ namespace Ocelot.UnitTests.Configuration .BDDfy(); } - - [Fact] public void should_create_with_authentication_properties() { @@ -499,6 +526,11 @@ namespace Ocelot.UnitTests.Configuration .Returns(fileReRouteOptions); } + private void ThenTheRateLimitOptionsCreatorIsCalledCorrectly() + { + _rateLimitOptions + .Verify(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } private void GivenTheConfigIsValid() { diff --git a/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs new file mode 100644 index 00000000..027de7d4 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class RateLimitOptionsCreatorTests + { + private FileReRoute _fileReRoute; + private FileGlobalConfiguration _fileGlobalConfig; + private bool _enabled; + private RateLimitOptionsCreator _creator; + private RateLimitOptions _result; + + public RateLimitOptionsCreatorTests() + { + _creator = new RateLimitOptionsCreator(); + } + + [Fact] + public void should_create_rate_limit_options() + { + var fileReRoute = new FileReRoute + { + RateLimitOptions = new FileRateLimitRule + { + ClientWhitelist = new List(), + Period = "Period", + Limit = 1, + PeriodTimespan = 1, + EnableRateLimiting = true + } + }; + var fileGlobalConfig = new FileGlobalConfiguration + { + RateLimitOptions = new FileRateLimitOptions + { + ClientIdHeader = "ClientIdHeader", + DisableRateLimitHeaders = true, + QuotaExceededMessage = "QuotaExceededMessage", + RateLimitCounterPrefix = "RateLimitCounterPrefix", + HttpStatusCode = 200 + } + }; + var expected = new RateLimitOptionsBuilder() + .WithClientIdHeader("ClientIdHeader") + .WithClientWhiteList(fileReRoute.RateLimitOptions.ClientWhitelist) + .WithDisableRateLimitHeaders(true) + .WithEnableRateLimiting(true) + .WithHttpStatusCode(200) + .WithQuotaExceededMessage("QuotaExceededMessage") + .WithRateLimitCounterPrefix("RateLimitCounterPrefix") + .WithRateLimitRule(new RateLimitRule(fileReRoute.RateLimitOptions.Period, + TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan), + fileReRoute.RateLimitOptions.Limit)) + .Build(); + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .And(x => x.GivenTheFollowingFileGlobalConfig(fileGlobalConfig)) + .And(x => x.GivenRateLimitingIsEnabled()) + .When(x => x.WhenICreate()) + .Then(x => x.ThenTheFollowingIsReturned(expected)) + .BDDfy(); + } + + private void GivenTheFollowingFileReRoute(FileReRoute fileReRoute) + { + _fileReRoute = fileReRoute; + } + + private void GivenTheFollowingFileGlobalConfig(FileGlobalConfiguration fileGlobalConfig) + { + _fileGlobalConfig = fileGlobalConfig; + } + + private void GivenRateLimitingIsEnabled() + { + _enabled = true; + } + + private void WhenICreate() + { + _result = _creator.Create(_fileReRoute, _fileGlobalConfig, _enabled); + } + + private void ThenTheFollowingIsReturned(RateLimitOptions expected) + { + _result.ClientIdHeader.ShouldBe(expected.ClientIdHeader); + _result.ClientWhitelist.ShouldBe(expected.ClientWhitelist); + _result.DisableRateLimitHeaders.ShouldBe(expected.DisableRateLimitHeaders); + _result.EnableRateLimiting.ShouldBe(expected.EnableRateLimiting); + _result.HttpStatusCode.ShouldBe(expected.HttpStatusCode); + _result.QuotaExceededMessage.ShouldBe(expected.QuotaExceededMessage); + _result.RateLimitCounterPrefix.ShouldBe(expected.RateLimitCounterPrefix); + _result.RateLimitRule.Limit.ShouldBe(expected.RateLimitRule.Limit); + _result.RateLimitRule.Period.ShouldBe(expected.RateLimitRule.Period); + _result.RateLimitRule.PeriodTimespan.Ticks.ShouldBe(expected.RateLimitRule.PeriodTimespan.Ticks); + } + } +}