From d01720c34917824a6c2b07730b1e1341218b981b Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Mon, 21 May 2018 18:46:39 +0100 Subject: [PATCH 01/24] =?UTF-8?q?#363=20added=20a=20test=20to=20prove=20rr?= =?UTF-8?q?=20lb=20works,=20this=20doesnt=20have=20a=20lock=20so=20it?= =?UTF-8?q?=E2=80=A6=20(#365)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #363 added a test to prove rr lb works, this doesnt have a lock so it isnt perfect, not sure what the tradeoff is between a lock and a bit of randomness, can change to have a lock anytie * #363 had a look at other oss roudn robin lbs and they all use a lock so imlemented a lock --- .../LoadBalancer/LoadBalancers/RoundRobin.cs | 16 +++--- .../LoadBalancerTests.cs | 54 ++++++++++++++++++- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs b/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs index c9a63b24..b130f6fe 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs @@ -10,6 +10,7 @@ namespace Ocelot.LoadBalancer.LoadBalancers public class RoundRobin : ILoadBalancer { private readonly Func>> _services; + private readonly object _lock = new object(); private int _last; @@ -21,14 +22,17 @@ namespace Ocelot.LoadBalancer.LoadBalancers public async Task> Lease(DownstreamContext downstreamContext) { var services = await _services(); - if (_last >= services.Count) + lock(_lock) { - _last = 0; - } + if (_last >= services.Count) + { + _last = 0; + } - var next = services[_last]; - _last++; - return new OkResponse(next.HostAndPort); + var next = services[_last]; + _last++; + return new OkResponse(next.HostAndPort); + } } public void Release(ServiceHostAndPort hostAndPort) diff --git a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs index a4ce1767..567271a0 100644 --- a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; +using Ocelot.LoadBalancer.LoadBalancers; using Shouldly; using TestStack.BDDfy; using Xunit; @@ -26,7 +27,7 @@ namespace Ocelot.AcceptanceTests } [Fact] - public void should_load_balance_request() + public void should_load_balance_request_with_least_connection() { var downstreamServiceOneUrl = "http://localhost:50881"; var downstreamServiceTwoUrl = "http://localhost:50892"; @@ -41,7 +42,7 @@ namespace Ocelot.AcceptanceTests DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = nameof(LeastConnection) }, DownstreamHostAndPorts = new List { new FileHostAndPort @@ -72,6 +73,55 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } + [Fact] + public void should_load_balance_request_with_round_robin() + { + var downstreamPortOne = 51881; + var downstreamPortTwo = 51892; + var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; + var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = nameof(RoundRobin) }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = downstreamPortOne + }, + new FileHostAndPort + { + Host = "localhost", + Port = downstreamPortTwo + } + } + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + } + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) + .BDDfy(); + } + private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) { _counterOne.ShouldBeInRange(bottom, top); From 32a258fd3f2dc677fdb5d615e4e65d4f821715fa Mon Sep 17 00:00:00 2001 From: Catcher Wong Date: Tue, 22 May 2018 14:13:45 +0800 Subject: [PATCH 02/24] Upgrade Pivotal.Discovery.Client to Pivotal.Discovery.ClientCore (#369) --- src/Ocelot/DependencyInjection/OcelotBuilder.cs | 3 ++- src/Ocelot/Ocelot.csproj | 2 +- .../Providers/EurekaServiceDiscoveryProvider.cs | 2 +- .../Providers/FakeEurekaDiscoveryClient.cs | 2 +- .../ServiceDiscoveryProviderFactory.cs | 6 +++--- .../Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs | 2 +- .../Middleware/OcelotPipelineExtensionsTests.cs | 13 ++++++++++++- .../EurekaServiceDiscoveryProviderTests.cs | 1 + .../ServiceDiscovery/ServiceProviderFactoryTests.cs | 5 +++-- 9 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 3ff2defc..40ac80ed 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -45,8 +45,9 @@ namespace Ocelot.DependencyInjection using Ocelot.Infrastructure.Consul; using Butterfly.Client.Tracing; using Ocelot.Middleware.Multiplexer; - using Pivotal.Discovery.Client; using ServiceDiscovery.Providers; + using Steeltoe.Common.Discovery; + using Pivotal.Discovery.Client; public class OcelotBuilder : IOcelotBuilder { diff --git a/src/Ocelot/Ocelot.csproj b/src/Ocelot/Ocelot.csproj index 5bd94b0c..01241f59 100644 --- a/src/Ocelot/Ocelot.csproj +++ b/src/Ocelot/Ocelot.csproj @@ -54,8 +54,8 @@ - + diff --git a/src/Ocelot/ServiceDiscovery/Providers/EurekaServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/Providers/EurekaServiceDiscoveryProvider.cs index ab78db1b..a9f12701 100644 --- a/src/Ocelot/ServiceDiscovery/Providers/EurekaServiceDiscoveryProvider.cs +++ b/src/Ocelot/ServiceDiscovery/Providers/EurekaServiceDiscoveryProvider.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; - using Pivotal.Discovery.Client; + using Steeltoe.Common.Discovery; using Values; public class EurekaServiceDiscoveryProvider : IServiceDiscoveryProvider diff --git a/src/Ocelot/ServiceDiscovery/Providers/FakeEurekaDiscoveryClient.cs b/src/Ocelot/ServiceDiscovery/Providers/FakeEurekaDiscoveryClient.cs index 78612148..5ccc382d 100644 --- a/src/Ocelot/ServiceDiscovery/Providers/FakeEurekaDiscoveryClient.cs +++ b/src/Ocelot/ServiceDiscovery/Providers/FakeEurekaDiscoveryClient.cs @@ -2,7 +2,7 @@ { using System.Collections.Generic; using System.Threading.Tasks; - using Pivotal.Discovery.Client; + using Steeltoe.Common.Discovery; public class FakeEurekaDiscoveryClient : IDiscoveryClient { diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs index cd678c4f..3ed38b98 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs @@ -7,9 +7,9 @@ using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; namespace Ocelot.ServiceDiscovery -{ - using Pivotal.Discovery.Client; - +{ + using Steeltoe.Common.Discovery; + public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory { private readonly IOcelotLoggerFactory _factory; diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs index 0d5f7d0c..2ec6d945 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -14,7 +14,7 @@ namespace Ocelot.AcceptanceTests using TestStack.BDDfy; using Xunit; using Newtonsoft.Json; - using Pivotal.Discovery.Client; + using Steeltoe.Common.Discovery; public class ServiceDiscoveryTests : IDisposable { diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs index 17f9e3e8..717144b0 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs @@ -7,6 +7,8 @@ namespace Ocelot.UnitTests.Middleware using Ocelot.Middleware.Pipeline; using Pivotal.Discovery.Client; using Shouldly; + using Steeltoe.Common.Discovery; + using Steeltoe.Discovery.Eureka; using TestStack.BDDfy; using Xunit; @@ -40,7 +42,16 @@ namespace Ocelot.UnitTests.Middleware var root = test.Build(); var services = new ServiceCollection(); services.AddSingleton(root); - services.AddDiscoveryClient(new DiscoveryOptions {ClientType = DiscoveryClientType.EUREKA}); + services.AddDiscoveryClient(new DiscoveryOptions + { + ClientType = DiscoveryClientType.EUREKA, + //options can not be null + ClientOptions = new EurekaClientOptions() + { + ShouldFetchRegistry = false, + ShouldRegisterWithEureka = false + } + }); services.AddOcelot(); var provider = services.BuildServiceProvider(); _builder = new OcelotPipelineBuilder(provider); diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/EurekaServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/EurekaServiceDiscoveryProviderTests.cs index 0ec071d1..adfb6957 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/EurekaServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/EurekaServiceDiscoveryProviderTests.cs @@ -7,6 +7,7 @@ using Ocelot.ServiceDiscovery.Providers; using Pivotal.Discovery.Client; using Shouldly; + using Steeltoe.Common.Discovery; using TestStack.BDDfy; using Values; using Xunit; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs index 4621913a..a46369e3 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs @@ -13,8 +13,9 @@ using Xunit; namespace Ocelot.UnitTests.ServiceDiscovery { - using Pivotal.Discovery.Client; - + using Pivotal.Discovery.Client; + using Steeltoe.Common.Discovery; + public class ServiceProviderFactoryTests { private ServiceProviderConfiguration _serviceConfig; From 34598f4edf71979990330a8c9e2f7be6707a6d3c Mon Sep 17 00:00:00 2001 From: Tsirkin Evgeny Date: Fri, 25 May 2018 00:39:27 +0300 Subject: [PATCH 03/24] made http client work under full .net 46 (#367) * made http client work under full .net 46 * Changed the way the requests without body are checked & comments * fixed a type --- src/Ocelot/Requester/HttpClientBuilder.cs | 28 ++++++++++++++----- .../Requester/HttpClientHttpRequester.cs | 19 ++++++++++++- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs index cb1fd6db..4b8fe530 100644 --- a/src/Ocelot/Requester/HttpClientBuilder.cs +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -42,15 +42,29 @@ namespace Ocelot.Requester { return httpClient; } - - var httpclientHandler = new HttpClientHandler + bool useCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer; + HttpClientHandler httpclientHandler; + // Dont' create the CookieContainer if UseCookies is not set ot the HttpClient will complain + // under .Net Full Framework + if (useCookies) { - AllowAutoRedirect = context.DownstreamReRoute.HttpHandlerOptions.AllowAutoRedirect, - UseCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer, - CookieContainer = new CookieContainer() - }; + httpclientHandler = new HttpClientHandler + { + AllowAutoRedirect = context.DownstreamReRoute.HttpHandlerOptions.AllowAutoRedirect, + UseCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer, + CookieContainer = new CookieContainer() + }; + } + else + { + httpclientHandler = new HttpClientHandler + { + AllowAutoRedirect = context.DownstreamReRoute.HttpHandlerOptions.AllowAutoRedirect, + UseCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer, + }; + } - if(context.DownstreamReRoute.DangerousAcceptAnyServerCertificateValidator) + if (context.DownstreamReRoute.DangerousAcceptAnyServerCertificateValidator) { httpclientHandler.ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) => true; diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index c7914c94..9fe5e74f 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -32,7 +32,24 @@ namespace Ocelot.Requester try { - var response = await httpClient.SendAsync(context.DownstreamRequest.ToHttpRequestMessage()); + var message = context.DownstreamRequest.ToHttpRequestMessage(); + /** + * According to https://tools.ietf.org/html/rfc7231 + * GET,HEAD,DELETE,CONNECT,TRACE + * Can have body but server can reject the request. + * And MS HttpClient in Full Framework actually rejects it. + * see #366 issue + **/ + + if (message.Method == HttpMethod.Get || + message.Method == HttpMethod.Head || + message.Method == HttpMethod.Delete || + message.Method == HttpMethod.Trace) + { + message.Content = null; + } + _logger.LogDebug(string.Format("Sending {0}", message)); + var response = await httpClient.SendAsync(message); return new OkResponse(response); } catch (TimeoutRejectedException exception) From e55b27de0f4ec609edb76078c84f8d5f252bd4ed Mon Sep 17 00:00:00 2001 From: Tom Gardham-Pallister Date: Thu, 24 May 2018 22:56:08 +0100 Subject: [PATCH 04/24] bit of refactoring --- .../Request/Middleware/DownstreamRequest.cs | 16 +++++ src/Ocelot/Requester/HttpClientBuilder.cs | 62 ++++++++++++------- .../Requester/HttpClientHttpRequester.cs | 19 +----- 3 files changed, 56 insertions(+), 41 deletions(-) diff --git a/src/Ocelot/Request/Middleware/DownstreamRequest.cs b/src/Ocelot/Request/Middleware/DownstreamRequest.cs index 75070bfd..fc1feb8a 100644 --- a/src/Ocelot/Request/Middleware/DownstreamRequest.cs +++ b/src/Ocelot/Request/Middleware/DownstreamRequest.cs @@ -48,6 +48,22 @@ namespace Ocelot.Request.Middleware Scheme = Scheme }; + /** + * According to https://tools.ietf.org/html/rfc7231 + * GET,HEAD,DELETE,CONNECT,TRACE + * Can have body but server can reject the request. + * And MS HttpClient in Full Framework actually rejects it. + * see #366 issue + **/ +#if NET461 || NET462 || NET47 || NET471 || NET472 + if (_request.Method == HttpMethod.Get || + _request.Method == HttpMethod.Head || + _request.Method == HttpMethod.Delete || + _request.Method == HttpMethod.Trace) + { + _request.Content = null; + } +#endif _request.RequestUri = uriBuilder.Uri; return _request; } diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs index 4b8fe530..1cada3d7 100644 --- a/src/Ocelot/Requester/HttpClientBuilder.cs +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -42,31 +42,12 @@ namespace Ocelot.Requester { return httpClient; } - bool useCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer; - HttpClientHandler httpclientHandler; - // Dont' create the CookieContainer if UseCookies is not set ot the HttpClient will complain - // under .Net Full Framework - if (useCookies) - { - httpclientHandler = new HttpClientHandler - { - AllowAutoRedirect = context.DownstreamReRoute.HttpHandlerOptions.AllowAutoRedirect, - UseCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer, - CookieContainer = new CookieContainer() - }; - } - else - { - httpclientHandler = new HttpClientHandler - { - AllowAutoRedirect = context.DownstreamReRoute.HttpHandlerOptions.AllowAutoRedirect, - UseCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer, - }; - } + + var handler = CreateHandler(context); if (context.DownstreamReRoute.DangerousAcceptAnyServerCertificateValidator) { - httpclientHandler.ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) => true; + handler.ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) => true; _logger .LogWarning($"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamReRoute, UpstreamPathTemplate: {context.DownstreamReRoute.UpstreamPathTemplate}, DownstreamPathTemplate: {context.DownstreamReRoute.DownstreamPathTemplate}"); @@ -76,7 +57,7 @@ namespace Ocelot.Requester ? _defaultTimeout : TimeSpan.FromMilliseconds(context.DownstreamReRoute.QosOptions.TimeoutValue); - _httpClient = new HttpClient(CreateHttpMessageHandler(httpclientHandler, context.DownstreamReRoute)) + _httpClient = new HttpClient(CreateHttpMessageHandler(handler, context.DownstreamReRoute)) { Timeout = timeout }; @@ -86,6 +67,41 @@ namespace Ocelot.Requester return _client; } + private HttpClientHandler CreateHandler(DownstreamContext context) + { + // Dont' create the CookieContainer if UseCookies is not set or the HttpClient will complain + // under .Net Full Framework + bool useCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer; + + if (useCookies) + { + return UseCookiesHandler(context); + } + else + { + return UseNonCookiesHandler(context); + } + } + + private HttpClientHandler UseNonCookiesHandler(DownstreamContext context) + { + return new HttpClientHandler + { + AllowAutoRedirect = context.DownstreamReRoute.HttpHandlerOptions.AllowAutoRedirect, + UseCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer, + }; + } + + private HttpClientHandler UseCookiesHandler(DownstreamContext context) + { + return new HttpClientHandler + { + AllowAutoRedirect = context.DownstreamReRoute.HttpHandlerOptions.AllowAutoRedirect, + UseCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer, + CookieContainer = new CookieContainer() + }; + } + public void Save() { _cacheHandlers.Set(_cacheKey, _client, TimeSpan.FromHours(24)); diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index 9fe5e74f..c7914c94 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -32,24 +32,7 @@ namespace Ocelot.Requester try { - var message = context.DownstreamRequest.ToHttpRequestMessage(); - /** - * According to https://tools.ietf.org/html/rfc7231 - * GET,HEAD,DELETE,CONNECT,TRACE - * Can have body but server can reject the request. - * And MS HttpClient in Full Framework actually rejects it. - * see #366 issue - **/ - - if (message.Method == HttpMethod.Get || - message.Method == HttpMethod.Head || - message.Method == HttpMethod.Delete || - message.Method == HttpMethod.Trace) - { - message.Content = null; - } - _logger.LogDebug(string.Format("Sending {0}", message)); - var response = await httpClient.SendAsync(message); + var response = await httpClient.SendAsync(context.DownstreamRequest.ToHttpRequestMessage()); return new OkResponse(response); } catch (TimeoutRejectedException exception) From 7cd3ff2ff7782bbbfb02952e20a11774f703aedd Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Thu, 31 May 2018 22:08:50 +0100 Subject: [PATCH 05/24] Feature/fix unstable int tests (#376) * updated packages but build wont work * #245 implementing more stable rafty * #245 OK so these raft integration tests are passing everytime on my local mac now...lets see about the build servergit log * #245 added donation button * #245 removed file we dont need --- README.md | 6 + .../FileConfigurationController.cs | 4 +- .../OcelotAdministrationBuilder.cs | 4 +- .../Middleware/OcelotMiddlewareExtensions.cs | 3 +- src/Ocelot/Ocelot.csproj | 46 +- src/Ocelot/Raft/FileFsm.cs | 36 -- src/Ocelot/Raft/FilePeersProvider.cs | 2 + src/Ocelot/Raft/HttpPeer.cs | 4 + src/Ocelot/Raft/OcelotFiniteStateMachine.cs | 2 +- src/Ocelot/Raft/RaftController.cs | 3 + src/Ocelot/Raft/SqlLiteLog.cs | 453 ++++++++++-------- .../Ocelot.AcceptanceTests.csproj | 120 ++--- .../Ocelot.IntegrationTests.csproj | 100 ++-- test/Ocelot.IntegrationTests/RaftTests.cs | 147 ++++-- .../FileConfigurationControllerTests.cs | 5 +- test/Ocelot.UnitTests/Ocelot.UnitTests.csproj | 116 ++--- 16 files changed, 567 insertions(+), 484 deletions(-) delete mode 100644 src/Ocelot/Raft/FileFsm.cs diff --git a/README.md b/README.md index 56665fc8..4434b56f 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,12 @@ advice on the easiest way to do things :) Finally we mark all existing issues as help wanted, small, medium and large effort. If you want to contriute for the first time I suggest looking at a help wanted & small effort issue :) +## Donate + +If you think this project is worth supporting financially please make a contribution using the button below! + +[![Support via PayPal](https://cdn.rawgit.com/twolfson/paypal-github-button/1.0.0/dist/button.svg)](https://www.paypal.me/ThreeMammals/) + ## Things that are currently annoying me [![](https://codescene.io/projects/697/status.svg) Get more details at **codescene.io**.](https://codescene.io/projects/697/jobs/latest-successful/results) diff --git a/src/Ocelot/Configuration/FileConfigurationController.cs b/src/Ocelot/Configuration/FileConfigurationController.cs index 1ea7fa7d..dbc77b77 100644 --- a/src/Ocelot/Configuration/FileConfigurationController.cs +++ b/src/Ocelot/Configuration/FileConfigurationController.cs @@ -5,10 +5,10 @@ using Microsoft.AspNetCore.Mvc; using Ocelot.Configuration.File; using Ocelot.Configuration.Setter; using Ocelot.Raft; -using Rafty.Concensus; namespace Ocelot.Configuration { + using Rafty.Concensus.Node; using Repository; [Authorize] @@ -50,7 +50,7 @@ namespace Ocelot.Configuration { var node = (INode)test; var result = await node.Accept(new UpdateFileConfiguration(fileConfiguration)); - if (result.GetType() == typeof(Rafty.Concensus.ErrorResponse)) + if (result.GetType() == typeof(Rafty.Infrastructure.ErrorResponse)) { return new BadRequestObjectResult("There was a problem. This error message sucks raise an issue in GitHub."); } diff --git a/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs b/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs index 580839c3..c96cdef3 100644 --- a/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs @@ -8,6 +8,8 @@ using Rafty.Log; namespace Ocelot.DependencyInjection { + using Rafty.Concensus.Node; + public class OcelotAdministrationBuilder : IOcelotAdministrationBuilder { private readonly IServiceCollection _services; @@ -21,7 +23,7 @@ namespace Ocelot.DependencyInjection public IOcelotAdministrationBuilder AddRafty() { - var settings = new InMemorySettings(4000, 5000, 100, 5000); + var settings = new InMemorySettings(4000, 6000, 100, 10000); _services.AddSingleton(); _services.AddSingleton(); _services.AddSingleton(settings); diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index 4fc3db7a..a35a8e1a 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -18,6 +18,7 @@ using Rafty.Infrastructure; using Ocelot.Middleware.Pipeline; using Pivotal.Discovery.Client; + using Rafty.Concensus.Node; public static class OcelotMiddlewareExtensions { @@ -91,7 +92,7 @@ applicationLifetime.ApplicationStopping.Register(() => OnShutdown(builder)); var node = (INode)builder.ApplicationServices.GetService(typeof(INode)); var nodeId = (NodeId)builder.ApplicationServices.GetService(typeof(NodeId)); - node.Start(nodeId.Id); + node.Start(nodeId); } private static async Task CreateConfiguration(IApplicationBuilder builder) diff --git a/src/Ocelot/Ocelot.csproj b/src/Ocelot/Ocelot.csproj index 01241f59..8224be2c 100644 --- a/src/Ocelot/Ocelot.csproj +++ b/src/Ocelot/Ocelot.csproj @@ -25,37 +25,37 @@ True - + NU1701 - - - - - - - - - + + + + + + + + + NU1701 - - - - + + + + all - - - - - - - - - + + + + + + + + + diff --git a/src/Ocelot/Raft/FileFsm.cs b/src/Ocelot/Raft/FileFsm.cs deleted file mode 100644 index afa47d26..00000000 --- a/src/Ocelot/Raft/FileFsm.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Rafty.FiniteStateMachine; -using Rafty.Infrastructure; -using Rafty.Log; - -namespace Ocelot.Raft -{ - [ExcludeFromCoverage] - public class FileFsm : IFiniteStateMachine - { - private string _id; - - public FileFsm(NodeId nodeId) - { - _id = nodeId.Id.Replace("/","").Replace(":",""); - } - - public Task Handle(LogEntry log) - { - try - { - var json = JsonConvert.SerializeObject(log.CommandData); - File.AppendAllText(_id, json); - } - catch(Exception exception) - { - Console.WriteLine(exception); - } - - return Task.CompletedTask; - } - } -} diff --git a/src/Ocelot/Raft/FilePeersProvider.cs b/src/Ocelot/Raft/FilePeersProvider.cs index 52b877df..58edae9f 100644 --- a/src/Ocelot/Raft/FilePeersProvider.cs +++ b/src/Ocelot/Raft/FilePeersProvider.cs @@ -9,6 +9,8 @@ using Rafty.Infrastructure; namespace Ocelot.Raft { + using Rafty.Concensus.Peers; + [ExcludeFromCoverage] public class FilePeersProvider : IPeersProvider { diff --git a/src/Ocelot/Raft/HttpPeer.cs b/src/Ocelot/Raft/HttpPeer.cs index 93d81cd4..639f1ee8 100644 --- a/src/Ocelot/Raft/HttpPeer.cs +++ b/src/Ocelot/Raft/HttpPeer.cs @@ -10,6 +10,10 @@ using Rafty.FiniteStateMachine; namespace Ocelot.Raft { + using Rafty.Concensus.Messages; + using Rafty.Concensus.Peers; + using Rafty.Infrastructure; + [ExcludeFromCoverage] public class HttpPeer : IPeer { diff --git a/src/Ocelot/Raft/OcelotFiniteStateMachine.cs b/src/Ocelot/Raft/OcelotFiniteStateMachine.cs index b3d7720f..618d7f5f 100644 --- a/src/Ocelot/Raft/OcelotFiniteStateMachine.cs +++ b/src/Ocelot/Raft/OcelotFiniteStateMachine.cs @@ -8,7 +8,7 @@ namespace Ocelot.Raft [ExcludeFromCoverage] public class OcelotFiniteStateMachine : IFiniteStateMachine { - private IFileConfigurationSetter _setter; + private readonly IFileConfigurationSetter _setter; public OcelotFiniteStateMachine(IFileConfigurationSetter setter) { diff --git a/src/Ocelot/Raft/RaftController.cs b/src/Ocelot/Raft/RaftController.cs index 0b8d4989..660449c6 100644 --- a/src/Ocelot/Raft/RaftController.cs +++ b/src/Ocelot/Raft/RaftController.cs @@ -14,6 +14,9 @@ using Rafty.FiniteStateMachine; namespace Ocelot.Raft { + using Rafty.Concensus.Messages; + using Rafty.Concensus.Node; + [ExcludeFromCoverage] [Authorize] [Route("raft")] diff --git a/src/Ocelot/Raft/SqlLiteLog.cs b/src/Ocelot/Raft/SqlLiteLog.cs index f0db2047..882df202 100644 --- a/src/Ocelot/Raft/SqlLiteLog.cs +++ b/src/Ocelot/Raft/SqlLiteLog.cs @@ -1,286 +1,321 @@ -using System.IO; -using Rafty.Log; -using Microsoft.Data.Sqlite; -using Newtonsoft.Json; -using System; -using Rafty.Infrastructure; -using System.Collections.Generic; -using System.Threading.Tasks; - namespace Ocelot.Raft -{ - //todo - use async await - [ExcludeFromCoverage] +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Data.Sqlite; + using Microsoft.Extensions.Logging; + using Newtonsoft.Json; + using Rafty.Infrastructure; + using Rafty.Log; + public class SqlLiteLog : ILog { - private string _path; - private readonly object _lock = new object(); + private readonly string _path; + private readonly SemaphoreSlim _sempaphore = new SemaphoreSlim(1, 1); + private readonly ILogger _logger; + private readonly NodeId _nodeId; - public SqlLiteLog(NodeId nodeId) + public SqlLiteLog(NodeId nodeId, ILoggerFactory loggerFactory) { - _path = $"{nodeId.Id.Replace("/","").Replace(":","")}.db"; - if(!File.Exists(_path)) - { - lock(_lock) - { - FileStream fs = File.Create(_path); - fs.Dispose(); - } + _logger = loggerFactory.CreateLogger(); + _nodeId = nodeId; + _path = $"{nodeId.Id.Replace("/", "").Replace(":", "")}.db"; + _sempaphore.Wait(); - using(var connection = new SqliteConnection($"Data Source={_path};")) + if (!File.Exists(_path)) + { + var fs = File.Create(_path); + + fs.Dispose(); + + using (var connection = new SqliteConnection($"Data Source={_path};")) { connection.Open(); - var sql = @"create table logs ( + + const string sql = @"create table logs ( id integer primary key, data text not null )"; - using(var command = new SqliteCommand(sql, connection)) + + using (var command = new SqliteCommand(sql, connection)) { var result = command.ExecuteNonQuery(); + + _logger.LogInformation(result == 0 + ? $"id: {_nodeId.Id} create database, result: {result}" + : $"id: {_nodeId.Id} did not create database., result: {result}"); } } } + + _sempaphore.Release(); } - public Task LastLogIndex() + public async Task LastLogIndex() { - lock(_lock) + _sempaphore.Wait(); + var result = 1; + using (var connection = new SqliteConnection($"Data Source={_path};")) { - var result = 1; - using(var connection = new SqliteConnection($"Data Source={_path};")) + connection.Open(); + var sql = @"select id from logs order by id desc limit 1"; + using (var command = new SqliteCommand(sql, connection)) { - connection.Open(); - var sql = @"select id from logs order by id desc limit 1"; - using(var command = new SqliteCommand(sql, connection)) + var index = Convert.ToInt32(await command.ExecuteScalarAsync()); + if (index > result) { - var index = Convert.ToInt32(command.ExecuteScalar()); - if(index > result) - { - result = index; - } + result = index; } } - - return Task.FromResult(result); } + + _sempaphore.Release(); + return result; } - public Task LastLogTerm () + public async Task LastLogTerm() { - lock(_lock) + _sempaphore.Wait(); + long result = 0; + using (var connection = new SqliteConnection($"Data Source={_path};")) { - long result = 0; - using(var connection = new SqliteConnection($"Data Source={_path};")) + connection.Open(); + var sql = @"select data from logs order by id desc limit 1"; + using (var command = new SqliteCommand(sql, connection)) { - connection.Open(); - var sql = @"select data from logs order by id desc limit 1"; - using(var command = new SqliteCommand(sql, connection)) - { - var data = Convert.ToString(command.ExecuteScalar()); - var jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - if(log != null && log.Term > result) - { - result = log.Term; - } - } - } - - return Task.FromResult(result); - } - } - - public Task Count () - { - lock(_lock) - { - var result = 0; - using(var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - var sql = @"select count(id) from logs"; - using(var command = new SqliteCommand(sql, connection)) - { - var index = Convert.ToInt32(command.ExecuteScalar()); - if(index > result) - { - result = index; - } - } - } - - return Task.FromResult(result); - } - } - - public Task Apply(LogEntry log) - { - lock(_lock) - { - using(var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - var jsonSerializerSettings = new JsonSerializerSettings() { + var data = Convert.ToString(await command.ExecuteScalarAsync()); + var jsonSerializerSettings = new JsonSerializerSettings() + { TypeNameHandling = TypeNameHandling.All }; - var data = JsonConvert.SerializeObject(log, jsonSerializerSettings); - - //todo - sql injection dont copy this.. - var sql = $"insert into logs (data) values ('{data}')"; - using(var command = new SqliteCommand(sql, connection)) + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + if (log != null && log.Term > result) { - var result = command.ExecuteNonQuery(); + result = log.Term; } - - sql = "select last_insert_rowid()"; - using(var command = new SqliteCommand(sql, connection)) - { - var result = command.ExecuteScalar(); - return Task.FromResult(Convert.ToInt32(result)); - } } } + _sempaphore.Release(); + return result; + } + + public async Task Count() + { + _sempaphore.Wait(); + var result = 0; + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var sql = @"select count(id) from logs"; + using (var command = new SqliteCommand(sql, connection)) + { + var index = Convert.ToInt32(await command.ExecuteScalarAsync()); + if (index > result) + { + result = index; + } + } + } + _sempaphore.Release(); + return result; + } + + public async Task Apply(LogEntry log) + { + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + var data = JsonConvert.SerializeObject(log, jsonSerializerSettings); + //todo - sql injection dont copy this.. + var sql = $"insert into logs (data) values ('{data}')"; + _logger.LogInformation($"id: {_nodeId.Id}, sql: {sql}"); + using (var command = new SqliteCommand(sql, connection)) + { + var result = await command.ExecuteNonQueryAsync(); + _logger.LogInformation($"id: {_nodeId.Id}, insert log result: {result}"); + } + + sql = "select last_insert_rowid()"; + using (var command = new SqliteCommand(sql, connection)) + { + var result = await command.ExecuteScalarAsync(); + _logger.LogInformation($"id: {_nodeId.Id}, about to release semaphore"); + _sempaphore.Release(); + _logger.LogInformation($"id: {_nodeId.Id}, saved log to sqlite"); + return Convert.ToInt32(result); + } + } } - public Task DeleteConflictsFromThisLog(int index, LogEntry logEntry) + public async Task DeleteConflictsFromThisLog(int index, LogEntry logEntry) { - lock(_lock) + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) { - using(var connection = new SqliteConnection($"Data Source={_path};")) + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index};"; + _logger.LogInformation($"id: {_nodeId.Id} sql: {sql}"); + using (var command = new SqliteCommand(sql, connection)) { - connection.Open(); - - //todo - sql injection dont copy this.. - var sql = $"select data from logs where id = {index};"; - using(var command = new SqliteCommand(sql, connection)) + var data = Convert.ToString(await command.ExecuteScalarAsync()); + var jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + + _logger.LogInformation($"id {_nodeId.Id} got log for index: {index}, data is {data} and new log term is {logEntry.Term}"); + + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + if (logEntry != null && log != null && logEntry.Term != log.Term) { - var data = Convert.ToString(command.ExecuteScalar()); - var jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - if(logEntry != null && log != null && logEntry.Term != log.Term) + //todo - sql injection dont copy this.. + var deleteSql = $"delete from logs where id >= {index};"; + _logger.LogInformation($"id: {_nodeId.Id} sql: {deleteSql}"); + using (var deleteCommand = new SqliteCommand(deleteSql, connection)) { - //todo - sql injection dont copy this.. - var deleteSql = $"delete from logs where id >= {index};"; - using(var deleteCommand = new SqliteCommand(deleteSql, connection)) - { - var result = deleteCommand.ExecuteNonQuery(); - } + var result = await deleteCommand.ExecuteNonQueryAsync(); } } } } - - return Task.CompletedTask; + _sempaphore.Release(); } - public Task Get(int index) + public async Task IsDuplicate(int index, LogEntry logEntry) { - lock(_lock) + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) { - using(var connection = new SqliteConnection($"Data Source={_path};")) + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index};"; + using (var command = new SqliteCommand(sql, connection)) { - connection.Open(); - - //todo - sql injection dont copy this.. - var sql = $"select data from logs where id = {index}"; - using(var command = new SqliteCommand(sql, connection)) + var data = Convert.ToString(await command.ExecuteScalarAsync()); + var jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + + if (logEntry != null && log != null && logEntry.Term == log.Term) { - var data = Convert.ToString(command.ExecuteScalar()); - var jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - return Task.FromResult(log); + _sempaphore.Release(); + return true; } } } + + _sempaphore.Release(); + return false; + } + + public async Task Get(int index) + { + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index}"; + using (var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(await command.ExecuteScalarAsync()); + var jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + _sempaphore.Release(); + return log; + } + } } - public Task> GetFrom(int index) + public async Task> GetFrom(int index) { - lock(_lock) - { - var logsToReturn = new List<(int, LogEntry)>(); + _sempaphore.Wait(); + var logsToReturn = new List<(int, LogEntry)>(); - using(var connection = new SqliteConnection($"Data Source={_path};")) + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select id, data from logs where id >= {index}"; + using (var command = new SqliteCommand(sql, connection)) { - connection.Open(); - - //todo - sql injection dont copy this.. - var sql = $"select id, data from logs where id >= {index}"; - using(var command = new SqliteCommand(sql, connection)) + using (var reader = await command.ExecuteReaderAsync()) { - using(var reader = command.ExecuteReader()) + while (reader.Read()) { - while(reader.Read()) - { - var id = Convert.ToInt32(reader[0]); - var data = (string)reader[1]; - var jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - logsToReturn.Add((id, log)); - } + var id = Convert.ToInt32(reader[0]); + var data = (string)reader[1]; + var jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + logsToReturn.Add((id, log)); + } } - } - - return Task.FromResult(logsToReturn); - } - } - - public Task GetTermAtIndex(int index) - { - lock(_lock) - { - long result = 0; - using(var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - - //todo - sql injection dont copy this.. - var sql = $"select data from logs where id = {index}"; - using(var command = new SqliteCommand(sql, connection)) - { - var data = Convert.ToString(command.ExecuteScalar()); - var jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - if(log != null && log.Term > result) - { - result = log.Term; - } - } - } - - return Task.FromResult(result); + } + _sempaphore.Release(); + return logsToReturn; } } - public Task Remove(int indexOfCommand) + public async Task GetTermAtIndex(int index) { - lock(_lock) + _sempaphore.Wait(); + long result = 0; + using (var connection = new SqliteConnection($"Data Source={_path};")) { - using(var connection = new SqliteConnection($"Data Source={_path};")) + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index}"; + using (var command = new SqliteCommand(sql, connection)) { - connection.Open(); - - //todo - sql injection dont copy this.. - var deleteSql = $"delete from logs where id >= {indexOfCommand};"; - using(var deleteCommand = new SqliteCommand(deleteSql, connection)) + var data = Convert.ToString(await command.ExecuteScalarAsync()); + var jsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + if (log != null && log.Term > result) { - var result = deleteCommand.ExecuteNonQuery(); + result = log.Term; } } } - - return Task.CompletedTask; + _sempaphore.Release(); + return result; + } + public async Task Remove(int indexOfCommand) + { + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var deleteSql = $"delete from logs where id >= {indexOfCommand};"; + _logger.LogInformation($"id: {_nodeId.Id} Remove {deleteSql}"); + using (var deleteCommand = new SqliteCommand(deleteSql, connection)) + { + var result = await deleteCommand.ExecuteNonQueryAsync(); + } + } + _sempaphore.Release(); } } -} +} diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index fe4877ec..dd9d855c 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -1,60 +1,60 @@ - - - 0.0.0-dev - netcoreapp2.0 - 2.0.0 - Ocelot.AcceptanceTests - Exe - Ocelot.AcceptanceTests - true - osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 - false - false - false - ..\..\codeanalysis.ruleset - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - - - - - - - - - - - all - - - - - - - - - - - - - - - - - - - + + + 0.0.0-dev + netcoreapp2.0 + 2.0.0 + Ocelot.AcceptanceTests + Exe + Ocelot.AcceptanceTests + true + osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 + false + false + false + ..\..\codeanalysis.ruleset + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + + all + + + + + + + + + + + + + + + + + + + diff --git a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj index 5ec67320..5b2cac21 100644 --- a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +++ b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj @@ -1,50 +1,50 @@ - - - 0.0.0-dev - netcoreapp2.0 - 2.0.0 - Ocelot.IntegrationTests - Exe - Ocelot.IntegrationTests - true - win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 - false - false - false - ..\..\codeanalysis.ruleset - - - - PreserveNewest - - - - - - - - - - - - - all - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + 0.0.0-dev + netcoreapp2.0 + 2.0.0 + Ocelot.IntegrationTests + Exe + Ocelot.IntegrationTests + true + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + false + ..\..\codeanalysis.ruleset + + + + PreserveNewest + + + + + + + + + + + + + all + + + + + + + + + + + + + + + + + + + diff --git a/test/Ocelot.IntegrationTests/RaftTests.cs b/test/Ocelot.IntegrationTests/RaftTests.cs index 96cd6687..37a7dff0 100644 --- a/test/Ocelot.IntegrationTests/RaftTests.cs +++ b/test/Ocelot.IntegrationTests/RaftTests.cs @@ -23,6 +23,7 @@ using Ocelot.Middleware; namespace Ocelot.IntegrationTests { + using System.Threading.Tasks; using Xunit.Abstractions; public class RaftTests : IDisposable @@ -31,7 +32,7 @@ namespace Ocelot.IntegrationTests private readonly List _webHostBuilders; private readonly List _threads; private FilePeers _peers; - private readonly HttpClient _httpClient; + private HttpClient _httpClient; private readonly HttpClient _httpClientForAssertions; private BearerToken _token; private HttpResponseMessage _response; @@ -42,18 +43,28 @@ namespace Ocelot.IntegrationTests { _output = output; _httpClientForAssertions = new HttpClient(); - _httpClient = new HttpClient(); - var ocelotBaseUrl = "http://localhost:5000"; - _httpClient.BaseAddress = new Uri(ocelotBaseUrl); _webHostBuilders = new List(); _builders = new List(); _threads = new List(); } - [Fact(Skip = "still broken waiting for work in rafty")] - public void should_persist_command_to_five_servers() + [Fact] + public async Task should_persist_command_to_five_servers() { - var configuration = new FileConfiguration + var peers = new List + { + new FilePeer {HostAndPort = "http://localhost:5000"}, + + new FilePeer {HostAndPort = "http://localhost:5001"}, + + new FilePeer {HostAndPort = "http://localhost:5002"}, + + new FilePeer {HostAndPort = "http://localhost:5003"}, + + new FilePeer {HostAndPort = "http://localhost:5004"} + }; + + var configuration = new FileConfiguration { GlobalConfiguration = new FileGlobalConfiguration { @@ -101,20 +112,34 @@ namespace Ocelot.IntegrationTests }; var command = new UpdateFileConfiguration(updatedConfiguration); + GivenThePeersAre(peers); GivenThereIsAConfiguration(configuration); GivenFiveServersAreRunning(); - GivenIHaveAnOcelotToken("/administration"); - WhenISendACommandIntoTheCluster(command); + await GivenIHaveAnOcelotToken("/administration"); + await WhenISendACommandIntoTheCluster(command); Thread.Sleep(5000); - ThenTheCommandIsReplicatedToAllStateMachines(command); + await ThenTheCommandIsReplicatedToAllStateMachines(command); } - [Fact(Skip = "still broken waiting for work in rafty")] - public void should_persist_command_to_five_servers_when_using_administration_api() + [Fact] + public async Task should_persist_command_to_five_servers_when_using_administration_api() { - var configuration = new FileConfiguration - { - }; + var peers = new List + { + new FilePeer {HostAndPort = "http://localhost:5005"}, + + new FilePeer {HostAndPort = "http://localhost:5006"}, + + new FilePeer {HostAndPort = "http://localhost:5007"}, + + new FilePeer {HostAndPort = "http://localhost:5008"}, + + new FilePeer {HostAndPort = "http://localhost:5009"} + }; + + var configuration = new FileConfiguration + { + }; var updatedConfiguration = new FileConfiguration { @@ -154,17 +179,29 @@ namespace Ocelot.IntegrationTests }; var command = new UpdateFileConfiguration(updatedConfiguration); + GivenThePeersAre(peers); GivenThereIsAConfiguration(configuration); GivenFiveServersAreRunning(); - GivenIHaveAnOcelotToken("/administration"); + await GivenIHaveAnOcelotToken("/administration"); GivenIHaveAddedATokenToMyRequest(); - WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration); - ThenTheCommandIsReplicatedToAllStateMachines(command); + await WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration); + await ThenTheCommandIsReplicatedToAllStateMachines(command); } - private void WhenISendACommandIntoTheCluster(UpdateFileConfiguration command) + private void GivenThePeersAre(List peers) { - bool SendCommand() + FilePeers filePeers = new FilePeers(); + filePeers.Peers.AddRange(peers); + var json = JsonConvert.SerializeObject(filePeers); + File.WriteAllText("peers.json", json); + _httpClient = new HttpClient(); + var ocelotBaseUrl = peers[0].HostAndPort; + _httpClient.BaseAddress = new Uri(ocelotBaseUrl); + } + + private async Task WhenISendACommandIntoTheCluster(UpdateFileConfiguration command) + { + async Task SendCommand() { try { @@ -174,13 +211,13 @@ namespace Ocelot.IntegrationTests TypeNameHandling = TypeNameHandling.All }); var httpContent = new StringContent(json); - httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); using (var httpClient = new HttpClient()) { httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - var response = httpClient.PostAsync($"{p.HostAndPort}/administration/raft/command", httpContent).GetAwaiter().GetResult(); + var response = await httpClient.PostAsync($"{p.HostAndPort}/administration/raft/command", httpContent); response.EnsureSuccessStatusCode(); - var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + var content = await response.Content.ReadAsStringAsync(); var errorResult = JsonConvert.DeserializeObject>(content); @@ -206,13 +243,19 @@ namespace Ocelot.IntegrationTests } } - var commandSent = WaitFor(20000).Until(() => SendCommand()); + var commandSent = await WaitFor(40000).Until(async () => + { + var result = await SendCommand(); + Thread.Sleep(1000); + return result; + }); + commandSent.ShouldBeTrue(); } - private void ThenTheCommandIsReplicatedToAllStateMachines(UpdateFileConfiguration expecteds) + private async Task ThenTheCommandIsReplicatedToAllStateMachines(UpdateFileConfiguration expecteds) { - bool CommandCalledOnAllStateMachines() + async Task CommandCalledOnAllStateMachines() { try { @@ -232,8 +275,8 @@ namespace Ocelot.IntegrationTests } _httpClientForAssertions.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - var result = _httpClientForAssertions.GetAsync($"{peer.HostAndPort}/administration/configuration").Result; - var json = result.Content.ReadAsStringAsync().Result; + var result = await _httpClientForAssertions.GetAsync($"{peer.HostAndPort}/administration/configuration"); + var json = await result.Content.ReadAsStringAsync(); var response = JsonConvert.DeserializeObject(json, new JsonSerializerSettings{TypeNameHandling = TypeNameHandling.All}); response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.Configuration.GlobalConfiguration.RequestIdKey); response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Host); @@ -268,19 +311,29 @@ namespace Ocelot.IntegrationTests } } - var commandOnAllStateMachines = WaitFor(20000).Until(() => CommandCalledOnAllStateMachines()); + var commandOnAllStateMachines = await WaitFor(40000).Until(async () => + { + var result = await CommandCalledOnAllStateMachines(); + Thread.Sleep(1000); + return result; + }); + commandOnAllStateMachines.ShouldBeTrue(); } - private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) + private async Task WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) { - bool SendCommand() + async Task SendCommand() { var json = JsonConvert.SerializeObject(updatedConfiguration); + var content = new StringContent(json); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - _response = _httpClient.PostAsync(url, content).Result; - var responseContent = _response.Content.ReadAsStringAsync().Result; + + _response = await _httpClient.PostAsync(url, content); + + var responseContent = await _response.Content.ReadAsStringAsync(); if(responseContent == "There was a problem. This error message sucks raise an issue in GitHub.") { @@ -295,8 +348,14 @@ namespace Ocelot.IntegrationTests return _response.IsSuccessStatusCode; } - var commandSent = WaitFor(20000).Until(() => SendCommand()); - commandSent.ShouldBeTrue(); + var commandSent = await WaitFor(40000).Until(async () => + { + var result = await SendCommand(); + Thread.Sleep(1000); + return result; + }); + + commandSent.ShouldBeTrue(); } private void GivenIHaveAddedATokenToMyRequest() @@ -304,9 +363,9 @@ namespace Ocelot.IntegrationTests _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); } - private void GivenIHaveAnOcelotToken(string adminPath) + private async Task GivenIHaveAnOcelotToken(string adminPath) { - bool AddToken() + async Task AddToken() { try { @@ -320,8 +379,8 @@ namespace Ocelot.IntegrationTests }; var content = new FormUrlEncodedContent(formData); - var response = _httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; + var response = await _httpClient.PostAsync(tokenUrl, content); + var responseContent = await response.Content.ReadAsStringAsync(); if(!response.IsSuccessStatusCode) { return false; @@ -329,7 +388,7 @@ namespace Ocelot.IntegrationTests _token = JsonConvert.DeserializeObject(responseContent); var configPath = $"{adminPath}/.well-known/openid-configuration"; - response = _httpClient.GetAsync(configPath).Result; + response = await _httpClient.GetAsync(configPath); return response.IsSuccessStatusCode; } catch(Exception) @@ -338,7 +397,13 @@ namespace Ocelot.IntegrationTests } } - var addToken = WaitFor(20000).Until(() => AddToken()); + var addToken = await WaitFor(40000).Until(async () => + { + var result = await AddToken(); + Thread.Sleep(1000); + return result; + }); + addToken.ShouldBeTrue(); } diff --git a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs index 41cc3573..8432d584 100644 --- a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs +++ b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs @@ -15,6 +15,7 @@ using Ocelot.Configuration; namespace Ocelot.UnitTests.Controllers { using Ocelot.Configuration.Repository; + using Rafty.Concensus.Node; public class FileConfigurationControllerTests { @@ -126,14 +127,14 @@ namespace Ocelot.UnitTests.Controllers { _node .Setup(x => x.Accept(It.IsAny())) - .ReturnsAsync(new Rafty.Concensus.OkResponse(new UpdateFileConfiguration(new FileConfiguration()))); + .ReturnsAsync(new Rafty.Infrastructure.OkResponse(new UpdateFileConfiguration(new FileConfiguration()))); } private void GivenTheNodeReturnsError() { _node .Setup(x => x.Accept(It.IsAny())) - .ReturnsAsync(new Rafty.Concensus.ErrorResponse("error", new UpdateFileConfiguration(new FileConfiguration()))); + .ReturnsAsync(new Rafty.Infrastructure.ErrorResponse("error", new UpdateFileConfiguration(new FileConfiguration()))); } private void GivenTheConfigSetterReturns(Response response) diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index 9aa7db12..bfef63e0 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -1,58 +1,58 @@ - - - - 0.0.0-dev - netcoreapp2.0 - 2.0.0 - Ocelot.UnitTests - Ocelot.UnitTests - Exe - true - osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 - false - false - false - ..\..\codeanalysis.ruleset - - - - full - True - - - - - - - - - - - - - - - - all - - - - - - - - - - - - - - - - - - - - - - + + + + 0.0.0-dev + netcoreapp2.0 + 2.0.0 + Ocelot.UnitTests + Ocelot.UnitTests + Exe + true + osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 + false + false + false + ..\..\codeanalysis.ruleset + + + + full + True + + + + + + + + + + + + + + + + all + + + + + + + + + + + + + + + + + + + + + + From 0023fe2599e7e22aa0197216001c49769f167fa5 Mon Sep 17 00:00:00 2001 From: Philip Wood Date: Sun, 3 Jun 2018 05:51:14 +0100 Subject: [PATCH 06/24] Hard-code coveralls.net version as 1.0.0 seems to only support .net core 2.1 (#379) --- build.cake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.cake b/build.cake index f757228c..619df853 100644 --- a/build.cake +++ b/build.cake @@ -4,7 +4,7 @@ #addin nuget:?package=Newtonsoft.Json&version=9.0.1 #tool "nuget:?package=OpenCover" #tool "nuget:?package=ReportGenerator" -#tool coveralls.net +#tool "nuget:?package=coveralls.net&version=0.7.0" #addin Cake.Coveralls // compile From 04139333ea2c2020a7cf45c7ce40207cf5b00053 Mon Sep 17 00:00:00 2001 From: Tom Gardham-Pallister Date: Fri, 8 Jun 2018 17:54:56 +0300 Subject: [PATCH 07/24] #245 ignored these tests against as still not working --- test/Ocelot.IntegrationTests/RaftTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Ocelot.IntegrationTests/RaftTests.cs b/test/Ocelot.IntegrationTests/RaftTests.cs index 37a7dff0..7afa3ae4 100644 --- a/test/Ocelot.IntegrationTests/RaftTests.cs +++ b/test/Ocelot.IntegrationTests/RaftTests.cs @@ -48,7 +48,7 @@ namespace Ocelot.IntegrationTests _threads = new List(); } - [Fact] + [Fact(Skip = "Still not stable, more work required in rafty..")] public async Task should_persist_command_to_five_servers() { var peers = new List @@ -121,7 +121,7 @@ namespace Ocelot.IntegrationTests await ThenTheCommandIsReplicatedToAllStateMachines(command); } - [Fact] + [Fact(Skip = "Still not stable, more work required in rafty..")] public async Task should_persist_command_to_five_servers_when_using_administration_api() { var peers = new List From 3ef978460cddaf5336019b88cf6b7c8fef650cdd Mon Sep 17 00:00:00 2001 From: geffzhang Date: Fri, 8 Jun 2018 22:55:58 +0800 Subject: [PATCH 08/24] update LoadBalancer Options (#388) update LoadBalancer with LoadBalancerOptions --- docs/features/servicediscovery.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index b212a2e6..5df1208a 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -35,7 +35,9 @@ and LeastConnection algorithm you can use. If no load balancer is specified Ocel "UpstreamPathTemplate": "/posts/{postId}", "UpstreamHttpMethod": [ "Put" ], "ServiceName": "product", - "LoadBalancer": "LeastConnection", + "LoadBalancerOptions": { + "Type": "LeastConnection" + }, "UseServiceDiscovery": true } @@ -151,4 +153,4 @@ The config might look something like } } -Please take a look through all of the docs to understand these options. \ No newline at end of file +Please take a look through all of the docs to understand these options. From 7d0320beca0568021d7dbb0767ecb420d2aceb44 Mon Sep 17 00:00:00 2001 From: Wayne Douglas Date: Mon, 11 Jun 2018 20:04:13 +0100 Subject: [PATCH 09/24] Update authentication.rst (#395) Fix spelling --- docs/features/authentication.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/authentication.rst b/docs/features/authentication.rst index 78aa890f..745440ea 100644 --- a/docs/features/authentication.rst +++ b/docs/features/authentication.rst @@ -93,7 +93,7 @@ Then map the authentication provider key to a ReRoute in your configuration e.g. Identity Server Bearer Tokens ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -In order to use IdentityServer bearer tokens register your IdentityServer services as usual in ConfigureServices with a scheme (key). If you don't understand how to do this please consul the IdentityServer documentation. +In order to use IdentityServer bearer tokens register your IdentityServer services as usual in ConfigureServices with a scheme (key). If you don't understand how to do this please consult the IdentityServer documentation. .. code-block:: csharp @@ -141,4 +141,4 @@ Allowed Scopes If you add scopes to AllowedScopes Ocelot will get all the user claims (from the token) of the type scope and make sure that the user has all of the scopes in the list. -This is a way to restrict access to a ReRoute on a per scope basis. \ No newline at end of file +This is a way to restrict access to a ReRoute on a per scope basis. From 3bde18f6f86f705abceb82224190743fb183603e Mon Sep 17 00:00:00 2001 From: Wayne Douglas Date: Mon, 11 Jun 2018 20:04:43 +0100 Subject: [PATCH 10/24] Update raft.rst (#394) --- docs/features/raft.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/raft.rst b/docs/features/raft.rst index dd0cf031..b193b407 100644 --- a/docs/features/raft.rst +++ b/docs/features/raft.rst @@ -42,4 +42,4 @@ In addition to this you must add a file called peers.json to your main project a Each instance of Ocelot must have it's address in the array so that they can communicate using Rafty. -Once you have made these configuration changes you must deploy and start each instance of Ocelot using the addresses in the peers.json file. The servers should then start communicating with each other! You can test if everything is working by posting a configuration update and checking it has replicated to all servers by getting there configuration. +Once you have made these configuration changes you must deploy and start each instance of Ocelot using the addresses in the peers.json file. The servers should then start communicating with each other! You can test if everything is working by posting a configuration update and checking it has replicated to all servers by getting their configuration. From 095406bd4593b84d0449e85b3a64640b21130c88 Mon Sep 17 00:00:00 2001 From: Alex Kuriatnyk Date: Mon, 11 Jun 2018 12:23:45 -0700 Subject: [PATCH 11/24] Fix incorrect response StatusCode for middleware added before Ocelot (#380) --- src/Ocelot/Responder/HttpContextResponder.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Ocelot/Responder/HttpContextResponder.cs b/src/Ocelot/Responder/HttpContextResponder.cs index bd265a61..0b2959b4 100644 --- a/src/Ocelot/Responder/HttpContextResponder.cs +++ b/src/Ocelot/Responder/HttpContextResponder.cs @@ -59,12 +59,8 @@ namespace Ocelot.Responder } public void SetErrorResponseOnContext(HttpContext context, int statusCode) - { - context.Response.OnStarting(x => - { - context.Response.StatusCode = statusCode; - return Task.CompletedTask; - }, context); + { + context.Response.StatusCode = statusCode; } private static void AddHeaderIfDoesntExist(HttpContext context, Header httpResponseHeader) From 14308ff5fb22cc98413092972189951eb5eff266 Mon Sep 17 00:00:00 2001 From: Tom Gardham-Pallister Date: Mon, 11 Jun 2018 22:57:43 +0100 Subject: [PATCH 12/24] added ignore code coverage to experimental sqllitelog class that cannot be unit tested anyway really --- src/Ocelot/Raft/SqlLiteLog.cs | 601 +++++++++++++++++----------------- 1 file changed, 301 insertions(+), 300 deletions(-) diff --git a/src/Ocelot/Raft/SqlLiteLog.cs b/src/Ocelot/Raft/SqlLiteLog.cs index 882df202..f4dfed49 100644 --- a/src/Ocelot/Raft/SqlLiteLog.cs +++ b/src/Ocelot/Raft/SqlLiteLog.cs @@ -1,321 +1,322 @@ -namespace Ocelot.Raft -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.Data.Sqlite; - using Microsoft.Extensions.Logging; - using Newtonsoft.Json; - using Rafty.Infrastructure; - using Rafty.Log; - - public class SqlLiteLog : ILog - { - private readonly string _path; - private readonly SemaphoreSlim _sempaphore = new SemaphoreSlim(1, 1); - private readonly ILogger _logger; - private readonly NodeId _nodeId; - - public SqlLiteLog(NodeId nodeId, ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - _nodeId = nodeId; - _path = $"{nodeId.Id.Replace("/", "").Replace(":", "")}.db"; - _sempaphore.Wait(); - - if (!File.Exists(_path)) - { - var fs = File.Create(_path); - +namespace Ocelot.Raft +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Data.Sqlite; + using Microsoft.Extensions.Logging; + using Newtonsoft.Json; + using Rafty.Infrastructure; + using Rafty.Log; + + [ExcludeFromCoverage] + public class SqlLiteLog : ILog + { + private readonly string _path; + private readonly SemaphoreSlim _sempaphore = new SemaphoreSlim(1, 1); + private readonly ILogger _logger; + private readonly NodeId _nodeId; + + public SqlLiteLog(NodeId nodeId, ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + _nodeId = nodeId; + _path = $"{nodeId.Id.Replace("/", "").Replace(":", "")}.db"; + _sempaphore.Wait(); + + if (!File.Exists(_path)) + { + var fs = File.Create(_path); + fs.Dispose(); - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - - const string sql = @"create table logs ( - id integer primary key, - data text not null - )"; - - using (var command = new SqliteCommand(sql, connection)) - { - var result = command.ExecuteNonQuery(); - - _logger.LogInformation(result == 0 - ? $"id: {_nodeId.Id} create database, result: {result}" - : $"id: {_nodeId.Id} did not create database., result: {result}"); - } - } - } - - _sempaphore.Release(); - } - - public async Task LastLogIndex() - { - _sempaphore.Wait(); - var result = 1; - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - var sql = @"select id from logs order by id desc limit 1"; - using (var command = new SqliteCommand(sql, connection)) - { - var index = Convert.ToInt32(await command.ExecuteScalarAsync()); - if (index > result) - { - result = index; - } - } - } - - _sempaphore.Release(); - return result; - } - - public async Task LastLogTerm() - { - _sempaphore.Wait(); - long result = 0; - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - var sql = @"select data from logs order by id desc limit 1"; - using (var command = new SqliteCommand(sql, connection)) - { - var data = Convert.ToString(await command.ExecuteScalarAsync()); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + + const string sql = @"create table logs ( + id integer primary key, + data text not null + )"; + + using (var command = new SqliteCommand(sql, connection)) + { + var result = command.ExecuteNonQuery(); + + _logger.LogInformation(result == 0 + ? $"id: {_nodeId.Id} create database, result: {result}" + : $"id: {_nodeId.Id} did not create database., result: {result}"); + } + } + } + + _sempaphore.Release(); + } + + public async Task LastLogIndex() + { + _sempaphore.Wait(); + var result = 1; + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var sql = @"select id from logs order by id desc limit 1"; + using (var command = new SqliteCommand(sql, connection)) + { + var index = Convert.ToInt32(await command.ExecuteScalarAsync()); + if (index > result) + { + result = index; + } + } + } + + _sempaphore.Release(); + return result; + } + + public async Task LastLogTerm() + { + _sempaphore.Wait(); + long result = 0; + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var sql = @"select data from logs order by id desc limit 1"; + using (var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(await command.ExecuteScalarAsync()); var jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - if (log != null && log.Term > result) - { - result = log.Term; - } - } - } - _sempaphore.Release(); - return result; - } - + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + if (log != null && log.Term > result) + { + result = log.Term; + } + } + } + _sempaphore.Release(); + return result; + } + public async Task Count() - { - _sempaphore.Wait(); - var result = 0; - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - var sql = @"select count(id) from logs"; - using (var command = new SqliteCommand(sql, connection)) - { - var index = Convert.ToInt32(await command.ExecuteScalarAsync()); - if (index > result) - { - result = index; - } - } - } - _sempaphore.Release(); - return result; - } - - public async Task Apply(LogEntry log) - { - _sempaphore.Wait(); - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); + { + _sempaphore.Wait(); + var result = 0; + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + var sql = @"select count(id) from logs"; + using (var command = new SqliteCommand(sql, connection)) + { + var index = Convert.ToInt32(await command.ExecuteScalarAsync()); + if (index > result) + { + result = index; + } + } + } + _sempaphore.Release(); + return result; + } + + public async Task Apply(LogEntry log) + { + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); var jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - var data = JsonConvert.SerializeObject(log, jsonSerializerSettings); - //todo - sql injection dont copy this.. - var sql = $"insert into logs (data) values ('{data}')"; - _logger.LogInformation($"id: {_nodeId.Id}, sql: {sql}"); - using (var command = new SqliteCommand(sql, connection)) - { - var result = await command.ExecuteNonQueryAsync(); - _logger.LogInformation($"id: {_nodeId.Id}, insert log result: {result}"); - } - - sql = "select last_insert_rowid()"; - using (var command = new SqliteCommand(sql, connection)) - { - var result = await command.ExecuteScalarAsync(); - _logger.LogInformation($"id: {_nodeId.Id}, about to release semaphore"); - _sempaphore.Release(); - _logger.LogInformation($"id: {_nodeId.Id}, saved log to sqlite"); - return Convert.ToInt32(result); + TypeNameHandling = TypeNameHandling.All + }; + var data = JsonConvert.SerializeObject(log, jsonSerializerSettings); + //todo - sql injection dont copy this.. + var sql = $"insert into logs (data) values ('{data}')"; + _logger.LogInformation($"id: {_nodeId.Id}, sql: {sql}"); + using (var command = new SqliteCommand(sql, connection)) + { + var result = await command.ExecuteNonQueryAsync(); + _logger.LogInformation($"id: {_nodeId.Id}, insert log result: {result}"); } - } - } - - public async Task DeleteConflictsFromThisLog(int index, LogEntry logEntry) - { - _sempaphore.Wait(); - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - //todo - sql injection dont copy this.. - var sql = $"select data from logs where id = {index};"; - _logger.LogInformation($"id: {_nodeId.Id} sql: {sql}"); - using (var command = new SqliteCommand(sql, connection)) - { - var data = Convert.ToString(await command.ExecuteScalarAsync()); + + sql = "select last_insert_rowid()"; + using (var command = new SqliteCommand(sql, connection)) + { + var result = await command.ExecuteScalarAsync(); + _logger.LogInformation($"id: {_nodeId.Id}, about to release semaphore"); + _sempaphore.Release(); + _logger.LogInformation($"id: {_nodeId.Id}, saved log to sqlite"); + return Convert.ToInt32(result); + } + } + } + + public async Task DeleteConflictsFromThisLog(int index, LogEntry logEntry) + { + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index};"; + _logger.LogInformation($"id: {_nodeId.Id} sql: {sql}"); + using (var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(await command.ExecuteScalarAsync()); var jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - - _logger.LogInformation($"id {_nodeId.Id} got log for index: {index}, data is {data} and new log term is {logEntry.Term}"); - - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - if (logEntry != null && log != null && logEntry.Term != log.Term) - { - //todo - sql injection dont copy this.. - var deleteSql = $"delete from logs where id >= {index};"; - _logger.LogInformation($"id: {_nodeId.Id} sql: {deleteSql}"); - using (var deleteCommand = new SqliteCommand(deleteSql, connection)) - { - var result = await deleteCommand.ExecuteNonQueryAsync(); - } - } - } - } - _sempaphore.Release(); - } - - public async Task IsDuplicate(int index, LogEntry logEntry) - { - _sempaphore.Wait(); - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - //todo - sql injection dont copy this.. - var sql = $"select data from logs where id = {index};"; - using (var command = new SqliteCommand(sql, connection)) - { - var data = Convert.ToString(await command.ExecuteScalarAsync()); + TypeNameHandling = TypeNameHandling.All + }; + + _logger.LogInformation($"id {_nodeId.Id} got log for index: {index}, data is {data} and new log term is {logEntry.Term}"); + + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + if (logEntry != null && log != null && logEntry.Term != log.Term) + { + //todo - sql injection dont copy this.. + var deleteSql = $"delete from logs where id >= {index};"; + _logger.LogInformation($"id: {_nodeId.Id} sql: {deleteSql}"); + using (var deleteCommand = new SqliteCommand(deleteSql, connection)) + { + var result = await deleteCommand.ExecuteNonQueryAsync(); + } + } + } + } + _sempaphore.Release(); + } + + public async Task IsDuplicate(int index, LogEntry logEntry) + { + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index};"; + using (var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(await command.ExecuteScalarAsync()); var jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - - if (logEntry != null && log != null && logEntry.Term == log.Term) - { - _sempaphore.Release(); - return true; - } - } - } - - _sempaphore.Release(); - return false; - } - - public async Task Get(int index) - { - _sempaphore.Wait(); - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - //todo - sql injection dont copy this.. - var sql = $"select data from logs where id = {index}"; - using (var command = new SqliteCommand(sql, connection)) - { - var data = Convert.ToString(await command.ExecuteScalarAsync()); + TypeNameHandling = TypeNameHandling.All + }; + + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + + if (logEntry != null && log != null && logEntry.Term == log.Term) + { + _sempaphore.Release(); + return true; + } + } + } + + _sempaphore.Release(); + return false; + } + + public async Task Get(int index) + { + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index}"; + using (var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(await command.ExecuteScalarAsync()); var jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - _sempaphore.Release(); - return log; - } - } - } - - public async Task> GetFrom(int index) - { - _sempaphore.Wait(); - var logsToReturn = new List<(int, LogEntry)>(); - - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - //todo - sql injection dont copy this.. - var sql = $"select id, data from logs where id >= {index}"; - using (var command = new SqliteCommand(sql, connection)) - { - using (var reader = await command.ExecuteReaderAsync()) - { - while (reader.Read()) - { - var id = Convert.ToInt32(reader[0]); - var data = (string)reader[1]; + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + _sempaphore.Release(); + return log; + } + } + } + + public async Task> GetFrom(int index) + { + _sempaphore.Wait(); + var logsToReturn = new List<(int, LogEntry)>(); + + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select id, data from logs where id >= {index}"; + using (var command = new SqliteCommand(sql, connection)) + { + using (var reader = await command.ExecuteReaderAsync()) + { + while (reader.Read()) + { + var id = Convert.ToInt32(reader[0]); + var data = (string)reader[1]; var jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - logsToReturn.Add((id, log)); - - } - } + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + logsToReturn.Add((id, log)); + + } + } } _sempaphore.Release(); - return logsToReturn; - } - } - - public async Task GetTermAtIndex(int index) - { - _sempaphore.Wait(); - long result = 0; - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - //todo - sql injection dont copy this.. - var sql = $"select data from logs where id = {index}"; - using (var command = new SqliteCommand(sql, connection)) - { - var data = Convert.ToString(await command.ExecuteScalarAsync()); + return logsToReturn; + } + } + + public async Task GetTermAtIndex(int index) + { + _sempaphore.Wait(); + long result = 0; + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var sql = $"select data from logs where id = {index}"; + using (var command = new SqliteCommand(sql, connection)) + { + var data = Convert.ToString(await command.ExecuteScalarAsync()); var jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - if (log != null && log.Term > result) - { - result = log.Term; - } - } - } - _sempaphore.Release(); - return result; - } - public async Task Remove(int indexOfCommand) - { - _sempaphore.Wait(); - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - //todo - sql injection dont copy this.. - var deleteSql = $"delete from logs where id >= {indexOfCommand};"; - _logger.LogInformation($"id: {_nodeId.Id} Remove {deleteSql}"); - using (var deleteCommand = new SqliteCommand(deleteSql, connection)) - { - var result = await deleteCommand.ExecuteNonQueryAsync(); - } - } - _sempaphore.Release(); - } - } + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + if (log != null && log.Term > result) + { + result = log.Term; + } + } + } + _sempaphore.Release(); + return result; + } + public async Task Remove(int indexOfCommand) + { + _sempaphore.Wait(); + using (var connection = new SqliteConnection($"Data Source={_path};")) + { + connection.Open(); + //todo - sql injection dont copy this.. + var deleteSql = $"delete from logs where id >= {indexOfCommand};"; + _logger.LogInformation($"id: {_nodeId.Id} Remove {deleteSql}"); + using (var deleteCommand = new SqliteCommand(deleteSql, connection)) + { + var result = await deleteCommand.ExecuteNonQueryAsync(); + } + } + _sempaphore.Release(); + } + } } From 0f2a9c1d0d22d11697d9ebaabd75316ab4465678 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Tue, 12 Jun 2018 00:58:08 +0300 Subject: [PATCH 13/24] Feature/poll consul (#392) * WIP - implement a consul service discovery poller, lots of shared code with existing, refactor next and a todo in the docs to finish * #374 implement polling for consul as option * #374 updated docs to remove todo * #374 fixed failing unit test * #374 fixed failing unit test * #374 fixed failing acceptance test --- docs/features/servicediscovery.rst | 327 +++++++++--------- .../ServiceProviderConfigurationBuilder.cs | 9 +- .../ServiceProviderConfigurationCreator.cs | 2 + .../File/FileServiceDiscoveryProvider.cs | 1 + .../ServiceProviderConfiguration.cs | 4 +- .../PolingConsulServiceDiscoveryProvider.cs | 56 +++ .../ServiceDiscoveryProviderFactory.cs | 126 +++---- .../ServiceDiscoveryTests.cs | 58 ++++ test/Ocelot.AcceptanceTests/Steps.cs | 19 + .../LoadBalancer/LoadBalancerHouseTests.cs | 2 +- ...lingConsulServiceDiscoveryProviderTests.cs | 89 +++++ .../ServiceProviderFactoryTests.cs | 325 +++++++++-------- 12 files changed, 649 insertions(+), 369 deletions(-) create mode 100644 src/Ocelot/ServiceDiscovery/Providers/PolingConsulServiceDiscoveryProvider.cs create mode 100644 test/Ocelot.UnitTests/ServiceDiscovery/PollingConsulServiceDiscoveryProviderTests.cs diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index 5df1208a..a39f5db8 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -1,156 +1,171 @@ -.. service-discovery: - -Service Discovery -================= - -Ocelot allows you to specify a service discovery provider and will use this to find the host and port -for the downstream service Ocelot is forwarding a request to. At the moment this is only supported in the -GlobalConfiguration section which means the same service discovery provider will be used for all ReRoutes -you specify a ServiceName for at ReRoute level. - -Consul -^^^^^^ - -The following is required in the GlobalConfiguration. The Provider is required and if you do not specify a host and port the Consul default -will be used. - -.. code-block:: json - - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 9500 - } - -In the future we can add a feature that allows ReRoute specfic configuration. - -In order to tell Ocelot a ReRoute is to use the service discovery provider for its host and port you must add the -ServiceName, UseServiceDiscovery and load balancer you wish to use when making requests downstream. At the moment Ocelot has a RoundRobin -and LeastConnection algorithm you can use. If no load balancer is specified Ocelot will not load balance requests. - -.. code-block:: json - - { - "DownstreamPathTemplate": "/api/posts/{postId}", - "DownstreamScheme": "https", - "UpstreamPathTemplate": "/posts/{postId}", - "UpstreamHttpMethod": [ "Put" ], - "ServiceName": "product", - "LoadBalancerOptions": { - "Type": "LeastConnection" - }, - "UseServiceDiscovery": true - } - -When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balance requests across any available services. - -ACL Token ---------- - -If you are using ACL with Consul Ocelot supports adding the X-Consul-Token header. In order so this to work you must add the additional property below. - -.. code-block:: json - - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 9500, - "Token": "footoken" - } - -Ocelot will add this token to the consul client that it uses to make requests and that is then used for every request. - -Eureka -^^^^^^ - -This feature was requested as part of `Issue 262 `_ . to add support for Netflix's -Eureka service discovery provider. The main reason for this is it is a key part of `Steeltoe `_ which is something -to do with `Pivotal `_! Anyway enough of the background. - -In order to get this working add the following to ocelot.json.. - -.. code-block:: json - - "ServiceDiscoveryProvider": { - "Type": "Eureka" - } - -And following the guide `Here `_ you may also need to add some stuff to appsettings.json. For example the json below tells the steeltoe / pivotal services where to look for the service discovery server and if the service should register with it. - -.. code-block:: json - - "eureka": { - "client": { - "serviceUrl": "http://localhost:8761/eureka/", - "shouldRegisterWithEureka": false, - "shouldFetchRegistry": true - } - } - -I am told that if shouldRegisterWithEureka is false then shouldFetchRegistry will defaut to true so you don't need it explicitly but left it in there. - -Ocelot will now register all the necessary services when it starts up and if you have the json above will register itself with -Eureka. One of the services polls Eureka every 30 seconds (default) and gets the latest service state and persists this in memory. -When Ocelot asks for a given service it is retrieved from memory so performance is not a big problem. Please note that this code -is provided by the Pivotal.Discovery.Client NuGet package so big thanks to them for all the hard work. - -Dynamic Routing -^^^^^^^^^^^^^^^ - -This feature was requested in `issue 340 `_. The idea is to enable dynamic routing when using -a service discovery provider (see that section of the docs for more info). In this mode Ocelot will use the first segmentof the upstream path to lookup the -downstream service with the service discovery provider. - -An example of this would be calling ocelot with a url like https://api.mywebsite.com/product/products. Ocelot will take the first segment of -the path which is product and use it as a key to look up the service in consul. If consul returns a service Ocelot will request it on whatever host and -port comes back from consul plus the remaining path segments in this case products thus making the downstream call http://hostfromconsul:portfromconsul/products. -Ocelot will apprend any query string to the downstream url as normal. - -In order to enable dynamic routing you need to have 0 ReRoutes in your config. At the moment you cannot mix dynamic and configuration ReRoutes. In addition to this you -need to specify the Service Discovery provider details as outlined above and the downstream http/https scheme as DownstreamScheme. - -In addition to that you can set RateLimitOptions, QoSOptions, LoadBalancerOptions and HttpHandlerOptions, DownstreamScheme (You might want to call Ocelot on https but -talk to private services over http) that will be applied to all of the dynamic ReRoutes. - -The config might look something like - -.. code-block:: json - - { - "ReRoutes": [], - "Aggregates": [], - "GlobalConfiguration": { - "RequestIdKey": null, - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 8510, - "Type": null, - "Token": null, - "ConfigurationKey": null - }, - "RateLimitOptions": { - "ClientIdHeader": "ClientId", - "QuotaExceededMessage": null, - "RateLimitCounterPrefix": "ocelot", - "DisableRateLimitHeaders": false, - "HttpStatusCode": 429 - }, - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 0, - "DurationOfBreak": 0, - "TimeoutValue": 0 - }, - "BaseUrl": null, - "LoadBalancerOptions": { - "Type": "LeastConnection", - "Key": null, - "Expiry": 0 - }, - "DownstreamScheme": "http", - "HttpHandlerOptions": { - "AllowAutoRedirect": false, - "UseCookieContainer": false, - "UseTracing": false - } - } - } - -Please take a look through all of the docs to understand these options. +.. service-discovery: + +Service Discovery +================= + +Ocelot allows you to specify a service discovery provider and will use this to find the host and port +for the downstream service Ocelot is forwarding a request to. At the moment this is only supported in the +GlobalConfiguration section which means the same service discovery provider will be used for all ReRoutes +you specify a ServiceName for at ReRoute level. + +Consul +^^^^^^ + +The following is required in the GlobalConfiguration. The Provider is required and if you do not specify a host and port the Consul default +will be used. + +.. code-block:: json + + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500 + } + +In the future we can add a feature that allows ReRoute specfic configuration. + +In order to tell Ocelot a ReRoute is to use the service discovery provider for its host and port you must add the +ServiceName, UseServiceDiscovery and load balancer you wish to use when making requests downstream. At the moment Ocelot has a RoundRobin +and LeastConnection algorithm you can use. If no load balancer is specified Ocelot will not load balance requests. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": [ "Put" ], + "ServiceName": "product", + "LoadBalancerOptions": { + "Type": "LeastConnection" + }, + "UseServiceDiscovery": true + } + +When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balance requests across any available services. + +A lot of people have asked me to implement a feature where Ocelot polls consul for latest service information rather than per request. If you want to poll consul for the latest services rather than per request (default behaviour) then you need to set the following configuration. + +.. code-block:: json + + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500, + "Type": "PollConsul", + "PollingInteral": 100 + } + +The polling interval is in milliseconds and tells Ocelot how often to call Consul for changes in service configuration. + +Please note there are tradeoffs here. If you poll Consul it is possible Ocelot will not know if a service is down depending on your polling interval and you might get more errors than if you get the latest services per request. This really depends on how volitile your services are. I doubt it will matter for most people and polling may give a tiny performance improvement over calling consul per request (as sidecar agent). If you are calling a remote consul agent then polling will be a good performance improvement. + +ACL Token +--------- + +If you are using ACL with Consul Ocelot supports adding the X-Consul-Token header. In order so this to work you must add the additional property below. + +.. code-block:: json + + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500, + "Token": "footoken" + } + +Ocelot will add this token to the consul client that it uses to make requests and that is then used for every request. + +Eureka +^^^^^^ + +This feature was requested as part of `Issue 262 `_ . to add support for Netflix's +Eureka service discovery provider. The main reason for this is it is a key part of `Steeltoe `_ which is something +to do with `Pivotal `_! Anyway enough of the background. + +In order to get this working add the following to ocelot.json.. + +.. code-block:: json + + "ServiceDiscoveryProvider": { + "Type": "Eureka" + } + +And following the guide `Here `_ you may also need to add some stuff to appsettings.json. For example the json below tells the steeltoe / pivotal services where to look for the service discovery server and if the service should register with it. + +.. code-block:: json + + "eureka": { + "client": { + "serviceUrl": "http://localhost:8761/eureka/", + "shouldRegisterWithEureka": false, + "shouldFetchRegistry": true + } + } + +I am told that if shouldRegisterWithEureka is false then shouldFetchRegistry will defaut to true so you don't need it explicitly but left it in there. + +Ocelot will now register all the necessary services when it starts up and if you have the json above will register itself with +Eureka. One of the services polls Eureka every 30 seconds (default) and gets the latest service state and persists this in memory. +When Ocelot asks for a given service it is retrieved from memory so performance is not a big problem. Please note that this code +is provided by the Pivotal.Discovery.Client NuGet package so big thanks to them for all the hard work. + +Dynamic Routing +^^^^^^^^^^^^^^^ + +This feature was requested in `issue 340 `_. The idea is to enable dynamic routing when using +a service discovery provider (see that section of the docs for more info). In this mode Ocelot will use the first segmentof the upstream path to lookup the +downstream service with the service discovery provider. + +An example of this would be calling ocelot with a url like https://api.mywebsite.com/product/products. Ocelot will take the first segment of +the path which is product and use it as a key to look up the service in consul. If consul returns a service Ocelot will request it on whatever host and +port comes back from consul plus the remaining path segments in this case products thus making the downstream call http://hostfromconsul:portfromconsul/products. +Ocelot will apprend any query string to the downstream url as normal. + +In order to enable dynamic routing you need to have 0 ReRoutes in your config. At the moment you cannot mix dynamic and configuration ReRoutes. In addition to this you +need to specify the Service Discovery provider details as outlined above and the downstream http/https scheme as DownstreamScheme. + +In addition to that you can set RateLimitOptions, QoSOptions, LoadBalancerOptions and HttpHandlerOptions, DownstreamScheme (You might want to call Ocelot on https but +talk to private services over http) that will be applied to all of the dynamic ReRoutes. + +The config might look something like + +.. code-block:: json + + { + "ReRoutes": [], + "Aggregates": [], + "GlobalConfiguration": { + "RequestIdKey": null, + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 8510, + "Type": null, + "Token": null, + "ConfigurationKey": null + }, + "RateLimitOptions": { + "ClientIdHeader": "ClientId", + "QuotaExceededMessage": null, + "RateLimitCounterPrefix": "ocelot", + "DisableRateLimitHeaders": false, + "HttpStatusCode": 429 + }, + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 0, + "DurationOfBreak": 0, + "TimeoutValue": 0 + }, + "BaseUrl": null, + "LoadBalancerOptions": { + "Type": "LeastConnection", + "Key": null, + "Expiry": 0 + }, + "DownstreamScheme": "http", + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + "UseCookieContainer": false, + "UseTracing": false + } + } + } + +Please take a look through all of the docs to understand these options. diff --git a/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs b/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs index 25df651c..1ea18067 100644 --- a/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs @@ -7,6 +7,7 @@ namespace Ocelot.Configuration.Builder private string _type; private string _token; private string _configurationKey; + private int _pollingInterval; public ServiceProviderConfigurationBuilder WithHost(string serviceDiscoveryProviderHost) { @@ -38,9 +39,15 @@ namespace Ocelot.Configuration.Builder return this; } + public ServiceProviderConfigurationBuilder WithPollingInterval(int pollingInterval) + { + _pollingInterval = pollingInterval; + return this; + } + public ServiceProviderConfiguration Build() { - return new ServiceProviderConfiguration(_type, _serviceDiscoveryProviderHost, _serviceDiscoveryProviderPort, _token, _configurationKey); + return new ServiceProviderConfiguration(_type, _serviceDiscoveryProviderHost, _serviceDiscoveryProviderPort, _token, _configurationKey, _pollingInterval); } } } diff --git a/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs index 4943aac6..3fd07454 100644 --- a/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs @@ -9,6 +9,7 @@ namespace Ocelot.Configuration.Creator { var port = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; var host = globalConfiguration?.ServiceDiscoveryProvider?.Host ?? "consul"; + var pollingInterval = globalConfiguration?.ServiceDiscoveryProvider?.PollingInterval ?? 0; return new ServiceProviderConfigurationBuilder() .WithHost(host) @@ -16,6 +17,7 @@ namespace Ocelot.Configuration.Creator .WithType(globalConfiguration?.ServiceDiscoveryProvider?.Type) .WithToken(globalConfiguration?.ServiceDiscoveryProvider?.Token) .WithConfigurationKey(globalConfiguration?.ServiceDiscoveryProvider?.ConfigurationKey) + .WithPollingInterval(pollingInterval) .Build(); } } diff --git a/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs index df6c7e27..e6edeee7 100644 --- a/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs +++ b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs @@ -7,5 +7,6 @@ namespace Ocelot.Configuration.File public string Type { get; set; } public string Token { get; set; } public string ConfigurationKey { get; set; } + public int PollingInterval { get; set; } } } diff --git a/src/Ocelot/Configuration/ServiceProviderConfiguration.cs b/src/Ocelot/Configuration/ServiceProviderConfiguration.cs index c0bb30fb..72c3abfb 100644 --- a/src/Ocelot/Configuration/ServiceProviderConfiguration.cs +++ b/src/Ocelot/Configuration/ServiceProviderConfiguration.cs @@ -2,13 +2,14 @@ { public class ServiceProviderConfiguration { - public ServiceProviderConfiguration(string type, string host, int port, string token, string configurationKey) + public ServiceProviderConfiguration(string type, string host, int port, string token, string configurationKey, int pollingInterval) { ConfigurationKey = configurationKey; Host = host; Port = port; Token = token; Type = type; + PollingInterval = pollingInterval; } public string Host { get; } @@ -16,5 +17,6 @@ public string Type { get; } public string Token { get; } public string ConfigurationKey { get; } + public int PollingInterval { get; } } } diff --git a/src/Ocelot/ServiceDiscovery/Providers/PolingConsulServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/Providers/PolingConsulServiceDiscoveryProvider.cs new file mode 100644 index 00000000..4206f7c1 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/Providers/PolingConsulServiceDiscoveryProvider.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Consul; +using Ocelot.Infrastructure.Consul; +using Ocelot.Infrastructure.Extensions; +using Ocelot.Logging; +using Ocelot.ServiceDiscovery.Configuration; +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery.Providers +{ + public class PollingConsulServiceDiscoveryProvider : IServiceDiscoveryProvider + { + private readonly IOcelotLogger _logger; + private readonly IServiceDiscoveryProvider _consulServiceDiscoveryProvider; + private readonly Timer _timer; + private bool _polling; + private List _services; + private string _keyOfServiceInConsul; + + public PollingConsulServiceDiscoveryProvider(int pollingInterval, string keyOfServiceInConsul, IOcelotLoggerFactory factory, IServiceDiscoveryProvider consulServiceDiscoveryProvider) + {; + _logger = factory.CreateLogger(); + _keyOfServiceInConsul = keyOfServiceInConsul; + _consulServiceDiscoveryProvider = consulServiceDiscoveryProvider; + _services = new List(); + + _timer = new Timer(async x => + { + if(_polling) + { + return; + } + + _polling = true; + await Poll(); + _polling = false; + + }, null, pollingInterval, pollingInterval); + } + + public Task> Get() + { + return Task.FromResult(_services); + } + + private async Task Poll() + { + _services = await _consulServiceDiscoveryProvider.Get(); + } + } +} diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs index 3ed38b98..88b4ce79 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs @@ -1,62 +1,70 @@ -using System.Collections.Generic; -using Ocelot.Configuration; -using Ocelot.Infrastructure.Consul; -using Ocelot.Logging; -using Ocelot.ServiceDiscovery.Configuration; -using Ocelot.ServiceDiscovery.Providers; -using Ocelot.Values; - -namespace Ocelot.ServiceDiscovery +using System.Collections.Generic; +using Ocelot.Configuration; +using Ocelot.Infrastructure.Consul; +using Ocelot.Logging; +using Ocelot.ServiceDiscovery.Configuration; +using Ocelot.ServiceDiscovery.Providers; +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery { using Steeltoe.Common.Discovery; - public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory - { - private readonly IOcelotLoggerFactory _factory; - private readonly IConsulClientFactory _consulFactory; - private readonly IDiscoveryClient _eurekaClient; - - public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IConsulClientFactory consulFactory, IDiscoveryClient eurekaClient) - { - _factory = factory; - _consulFactory = consulFactory; - _eurekaClient = eurekaClient; - } - - public IServiceDiscoveryProvider Get(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute) - { - if (reRoute.UseServiceDiscovery) - { - return GetServiceDiscoveryProvider(serviceConfig, reRoute.ServiceName); - } - - var services = new List(); - - foreach (var downstreamAddress in reRoute.DownstreamAddresses) - { - var service = new Service(reRoute.ServiceName, new ServiceHostAndPort(downstreamAddress.Host, downstreamAddress.Port), string.Empty, string.Empty, new string[0]); - - services.Add(service); - } - - return new ConfigurationServiceProvider(services); - } - - private IServiceDiscoveryProvider GetServiceDiscoveryProvider(ServiceProviderConfiguration serviceConfig, string serviceName) - { - if (serviceConfig.Type?.ToLower() == "servicefabric") - { - var config = new ServiceFabricConfiguration(serviceConfig.Host, serviceConfig.Port, serviceName); - return new ServiceFabricServiceDiscoveryProvider(config); - } - - if (serviceConfig.Type?.ToLower() == "eureka") - { - return new EurekaServiceDiscoveryProvider(serviceName, _eurekaClient); - } - - var consulRegistryConfiguration = new ConsulRegistryConfiguration(serviceConfig.Host, serviceConfig.Port, serviceName, serviceConfig.Token); - return new ConsulServiceDiscoveryProvider(consulRegistryConfiguration, _factory, _consulFactory); - } - } -} + public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory + { + private readonly IOcelotLoggerFactory _factory; + private readonly IConsulClientFactory _consulFactory; + private readonly IDiscoveryClient _eurekaClient; + + public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IConsulClientFactory consulFactory, IDiscoveryClient eurekaClient) + { + _factory = factory; + _consulFactory = consulFactory; + _eurekaClient = eurekaClient; + } + + public IServiceDiscoveryProvider Get(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute) + { + if (reRoute.UseServiceDiscovery) + { + return GetServiceDiscoveryProvider(serviceConfig, reRoute.ServiceName); + } + + var services = new List(); + + foreach (var downstreamAddress in reRoute.DownstreamAddresses) + { + var service = new Service(reRoute.ServiceName, new ServiceHostAndPort(downstreamAddress.Host, downstreamAddress.Port), string.Empty, string.Empty, new string[0]); + + services.Add(service); + } + + return new ConfigurationServiceProvider(services); + } + + private IServiceDiscoveryProvider GetServiceDiscoveryProvider(ServiceProviderConfiguration serviceConfig, string serviceName) + { + if (serviceConfig.Type?.ToLower() == "servicefabric") + { + var config = new ServiceFabricConfiguration(serviceConfig.Host, serviceConfig.Port, serviceName); + return new ServiceFabricServiceDiscoveryProvider(config); + } + + if (serviceConfig.Type?.ToLower() == "eureka") + { + return new EurekaServiceDiscoveryProvider(serviceName, _eurekaClient); + } + + var consulRegistryConfiguration = new ConsulRegistryConfiguration(serviceConfig.Host, serviceConfig.Port, serviceName, serviceConfig.Token); + + var consulServiceDiscoveryProvider = new ConsulServiceDiscoveryProvider(consulRegistryConfiguration, _factory, _consulFactory); + + if (serviceConfig.Type?.ToLower() == "pollconsul") + { + return new PollingConsulServiceDiscoveryProvider(serviceConfig.PollingInterval, consulRegistryConfiguration.KeyOfServiceInConsul, _factory, consulServiceDiscoveryProvider); + } + + return consulServiceDiscoveryProvider; + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs index 2ec6d945..9d21aa34 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -458,6 +458,64 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } + [Fact] + public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() + { + const int consulPort = 8518; + const string serviceName = "web"; + const int downstreamServicePort = 8082; + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry() + { + Service = new AgentService() + { + Service = serviceName, + Address = "localhost", + Port = downstreamServicePort, + ID = $"web_90_0_2_224_{downstreamServicePort}", + Tags = new[] {"version-v1"} + }, + }; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "Get", "Options" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + UseServiceDiscovery = true, + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() + { + Host = "localhost", + Port = consulPort, + Type = "PollConsul", + PollingInterval = 0 + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) { _consulServices.Add(serviceEntryTwo); diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 5041088e..f3c1a6d2 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -27,6 +27,7 @@ using System.Text; using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; using Ocelot.Requester; using Ocelot.Middleware.Multiplexer; +using static Ocelot.Infrastructure.Wait; namespace Ocelot.AcceptanceTests { @@ -675,6 +676,24 @@ namespace Ocelot.AcceptanceTests _response = _ocelotClient.GetAsync(url).Result; } + public void WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string url) + { + var result = WaitFor(2000).Until(() => { + try + { + _response = _ocelotClient.GetAsync(url).Result; + _response.EnsureSuccessStatusCode(); + return true; + } + catch(Exception) + { + return false; + } + }); + + result.ShouldBeTrue(); + } + public void WhenIGetUrlOnTheApiGateway(string url, string cookie, string value) { var request = _ocelotServer.CreateRequest(url); diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs index 6e0ae9bd..dbab895d 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs @@ -26,7 +26,7 @@ namespace Ocelot.UnitTests.LoadBalancer { _factory = new Mock(); _loadBalancerHouse = new LoadBalancerHouse(_factory.Object); - _serviceProviderConfig = new ServiceProviderConfiguration("myType","myHost",123, string.Empty, "configKey"); + _serviceProviderConfig = new ServiceProviderConfiguration("myType","myHost",123, string.Empty, "configKey", 0); } [Fact] diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/PollingConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/PollingConsulServiceDiscoveryProviderTests.cs new file mode 100644 index 00000000..e75bfbca --- /dev/null +++ b/test/Ocelot.UnitTests/ServiceDiscovery/PollingConsulServiceDiscoveryProviderTests.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Consul; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Infrastructure.Consul; +using Ocelot.Logging; +using Ocelot.ServiceDiscovery.Configuration; +using Ocelot.ServiceDiscovery.Providers; +using Ocelot.Values; +using Xunit; +using TestStack.BDDfy; +using Shouldly; +using static Ocelot.Infrastructure.Wait; + +namespace Ocelot.UnitTests.ServiceDiscovery +{ + public class PollingConsulServiceDiscoveryProviderTests + { + private readonly int _delay; + private PollingConsulServiceDiscoveryProvider _provider; + private readonly string _serviceName; + private List _services; + private readonly Mock _factory; + private readonly Mock _logger; + private Mock _consulServiceDiscoveryProvider; + private List _result; + + public PollingConsulServiceDiscoveryProviderTests() + { + _services = new List(); + _delay = 1; + _factory = new Mock(); + _logger = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _consulServiceDiscoveryProvider = new Mock(); + } + + [Fact] + public void should_return_service_from_consul() + { + var service = new Service("", new ServiceHostAndPort("", 0), "", "", new List()); + + this.Given(x => GivenConsulReturns(service)) + .When(x => WhenIGetTheServices(1)) + .Then(x => ThenTheCountIs(1)) + .BDDfy(); + } + + private void GivenConsulReturns(Service service) + { + _services.Add(service); + _consulServiceDiscoveryProvider.Setup(x => x.Get()).ReturnsAsync(_services); + } + + private void ThenTheCountIs(int count) + { + _result.Count.ShouldBe(count); + } + + private void WhenIGetTheServices(int expected) + { + _provider = new PollingConsulServiceDiscoveryProvider(_delay, _serviceName, _factory.Object, _consulServiceDiscoveryProvider.Object); + + var result = WaitFor(3000).Until(() => { + try + { + _result = _provider.Get().GetAwaiter().GetResult(); + if(_result.Count == expected) + { + return true; + } + + return false; + } + catch(Exception) + { + return false; + } + }); + + result.ShouldBeTrue(); + } + } +} diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs index a46369e3..9ef05f3c 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs @@ -1,154 +1,177 @@ -using System; -using System.Collections.Generic; -using Moq; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Infrastructure.Consul; -using Ocelot.Logging; -using Ocelot.ServiceDiscovery; -using Ocelot.ServiceDiscovery.Providers; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.ServiceDiscovery -{ +using System; +using System.Collections.Generic; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Infrastructure.Consul; +using Ocelot.Logging; +using Ocelot.ServiceDiscovery; +using Ocelot.ServiceDiscovery.Providers; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.ServiceDiscovery +{ using Pivotal.Discovery.Client; using Steeltoe.Common.Discovery; - public class ServiceProviderFactoryTests - { - private ServiceProviderConfiguration _serviceConfig; - private IServiceDiscoveryProvider _result; - private readonly ServiceDiscoveryProviderFactory _factory; - private DownstreamReRoute _reRoute; - private Mock _loggerFactory; - private Mock _discoveryClient; - - public ServiceProviderFactoryTests() - { - _loggerFactory = new Mock(); - _discoveryClient = new Mock(); - _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, new ConsulClientFactory(), _discoveryClient.Object); - } - - [Fact] - public void should_return_no_service_provider() - { - var serviceConfig = new ServiceProviderConfigurationBuilder() - .Build(); - - var reRoute = new DownstreamReRouteBuilder().Build(); - - this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) - .When(x => x.WhenIGetTheServiceProvider()) - .Then(x => x.ThenTheServiceProviderIs()) - .BDDfy(); - } - - [Fact] - public void should_return_list_of_configuration_services() - { - var serviceConfig = new ServiceProviderConfigurationBuilder() - .Build(); - - var downstreamAddresses = new List() - { - new DownstreamHostAndPort("asdf.com", 80), - new DownstreamHostAndPort("abc.com", 80) - }; - - var reRoute = new DownstreamReRouteBuilder().WithDownstreamAddresses(downstreamAddresses).Build(); - - this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) - .When(x => x.WhenIGetTheServiceProvider()) - .Then(x => x.ThenTheServiceProviderIs()) - .Then(x => ThenTheFollowingServicesAreReturned(downstreamAddresses)) - .BDDfy(); - } - - [Fact] - public void should_return_consul_service_provider() - { - var reRoute = new DownstreamReRouteBuilder() - .WithServiceName("product") - .WithUseServiceDiscovery(true) - .Build(); - - var serviceConfig = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) - .When(x => x.WhenIGetTheServiceProvider()) - .Then(x => x.ThenTheServiceProviderIs()) - .BDDfy(); - } - - [Fact] - public void should_return_service_fabric_provider() - { - var reRoute = new DownstreamReRouteBuilder() - .WithServiceName("product") - .WithUseServiceDiscovery(true) - .Build(); - - var serviceConfig = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .Build(); - - this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) - .When(x => x.WhenIGetTheServiceProvider()) - .Then(x => x.ThenTheServiceProviderIs()) - .BDDfy(); - } - - [Fact] - public void should_return_eureka_provider() - { - var reRoute = new DownstreamReRouteBuilder() - .WithServiceName("product") - .WithUseServiceDiscovery(true) - .Build(); - - var serviceConfig = new ServiceProviderConfigurationBuilder() - .WithType("Eureka") - .Build(); - - this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) - .When(x => x.WhenIGetTheServiceProvider()) - .Then(x => x.ThenTheServiceProviderIs()) - .BDDfy(); - } - - private void ThenTheFollowingServicesAreReturned(List downstreamAddresses) - { - var result = (ConfigurationServiceProvider)_result; - var services = result.Get().Result; - - for (int i = 0; i < services.Count; i++) - { - var service = services[i]; - var downstreamAddress = downstreamAddresses[i]; - - service.HostAndPort.DownstreamHost.ShouldBe(downstreamAddress.Host); - service.HostAndPort.DownstreamPort.ShouldBe(downstreamAddress.Port); - } - } - - private void GivenTheReRoute(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute) - { - _serviceConfig = serviceConfig; - _reRoute = reRoute; - } - - private void WhenIGetTheServiceProvider() - { - _result = _factory.Get(_serviceConfig, _reRoute); - } - - private void ThenTheServiceProviderIs() - { - _result.ShouldBeOfType(); - } - } -} + public class ServiceProviderFactoryTests + { + private ServiceProviderConfiguration _serviceConfig; + private IServiceDiscoveryProvider _result; + private readonly ServiceDiscoveryProviderFactory _factory; + private DownstreamReRoute _reRoute; + private Mock _loggerFactory; + private Mock _discoveryClient; + private Mock _logger; + + public ServiceProviderFactoryTests() + { + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _discoveryClient = new Mock(); + var consulClient = new Mock(); + _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, consulClient.Object, _discoveryClient.Object); + } + + [Fact] + public void should_return_no_service_provider() + { + var serviceConfig = new ServiceProviderConfigurationBuilder() + .Build(); + + var reRoute = new DownstreamReRouteBuilder().Build(); + + this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .BDDfy(); + } + + [Fact] + public void should_return_list_of_configuration_services() + { + var serviceConfig = new ServiceProviderConfigurationBuilder() + .Build(); + + var downstreamAddresses = new List() + { + new DownstreamHostAndPort("asdf.com", 80), + new DownstreamHostAndPort("abc.com", 80) + }; + + var reRoute = new DownstreamReRouteBuilder().WithDownstreamAddresses(downstreamAddresses).Build(); + + this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .Then(x => ThenTheFollowingServicesAreReturned(downstreamAddresses)) + .BDDfy(); + } + + [Fact] + public void should_return_consul_service_provider() + { + var reRoute = new DownstreamReRouteBuilder() + .WithServiceName("product") + .WithUseServiceDiscovery(true) + .Build(); + + var serviceConfig = new ServiceProviderConfigurationBuilder() + .Build(); + + this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .BDDfy(); + } + + [Fact] + public void should_return_polling_consul_service_provider() + { + var reRoute = new DownstreamReRouteBuilder() + .WithServiceName("product") + .WithUseServiceDiscovery(true) + .Build(); + + var serviceConfig = new ServiceProviderConfigurationBuilder() + .WithType("PollConsul") + .WithPollingInterval(100000) + .Build(); + + this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .BDDfy(); + } + + [Fact] + public void should_return_service_fabric_provider() + { + var reRoute = new DownstreamReRouteBuilder() + .WithServiceName("product") + .WithUseServiceDiscovery(true) + .Build(); + + var serviceConfig = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .Build(); + + this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .BDDfy(); + } + + [Fact] + public void should_return_eureka_provider() + { + var reRoute = new DownstreamReRouteBuilder() + .WithServiceName("product") + .WithUseServiceDiscovery(true) + .Build(); + + var serviceConfig = new ServiceProviderConfigurationBuilder() + .WithType("Eureka") + .Build(); + + this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .BDDfy(); + } + + private void ThenTheFollowingServicesAreReturned(List downstreamAddresses) + { + var result = (ConfigurationServiceProvider)_result; + var services = result.Get().Result; + + for (int i = 0; i < services.Count; i++) + { + var service = services[i]; + var downstreamAddress = downstreamAddresses[i]; + + service.HostAndPort.DownstreamHost.ShouldBe(downstreamAddress.Host); + service.HostAndPort.DownstreamPort.ShouldBe(downstreamAddress.Port); + } + } + + private void GivenTheReRoute(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute) + { + _serviceConfig = serviceConfig; + _reRoute = reRoute; + } + + private void WhenIGetTheServiceProvider() + { + _result = _factory.Get(_serviceConfig, _reRoute); + } + + private void ThenTheServiceProviderIs() + { + _result.ShouldBeOfType(); + } + } +} From 87c13bd9b417b59268260ca04055f339c9072c00 Mon Sep 17 00:00:00 2001 From: Marco Antonio Araujo Date: Tue, 12 Jun 2018 06:08:25 +0100 Subject: [PATCH 14/24] Add ability to specify whether to UseProxy or not on ReRoutes (#390) (#391) * Add ability to specify whether to UseProxy or not on ReRoutes (#390) * Remove useProxy default value from HttpHandlerOptions constructor --- .../Creator/HttpHandlerOptionsCreator.cs | 4 +- .../File/FileHttpHandlerOptions.cs | 7 ++- .../Configuration/HttpHandlerOptions.cs | 12 +++-- .../HttpHandlerOptionsBuilder.cs | 9 +++- src/Ocelot/Requester/HttpClientBuilder.cs | 2 + .../FileInternalConfigurationCreatorTests.cs | 2 +- .../HttpHandlerOptionsCreatorTests.cs | 46 +++++++++++++++++-- ...atingHandlerHandlerProviderFactoryTests.cs | 20 ++++---- .../Requester/HttpClientBuilderTests.cs | 12 ++--- .../Requester/HttpClientHttpRequesterTest.cs | 6 +-- 10 files changed, 86 insertions(+), 34 deletions(-) diff --git a/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs b/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs index 7ce77b34..fbe659cf 100644 --- a/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs @@ -15,10 +15,10 @@ namespace Ocelot.Configuration.Creator public HttpHandlerOptions Create(FileHttpHandlerOptions options) { - var useTracing = _tracer.GetType() != typeof(FakeServiceTracer) && options.UseTracing; + var useTracing = _tracer.GetType() != typeof(FakeServiceTracer) && options.UseTracing; return new HttpHandlerOptions(options.AllowAutoRedirect, - options.UseCookieContainer, useTracing); + options.UseCookieContainer, useTracing, options.UseProxy); } } } diff --git a/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs b/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs index 2934254c..e10e7345 100644 --- a/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs +++ b/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs @@ -5,13 +5,16 @@ public FileHttpHandlerOptions() { AllowAutoRedirect = false; - UseCookieContainer = false; + UseCookieContainer = false; + UseProxy = true; } public bool AllowAutoRedirect { get; set; } public bool UseCookieContainer { get; set; } - public bool UseTracing { get; set; } + public bool UseTracing { get; set; } + + public bool UseProxy { get; set; } } } diff --git a/src/Ocelot/Configuration/HttpHandlerOptions.cs b/src/Ocelot/Configuration/HttpHandlerOptions.cs index ef0edd9c..c8f88aa5 100644 --- a/src/Ocelot/Configuration/HttpHandlerOptions.cs +++ b/src/Ocelot/Configuration/HttpHandlerOptions.cs @@ -6,11 +6,12 @@ /// public class HttpHandlerOptions { - public HttpHandlerOptions(bool allowAutoRedirect, bool useCookieContainer, bool useTracing) + public HttpHandlerOptions(bool allowAutoRedirect, bool useCookieContainer, bool useTracing, bool useProxy) { AllowAutoRedirect = allowAutoRedirect; UseCookieContainer = useCookieContainer; UseTracing = useTracing; + UseProxy = useProxy; } /// @@ -23,9 +24,14 @@ /// public bool UseCookieContainer { get; private set; } - // + /// /// Specify is handler has to use a opentracing /// - public bool UseTracing { get; private set; } + public bool UseTracing { get; private set; } + + /// + /// Specify if handler has to use a proxy + /// + public bool UseProxy { get; private set; } } } diff --git a/src/Ocelot/Configuration/HttpHandlerOptionsBuilder.cs b/src/Ocelot/Configuration/HttpHandlerOptionsBuilder.cs index 1cfd69c6..bf8aa042 100644 --- a/src/Ocelot/Configuration/HttpHandlerOptionsBuilder.cs +++ b/src/Ocelot/Configuration/HttpHandlerOptionsBuilder.cs @@ -5,6 +5,7 @@ private bool _allowAutoRedirect; private bool _useCookieContainer; private bool _useTracing; + private bool _useProxy; public HttpHandlerOptionsBuilder WithAllowAutoRedirect(bool input) { @@ -24,9 +25,15 @@ return this; } + public HttpHandlerOptionsBuilder WithUseProxy(bool useProxy) + { + _useProxy = useProxy; + return this; + } + public HttpHandlerOptions Build() { - return new HttpHandlerOptions(_allowAutoRedirect, _useCookieContainer, _useTracing); + return new HttpHandlerOptions(_allowAutoRedirect, _useCookieContainer, _useTracing, _useProxy); } } } diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs index 1cada3d7..0cd810cf 100644 --- a/src/Ocelot/Requester/HttpClientBuilder.cs +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -89,6 +89,7 @@ namespace Ocelot.Requester { AllowAutoRedirect = context.DownstreamReRoute.HttpHandlerOptions.AllowAutoRedirect, UseCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer, + UseProxy = context.DownstreamReRoute.HttpHandlerOptions.UseProxy }; } @@ -98,6 +99,7 @@ namespace Ocelot.Requester { AllowAutoRedirect = context.DownstreamReRoute.HttpHandlerOptions.AllowAutoRedirect, UseCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer, + UseProxy = context.DownstreamReRoute.HttpHandlerOptions.UseProxy, CookieContainer = new CookieContainer() }; } diff --git a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs index c5bba51c..459976f3 100644 --- a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs @@ -688,7 +688,7 @@ { var reRouteOptions = new ReRouteOptionsBuilder() .Build(); - var httpHandlerOptions = new HttpHandlerOptions(true, true,false); + var httpHandlerOptions = new HttpHandlerOptions(true, true,false, true); this.Given(x => x.GivenTheConfigIs(new FileConfiguration { diff --git a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs index c86c55d0..94b5413b 100644 --- a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs @@ -35,7 +35,7 @@ namespace Ocelot.UnitTests.Configuration } }; - var expectedOptions = new HttpHandlerOptions(false, false, false); + var expectedOptions = new HttpHandlerOptions(false, false, false, true); this.Given(x => GivenTheFollowing(fileReRoute)) .When(x => WhenICreateHttpHandlerOptions()) @@ -54,7 +54,7 @@ namespace Ocelot.UnitTests.Configuration } }; - var expectedOptions = new HttpHandlerOptions(false, false, true); + var expectedOptions = new HttpHandlerOptions(false, false, true, true); this.Given(x => GivenTheFollowing(fileReRoute)) .And(x => GivenARealTracer()) @@ -67,7 +67,7 @@ namespace Ocelot.UnitTests.Configuration public void should_create_options_with_useCookie_false_and_allowAutoRedirect_true_as_default() { var fileReRoute = new FileReRoute(); - var expectedOptions = new HttpHandlerOptions(false, false, false); + var expectedOptions = new HttpHandlerOptions(false, false, false, true); this.Given(x => GivenTheFollowing(fileReRoute)) .When(x => WhenICreateHttpHandlerOptions()) @@ -88,7 +88,42 @@ namespace Ocelot.UnitTests.Configuration } }; - var expectedOptions = new HttpHandlerOptions(false, false, false); + var expectedOptions = new HttpHandlerOptions(false, false, false, true); + + this.Given(x => GivenTheFollowing(fileReRoute)) + .When(x => WhenICreateHttpHandlerOptions()) + .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) + .BDDfy(); + } + + [Fact] + public void should_create_options_with_useproxy_true_as_default() + { + var fileReRoute = new FileReRoute + { + HttpHandlerOptions = new FileHttpHandlerOptions() + }; + + var expectedOptions = new HttpHandlerOptions(false, false, false, true); + + this.Given(x => GivenTheFollowing(fileReRoute)) + .When(x => WhenICreateHttpHandlerOptions()) + .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) + .BDDfy(); + } + + [Fact] + public void should_create_options_with_specified_useproxy() + { + var fileReRoute = new FileReRoute + { + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseProxy = false + } + }; + + var expectedOptions = new HttpHandlerOptions(false, false, false, false); this.Given(x => GivenTheFollowing(fileReRoute)) .When(x => WhenICreateHttpHandlerOptions()) @@ -111,7 +146,8 @@ namespace Ocelot.UnitTests.Configuration _httpHandlerOptions.ShouldNotBeNull(); _httpHandlerOptions.AllowAutoRedirect.ShouldBe(expected.AllowAutoRedirect); _httpHandlerOptions.UseCookieContainer.ShouldBe(expected.UseCookieContainer); - _httpHandlerOptions.UseTracing.ShouldBe(expected.UseTracing); + _httpHandlerOptions.UseTracing.ShouldBe(expected.UseTracing); + _httpHandlerOptions.UseProxy.ShouldBe(expected.UseProxy); } private void GivenARealTracer() diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs index 2ca7a8d5..2882ed8b 100644 --- a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs @@ -46,7 +46,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true)) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true)) .WithDelegatingHandlers(new List { "FakeDelegatingHandler", @@ -82,7 +82,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true)) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true)) .WithDelegatingHandlers(new List { "FakeDelegatingHandlerTwo", @@ -119,7 +119,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true)) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true)) .WithDelegatingHandlers(new List { "FakeDelegatingHandlerTwo", @@ -155,7 +155,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true)) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true)) .WithDelegatingHandlers(new List { "FakeDelegatingHandler", @@ -189,7 +189,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true)) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true)) .WithLoadBalancerKey("") .Build(); @@ -215,7 +215,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false)) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true)) .WithDelegatingHandlers(new List { "FakeDelegatingHandler", @@ -244,7 +244,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false)).WithLoadBalancerKey("").Build(); + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true)).WithLoadBalancerKey("").Build(); this.Given(x => GivenTheFollowingRequest(reRoute)) .And(x => GivenTheQosProviderHouseReturns(new OkResponse(It.IsAny()))) @@ -264,7 +264,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false)).WithLoadBalancerKey("").Build(); + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true)).WithLoadBalancerKey("").Build(); this.Given(x => GivenTheFollowingRequest(reRoute)) .And(x => GivenTheServiceProviderReturnsNothing()) @@ -284,7 +284,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false)).WithLoadBalancerKey("").Build(); + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true)).WithLoadBalancerKey("").Build(); this.Given(x => GivenTheFollowingRequest(reRoute)) .And(x => GivenTheQosProviderHouseReturns(new OkResponse(It.IsAny()))) @@ -306,7 +306,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false)).WithLoadBalancerKey("").Build(); + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true)).WithLoadBalancerKey("").Build(); this.Given(x => GivenTheFollowingRequest(reRoute)) .And(x => GivenTheQosProviderHouseReturns(new ErrorResponse(It.IsAny()))) diff --git a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs index 8b2ae449..1dd70591 100644 --- a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net; using System.Net.Http; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -51,7 +49,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true)) .WithLoadBalancerKey("") .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); @@ -71,7 +69,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true)) .WithLoadBalancerKey("") .WithQosOptions(new QoSOptionsBuilder().Build()) .WithDangerousAcceptAnyServerCertificateValidator(true) @@ -93,7 +91,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true)) .WithLoadBalancerKey("") .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); @@ -124,7 +122,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, true, false)) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, true, false, true)) .WithLoadBalancerKey("") .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); @@ -159,7 +157,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true)) .WithLoadBalancerKey("") .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); diff --git a/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs b/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs index 3530c2b5..ae9a25af 100644 --- a/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs +++ b/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs @@ -52,7 +52,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true)) .WithLoadBalancerKey("") .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); @@ -78,7 +78,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true)) .WithLoadBalancerKey("") .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); @@ -103,7 +103,7 @@ namespace Ocelot.UnitTests.Requester var reRoute = new DownstreamReRouteBuilder() .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true)) .WithLoadBalancerKey("") .WithQosOptions(new QoSOptionsBuilder().WithTimeoutValue(1).Build()) .Build(); From 9979f8a4b8d369f0c2476ddac9dc5995f726cdca Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Fri, 15 Jun 2018 20:29:49 +0100 Subject: [PATCH 15/24] #372 use period timespan to decide when client can make requests again (#404) --- docs/features/ratelimiting.rst | 80 ++-- src/Ocelot/RateLimit/RateLimitCore.cs | 296 ++++++------- .../ClientRateLimitTests.cs | 407 ++++++++++-------- test/Ocelot.AcceptanceTests/Steps.cs | 5 + 4 files changed, 429 insertions(+), 359 deletions(-) diff --git a/docs/features/ratelimiting.rst b/docs/features/ratelimiting.rst index 7f9c0768..547d032f 100644 --- a/docs/features/ratelimiting.rst +++ b/docs/features/ratelimiting.rst @@ -1,40 +1,40 @@ -Rate Limiting -============= - -Thanks to `@catcherwong article `_ for inspiring me to finally write this documentation. - -Ocelot supports rate limiting of upstream requests so that your downstream services do not become overloaded. This feature was added by @geffzhang on GitHub! Thanks very much. - -OK so to get rate limiting working for a ReRoute you need to add the following json to it. - -.. code-block:: json - - "RateLimitOptions": { - "ClientWhitelist": [], - "EnableRateLimiting": true, - "Period": "1s", - "PeriodTimespan": 1, - "Limit": 1 - } - -ClientWhitelist - This is an array that contains the whitelist of the client. It means that the client in this array will not be affected by the rate limiting. -EnableRateLimiting - This value specifies enable endpoint rate limiting. -Period - This value specifies the period, such as 1s, 5m, 1h,1d and so on. -PeriodTimespan - This value specifies that we can retry after a certain number of seconds. -Limit - This value specifies the maximum number of requests that a client can make in a defined period. - -You can also set the following in the GlobalConfiguration part of ocelot.json - -.. code-block:: json - - "RateLimitOptions": { - "DisableRateLimitHeaders": false, - "QuotaExceededMessage": "Customize Tips!", - "HttpStatusCode": 999, - "ClientIdHeader" : "Test" - } - -DisableRateLimitHeaders - This value specifies whether X-Rate-Limit and Rety-After headers are disabled. -QuotaExceededMessage - This value specifies the exceeded message. -HttpStatusCode - This value specifies the returned HTTP Status code when rate limiting occurs. -ClientIdHeader - Allows you to specifiy the header that should be used to identify clients. By default it is "ClientId" +Rate Limiting +============= + +Thanks to `@catcherwong article `_ for inspiring me to finally write this documentation. + +Ocelot supports rate limiting of upstream requests so that your downstream services do not become overloaded. This feature was added by @geffzhang on GitHub! Thanks very much. + +OK so to get rate limiting working for a ReRoute you need to add the following json to it. + +.. code-block:: json + + "RateLimitOptions": { + "ClientWhitelist": [], + "EnableRateLimiting": true, + "Period": "1s", + "PeriodTimespan": 1, + "Limit": 1 + } + +ClientWhitelist - This is an array that contains the whitelist of the client. It means that the client in this array will not be affected by the rate limiting. +EnableRateLimiting - This value specifies enable endpoint rate limiting. +Period - This value specifies the period that the limit applies to, such as 1s, 5m, 1h,1d and so on. If you make more requests in the period than the limit allows then you need to wait for PeriodTimespan to elapse before you make another request. +PeriodTimespan - This value specifies that we can retry after a certain number of seconds. +Limit - This value specifies the maximum number of requests that a client can make in a defined period. + +You can also set the following in the GlobalConfiguration part of ocelot.json + +.. code-block:: json + + "RateLimitOptions": { + "DisableRateLimitHeaders": false, + "QuotaExceededMessage": "Customize Tips!", + "HttpStatusCode": 999, + "ClientIdHeader" : "Test" + } + +DisableRateLimitHeaders - This value specifies whether X-Rate-Limit and Rety-After headers are disabled. +QuotaExceededMessage - This value specifies the exceeded message. +HttpStatusCode - This value specifies the returned HTTP Status code when rate limiting occurs. +ClientIdHeader - Allows you to specifiy the header that should be used to identify clients. By default it is "ClientId" diff --git a/src/Ocelot/RateLimit/RateLimitCore.cs b/src/Ocelot/RateLimit/RateLimitCore.cs index 82124c85..4b0d5165 100644 --- a/src/Ocelot/RateLimit/RateLimitCore.cs +++ b/src/Ocelot/RateLimit/RateLimitCore.cs @@ -1,148 +1,148 @@ -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 + ConvertToTimeSpan(rule.Period) >= DateTime.UtcNow) - { - // increment request count - var totalRequests = entry.Value.TotalRequests + 1; - - // deep copy - counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests); - } - } - } - - if (counter.TotalRequests > rule.Limit) - { - var retryAfter = RetryAfterFrom(counter.Timestamp, rule); - if (retryAfter > 0) - { - var expirationTime = TimeSpan.FromSeconds(rule.PeriodTimespan); - _counterHandler.Set(counterId, counter, expirationTime); - } - else - { - _counterHandler.Remove(counterId); - } - } - else - { - var expirationTime = ConvertToTimeSpan(rule.Period); - _counterHandler.Set(counterId, counter, expirationTime); - } - - return counter; - } - - public void SaveRateLimitCounter(ClientRequestIdentity requestIdentity, RateLimitOptions option, RateLimitCounter counter, TimeSpan expirationTime) - { - var counterId = ComputeCounterKey(requestIdentity, option); - var rule = option.RateLimitRule; - - // stores: id (string) - timestamp (datetime) - total_requests (long) - _counterHandler.Set(counterId, counter, expirationTime); - } - - 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 int RetryAfterFrom(DateTime timestamp, RateLimitRule rule) - { - var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds); - var retryAfter = Convert.ToInt32(TimeSpan.FromSeconds(rule.PeriodTimespan).TotalSeconds); - retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1; - return retryAfter; - } - - 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}"); - } - } - } -} +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 + TimeSpan.FromSeconds(rule.PeriodTimespan) >= DateTime.UtcNow) + { + // increment request count + var totalRequests = entry.Value.TotalRequests + 1; + + // deep copy + counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests); + } + } + } + + if (counter.TotalRequests > rule.Limit) + { + var retryAfter = RetryAfterFrom(counter.Timestamp, rule); + if (retryAfter > 0) + { + var expirationTime = TimeSpan.FromSeconds(rule.PeriodTimespan); + _counterHandler.Set(counterId, counter, expirationTime); + } + else + { + _counterHandler.Remove(counterId); + } + } + else + { + var expirationTime = ConvertToTimeSpan(rule.Period); + _counterHandler.Set(counterId, counter, expirationTime); + } + + return counter; + } + + public void SaveRateLimitCounter(ClientRequestIdentity requestIdentity, RateLimitOptions option, RateLimitCounter counter, TimeSpan expirationTime) + { + var counterId = ComputeCounterKey(requestIdentity, option); + var rule = option.RateLimitRule; + + // stores: id (string) - timestamp (datetime) - total_requests (long) + _counterHandler.Set(counterId, counter, expirationTime); + } + + 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 int RetryAfterFrom(DateTime timestamp, RateLimitRule rule) + { + var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds); + var retryAfter = Convert.ToInt32(TimeSpan.FromSeconds(rule.PeriodTimespan).TotalSeconds); + retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1; + return retryAfter; + } + + 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/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs index dc9d5eef..6963f20c 100644 --- a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs +++ b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs @@ -1,171 +1,236 @@ -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", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51876, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "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:51876", "/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", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51876, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "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:51876", "/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 baseUrl, string basePath) - { - _builder = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(baseUrl) - .Configure(app => - { - app.UsePathBase(basePath); - app.Run(context => - { - _counterOne++; - context.Response.StatusCode = 200; - context.Response.WriteAsync(_counterOne.ToString()); - return Task.CompletedTask; - }); - }) - .Build(); - - _builder.Start(); - } - } -} +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", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51876, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/ClientRateLimit", + UpstreamHttpMethod = new List { "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:51876", "/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_wait_for_period_timespan_to_elapse_before_making_next_request() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/ClientRateLimit", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51926, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/ClientRateLimit", + UpstreamHttpMethod = new List { "Get" }, + RequestIdKey = _steps.RequestIdKey, + + RateLimitOptions = new FileRateLimitRule() + { + EnableRateLimiting = true, + ClientWhitelist = new List(), + Limit = 3, + Period = "1s", + PeriodTimespan = 2 + } + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + RateLimitOptions = new FileRateLimitOptions() + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = "", + RateLimitCounterPrefix = "", + HttpStatusCode = 428 + }, + RequestIdKey ="oceclientrequest" + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51926", "/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)) + .And(x => _steps.GivenIWait(1000)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) + .And(x => _steps.GivenIWait(1000)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .BDDfy(); + } + + [Fact] + public void should_call_middleware_withWhitelistClient() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/ClientRateLimit", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51876, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/ClientRateLimit", + UpstreamHttpMethod = new List { "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:51876", "/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 baseUrl, string basePath) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(baseUrl) + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(context => + { + _counterOne++; + context.Response.StatusCode = 200; + context.Response.WriteAsync(_counterOne.ToString()); + return Task.CompletedTask; + }); + }) + .Build(); + + _builder.Start(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index f3c1a6d2..67723ccb 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -183,6 +183,11 @@ namespace Ocelot.AcceptanceTests _ocelotClient = _ocelotServer.CreateClient(); } + internal void GivenIWait(int wait) + { + Thread.Sleep(wait); + } + public void GivenOcelotIsRunningWithMiddleareBeforePipeline(Func callback) { _webHostBuilder = new WebHostBuilder(); From 8e1a5ce8277924790d66cb60bab3395c9e8354b8 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Fri, 15 Jun 2018 20:30:25 +0100 Subject: [PATCH 16/24] Feature/dont validate cached content headers (#406) * #372 use period timespan to decide when client can make requests again * #400 dont validate cached body headers --- .../Cache/Middleware/OutputCacheMiddleware.cs | 240 +++++----- test/Ocelot.AcceptanceTests/CachingTests.cs | 430 ++++++++++-------- test/Ocelot.AcceptanceTests/Steps.cs | 6 + .../Cache/OutputCacheMiddlewareTests.cs | 264 ++++++----- 4 files changed, 505 insertions(+), 435 deletions(-) diff --git a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs index aee3bec3..8a27ccba 100644 --- a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs +++ b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs @@ -1,120 +1,120 @@ -using System; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Ocelot.Logging; -using Ocelot.Middleware; -using System.IO; -using Ocelot.Middleware.Multiplexer; - -namespace Ocelot.Cache.Middleware -{ - public class OutputCacheMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly IOcelotCache _outputCache; - private readonly IRegionCreator _regionCreator; - - public OutputCacheMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IOcelotCache outputCache, - IRegionCreator regionCreator) - :base(loggerFactory.CreateLogger()) - { - _next = next; - _outputCache = outputCache; - _regionCreator = regionCreator; - } - - public async Task Invoke(DownstreamContext context) - { - if (!context.DownstreamReRoute.IsCached) - { - await _next.Invoke(context); - return; - } - - var downstreamUrlKey = $"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}"; - - Logger.LogDebug($"Started checking cache for {downstreamUrlKey}"); - - var cached = _outputCache.Get(downstreamUrlKey, context.DownstreamReRoute.CacheOptions.Region); - - if (cached != null) - { - Logger.LogDebug($"cache entry exists for {downstreamUrlKey}"); - - var response = CreateHttpResponseMessage(cached); - SetHttpResponseMessageThisRequest(context, response); - - Logger.LogDebug($"finished returned cached response for {downstreamUrlKey}"); - - return; - } - - Logger.LogDebug($"no resonse cached for {downstreamUrlKey}"); - - await _next.Invoke(context); - - if (context.IsError) - { - Logger.LogDebug($"there was a pipeline error for {downstreamUrlKey}"); - - return; - } - - cached = await CreateCachedResponse(context.DownstreamResponse); - - _outputCache.Add(downstreamUrlKey, cached, TimeSpan.FromSeconds(context.DownstreamReRoute.CacheOptions.TtlSeconds), context.DownstreamReRoute.CacheOptions.Region); - - Logger.LogDebug($"finished response added to cache for {downstreamUrlKey}"); - } - - private void SetHttpResponseMessageThisRequest(DownstreamContext context, DownstreamResponse response) - { - context.DownstreamResponse = response; - } - - internal DownstreamResponse CreateHttpResponseMessage(CachedResponse cached) - { - if (cached == null) - { - return null; - } - - var content = new MemoryStream(Convert.FromBase64String(cached.Body)); - - var streamContent = new StreamContent(content); - - foreach (var header in cached.ContentHeaders) - { - streamContent.Headers.Add(header.Key, header.Value); - } - - return new DownstreamResponse(streamContent, cached.StatusCode, cached.Headers.ToList()); - } - - internal async Task CreateCachedResponse(DownstreamResponse response) - { - if (response == null) - { - return null; - } - - var statusCode = response.StatusCode; - var headers = response.Headers.ToDictionary(v => v.Key, v => v.Values); - string body = null; - - if (response.Content != null) - { - var content = await response.Content.ReadAsByteArrayAsync(); - body = Convert.ToBase64String(content); - } - - var contentHeaders = response?.Content?.Headers.ToDictionary(v => v.Key, v => v.Value); - - var cached = new CachedResponse(statusCode, headers, body, contentHeaders); - return cached; - } - } -} +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Ocelot.Logging; +using Ocelot.Middleware; +using System.IO; +using Ocelot.Middleware.Multiplexer; + +namespace Ocelot.Cache.Middleware +{ + public class OutputCacheMiddleware : OcelotMiddleware + { + private readonly OcelotRequestDelegate _next; + private readonly IOcelotCache _outputCache; + private readonly IRegionCreator _regionCreator; + + public OutputCacheMiddleware(OcelotRequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IOcelotCache outputCache, + IRegionCreator regionCreator) + :base(loggerFactory.CreateLogger()) + { + _next = next; + _outputCache = outputCache; + _regionCreator = regionCreator; + } + + public async Task Invoke(DownstreamContext context) + { + if (!context.DownstreamReRoute.IsCached) + { + await _next.Invoke(context); + return; + } + + var downstreamUrlKey = $"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}"; + + Logger.LogDebug($"Started checking cache for {downstreamUrlKey}"); + + var cached = _outputCache.Get(downstreamUrlKey, context.DownstreamReRoute.CacheOptions.Region); + + if (cached != null) + { + Logger.LogDebug($"cache entry exists for {downstreamUrlKey}"); + + var response = CreateHttpResponseMessage(cached); + SetHttpResponseMessageThisRequest(context, response); + + Logger.LogDebug($"finished returned cached response for {downstreamUrlKey}"); + + return; + } + + Logger.LogDebug($"no resonse cached for {downstreamUrlKey}"); + + await _next.Invoke(context); + + if (context.IsError) + { + Logger.LogDebug($"there was a pipeline error for {downstreamUrlKey}"); + + return; + } + + cached = await CreateCachedResponse(context.DownstreamResponse); + + _outputCache.Add(downstreamUrlKey, cached, TimeSpan.FromSeconds(context.DownstreamReRoute.CacheOptions.TtlSeconds), context.DownstreamReRoute.CacheOptions.Region); + + Logger.LogDebug($"finished response added to cache for {downstreamUrlKey}"); + } + + private void SetHttpResponseMessageThisRequest(DownstreamContext context, DownstreamResponse response) + { + context.DownstreamResponse = response; + } + + internal DownstreamResponse CreateHttpResponseMessage(CachedResponse cached) + { + if (cached == null) + { + return null; + } + + var content = new MemoryStream(Convert.FromBase64String(cached.Body)); + + var streamContent = new StreamContent(content); + + foreach (var header in cached.ContentHeaders) + { + streamContent.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + return new DownstreamResponse(streamContent, cached.StatusCode, cached.Headers.ToList()); + } + + internal async Task CreateCachedResponse(DownstreamResponse response) + { + if (response == null) + { + return null; + } + + var statusCode = response.StatusCode; + var headers = response.Headers.ToDictionary(v => v.Key, v => v.Values); + string body = null; + + if (response.Content != null) + { + var content = await response.Content.ReadAsByteArrayAsync(); + body = Convert.ToBase64String(content); + } + + var contentHeaders = response?.Content?.Headers.ToDictionary(v => v.Key, v => v.Value); + + var cached = new CachedResponse(statusCode, headers, body, contentHeaders); + return cached; + } + } +} diff --git a/test/Ocelot.AcceptanceTests/CachingTests.cs b/test/Ocelot.AcceptanceTests/CachingTests.cs index f64e2ce2..ad2a12b8 100644 --- a/test/Ocelot.AcceptanceTests/CachingTests.cs +++ b/test/Ocelot.AcceptanceTests/CachingTests.cs @@ -1,191 +1,239 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Threading; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.AcceptanceTests -{ - public class CachingTests : IDisposable - { - private IWebHost _builder; - private readonly Steps _steps; - - public CachingTests() - { - _steps = new Steps(); - } - - [Fact] - public void should_return_cached_response() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51899, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 100 - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.ThenTheContentLengthIs(16)) - .BDDfy(); - } - - [Fact] - public void should_return_cached_response_when_using_jsonserialized_cache() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51899, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 100 - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingJsonSerializedCache()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_not_return_cached_response_as_ttl_expires() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51899, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 1 - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom")) - .And(x => x.GivenTheCacheExpires()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) - .BDDfy(); - } - - private void GivenTheCacheExpires() - { - Thread.Sleep(1000); - } - - private void GivenTheServiceNowReturns(string url, int statusCode, string responseBody) - { - _builder.Dispose(); - GivenThereIsAServiceRunningOn(url, statusCode, responseBody); - } - - private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) - { - _builder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - }) - .Build(); - - _builder.Start(); - } - - public void Dispose() - { - _builder?.Dispose(); - _steps.Dispose(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class CachingTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + + public CachingTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_return_cached_response() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51899, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 100 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.ThenTheContentLengthIs(16)) + .BDDfy(); + } + + [Fact] + public void should_return_cached_response_with_expires_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 52839, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 100 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52839", 200, "Hello from Laura", "Expires", "-1")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => x.GivenTheServiceNowReturns("http://localhost:52839", 200, "Hello from Tom")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.ThenTheContentLengthIs(16)) + .And(x => _steps.ThenTheResponseBodyHeaderIs("Expires", "-1")) + .BDDfy(); + } + + [Fact] + public void should_return_cached_response_when_using_jsonserialized_cache() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51899, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 100 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingJsonSerializedCache()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_not_return_cached_response_as_ttl_expires() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51899, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 1 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", 200, "Hello from Laura", null, null)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => x.GivenTheServiceNowReturns("http://localhost:51899", 200, "Hello from Tom")) + .And(x => x.GivenTheCacheExpires()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .BDDfy(); + } + + private void GivenTheCacheExpires() + { + Thread.Sleep(1000); + } + + private void GivenTheServiceNowReturns(string url, int statusCode, string responseBody) + { + _builder.Dispose(); + GivenThereIsAServiceRunningOn(url, statusCode, responseBody, null, null); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, string key, string value) + { + _builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if(!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(key)) + { + context.Response.Headers.Add(key, value); + } + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 67723ccb..5dd95831 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -392,6 +392,12 @@ namespace Ocelot.AcceptanceTests header.First().ShouldBe(value); } + public void ThenTheResponseBodyHeaderIs(string key, string value) + { + var header = _response.Content.Headers.GetValues(key); + header.First().ShouldBe(value); + } + public void ThenTheTraceHeaderIsSet(string key) { var header = _response.Headers.GetValues(key); diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index cd0f9719..c0b59350 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -1,124 +1,140 @@ -namespace Ocelot.UnitTests.Cache -{ - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Cache; - using Ocelot.Cache.Middleware; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.DownstreamRouteFinder; - using Ocelot.DownstreamRouteFinder.UrlMatcher; - using Ocelot.Logging; - using TestStack.BDDfy; - using Xunit; - using System.Net; - using Ocelot.Middleware; - using Ocelot.Middleware.Multiplexer; - - public class OutputCacheMiddlewareTests - { - private readonly Mock> _cacheManager; - private readonly Mock _loggerFactory; - private Mock _logger; - private OutputCacheMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private readonly OcelotRequestDelegate _next; - private CachedResponse _response; - private readonly IRegionCreator _regionCreator; - - public OutputCacheMiddlewareTests() - { - _cacheManager = new Mock>(); - _regionCreator = new RegionCreator(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _next = context => Task.CompletedTask; - _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")); - } - - [Fact] - public void should_returned_cached_item_when_it_is_in_cache() - { - var headers = new Dictionary> - { - { "test", new List { "test" } } - }; - - var contentHeaders = new Dictionary> - { - { "content-type", new List { "application/json" } } - }; - - var cachedResponse = new CachedResponse(HttpStatusCode.OK, headers, "", contentHeaders); - this.Given(x => x.GivenThereIsACachedResponse(cachedResponse)) - .And(x => x.GivenTheDownstreamRouteIs()) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheCacheGetIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_continue_with_pipeline_and_cache_response() - { - this.Given(x => x.GivenResponseIsNotCached()) - .And(x => x.GivenTheDownstreamRouteIs()) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheCacheAddIsCalledCorrectly()) - .BDDfy(); - } - - private void WhenICallTheMiddleware() - { - _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager.Object, _regionCreator); - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - } - - private void GivenThereIsACachedResponse(CachedResponse response) - { - _response = response; - _cacheManager - .Setup(x => x.Get(It.IsAny(), It.IsAny())) - .Returns(_response); - } - - private void GivenResponseIsNotCached() - { - _downstreamContext.DownstreamResponse = new DownstreamResponse(new HttpResponseMessage()); - } - - private void GivenTheDownstreamRouteIs() - { - var reRoute = new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithIsCached(true) - .WithCacheOptions(new CacheOptions(100, "kanken")) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build(); - - var downstreamRoute = new DownstreamRoute(new List(), reRoute); - - _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; - _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; - } - - private void ThenTheCacheGetIsCalledCorrectly() - { - _cacheManager - .Verify(x => x.Get(It.IsAny(), It.IsAny()), Times.Once); - } - - private void ThenTheCacheAddIsCalledCorrectly() - { - _cacheManager - .Verify(x => x.Add(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - } - } -} +namespace Ocelot.UnitTests.Cache +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Http; + using Moq; + using Ocelot.Cache; + using Ocelot.Cache.Middleware; + using Ocelot.Configuration; + using Ocelot.Configuration.Builder; + using Ocelot.DownstreamRouteFinder; + using Ocelot.DownstreamRouteFinder.UrlMatcher; + using Ocelot.Logging; + using TestStack.BDDfy; + using Xunit; + using System.Net; + using Ocelot.Middleware; + using Ocelot.Middleware.Multiplexer; + + public class OutputCacheMiddlewareTests + { + private readonly Mock> _cacheManager; + private readonly Mock _loggerFactory; + private Mock _logger; + private OutputCacheMiddleware _middleware; + private readonly DownstreamContext _downstreamContext; + private readonly OcelotRequestDelegate _next; + private CachedResponse _response; + private readonly IRegionCreator _regionCreator; + + public OutputCacheMiddlewareTests() + { + _cacheManager = new Mock>(); + _regionCreator = new RegionCreator(); + _downstreamContext = new DownstreamContext(new DefaultHttpContext()); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => Task.CompletedTask; + _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")); + } + + [Fact] + public void should_returned_cached_item_when_it_is_in_cache() + { + var headers = new Dictionary> + { + { "test", new List { "test" } } + }; + + var contentHeaders = new Dictionary> + { + { "content-type", new List { "application/json" } } + }; + + var cachedResponse = new CachedResponse(HttpStatusCode.OK, headers, "", contentHeaders); + this.Given(x => x.GivenThereIsACachedResponse(cachedResponse)) + .And(x => x.GivenTheDownstreamRouteIs()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheCacheGetIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_returned_cached_item_when_it_is_in_cache_expires_header() + { + var contentHeaders = new Dictionary> + { + { "Expires", new List { "-1" } } + }; + + var cachedResponse = new CachedResponse(HttpStatusCode.OK, new Dictionary>(), "", contentHeaders); + this.Given(x => x.GivenThereIsACachedResponse(cachedResponse)) + .And(x => x.GivenTheDownstreamRouteIs()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheCacheGetIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_continue_with_pipeline_and_cache_response() + { + this.Given(x => x.GivenResponseIsNotCached(new HttpResponseMessage())) + .And(x => x.GivenTheDownstreamRouteIs()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheCacheAddIsCalledCorrectly()) + .BDDfy(); + } + + private void WhenICallTheMiddleware() + { + _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager.Object, _regionCreator); + _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + } + + private void GivenThereIsACachedResponse(CachedResponse response) + { + _response = response; + _cacheManager + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .Returns(_response); + } + + private void GivenResponseIsNotCached(HttpResponseMessage responseMessage) + { + _downstreamContext.DownstreamResponse = new DownstreamResponse(responseMessage); + } + + private void GivenTheDownstreamRouteIs() + { + var reRoute = new ReRouteBuilder() + .WithDownstreamReRoute(new DownstreamReRouteBuilder() + .WithIsCached(true) + .WithCacheOptions(new CacheOptions(100, "kanken")) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build(); + + var downstreamRoute = new DownstreamRoute(new List(), reRoute); + + _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; + _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; + } + + private void ThenTheCacheGetIsCalledCorrectly() + { + _cacheManager + .Verify(x => x.Get(It.IsAny(), It.IsAny()), Times.Once); + } + + private void ThenTheCacheAddIsCalledCorrectly() + { + _cacheManager + .Verify(x => x.Add(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + } +} From 347303ee7b3fb425bc82a0e539b32887cdc36331 Mon Sep 17 00:00:00 2001 From: Ni Yanwei Date: Mon, 18 Jun 2018 02:25:45 +0800 Subject: [PATCH 17/24] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=9F=A5=E6=89=BEocelo?= =?UTF-8?q?t=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E6=AD=A3=E5=88=99?= =?UTF-8?q?=E8=A1=A8=E8=BE=BE=E5=BC=8F=E4=B8=AD=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20(#410)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 正则表达式"(?i)ocelot.([a-zA-Z0-9]*).json",“ocelot.”中的“.”能匹配除"\n"外的任意字符;".json"原因是匹配json文件,但实际能匹配任何"*.json*"文件 --- .../DependencyInjection/ConfigurationBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs b/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs index 9e471475..7eb94329 100644 --- a/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs +++ b/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs @@ -30,7 +30,7 @@ namespace Ocelot.DependencyInjection public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder) { - const string pattern = "(?i)ocelot.([a-zA-Z0-9]*).json"; + const string pattern = "(?i)ocelot\\.([a-zA-Z0-9]*)(\\.json)$"; var reg = new Regex(pattern); From e0f76210f752a63d1956c90d527ddc383d6715da Mon Sep 17 00:00:00 2001 From: JoJo2406 Date: Tue, 19 Jun 2018 20:54:57 +0100 Subject: [PATCH 18/24] Change port in Consul documentation (#418) * Fix Consul default port the default port for the consul api is 8500 not 9500 - https://www.consul.io/docs/agent/options.html * Fix typo --- docs/features/servicediscovery.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index a39f5db8..be519013 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -18,7 +18,7 @@ will be used. "ServiceDiscoveryProvider": { "Host": "localhost", - "Port": 9500 + "Port": 8500 } In the future we can add a feature that allows ReRoute specfic configuration. @@ -49,9 +49,9 @@ A lot of people have asked me to implement a feature where Ocelot polls consul f "ServiceDiscoveryProvider": { "Host": "localhost", - "Port": 9500, + "Port": 8500, "Type": "PollConsul", - "PollingInteral": 100 + "PollingInterval": 100 } The polling interval is in milliseconds and tells Ocelot how often to call Consul for changes in service configuration. @@ -67,7 +67,7 @@ If you are using ACL with Consul Ocelot supports adding the X-Consul-Token heade "ServiceDiscoveryProvider": { "Host": "localhost", - "Port": 9500, + "Port": 8500, "Token": "footoken" } From b5a827cf7023acbb600476467c90c362a6c74a34 Mon Sep 17 00:00:00 2001 From: Joseph Woodward Date: Tue, 19 Jun 2018 20:56:35 +0100 Subject: [PATCH 19/24] Minor tweaks (#413) --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4434b56f..84455545 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ # Ocelot -Ocelot is a .NET Api Gateway. This project is aimed at people using .NET running +Ocelot is a .NET API Gateway. This project is aimed at people using .NET running a micro services / service orientated architecture -that need a unified point of entry into their system. However it will work with anything that speaks HTTP and run on any platform that asp.net core supports. +that need a unified point of entry into their system. However it will work with anything that speaks HTTP and run on any platform that ASP.NET Core supports. In particular I want easy integration with IdentityServer reference and bearer tokens. @@ -61,6 +61,10 @@ Install Ocelot and it's dependencies using NuGet. `Install-Package Ocelot` +Or via the .NET Core CLI: + +`dotnet add package ocelot` + All versions can be found [here](https://www.nuget.org/packages/Ocelot/) ## Documentation From e636cefdb1a2e2c6a703b4355f74d6de94c9d5c7 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Wed, 20 Jun 2018 20:44:38 +0100 Subject: [PATCH 20/24] check which version of .net framework before creating http handler (#412) * #405 needto check which version of .net we are using but cannot use compiler directives * #405 started puttig abstraction around static method to get frameworks so we can test this logic * #405 added test for all methods and tidied up tests * #405 made contains as ms docs are wrong, thanks to davidni for the heads up --- .../DependencyInjection/OcelotBuilder.cs | 3 + .../Infrastructure/FrameworkDescription.cs | 12 + .../Infrastructure/IFrameworkDescription.cs | 7 + .../Creator/DownstreamRequestCreator.cs | 42 +++ .../Creator/IDownstreamRequestCreator.cs | 10 + .../Request/Middleware/DownstreamRequest.cs | 16 - .../DownstreamRequestInitialiserMiddleware.cs | 81 ++--- test/Ocelot.ManualTest/ocelot.json | 3 +- test/Ocelot.UnitTests/Ocelot.UnitTests.csproj | 9 + .../Creator/DownstreamRequestCreatorTests.cs | 93 ++++++ ...streamRequestInitialiserMiddlewareTests.cs | 289 +++++++++--------- test/Ocelot.UnitTests/idsrv3test.pfx | Bin 0 -> 3395 bytes 12 files changed, 366 insertions(+), 199 deletions(-) create mode 100644 src/Ocelot/Infrastructure/FrameworkDescription.cs create mode 100644 src/Ocelot/Infrastructure/IFrameworkDescription.cs create mode 100644 src/Ocelot/Request/Creator/DownstreamRequestCreator.cs create mode 100644 src/Ocelot/Request/Creator/IDownstreamRequestCreator.cs create mode 100644 test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs create mode 100644 test/Ocelot.UnitTests/idsrv3test.pfx diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 40ac80ed..7132c0b2 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -48,6 +48,7 @@ namespace Ocelot.DependencyInjection using ServiceDiscovery.Providers; using Steeltoe.Common.Discovery; using Pivotal.Discovery.Client; + using Ocelot.Request.Creator; public class OcelotBuilder : IOcelotBuilder { @@ -161,6 +162,8 @@ namespace Ocelot.DependencyInjection _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); } public IOcelotAdministrationBuilder AddAdministration(string path, string secret) diff --git a/src/Ocelot/Infrastructure/FrameworkDescription.cs b/src/Ocelot/Infrastructure/FrameworkDescription.cs new file mode 100644 index 00000000..927d4059 --- /dev/null +++ b/src/Ocelot/Infrastructure/FrameworkDescription.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace Ocelot.Infrastructure +{ + public class FrameworkDescription : IFrameworkDescription + { + public string Get() + { + return RuntimeInformation.FrameworkDescription; + } + } +} diff --git a/src/Ocelot/Infrastructure/IFrameworkDescription.cs b/src/Ocelot/Infrastructure/IFrameworkDescription.cs new file mode 100644 index 00000000..e59ac794 --- /dev/null +++ b/src/Ocelot/Infrastructure/IFrameworkDescription.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Infrastructure +{ + public interface IFrameworkDescription + { + string Get(); + } +} diff --git a/src/Ocelot/Request/Creator/DownstreamRequestCreator.cs b/src/Ocelot/Request/Creator/DownstreamRequestCreator.cs new file mode 100644 index 00000000..e4bdb1fc --- /dev/null +++ b/src/Ocelot/Request/Creator/DownstreamRequestCreator.cs @@ -0,0 +1,42 @@ +namespace Ocelot.Request.Creator +{ + using System.Net.Http; + using Ocelot.Request.Middleware; + using System.Runtime.InteropServices; + using Ocelot.Infrastructure; + + public class DownstreamRequestCreator : IDownstreamRequestCreator + { + private readonly IFrameworkDescription _framework; + private const string dotNetFramework = ".NET Framework"; + + public DownstreamRequestCreator(IFrameworkDescription framework) + { + _framework = framework; + } + + public DownstreamRequest Create(HttpRequestMessage request) + { + /** + * According to https://tools.ietf.org/html/rfc7231 + * GET,HEAD,DELETE,CONNECT,TRACE + * Can have body but server can reject the request. + * And MS HttpClient in Full Framework actually rejects it. + * see #366 issue + **/ + + if(_framework.Get().Contains(dotNetFramework)) + { + if (request.Method == HttpMethod.Get || + request.Method == HttpMethod.Head || + request.Method == HttpMethod.Delete || + request.Method == HttpMethod.Trace) + { + request.Content = null; + } + } + + return new DownstreamRequest(request); + } + } +} diff --git a/src/Ocelot/Request/Creator/IDownstreamRequestCreator.cs b/src/Ocelot/Request/Creator/IDownstreamRequestCreator.cs new file mode 100644 index 00000000..e6755399 --- /dev/null +++ b/src/Ocelot/Request/Creator/IDownstreamRequestCreator.cs @@ -0,0 +1,10 @@ +namespace Ocelot.Request.Creator +{ + using System.Net.Http; + using Ocelot.Request.Middleware; + + public interface IDownstreamRequestCreator + { + DownstreamRequest Create(HttpRequestMessage request); + } +} diff --git a/src/Ocelot/Request/Middleware/DownstreamRequest.cs b/src/Ocelot/Request/Middleware/DownstreamRequest.cs index fc1feb8a..75070bfd 100644 --- a/src/Ocelot/Request/Middleware/DownstreamRequest.cs +++ b/src/Ocelot/Request/Middleware/DownstreamRequest.cs @@ -48,22 +48,6 @@ namespace Ocelot.Request.Middleware Scheme = Scheme }; - /** - * According to https://tools.ietf.org/html/rfc7231 - * GET,HEAD,DELETE,CONNECT,TRACE - * Can have body but server can reject the request. - * And MS HttpClient in Full Framework actually rejects it. - * see #366 issue - **/ -#if NET461 || NET462 || NET47 || NET471 || NET472 - if (_request.Method == HttpMethod.Get || - _request.Method == HttpMethod.Head || - _request.Method == HttpMethod.Delete || - _request.Method == HttpMethod.Trace) - { - _request.Content = null; - } -#endif _request.RequestUri = uriBuilder.Uri; return _request; } diff --git a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs index 0cb7c7dd..ce226d98 100644 --- a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs +++ b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs @@ -1,39 +1,42 @@ -namespace Ocelot.Request.Middleware -{ - using System.Net.Http; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - using Ocelot.DownstreamRouteFinder.Middleware; - using Ocelot.Infrastructure.RequestData; - using Ocelot.Logging; - using Ocelot.Middleware; - - public class DownstreamRequestInitialiserMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly Mapper.IRequestMapper _requestMapper; - - public DownstreamRequestInitialiserMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - Mapper.IRequestMapper requestMapper) - :base(loggerFactory.CreateLogger()) - { - _next = next; - _requestMapper = requestMapper; - } - - public async Task Invoke(DownstreamContext context) - { - var downstreamRequest = await _requestMapper.Map(context.HttpContext.Request); - if (downstreamRequest.IsError) - { - SetPipelineError(context, downstreamRequest.Errors); - return; - } - - context.DownstreamRequest = new DownstreamRequest(downstreamRequest.Data); - - await _next.Invoke(context); - } - } -} +namespace Ocelot.Request.Middleware +{ + using System.Threading.Tasks; + using Microsoft.AspNetCore.Http; + using Ocelot.DownstreamRouteFinder.Middleware; + using Ocelot.Infrastructure.RequestData; + using Ocelot.Logging; + using Ocelot.Middleware; + using Ocelot.Request.Creator; + + public class DownstreamRequestInitialiserMiddleware : OcelotMiddleware + { + private readonly OcelotRequestDelegate _next; + private readonly Mapper.IRequestMapper _requestMapper; + private readonly IDownstreamRequestCreator _creator; + + public DownstreamRequestInitialiserMiddleware(OcelotRequestDelegate next, + IOcelotLoggerFactory loggerFactory, + Mapper.IRequestMapper requestMapper, + IDownstreamRequestCreator creator) + :base(loggerFactory.CreateLogger()) + { + _next = next; + _requestMapper = requestMapper; + _creator = creator; + } + + public async Task Invoke(DownstreamContext context) + { + var downstreamRequest = await _requestMapper.Map(context.HttpContext.Request); + if (downstreamRequest.IsError) + { + SetPipelineError(context, downstreamRequest.Errors); + return; + } + + context.DownstreamRequest = _creator.Create(downstreamRequest.Data); + + await _next.Invoke(context); + } + } +} diff --git a/test/Ocelot.ManualTest/ocelot.json b/test/Ocelot.ManualTest/ocelot.json index c21f8abb..6f531414 100644 --- a/test/Ocelot.ManualTest/ocelot.json +++ b/test/Ocelot.ManualTest/ocelot.json @@ -114,7 +114,8 @@ "HttpHandlerOptions": { "AllowAutoRedirect": true, "UseCookieContainer": true, - "UseTracing": true + "UseTracing": true, + "UseProxy": true }, "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index bfef63e0..a1908e7a 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -28,6 +28,15 @@ + + + PreserveNewest + + + PreserveNewest + + + diff --git a/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs b/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs new file mode 100644 index 00000000..f30f4cd8 --- /dev/null +++ b/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Moq; +using Ocelot.Infrastructure; +using Ocelot.Request.Creator; +using Ocelot.Request.Middleware; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Request.Creator +{ + public class DownstreamRequestCreatorTests + { + private Mock _framework; + private DownstreamRequestCreator _downstreamRequestCreator; + private HttpRequestMessage _request; + private DownstreamRequest _result; + + public DownstreamRequestCreatorTests() + { + _framework = new Mock(); + _downstreamRequestCreator = new DownstreamRequestCreator(_framework.Object); + } + + [Fact] + public void should_create_downstream_request() + { + var request = new HttpRequestMessage(HttpMethod.Get, "http://www.test.com"); + var content = new StringContent("test"); + request.Content = content; + + this.Given(_ => GivenTheFrameworkIs("")) + .And(_ => GivenTheRequestIs(request)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheDownstreamRequestHasABody()) + .BDDfy(); + } + + [Fact] + public void should_remove_body_for_http_methods() + { + var methods = new List { HttpMethod.Get, HttpMethod.Head, HttpMethod.Delete, HttpMethod.Trace }; + var request = new HttpRequestMessage(HttpMethod.Get, "http://www.test.com"); + var content = new StringContent("test"); + request.Content = content; + + methods.ForEach(m => { + this.Given(_ => GivenTheFrameworkIs(".NET Framework")) + .And(_ => GivenTheRequestIs(request)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheDownstreamRequestDoesNotHaveABody()) + .BDDfy(); + }); + } + + private void GivenTheFrameworkIs(string framework) + { + _framework.Setup(x => x.Get()).Returns(framework); + } + + private void GivenTheRequestIs(HttpRequestMessage request) + { + _request = request; + } + + private void WhenICreate() + { + _result = _downstreamRequestCreator.Create(_request); + } + + private async Task ThenTheDownstreamRequestHasABody() + { + _result.ShouldNotBeNull(); + _result.Method.ToLower().ShouldBe("get"); + _result.Scheme.ToLower().ShouldBe("http"); + _result.Host.ToLower().ShouldBe("www.test.com"); + var resultContent = await _result.ToHttpRequestMessage().Content.ReadAsStringAsync(); + resultContent.ShouldBe("test"); + } + + private void ThenTheDownstreamRequestDoesNotHaveABody() + { + _result.ShouldNotBeNull(); + _result.Method.ToLower().ShouldBe("get"); + _result.Scheme.ToLower().ShouldBe("http"); + _result.Host.ToLower().ShouldBe("www.test.com"); + _result.ToHttpRequestMessage().Content.ShouldBeNull(); + } + } +} diff --git a/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs b/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs index d37efc42..d571a8b7 100644 --- a/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs @@ -1,143 +1,146 @@ -using Ocelot.Middleware; - -namespace Ocelot.UnitTests.Request -{ - using System.Net.Http; - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Logging; - using Ocelot.Request.Mapper; - using Ocelot.Request.Middleware; - using Ocelot.Infrastructure.RequestData; - using TestStack.BDDfy; - using Xunit; - using Ocelot.Responses; - using Ocelot.DownstreamRouteFinder.Middleware; - using Shouldly; - - public class DownstreamRequestInitialiserMiddlewareTests - { - readonly DownstreamRequestInitialiserMiddleware _middleware; - - readonly Mock _httpContext; - - readonly Mock _httpRequest; - - readonly Mock _next; - - readonly Mock _requestMapper; - - readonly Mock _loggerFactory; - - readonly Mock _logger; - - Response _mappedRequest; - private DownstreamContext _downstreamContext; - - public DownstreamRequestInitialiserMiddlewareTests() - { - _httpContext = new Mock(); - _httpRequest = new Mock(); - _requestMapper = new Mock(); - _next = new Mock(); - _logger = new Mock(); - - _loggerFactory = new Mock(); - _loggerFactory - .Setup(lf => lf.CreateLogger()) - .Returns(_logger.Object); - - _middleware = new DownstreamRequestInitialiserMiddleware( - _next.Object, - _loggerFactory.Object, - _requestMapper.Object); - - _downstreamContext = new DownstreamContext(_httpContext.Object); - } - - [Fact] - public void Should_handle_valid_httpRequest() - { - this.Given(_ => GivenTheHttpContextContainsARequest()) - .And(_ => GivenTheMapperWillReturnAMappedRequest()) - .When(_ => WhenTheMiddlewareIsInvoked()) - .Then(_ => ThenTheContexRequestIsMappedToADownstreamRequest()) - .And(_ => ThenTheDownstreamRequestIsStored()) - .And(_ => ThenTheNextMiddlewareIsInvoked()) - .BDDfy(); - } - - [Fact] - public void Should_handle_mapping_failure() - { - this.Given(_ => GivenTheHttpContextContainsARequest()) - .And(_ => GivenTheMapperWillReturnAnError()) - .When(_ => WhenTheMiddlewareIsInvoked()) - .And(_ => ThenTheDownstreamRequestIsNotStored()) - .And(_ => ThenAPipelineErrorIsStored()) - .And(_ => ThenTheNextMiddlewareIsNotInvoked()) - .BDDfy(); - } - - private void GivenTheHttpContextContainsARequest() - { - _httpContext - .Setup(hc => hc.Request) - .Returns(_httpRequest.Object); - } - - private void GivenTheMapperWillReturnAMappedRequest() - { - _mappedRequest = new OkResponse(new HttpRequestMessage(HttpMethod.Get, "http://www.bbc.co.uk")); - - _requestMapper - .Setup(rm => rm.Map(It.IsAny())) - .ReturnsAsync(_mappedRequest); - } - - private void GivenTheMapperWillReturnAnError() - { - _mappedRequest = new ErrorResponse(new UnmappableRequestError(new System.Exception("boooom!"))); - - _requestMapper - .Setup(rm => rm.Map(It.IsAny())) - .ReturnsAsync(_mappedRequest); - } - - private void WhenTheMiddlewareIsInvoked() - { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - } - - private void ThenTheContexRequestIsMappedToADownstreamRequest() - { - _requestMapper.Verify(rm => rm.Map(_httpRequest.Object), Times.Once); - } - - private void ThenTheDownstreamRequestIsStored() - { - _downstreamContext.DownstreamRequest.ShouldNotBeNull(); - } - - private void ThenTheDownstreamRequestIsNotStored() - { - _downstreamContext.DownstreamRequest.ShouldBeNull(); - } - - private void ThenAPipelineErrorIsStored() - { - _downstreamContext.IsError.ShouldBeTrue(); - _downstreamContext.Errors.ShouldBe(_mappedRequest.Errors); - } - - private void ThenTheNextMiddlewareIsInvoked() - { - _next.Verify(n => n(_downstreamContext), Times.Once); - } - - private void ThenTheNextMiddlewareIsNotInvoked() - { - _next.Verify(n => n(It.IsAny()), Times.Never); - } - } -} +using Ocelot.Middleware; + +namespace Ocelot.UnitTests.Request +{ + using System.Net.Http; + using Microsoft.AspNetCore.Http; + using Moq; + using Ocelot.Logging; + using Ocelot.Request.Mapper; + using Ocelot.Request.Middleware; + using Ocelot.Infrastructure.RequestData; + using TestStack.BDDfy; + using Xunit; + using Ocelot.Responses; + using Ocelot.DownstreamRouteFinder.Middleware; + using Shouldly; + using Ocelot.Request.Creator; + using Ocelot.Infrastructure; + + public class DownstreamRequestInitialiserMiddlewareTests + { + readonly DownstreamRequestInitialiserMiddleware _middleware; + + readonly Mock _httpContext; + + readonly Mock _httpRequest; + + readonly Mock _next; + + readonly Mock _requestMapper; + + readonly Mock _loggerFactory; + + readonly Mock _logger; + + Response _mappedRequest; + private DownstreamContext _downstreamContext; + + public DownstreamRequestInitialiserMiddlewareTests() + { + _httpContext = new Mock(); + _httpRequest = new Mock(); + _requestMapper = new Mock(); + _next = new Mock(); + _logger = new Mock(); + + _loggerFactory = new Mock(); + _loggerFactory + .Setup(lf => lf.CreateLogger()) + .Returns(_logger.Object); + + _middleware = new DownstreamRequestInitialiserMiddleware( + _next.Object, + _loggerFactory.Object, + _requestMapper.Object, + new DownstreamRequestCreator(new FrameworkDescription())); + + _downstreamContext = new DownstreamContext(_httpContext.Object); + } + + [Fact] + public void Should_handle_valid_httpRequest() + { + this.Given(_ => GivenTheHttpContextContainsARequest()) + .And(_ => GivenTheMapperWillReturnAMappedRequest()) + .When(_ => WhenTheMiddlewareIsInvoked()) + .Then(_ => ThenTheContexRequestIsMappedToADownstreamRequest()) + .And(_ => ThenTheDownstreamRequestIsStored()) + .And(_ => ThenTheNextMiddlewareIsInvoked()) + .BDDfy(); + } + + [Fact] + public void Should_handle_mapping_failure() + { + this.Given(_ => GivenTheHttpContextContainsARequest()) + .And(_ => GivenTheMapperWillReturnAnError()) + .When(_ => WhenTheMiddlewareIsInvoked()) + .And(_ => ThenTheDownstreamRequestIsNotStored()) + .And(_ => ThenAPipelineErrorIsStored()) + .And(_ => ThenTheNextMiddlewareIsNotInvoked()) + .BDDfy(); + } + + private void GivenTheHttpContextContainsARequest() + { + _httpContext + .Setup(hc => hc.Request) + .Returns(_httpRequest.Object); + } + + private void GivenTheMapperWillReturnAMappedRequest() + { + _mappedRequest = new OkResponse(new HttpRequestMessage(HttpMethod.Get, "http://www.bbc.co.uk")); + + _requestMapper + .Setup(rm => rm.Map(It.IsAny())) + .ReturnsAsync(_mappedRequest); + } + + private void GivenTheMapperWillReturnAnError() + { + _mappedRequest = new ErrorResponse(new UnmappableRequestError(new System.Exception("boooom!"))); + + _requestMapper + .Setup(rm => rm.Map(It.IsAny())) + .ReturnsAsync(_mappedRequest); + } + + private void WhenTheMiddlewareIsInvoked() + { + _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + } + + private void ThenTheContexRequestIsMappedToADownstreamRequest() + { + _requestMapper.Verify(rm => rm.Map(_httpRequest.Object), Times.Once); + } + + private void ThenTheDownstreamRequestIsStored() + { + _downstreamContext.DownstreamRequest.ShouldNotBeNull(); + } + + private void ThenTheDownstreamRequestIsNotStored() + { + _downstreamContext.DownstreamRequest.ShouldBeNull(); + } + + private void ThenAPipelineErrorIsStored() + { + _downstreamContext.IsError.ShouldBeTrue(); + _downstreamContext.Errors.ShouldBe(_mappedRequest.Errors); + } + + private void ThenTheNextMiddlewareIsInvoked() + { + _next.Verify(n => n(_downstreamContext), Times.Once); + } + + private void ThenTheNextMiddlewareIsNotInvoked() + { + _next.Verify(n => n(It.IsAny()), Times.Never); + } + } +} diff --git a/test/Ocelot.UnitTests/idsrv3test.pfx b/test/Ocelot.UnitTests/idsrv3test.pfx new file mode 100644 index 0000000000000000000000000000000000000000..0247dea03f0cc23694291f21310f3ae88880e2bb GIT binary patch literal 3395 zcmY*ac{tQ<7yiu{V_$|rA%;e>y=E+9O_nU#g|hFBC`-tmWsqeoSyGW9LYA!AlQm0d zlqs?cuRUcMvVK$7_r34+{c)aipZh-Nxy~QY_1q{N(`7J-3WZ}lgwlyV(0Q=O1fl`u z;TYE;IL2iPy@0|&nf_0rK7rt<4^TL2G9|X44F8>Cqz8fXaF7!e4sw9vh0_0zrd-Yp zq5aB`c0pwf*#!pE3`1~`u};|qLmAL66%SqGD&c1ok7w*g=3CPGVk4GBqUnz5R$^lb z8Dv(rRpfX7yvJ$AZ8B=IukK|?oWq7THPW9AE8<%>%oONtPAOw&x8_?KHa0J|WVwA0 zIe9iq|#j@0h-r2z9#p>N7n4=mGfXBZdZv zm>}$|9($ZRdyt-g#VGBa?>B!qNzif-i+FE)kucwfM0uQ_?eH5E22H7{O&W(b9&xxe z%p<>vWCX)-exQO)Be=&=gf&-c#+j`(NUetfn}WVXG{= z^!3S{N|*XdJW@10Ikf3}LcuN>qA~Ixlg<}c;VO{NzpbcV)gX{XXMvCF$|Bihu8%Mj`v7 z@JI#bMy0mL?ntjDyu>tItFCrcM?2T4qxi{DAYXF4re+jt!0KM!4AX1-`m6J2B-j7$ ztQmXW9+nsyVA76pGD!SNDBJX7<=P3^TAtMP*S&|$8V_zcInNp6F})=P6L9WM3skx( zrU*k+zF?-S=hmjpL4Q3zv>!AS5ZdH` zP7@1%4o~2pGsTCkqHI#fTE9t6L}0I0RV#X80*5W8dQ!d^3i!EAcx!{g?Ymhx9_uH| z%5-;5L5^5@FPajHS9ShoBMyy!p(c{qxOAL#hI6ENh505_rZ0?SGHg>G?cH-JcX$bP zvvcygKZ|q33xcOvl0F>Lq;-3oT1}&U{+hFQhdrnZ&f3Cd?*G~+e;NZj-CLQ#d7u*d z-zLck*=~$_*oTD=7glD2s_n4ZBbndKCJM<*Y#U_RIHLGB-|y!WU`T^)1|P6xbeP|G zVeM+?bDY~u1~eh71YCS>5m|2W++)$^^VxHSdmxwhWqlh$#}_R*QJIE}!YhyC22(}y z-pGi)Mp$4isupi_SdyK1kwa|ypqYxDZM%%-W8XLUrq=uHuIVLfoLXn0Ft*+*&7DasMmP3gdi3$so3cjv zU3_I_!HIUJ-KLn$?yVs^q%Nt?{K4vH$8|KG-fP7I-JGh){ZkukKp&IeTFS zofK|@;`zesc<{wV&~=^Lpxwgq@1SZU!pFuL4xnXwJhXzpFXWPHqe5C^&F$XOKSyA*?hARwF^42%X)?En0pbR1|X1Ofs80A>9z2}c|9=>s8v zEFceP0#bk)B`W|LfCL~z!7_mQA0!RPQ8WpPf}*g$)hhsoqDlYhLQ^z_KfESzA7%UR z0wA<8pCMoXxBgEJg#e8I z^!ZaN7vLt~Loo#6Kiktl^Kj613iSpI0w}5OUj_7kE&%=Q0@7Z?>>U#@$=@yzfrG{o ztFTv(L~LX}xO!x0^EITtLxl@_o6uy5gghAR{hz9rAUI9X6qKa_Nw%q za~SdO27));Ss1O7WmAmU?z>@+sX7%|EH>F*@OZUVn!`%vFPjg13@;Tl|_JIFJuO?ibe+@(=CitY0KN zmhw8P&DGlJBqvEH_i~51(xCCqvU$O5a^w(gap!{;x$=mI;>(I{4_^3{xSVlt0*&Z-y38aD8;?f`*U1VzA?{YPa$fn^V7$cGLd)&c%khfmt-qvZ_d8X! z7hHsG8{dHEPrBwl**uN9qgJ5pDa-DS;*TkBvMr}WsGRp(tl&q zOLj#>q5fr!g3h>N*4Lo!^2f&yedb9`Kc@UII#(J*#=~mQpg7_^@Qad_`7&Rw^Q13P zmkj26C2^Lfg&(Un^M{l&&Z~Al#>~&po-IRgbH;zV|EZU6sq2W4r<`>`jAnHJX0F#X zoYLuTJJ&S__HOHM}CU)!}{mUnHM4&H-PJ zDgU|rTaFE6VJ^#8$-7}h}^b=$AFm^Ju%|Irt#Xm@y!x8ht)nP}yX zak6LD=XrWjz}YIk=NKi;Oyzuyhr4N#>$;BIHeVmO7CwR&BH~$h($R>lxm#|jH)hMo z7Cl?fME$4w@i!`TUwnfzepq`tb2MXQ>vjOez4DO&G+ zwbxqf;c;Lz7e^2GJN4&pn)*n036&#X{M)L}3jNt9WQoG#Ltw0 zBSd@4uASn_19~vFMd|jhEOlmOnzg#t-W`Y8`{ihls#Ej*@-YyvQR5@XB{Zgn*UU@bPjBb)ma-dM*TyAY#Qr-I?}ssTqWiQUU~9nVL8urj8g zB=?6~(E%Bt>5<*!OPB%-9y0pkl!uu8}JyuP^C{VwK-!6&8CcOsFR z#AD|e+mNE9i#41w#l(h}rbw&h^*Xp8>93ZTvg}r-DJps1W6hRpeV*HGw|(EWnX7>t zi;7~9X)yDN{8DJzLpxCoH*tL3SHK!$Z}tQc<%NTk$t)S*4<=4>wFvMd!y)pV_liw) z7Z+8=AXg^QgwL(&DRsQU5*({(LDt{G-4Rx#dhx6AP+_msH%Jue6QCy=B0w?y#4k$7;> z=5ttmpV&vFVv}ZY>6NE%#+W))M)nU;WMS%-mtLT!)&4oAMhnY2Hb@dJUGXLb^4wIex}=co7n{7tD1N!| zw63xzN%ImPTf3iZ?X@yq6*F$jX5my$Q%SSyOrlD)y}jkyw`e{y&l34ahp)821A!iS z4-;-p@j6Gn!f>FJQ2ZzwD76?f6_^_WN5dA?3G%E0bF79+L#MT|(Yv~t5ct?-mV0Fj V%$88{h~I%@Xjg7x^oQR@_8&Ry9S;Bi literal 0 HcmV?d00001 From fffc4c8d3c27de1dc150f90096b2803782e47646 Mon Sep 17 00:00:00 2001 From: David Nissimoff Date: Wed, 20 Jun 2018 14:41:00 -0700 Subject: [PATCH 21/24] #419 Incorrect routing when UpstreamHost is specified and UpstreamHttpMethod is empty (#420) --- .../Finder/DownstreamRouteFinder.cs | 3 ++- .../DownstreamRouteFinderTests.cs | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs index d7713aee..b236c291 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -49,7 +49,8 @@ namespace Ocelot.DownstreamRouteFinder.Finder private bool RouteIsApplicableToThisRequest(ReRoute reRoute, string httpMethod, string upstreamHost) { - return reRoute.UpstreamHttpMethod.Count == 0 || reRoute.UpstreamHttpMethod.Select(x => x.Method.ToLower()).Contains(httpMethod.ToLower()) && !(!string.IsNullOrEmpty(reRoute.UpstreamHost) && reRoute.UpstreamHost != upstreamHost); + return (reRoute.UpstreamHttpMethod.Count == 0 || reRoute.UpstreamHttpMethod.Select(x => x.Method.ToLower()).Contains(httpMethod.ToLower())) && + (string.IsNullOrEmpty(reRoute.UpstreamHost) || reRoute.UpstreamHost == upstreamHost); } private DownstreamRoute GetPlaceholderNamesAndValues(string path, ReRoute reRoute) diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index 6b66abe3..b1ea2c81 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -583,6 +583,19 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .WithUpstreamHttpMethod(new List { "Get" }) .WithUpstreamTemplatePattern(new UpstreamPathTemplate("someUpstreamPath", 1)) .WithUpstreamHost("MATCH") + .Build(), + new ReRouteBuilder() + .WithDownstreamReRoute(new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamPathTemplate("someUpstreamPath") + .WithUpstreamHttpMethod(new List { }) // empty list of methods + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("someUpstreamPath", 1)) + .WithUpstreamHost("MATCH") + .Build()) + .WithUpstreamPathTemplate("someUpstreamPath") + .WithUpstreamHttpMethod(new List { }) // empty list of methods + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("someUpstreamPath", 1)) + .WithUpstreamHost("MATCH") .Build() }, string.Empty, serviceProviderConfig )) From 3eb9b4da899906cfaaa154d30bd458f1887318c2 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Thu, 21 Jun 2018 22:45:24 +0100 Subject: [PATCH 22/24] Feature/fix admin api caching wrong re routes (#421) * #383 added failing test for this issue * #383 identified issue was with cached load balancer for a given upstream path template based on the key we use, have modified this to include more data, I guess this might be an issue again for other things so I will have a think about it * #383 fixed failing tests after key change * Seems to be an issue with coveralls new package not being on nuget...try same version as their nuget package * bash the old manual tests json back in --- build.cake | 1028 +++++------ .../FileInternalConfigurationCreator.cs | 2 +- .../Setter/IFileConfigurationSetter.cs | 22 +- .../Headers/HttpResponseHeaderReplacer.cs | 110 +- .../HttpHeadersTransformationMiddleware.cs | 96 +- test/Ocelot.AcceptanceTests/HeaderTests.cs | 777 ++++---- .../AdministrationTests.cs | 1579 +++++++++-------- .../FileInternalConfigurationCreatorTests.cs | 20 +- 8 files changed, 1903 insertions(+), 1731 deletions(-) diff --git a/build.cake b/build.cake index 619df853..ef8cb5c6 100644 --- a/build.cake +++ b/build.cake @@ -1,514 +1,514 @@ -#tool "nuget:?package=GitVersion.CommandLine" -#tool "nuget:?package=GitReleaseNotes" -#addin nuget:?package=Cake.Json -#addin nuget:?package=Newtonsoft.Json&version=9.0.1 -#tool "nuget:?package=OpenCover" -#tool "nuget:?package=ReportGenerator" -#tool "nuget:?package=coveralls.net&version=0.7.0" -#addin Cake.Coveralls - -// compile -var compileConfig = Argument("configuration", "Release"); -var slnFile = "./Ocelot.sln"; - -// build artifacts -var artifactsDir = Directory("artifacts"); - -// unit testing -var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests"); -var unitTestAssemblies = @"./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj"; -var minCodeCoverage = 82d; -var coverallsRepoToken = "coveralls-repo-token-ocelot"; -var coverallsRepo = "https://coveralls.io/github/TomPallister/Ocelot"; - -// acceptance testing -var artifactsForAcceptanceTestsDir = artifactsDir + Directory("AcceptanceTests"); -var acceptanceTestAssemblies = @"./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj"; - -// integration testing -var artifactsForIntegrationTestsDir = artifactsDir + Directory("IntegrationTests"); -var integrationTestAssemblies = @"./test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj"; - -// benchmark testing -var artifactsForBenchmarkTestsDir = artifactsDir + Directory("BenchmarkTests"); -var benchmarkTestAssemblies = @"./test/Ocelot.Benchmarks"; - -// packaging -var packagesDir = artifactsDir + Directory("Packages"); -var releaseNotesFile = packagesDir + File("releasenotes.md"); -var artifactsFile = packagesDir + File("artifacts.txt"); - -// unstable releases -var nugetFeedUnstableKey = EnvironmentVariable("nuget-apikey-unstable"); -var nugetFeedUnstableUploadUrl = "https://www.nuget.org/api/v2/package"; -var nugetFeedUnstableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package"; - -// stable releases -var tagsUrl = "https://api.github.com/repos/tompallister/ocelot/releases/tags/"; -var nugetFeedStableKey = EnvironmentVariable("nuget-apikey-stable"); -var nugetFeedStableUploadUrl = "https://www.nuget.org/api/v2/package"; -var nugetFeedStableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package"; - -// internal build variables - don't change these. -var releaseTag = ""; -string committedVersion = "0.0.0-dev"; -var buildVersion = committedVersion; -GitVersion versioning = null; -var nugetFeedUnstableBranchFilter = "^(develop)$|^(PullRequest/)"; - -var target = Argument("target", "Default"); - - -Information("target is " +target); -Information("Build configuration is " + compileConfig); - -Task("Default") - .IsDependentOn("Build"); - -Task("Build") - .IsDependentOn("RunTests") - .IsDependentOn("CreatePackages"); - -Task("BuildAndReleaseUnstable") - .IsDependentOn("Build") - .IsDependentOn("ReleasePackagesToUnstableFeed"); - -Task("Clean") - .Does(() => - { - if (DirectoryExists(artifactsDir)) - { - DeleteDirectory(artifactsDir, recursive:true); - } - CreateDirectory(artifactsDir); - }); - -Task("Version") - .Does(() => - { - versioning = GetNuGetVersionForCommit(); - var nugetVersion = versioning.NuGetVersion; - Information("SemVer version number: " + nugetVersion); - - if (AppVeyor.IsRunningOnAppVeyor) - { - Information("Persisting version number..."); - PersistVersion(committedVersion, nugetVersion); - buildVersion = nugetVersion; - } - else - { - Information("We are not running on build server, so we won't persist the version number."); - } - }); - -Task("Compile") - .IsDependentOn("Clean") - .IsDependentOn("Version") - .Does(() => - { - var settings = new DotNetCoreBuildSettings - { - Configuration = compileConfig, - }; - - DotNetCoreBuild(slnFile, settings); - }); - -Task("RunUnitTests") - .IsDependentOn("Compile") - .Does(() => - { - if (IsRunningOnWindows()) - { - var coverageSummaryFile = artifactsForUnitTestsDir + File("coverage.xml"); - - EnsureDirectoryExists(artifactsForUnitTestsDir); - - OpenCover(tool => - { - tool.DotNetCoreTest(unitTestAssemblies); - }, - new FilePath(coverageSummaryFile), - new OpenCoverSettings() - { - Register="user", - ArgumentCustomization=args=>args.Append(@"-oldstyle -returntargetcode -excludebyattribute:*.ExcludeFromCoverage*") - } - .WithFilter("+[Ocelot*]*") - .WithFilter("-[xunit*]*") - .WithFilter("-[Ocelot*Tests]*") - ); - - ReportGenerator(coverageSummaryFile, artifactsForUnitTestsDir); - - if (AppVeyor.IsRunningOnAppVeyor) - { - var repoToken = EnvironmentVariable(coverallsRepoToken); - if (string.IsNullOrEmpty(repoToken)) - { - throw new Exception(string.Format("Coveralls repo token not found. Set environment variable '{0}'", coverallsRepoToken)); - } - - Information(string.Format("Uploading test coverage to {0}", coverallsRepo)); - CoverallsNet(coverageSummaryFile, CoverallsNetReportType.OpenCover, new CoverallsNetSettings() - { - RepoToken = repoToken - }); - } - else - { - Information("We are not running on the build server so we won't publish the coverage report to coveralls.io"); - } - - var sequenceCoverage = XmlPeek(coverageSummaryFile, "//CoverageSession/Summary/@sequenceCoverage"); - var branchCoverage = XmlPeek(coverageSummaryFile, "//CoverageSession/Summary/@branchCoverage"); - - Information("Sequence Coverage: " + sequenceCoverage); - - if(double.Parse(sequenceCoverage) < minCodeCoverage) - { - var whereToCheck = !AppVeyor.IsRunningOnAppVeyor ? coverallsRepo : artifactsForUnitTestsDir; - throw new Exception(string.Format("Code coverage fell below the threshold of {0}%. You can find the code coverage report at {1}", minCodeCoverage, whereToCheck)); - }; - - } - else - { - var settings = new DotNetCoreTestSettings - { - Configuration = compileConfig, - }; - - EnsureDirectoryExists(artifactsForUnitTestsDir); - DotNetCoreTest(unitTestAssemblies, settings); - } - }); - -Task("RunAcceptanceTests") - .IsDependentOn("Compile") - .Does(() => - { - if(TravisCI.IsRunningOnTravisCI) - { - Information( - @"Job: - JobId: {0} - JobNumber: {1} - OSName: {2}", - BuildSystem.TravisCI.Environment.Job.JobId, - BuildSystem.TravisCI.Environment.Job.JobNumber, - BuildSystem.TravisCI.Environment.Job.OSName - ); - - if(TravisCI.Environment.Job.OSName.ToLower() == "osx") - { - return; - } - } - - var settings = new DotNetCoreTestSettings - { - Configuration = compileConfig, - ArgumentCustomization = args => args - .Append("--no-restore") - .Append("--no-build") - }; - - EnsureDirectoryExists(artifactsForAcceptanceTestsDir); - DotNetCoreTest(acceptanceTestAssemblies, settings); - }); - -Task("RunIntegrationTests") - .IsDependentOn("Compile") - .Does(() => - { - if(TravisCI.IsRunningOnTravisCI) - { - Information( - @"Job: - JobId: {0} - JobNumber: {1} - OSName: {2}", - BuildSystem.TravisCI.Environment.Job.JobId, - BuildSystem.TravisCI.Environment.Job.JobNumber, - BuildSystem.TravisCI.Environment.Job.OSName - ); - - if(TravisCI.Environment.Job.OSName.ToLower() == "osx") - { - return; - } - } - - var settings = new DotNetCoreTestSettings - { - Configuration = compileConfig, - ArgumentCustomization = args => args - .Append("--no-restore") - .Append("--no-build") - }; - - EnsureDirectoryExists(artifactsForIntegrationTestsDir); - DotNetCoreTest(integrationTestAssemblies, settings); - }); - -Task("RunTests") - .IsDependentOn("RunUnitTests") - .IsDependentOn("RunAcceptanceTests") - .IsDependentOn("RunIntegrationTests"); - -Task("CreatePackages") - .IsDependentOn("Compile") - .Does(() => - { - EnsureDirectoryExists(packagesDir); - CopyFiles("./src/**/Ocelot.*.nupkg", packagesDir); - - //GenerateReleaseNotes(releaseNotesFile); - - System.IO.File.WriteAllLines(artifactsFile, new[]{ - "nuget:Ocelot." + buildVersion + ".nupkg", - //"releaseNotes:releasenotes.md" - }); - - if (AppVeyor.IsRunningOnAppVeyor) - { - var path = packagesDir.ToString() + @"/**/*"; - - foreach (var file in GetFiles(path)) - { - AppVeyor.UploadArtifact(file.FullPath); - } - } - }); - -Task("ReleasePackagesToUnstableFeed") - .IsDependentOn("CreatePackages") - .Does(() => - { - if (ShouldPublishToUnstableFeed(nugetFeedUnstableBranchFilter, versioning.BranchName)) - { - PublishPackages(packagesDir, artifactsFile, nugetFeedUnstableKey, nugetFeedUnstableUploadUrl, nugetFeedUnstableSymbolsUploadUrl); - } - }); - -Task("EnsureStableReleaseRequirements") - .Does(() => - { - Information("Check if stable release..."); - - if (!AppVeyor.IsRunningOnAppVeyor) - { - throw new Exception("Stable release should happen via appveyor"); - } - - Information("Running on AppVeyor..."); - - Information("IsTag = " + AppVeyor.Environment.Repository.Tag.IsTag); - - Information("Name = " + AppVeyor.Environment.Repository.Tag.Name); - - var isTag = - AppVeyor.Environment.Repository.Tag.IsTag && - !string.IsNullOrWhiteSpace(AppVeyor.Environment.Repository.Tag.Name); - - if (!isTag) - { - throw new Exception("Stable release should happen from a published GitHub release"); - } - - Information("Release is stable..."); - }); - -Task("UpdateVersionInfo") - .IsDependentOn("EnsureStableReleaseRequirements") - .Does(() => - { - releaseTag = AppVeyor.Environment.Repository.Tag.Name; - AppVeyor.UpdateBuildVersion(releaseTag); - }); - -Task("DownloadGitHubReleaseArtifacts") - .IsDependentOn("UpdateVersionInfo") - .Does(() => - { - try - { - Information("DownloadGitHubReleaseArtifacts"); - - EnsureDirectoryExists(packagesDir); - - Information("Directory exists..."); - - var releaseUrl = tagsUrl + releaseTag; - - Information("Release url " + releaseUrl); - - //var releaseJson = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl)); - - var assets_url = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl)) - .GetValue("assets_url") - .Value(); - - Information("Assets url " + assets_url); - - var assets = GetResource(assets_url); - - Information("Assets " + assets_url); - - foreach(var asset in Newtonsoft.Json.JsonConvert.DeserializeObject(assets)) - { - Information("In the loop.."); - - var file = packagesDir + File(asset.Value("name")); - - Information("Downloading " + file); - - DownloadFile(asset.Value("browser_download_url"), file); - } - - Information("Out of the loop..."); - } - catch(Exception exception) - { - Information("There was an exception " + exception); - throw; - } - }); - -Task("ReleasePackagesToStableFeed") - .IsDependentOn("DownloadGitHubReleaseArtifacts") - .Does(() => - { - PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); - }); - -Task("Release") - .IsDependentOn("ReleasePackagesToStableFeed"); - -RunTarget(target); - -/// Gets nuique nuget version for this commit -private GitVersion GetNuGetVersionForCommit() -{ - GitVersion(new GitVersionSettings{ - UpdateAssemblyInfo = false, - OutputType = GitVersionOutput.BuildServer - }); - - return GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json }); -} - -/// Updates project version in all of our projects -private void PersistVersion(string committedVersion, string newVersion) -{ - Information(string.Format("We'll search all csproj files for {0} and replace with {1}...", committedVersion, newVersion)); - - var projectFiles = GetFiles("./**/*.csproj"); - - foreach(var projectFile in projectFiles) - { - var file = projectFile.ToString(); - - Information(string.Format("Updating {0}...", file)); - - var updatedProjectFile = System.IO.File.ReadAllText(file) - .Replace(committedVersion, newVersion); - - System.IO.File.WriteAllText(file, updatedProjectFile); - } -} - -/// generates release notes based on issues closed in GitHub since the last release -private void GenerateReleaseNotes(ConvertableFilePath file) -{ - if(!IsRunningOnWindows()) - { - Warning("We are not running on Windows so we cannot generate release notes."); - return; - } - - Information("Generating release notes at " + file); - - var releaseNotesExitCode = StartProcess( - @"tools/GitReleaseNotes/tools/gitreleasenotes.exe", - new ProcessSettings { Arguments = ". /o " + file }); - - if (string.IsNullOrEmpty(System.IO.File.ReadAllText(file))) - { - System.IO.File.WriteAllText(file, "No issues closed since last release"); - } - - if (releaseNotesExitCode != 0) - { - throw new Exception("Failed to generate release notes"); - } -} - -/// Publishes code and symbols packages to nuget feed, based on contents of artifacts file -private void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFilePath artifactsFile, string feedApiKey, string codeFeedUrl, string symbolFeedUrl) -{ - var artifacts = System.IO.File - .ReadAllLines(artifactsFile) - .Select(l => l.Split(':')) - .ToDictionary(v => v[0], v => v[1]); - - var codePackage = packagesDir + File(artifacts["nuget"]); - - Information("Pushing package " + codePackage); - - Information("Calling NuGetPush"); - - NuGetPush( - codePackage, - new NuGetPushSettings { - ApiKey = feedApiKey, - Source = codeFeedUrl - }); -} - -/// gets the resource from the specified url -private string GetResource(string url) -{ - try - { - Information("Getting resource from " + url); - - var assetsRequest = System.Net.WebRequest.CreateHttp(url); - assetsRequest.Method = "GET"; - assetsRequest.Accept = "application/vnd.github.v3+json"; - assetsRequest.UserAgent = "BuildScript"; - - using (var assetsResponse = assetsRequest.GetResponse()) - { - var assetsStream = assetsResponse.GetResponseStream(); - var assetsReader = new StreamReader(assetsStream); - var response = assetsReader.ReadToEnd(); - - Information("Response is " + response); - - return response; - } - } - catch(Exception exception) - { - Information("There was an exception " + exception); - throw; - } -} - -private bool ShouldPublishToUnstableFeed(string filter, string branchName) -{ - var regex = new System.Text.RegularExpressions.Regex(filter); - var publish = regex.IsMatch(branchName); - if (publish) - { - Information("Branch " + branchName + " will be published to the unstable feed"); - } - else - { - Information("Branch " + branchName + " will not be published to the unstable feed"); - } - return publish; -} +#tool "nuget:?package=GitVersion.CommandLine" +#tool "nuget:?package=GitReleaseNotes" +#addin nuget:?package=Cake.Json +#addin nuget:?package=Newtonsoft.Json&version=9.0.1 +#tool "nuget:?package=OpenCover" +#tool "nuget:?package=ReportGenerator" +#tool "nuget:?package=coveralls.net&version=0.7.0" +#addin Cake.Coveralls&version=0.7.0 + +// compile +var compileConfig = Argument("configuration", "Release"); +var slnFile = "./Ocelot.sln"; + +// build artifacts +var artifactsDir = Directory("artifacts"); + +// unit testing +var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests"); +var unitTestAssemblies = @"./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj"; +var minCodeCoverage = 82d; +var coverallsRepoToken = "coveralls-repo-token-ocelot"; +var coverallsRepo = "https://coveralls.io/github/TomPallister/Ocelot"; + +// acceptance testing +var artifactsForAcceptanceTestsDir = artifactsDir + Directory("AcceptanceTests"); +var acceptanceTestAssemblies = @"./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj"; + +// integration testing +var artifactsForIntegrationTestsDir = artifactsDir + Directory("IntegrationTests"); +var integrationTestAssemblies = @"./test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj"; + +// benchmark testing +var artifactsForBenchmarkTestsDir = artifactsDir + Directory("BenchmarkTests"); +var benchmarkTestAssemblies = @"./test/Ocelot.Benchmarks"; + +// packaging +var packagesDir = artifactsDir + Directory("Packages"); +var releaseNotesFile = packagesDir + File("releasenotes.md"); +var artifactsFile = packagesDir + File("artifacts.txt"); + +// unstable releases +var nugetFeedUnstableKey = EnvironmentVariable("nuget-apikey-unstable"); +var nugetFeedUnstableUploadUrl = "https://www.nuget.org/api/v2/package"; +var nugetFeedUnstableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package"; + +// stable releases +var tagsUrl = "https://api.github.com/repos/tompallister/ocelot/releases/tags/"; +var nugetFeedStableKey = EnvironmentVariable("nuget-apikey-stable"); +var nugetFeedStableUploadUrl = "https://www.nuget.org/api/v2/package"; +var nugetFeedStableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package"; + +// internal build variables - don't change these. +var releaseTag = ""; +string committedVersion = "0.0.0-dev"; +var buildVersion = committedVersion; +GitVersion versioning = null; +var nugetFeedUnstableBranchFilter = "^(develop)$|^(PullRequest/)"; + +var target = Argument("target", "Default"); + + +Information("target is " +target); +Information("Build configuration is " + compileConfig); + +Task("Default") + .IsDependentOn("Build"); + +Task("Build") + .IsDependentOn("RunTests") + .IsDependentOn("CreatePackages"); + +Task("BuildAndReleaseUnstable") + .IsDependentOn("Build") + .IsDependentOn("ReleasePackagesToUnstableFeed"); + +Task("Clean") + .Does(() => + { + if (DirectoryExists(artifactsDir)) + { + DeleteDirectory(artifactsDir, recursive:true); + } + CreateDirectory(artifactsDir); + }); + +Task("Version") + .Does(() => + { + versioning = GetNuGetVersionForCommit(); + var nugetVersion = versioning.NuGetVersion; + Information("SemVer version number: " + nugetVersion); + + if (AppVeyor.IsRunningOnAppVeyor) + { + Information("Persisting version number..."); + PersistVersion(committedVersion, nugetVersion); + buildVersion = nugetVersion; + } + else + { + Information("We are not running on build server, so we won't persist the version number."); + } + }); + +Task("Compile") + .IsDependentOn("Clean") + .IsDependentOn("Version") + .Does(() => + { + var settings = new DotNetCoreBuildSettings + { + Configuration = compileConfig, + }; + + DotNetCoreBuild(slnFile, settings); + }); + +Task("RunUnitTests") + .IsDependentOn("Compile") + .Does(() => + { + if (IsRunningOnWindows()) + { + var coverageSummaryFile = artifactsForUnitTestsDir + File("coverage.xml"); + + EnsureDirectoryExists(artifactsForUnitTestsDir); + + OpenCover(tool => + { + tool.DotNetCoreTest(unitTestAssemblies); + }, + new FilePath(coverageSummaryFile), + new OpenCoverSettings() + { + Register="user", + ArgumentCustomization=args=>args.Append(@"-oldstyle -returntargetcode -excludebyattribute:*.ExcludeFromCoverage*") + } + .WithFilter("+[Ocelot*]*") + .WithFilter("-[xunit*]*") + .WithFilter("-[Ocelot*Tests]*") + ); + + ReportGenerator(coverageSummaryFile, artifactsForUnitTestsDir); + + if (AppVeyor.IsRunningOnAppVeyor) + { + var repoToken = EnvironmentVariable(coverallsRepoToken); + if (string.IsNullOrEmpty(repoToken)) + { + throw new Exception(string.Format("Coveralls repo token not found. Set environment variable '{0}'", coverallsRepoToken)); + } + + Information(string.Format("Uploading test coverage to {0}", coverallsRepo)); + CoverallsNet(coverageSummaryFile, CoverallsNetReportType.OpenCover, new CoverallsNetSettings() + { + RepoToken = repoToken + }); + } + else + { + Information("We are not running on the build server so we won't publish the coverage report to coveralls.io"); + } + + var sequenceCoverage = XmlPeek(coverageSummaryFile, "//CoverageSession/Summary/@sequenceCoverage"); + var branchCoverage = XmlPeek(coverageSummaryFile, "//CoverageSession/Summary/@branchCoverage"); + + Information("Sequence Coverage: " + sequenceCoverage); + + if(double.Parse(sequenceCoverage) < minCodeCoverage) + { + var whereToCheck = !AppVeyor.IsRunningOnAppVeyor ? coverallsRepo : artifactsForUnitTestsDir; + throw new Exception(string.Format("Code coverage fell below the threshold of {0}%. You can find the code coverage report at {1}", minCodeCoverage, whereToCheck)); + }; + + } + else + { + var settings = new DotNetCoreTestSettings + { + Configuration = compileConfig, + }; + + EnsureDirectoryExists(artifactsForUnitTestsDir); + DotNetCoreTest(unitTestAssemblies, settings); + } + }); + +Task("RunAcceptanceTests") + .IsDependentOn("Compile") + .Does(() => + { + if(TravisCI.IsRunningOnTravisCI) + { + Information( + @"Job: + JobId: {0} + JobNumber: {1} + OSName: {2}", + BuildSystem.TravisCI.Environment.Job.JobId, + BuildSystem.TravisCI.Environment.Job.JobNumber, + BuildSystem.TravisCI.Environment.Job.OSName + ); + + if(TravisCI.Environment.Job.OSName.ToLower() == "osx") + { + return; + } + } + + var settings = new DotNetCoreTestSettings + { + Configuration = compileConfig, + ArgumentCustomization = args => args + .Append("--no-restore") + .Append("--no-build") + }; + + EnsureDirectoryExists(artifactsForAcceptanceTestsDir); + DotNetCoreTest(acceptanceTestAssemblies, settings); + }); + +Task("RunIntegrationTests") + .IsDependentOn("Compile") + .Does(() => + { + if(TravisCI.IsRunningOnTravisCI) + { + Information( + @"Job: + JobId: {0} + JobNumber: {1} + OSName: {2}", + BuildSystem.TravisCI.Environment.Job.JobId, + BuildSystem.TravisCI.Environment.Job.JobNumber, + BuildSystem.TravisCI.Environment.Job.OSName + ); + + if(TravisCI.Environment.Job.OSName.ToLower() == "osx") + { + return; + } + } + + var settings = new DotNetCoreTestSettings + { + Configuration = compileConfig, + ArgumentCustomization = args => args + .Append("--no-restore") + .Append("--no-build") + }; + + EnsureDirectoryExists(artifactsForIntegrationTestsDir); + DotNetCoreTest(integrationTestAssemblies, settings); + }); + +Task("RunTests") + .IsDependentOn("RunUnitTests") + .IsDependentOn("RunAcceptanceTests") + .IsDependentOn("RunIntegrationTests"); + +Task("CreatePackages") + .IsDependentOn("Compile") + .Does(() => + { + EnsureDirectoryExists(packagesDir); + CopyFiles("./src/**/Ocelot.*.nupkg", packagesDir); + + //GenerateReleaseNotes(releaseNotesFile); + + System.IO.File.WriteAllLines(artifactsFile, new[]{ + "nuget:Ocelot." + buildVersion + ".nupkg", + //"releaseNotes:releasenotes.md" + }); + + if (AppVeyor.IsRunningOnAppVeyor) + { + var path = packagesDir.ToString() + @"/**/*"; + + foreach (var file in GetFiles(path)) + { + AppVeyor.UploadArtifact(file.FullPath); + } + } + }); + +Task("ReleasePackagesToUnstableFeed") + .IsDependentOn("CreatePackages") + .Does(() => + { + if (ShouldPublishToUnstableFeed(nugetFeedUnstableBranchFilter, versioning.BranchName)) + { + PublishPackages(packagesDir, artifactsFile, nugetFeedUnstableKey, nugetFeedUnstableUploadUrl, nugetFeedUnstableSymbolsUploadUrl); + } + }); + +Task("EnsureStableReleaseRequirements") + .Does(() => + { + Information("Check if stable release..."); + + if (!AppVeyor.IsRunningOnAppVeyor) + { + throw new Exception("Stable release should happen via appveyor"); + } + + Information("Running on AppVeyor..."); + + Information("IsTag = " + AppVeyor.Environment.Repository.Tag.IsTag); + + Information("Name = " + AppVeyor.Environment.Repository.Tag.Name); + + var isTag = + AppVeyor.Environment.Repository.Tag.IsTag && + !string.IsNullOrWhiteSpace(AppVeyor.Environment.Repository.Tag.Name); + + if (!isTag) + { + throw new Exception("Stable release should happen from a published GitHub release"); + } + + Information("Release is stable..."); + }); + +Task("UpdateVersionInfo") + .IsDependentOn("EnsureStableReleaseRequirements") + .Does(() => + { + releaseTag = AppVeyor.Environment.Repository.Tag.Name; + AppVeyor.UpdateBuildVersion(releaseTag); + }); + +Task("DownloadGitHubReleaseArtifacts") + .IsDependentOn("UpdateVersionInfo") + .Does(() => + { + try + { + Information("DownloadGitHubReleaseArtifacts"); + + EnsureDirectoryExists(packagesDir); + + Information("Directory exists..."); + + var releaseUrl = tagsUrl + releaseTag; + + Information("Release url " + releaseUrl); + + //var releaseJson = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl)); + + var assets_url = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl)) + .GetValue("assets_url") + .Value(); + + Information("Assets url " + assets_url); + + var assets = GetResource(assets_url); + + Information("Assets " + assets_url); + + foreach(var asset in Newtonsoft.Json.JsonConvert.DeserializeObject(assets)) + { + Information("In the loop.."); + + var file = packagesDir + File(asset.Value("name")); + + Information("Downloading " + file); + + DownloadFile(asset.Value("browser_download_url"), file); + } + + Information("Out of the loop..."); + } + catch(Exception exception) + { + Information("There was an exception " + exception); + throw; + } + }); + +Task("ReleasePackagesToStableFeed") + .IsDependentOn("DownloadGitHubReleaseArtifacts") + .Does(() => + { + PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); + }); + +Task("Release") + .IsDependentOn("ReleasePackagesToStableFeed"); + +RunTarget(target); + +/// Gets nuique nuget version for this commit +private GitVersion GetNuGetVersionForCommit() +{ + GitVersion(new GitVersionSettings{ + UpdateAssemblyInfo = false, + OutputType = GitVersionOutput.BuildServer + }); + + return GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json }); +} + +/// Updates project version in all of our projects +private void PersistVersion(string committedVersion, string newVersion) +{ + Information(string.Format("We'll search all csproj files for {0} and replace with {1}...", committedVersion, newVersion)); + + var projectFiles = GetFiles("./**/*.csproj"); + + foreach(var projectFile in projectFiles) + { + var file = projectFile.ToString(); + + Information(string.Format("Updating {0}...", file)); + + var updatedProjectFile = System.IO.File.ReadAllText(file) + .Replace(committedVersion, newVersion); + + System.IO.File.WriteAllText(file, updatedProjectFile); + } +} + +/// generates release notes based on issues closed in GitHub since the last release +private void GenerateReleaseNotes(ConvertableFilePath file) +{ + if(!IsRunningOnWindows()) + { + Warning("We are not running on Windows so we cannot generate release notes."); + return; + } + + Information("Generating release notes at " + file); + + var releaseNotesExitCode = StartProcess( + @"tools/GitReleaseNotes/tools/gitreleasenotes.exe", + new ProcessSettings { Arguments = ". /o " + file }); + + if (string.IsNullOrEmpty(System.IO.File.ReadAllText(file))) + { + System.IO.File.WriteAllText(file, "No issues closed since last release"); + } + + if (releaseNotesExitCode != 0) + { + throw new Exception("Failed to generate release notes"); + } +} + +/// Publishes code and symbols packages to nuget feed, based on contents of artifacts file +private void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFilePath artifactsFile, string feedApiKey, string codeFeedUrl, string symbolFeedUrl) +{ + var artifacts = System.IO.File + .ReadAllLines(artifactsFile) + .Select(l => l.Split(':')) + .ToDictionary(v => v[0], v => v[1]); + + var codePackage = packagesDir + File(artifacts["nuget"]); + + Information("Pushing package " + codePackage); + + Information("Calling NuGetPush"); + + NuGetPush( + codePackage, + new NuGetPushSettings { + ApiKey = feedApiKey, + Source = codeFeedUrl + }); +} + +/// gets the resource from the specified url +private string GetResource(string url) +{ + try + { + Information("Getting resource from " + url); + + var assetsRequest = System.Net.WebRequest.CreateHttp(url); + assetsRequest.Method = "GET"; + assetsRequest.Accept = "application/vnd.github.v3+json"; + assetsRequest.UserAgent = "BuildScript"; + + using (var assetsResponse = assetsRequest.GetResponse()) + { + var assetsStream = assetsResponse.GetResponseStream(); + var assetsReader = new StreamReader(assetsStream); + var response = assetsReader.ReadToEnd(); + + Information("Response is " + response); + + return response; + } + } + catch(Exception exception) + { + Information("There was an exception " + exception); + throw; + } +} + +private bool ShouldPublishToUnstableFeed(string filter, string branchName) +{ + var regex = new System.Text.RegularExpressions.Regex(filter); + var publish = regex.IsMatch(branchName); + if (publish) + { + Information("Branch " + branchName + " will be published to the unstable feed"); + } + else + { + Information("Branch " + branchName + " will not be published to the unstable feed"); + } + return publish; +} diff --git a/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs index 97045bc8..76934434 100644 --- a/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs @@ -252,7 +252,7 @@ namespace Ocelot.Configuration.Creator return $"{nameof(CookieStickySessions)}:{fileReRoute.LoadBalancerOptions.Key}"; } - return $"{fileReRoute.UpstreamPathTemplate}|{string.Join(",", fileReRoute.UpstreamHttpMethod)}"; + return $"{fileReRoute.UpstreamPathTemplate}|{string.Join(",", fileReRoute.UpstreamHttpMethod)}|{string.Join(",", fileReRoute.DownstreamHostAndPorts.Select(x => $"{x.Host}:{x.Port}"))}"; } } } diff --git a/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs b/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs index 21fcfcda..28fec505 100644 --- a/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs +++ b/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs @@ -1,11 +1,11 @@ -using System.Threading.Tasks; -using Ocelot.Configuration.File; -using Ocelot.Responses; - -namespace Ocelot.Configuration.Setter -{ - public interface IFileConfigurationSetter - { - Task Set(FileConfiguration config); - } -} \ No newline at end of file +using System.Threading.Tasks; +using Ocelot.Configuration.File; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Setter +{ + public interface IFileConfigurationSetter + { + Task Set(FileConfiguration config); + } +} diff --git a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs index 2bc43cfe..d1fb295f 100644 --- a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs +++ b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs @@ -1,55 +1,55 @@ -using System.Collections.Generic; -using System.Linq; -using Ocelot.Configuration; -using Ocelot.Infrastructure; -using Ocelot.Infrastructure.Extensions; -using Ocelot.Middleware; -using Ocelot.Middleware.Multiplexer; -using Ocelot.Request.Middleware; -using Ocelot.Responses; - -namespace Ocelot.Headers -{ - public class HttpResponseHeaderReplacer : IHttpResponseHeaderReplacer - { - private readonly IPlaceholders _placeholders; - - public HttpResponseHeaderReplacer(IPlaceholders placeholders) - { - _placeholders = placeholders; - } - - public Response Replace(DownstreamResponse response, List fAndRs, DownstreamRequest request) - { - foreach (var f in fAndRs) - { - var dict = response.Headers.ToDictionary(x => x.Key); - - //if the response headers contain a matching find and replace - if(dict.TryGetValue(f.Key, out var values)) - { - //check to see if it is a placeholder in the find... - var placeholderValue = _placeholders.Get(f.Find, request); - - if(!placeholderValue.IsError) - { - //if it is we need to get the value of the placeholder - var replaced = values.Values.ToList()[f.Index].Replace(placeholderValue.Data, f.Replace.LastCharAsForwardSlash()); - - response.Headers.Remove(response.Headers.First(item => item.Key == f.Key)); - response.Headers.Add(new Header(f.Key, new List { replaced })); - } - else - { - var replaced = values.Values.ToList()[f.Index].Replace(f.Find, f.Replace); - - response.Headers.Remove(response.Headers.First(item => item.Key == f.Key)); - response.Headers.Add(new Header(f.Key, new List { replaced })); - } - } - } - - return new OkResponse(); - } - } -} +using System.Collections.Generic; +using System.Linq; +using Ocelot.Configuration; +using Ocelot.Infrastructure; +using Ocelot.Infrastructure.Extensions; +using Ocelot.Middleware; +using Ocelot.Middleware.Multiplexer; +using Ocelot.Request.Middleware; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public class HttpResponseHeaderReplacer : IHttpResponseHeaderReplacer + { + private readonly IPlaceholders _placeholders; + + public HttpResponseHeaderReplacer(IPlaceholders placeholders) + { + _placeholders = placeholders; + } + + public Response Replace(DownstreamResponse response, List fAndRs, DownstreamRequest request) + { + foreach (var f in fAndRs) + { + var dict = response.Headers.ToDictionary(x => x.Key); + + //if the response headers contain a matching find and replace + if(dict.TryGetValue(f.Key, out var values)) + { + //check to see if it is a placeholder in the find... + var placeholderValue = _placeholders.Get(f.Find, request); + + if(!placeholderValue.IsError) + { + //if it is we need to get the value of the placeholder + var replaced = values.Values.ToList()[f.Index].Replace(placeholderValue.Data, f.Replace.LastCharAsForwardSlash()); + + response.Headers.Remove(response.Headers.First(item => item.Key == f.Key)); + response.Headers.Add(new Header(f.Key, new List { replaced })); + } + else + { + var replaced = values.Values.ToList()[f.Index].Replace(f.Find, f.Replace); + + response.Headers.Remove(response.Headers.First(item => item.Key == f.Key)); + response.Headers.Add(new Header(f.Key, new List { replaced })); + } + } + } + + return new OkResponse(); + } + } +} diff --git a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs index 9dfe3d64..a870cc0f 100644 --- a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs +++ b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs @@ -1,48 +1,48 @@ -using System.Threading.Tasks; -using Ocelot.Logging; -using Ocelot.Middleware; - -namespace Ocelot.Headers.Middleware -{ - public class HttpHeadersTransformationMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly IHttpContextRequestHeaderReplacer _preReplacer; - private readonly IHttpResponseHeaderReplacer _postReplacer; - private readonly IAddHeadersToResponse _addHeadersToResponse; - private readonly IAddHeadersToRequest _addHeadersToRequest; - - public HttpHeadersTransformationMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IHttpContextRequestHeaderReplacer preReplacer, - IHttpResponseHeaderReplacer postReplacer, - IAddHeadersToResponse addHeadersToResponse, - IAddHeadersToRequest addHeadersToRequest) - :base(loggerFactory.CreateLogger()) - { - _addHeadersToResponse = addHeadersToResponse; - _addHeadersToRequest = addHeadersToRequest; - _next = next; - _postReplacer = postReplacer; - _preReplacer = preReplacer; - } - - public async Task Invoke(DownstreamContext context) - { - var preFAndRs = context.DownstreamReRoute.UpstreamHeadersFindAndReplace; - - //todo - this should be on httprequestmessage not httpcontext? - _preReplacer.Replace(context.HttpContext, preFAndRs); - - _addHeadersToRequest.SetHeadersOnDownstreamRequest(context.DownstreamReRoute.AddHeadersToUpstream, context.HttpContext); - - await _next.Invoke(context); - - var postFAndRs = context.DownstreamReRoute.DownstreamHeadersFindAndReplace; - - _postReplacer.Replace(context.DownstreamResponse, postFAndRs, context.DownstreamRequest); - - _addHeadersToResponse.Add(context.DownstreamReRoute.AddHeadersToDownstream, context.DownstreamResponse); - } - } -} +using System.Threading.Tasks; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.Headers.Middleware +{ + public class HttpHeadersTransformationMiddleware : OcelotMiddleware + { + private readonly OcelotRequestDelegate _next; + private readonly IHttpContextRequestHeaderReplacer _preReplacer; + private readonly IHttpResponseHeaderReplacer _postReplacer; + private readonly IAddHeadersToResponse _addHeadersToResponse; + private readonly IAddHeadersToRequest _addHeadersToRequest; + + public HttpHeadersTransformationMiddleware(OcelotRequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IHttpContextRequestHeaderReplacer preReplacer, + IHttpResponseHeaderReplacer postReplacer, + IAddHeadersToResponse addHeadersToResponse, + IAddHeadersToRequest addHeadersToRequest) + :base(loggerFactory.CreateLogger()) + { + _addHeadersToResponse = addHeadersToResponse; + _addHeadersToRequest = addHeadersToRequest; + _next = next; + _postReplacer = postReplacer; + _preReplacer = preReplacer; + } + + public async Task Invoke(DownstreamContext context) + { + var preFAndRs = context.DownstreamReRoute.UpstreamHeadersFindAndReplace; + + //todo - this should be on httprequestmessage not httpcontext? + _preReplacer.Replace(context.HttpContext, preFAndRs); + + _addHeadersToRequest.SetHeadersOnDownstreamRequest(context.DownstreamReRoute.AddHeadersToUpstream, context.HttpContext); + + await _next.Invoke(context); + + var postFAndRs = context.DownstreamReRoute.DownstreamHeadersFindAndReplace; + + _postReplacer.Replace(context.DownstreamResponse, postFAndRs, context.DownstreamRequest); + + _addHeadersToResponse.Add(context.DownstreamReRoute.AddHeadersToDownstream, context.DownstreamResponse); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/HeaderTests.cs b/test/Ocelot.AcceptanceTests/HeaderTests.cs index b964161a..fd8b2767 100644 --- a/test/Ocelot.AcceptanceTests/HeaderTests.cs +++ b/test/Ocelot.AcceptanceTests/HeaderTests.cs @@ -1,365 +1,412 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.AcceptanceTests -{ - public class HeaderTests : IDisposable - { - private IWebHost _builder; - private int _count; - private readonly Steps _steps; - - public HeaderTests() - { - _steps = new Steps(); - } - - [Fact] - public void should_transform_upstream_header() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51871, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - UpstreamHeaderTransform = new Dictionary - { - {"Laz", "D, GP"} - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51871", "/", 200, "Laz")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.GivenIAddAHeader("Laz", "D")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("GP")) - .BDDfy(); - } - - [Fact] - public void should_transform_downstream_header() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51871, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHeaderTransform = new Dictionary - { - {"Location", "http://www.bbc.co.uk/, http://ocelot.com/"} - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51871", "/", 200, "Location", "http://www.bbc.co.uk/")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://ocelot.com/")) - .BDDfy(); - } - - [Fact] - public void should_fix_issue_190() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 6773, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHeaderTransform = new Dictionary - { - {"Location", "http://localhost:6773, {BaseUrl}"} - }, - HttpHandlerOptions = new FileHttpHandlerOptions - { - AllowAutoRedirect = false - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) - .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://localhost:5000/pay/Receive")) - .BDDfy(); - } - - [Fact] - public void should_fix_issue_205() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 6773, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHeaderTransform = new Dictionary - { - {"Location", "{DownstreamBaseUrl}, {BaseUrl}"} - }, - HttpHandlerOptions = new FileHttpHandlerOptions - { - AllowAutoRedirect = false - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) - .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://localhost:5000/pay/Receive")) - .BDDfy(); - } - - [Fact] - public void request_should_reuse_cookies_with_cookie_container() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/sso/{everything}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 6774, - } - }, - UpstreamPathTemplate = "/sso/{everything}", - UpstreamHttpMethod = new List { "Get", "Post", "Options" }, - HttpHandlerOptions = new FileHttpHandlerOptions - { - UseCookieContainer = true - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6774", "/sso/test", 200)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) - .And(x => _steps.GivenIAddCookieToMyRequest("test=1; path=/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void request_should_have_own_cookies_no_cookie_container() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/sso/{everything}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 6775, - } - }, - UpstreamPathTemplate = "/sso/{everything}", - UpstreamHttpMethod = new List { "Get", "Post", "Options" }, - HttpHandlerOptions = new FileHttpHandlerOptions - { - UseCookieContainer = false - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6775", "/sso/test", 200)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) - .And(x => _steps.GivenIAddCookieToMyRequest("test=1; path=/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode) - { - _builder = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .Configure(app => - { - app.UsePathBase(basePath); - app.Run(context => - { - if (_count == 0) - { - context.Response.Cookies.Append("test", "0"); - _count++; - context.Response.StatusCode = statusCode; - return Task.CompletedTask; - } - - if (context.Request.Cookies.TryGetValue("test", out var cookieValue) || context.Request.Headers.TryGetValue("Set-Cookie", out var headerValue)) - { - if (cookieValue == "0" || headerValue == "test=1; path=/") - { - context.Response.StatusCode = statusCode; - return Task.CompletedTask; - } - } - - context.Response.StatusCode = 500; - return Task.CompletedTask; - }); - }) - .Build(); - - _builder.Start(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey) - { - _builder = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .Configure(app => - { - app.UsePathBase(basePath); - app.Run(async context => - { - if(context.Request.Headers.TryGetValue(headerKey, out var values)) - { - var result = values.First(); - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(result); - } - }); - }) - .Build(); - - _builder.Start(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey, string headerValue) - { - _builder = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .Configure(app => - { - app.UsePathBase(basePath); - app.Run(context => - { - context.Response.OnStarting(() => { - context.Response.Headers.Add(headerKey, headerValue); - context.Response.StatusCode = statusCode; - return Task.CompletedTask; - }); - - return Task.CompletedTask; - }); - }) - .Build(); - - _builder.Start(); - } - - public void Dispose() - { - _builder?.Dispose(); - _steps.Dispose(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class HeaderTests : IDisposable + { + private IWebHost _builder; + private int _count; + private readonly Steps _steps; + + public HeaderTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_transform_upstream_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51871, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + UpstreamHeaderTransform = new Dictionary + { + {"Laz", "D, GP"} + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51871", "/", 200, "Laz")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenIAddAHeader("Laz", "D")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("GP")) + .BDDfy(); + } + + [Fact] + public void should_transform_downstream_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51871, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/, http://ocelot.com/"} + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51871", "/", 200, "Location", "http://www.bbc.co.uk/")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://ocelot.com/")) + .BDDfy(); + } + + [Fact] + public void should_fix_issue_190() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6773, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://localhost:6773, {BaseUrl}"} + }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + AllowAutoRedirect = false + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) + .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://localhost:5000/pay/Receive")) + .BDDfy(); + } + + [Fact] + public void should_fix_issue_205() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6773, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHeaderTransform = new Dictionary + { + {"Location", "{DownstreamBaseUrl}, {BaseUrl}"} + }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + AllowAutoRedirect = false + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) + .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://localhost:5000/pay/Receive")) + .BDDfy(); + } + + [Fact] + public void should_fix_issue_417() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6773, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHeaderTransform = new Dictionary + { + {"Location", "{DownstreamBaseUrl}, {BaseUrl}"} + }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + AllowAutoRedirect = false + } + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + BaseUrl = "http://anotherapp.azurewebsites.net" + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) + .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://anotherapp.azurewebsites.net/pay/Receive")) + .BDDfy(); + } + + [Fact] + public void request_should_reuse_cookies_with_cookie_container() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/sso/{everything}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6774, + } + }, + UpstreamPathTemplate = "/sso/{everything}", + UpstreamHttpMethod = new List { "Get", "Post", "Options" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseCookieContainer = true + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6774", "/sso/test", 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) + .And(x => _steps.GivenIAddCookieToMyRequest("test=1; path=/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void request_should_have_own_cookies_no_cookie_container() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/sso/{everything}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6775, + } + }, + UpstreamPathTemplate = "/sso/{everything}", + UpstreamHttpMethod = new List { "Get", "Post", "Options" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseCookieContainer = false + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6775", "/sso/test", 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) + .And(x => _steps.GivenIAddCookieToMyRequest("test=1; path=/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(context => + { + if (_count == 0) + { + context.Response.Cookies.Append("test", "0"); + _count++; + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + } + + if (context.Request.Cookies.TryGetValue("test", out var cookieValue) || context.Request.Headers.TryGetValue("Set-Cookie", out var headerValue)) + { + if (cookieValue == "0" || headerValue == "test=1; path=/") + { + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + } + } + + context.Response.StatusCode = 500; + return Task.CompletedTask; + }); + }) + .Build(); + + _builder.Start(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + if (context.Request.Headers.TryGetValue(headerKey, out var values)) + { + var result = values.First(); + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(result); + } + }); + }) + .Build(); + + _builder.Start(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey, string headerValue) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(context => + { + context.Response.OnStarting(() => + { + context.Response.Headers.Add(headerKey, headerValue); + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + }); + + return Task.CompletedTask; + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 84b0c0bb..44a89929 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -1,727 +1,852 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Security.Claims; -using CacheManager.Core; -using IdentityServer4.AccessTokenValidation; -using IdentityServer4.Models; -using IdentityServer4.Test; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Ocelot.Cache; -using Ocelot.Configuration.File; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -[assembly: CollectionBehavior(DisableTestParallelization = true)] -namespace Ocelot.IntegrationTests -{ - public class AdministrationTests : IDisposable - { - private HttpClient _httpClient; - private readonly HttpClient _httpClientTwo; - private HttpResponseMessage _response; - private IWebHost _builder; - private IWebHostBuilder _webHostBuilder; - private string _ocelotBaseUrl; - private BearerToken _token; - private IWebHostBuilder _webHostBuilderTwo; - private IWebHost _builderTwo; - private IWebHost _identityServerBuilder; - - public AdministrationTests() - { - _httpClient = new HttpClient(); - _httpClientTwo = new HttpClient(); - _ocelotBaseUrl = "http://localhost:5000"; - _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); - } - - [Fact] - public void should_return_response_401_with_call_re_routes_controller() - { - var configuration = new FileConfiguration(); - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunning()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_call_re_routes_controller() - { - var configuration = new FileConfiguration(); - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_call_re_routes_controller_using_base_url_added_in_file_config() - { - _httpClient = new HttpClient(); - _ocelotBaseUrl = "http://localhost:5011"; - _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - BaseUrl = _ocelotBaseUrl - } - }; - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunningWithNoWebHostBuilder(_ocelotBaseUrl)) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void should_be_able_to_use_token_from_ocelot_a_on_ocelot_b() - { - var configuration = new FileConfiguration(); - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenIdentityServerSigningEnvironmentalVariablesAreSet()) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenAnotherOcelotIsRunning("http://localhost:5007")) - .When(x => WhenIGetUrlOnTheSecondOcelot("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void should_return_file_configuration() - { - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - RequestIdKey = "RequestId", - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "127.0.0.1", - } - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/", - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 10, - Region = "Geoff" - } - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/test", - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 10, - Region = "Dave" - } - } - } - }; - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseShouldBe(configuration)) - .BDDfy(); - } - - [Fact] - public void should_get_file_configuration_edit_and_post_updated_version() - { - var initialConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/" - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/test" - } - } - }; - - var updatedConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "http", - DownstreamPathTemplate = "/geoffrey", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/" - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "123.123.123", - Port = 443, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/blooper/{productId}", - UpstreamHttpMethod = new List { "post" }, - UpstreamPathTemplate = "/test" - } - } - }; - - this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .When(x => WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration)) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseShouldBe(updatedConfiguration)) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .And(x => ThenTheResponseShouldBe(updatedConfiguration)) - .BDDfy(); - } - - [Fact] - public void should_clear_region() - { - var initialConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/", - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 10 - } - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/test", - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 10 - } - } - } - }; - - var regionToClear = "gettest"; - - this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIDeleteOnTheApiGateway($"/administration/outputcache/{regionToClear}")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NoContent)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_call_re_routes_controller_when_using_own_identity_server_to_secure_admin_area() - { - var configuration = new FileConfiguration(); - - var identityServerRootUrl = "http://localhost:5123"; - - Action options = o => { - o.Authority = identityServerRootUrl; - o.ApiName = "api"; - o.RequireHttpsMetadata = false; - o.SupportedTokens = SupportedTokens.Both; - o.ApiSecret = "secret"; - }; - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenThereIsAnIdentityServerOn(identityServerRootUrl, "api")) - .And(x => GivenOcelotIsRunningWithIdentityServerSettings(options)) - .And(x => GivenIHaveAToken(identityServerRootUrl)) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - private void GivenIHaveAToken(string url) - { - var formData = new List> - { - new KeyValuePair("client_id", "api"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "api"), - new KeyValuePair("username", "test"), - new KeyValuePair("password", "test"), - new KeyValuePair("grant_type", "password") - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) - { - var response = httpClient.PostAsync($"{url}/connect/token", content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - } - - private void GivenThereIsAnIdentityServerOn(string url, string apiName) - { - _identityServerBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureServices(services => - { - services.AddLogging(); - services.AddIdentityServer() - .AddDeveloperSigningCredential() - .AddInMemoryApiResources(new List - { - new ApiResource - { - Name = apiName, - Description = apiName, - Enabled = true, - DisplayName = apiName, - Scopes = new List() - { - new Scope(apiName) - } - } - }) - .AddInMemoryClients(new List - { - new Client - { - ClientId = apiName, - AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, - ClientSecrets = new List {new Secret("secret".Sha256())}, - AllowedScopes = new List { apiName }, - AccessTokenType = AccessTokenType.Jwt, - Enabled = true - } - }) - .AddTestUsers(new List - { - new TestUser - { - Username = "test", - Password = "test", - SubjectId = "1231231" - } - }); - }) - .Configure(app => - { - app.UseIdentityServer(); - }) - .Build(); - - _identityServerBuilder.Start(); - - using (var httpClient = new HttpClient()) - { - var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; - response.EnsureSuccessStatusCode(); - } - } - - private void GivenAnotherOcelotIsRunning(string baseUrl) - { - _httpClientTwo.BaseAddress = new Uri(baseUrl); - - _webHostBuilderTwo = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => - { - Action settings = (s) => - { - s.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithDictionaryHandle(); - }; - - x.AddOcelot() - .AddCacheManager(settings) - .AddAdministration("/administration", "secret"); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _builderTwo = _webHostBuilderTwo.Build(); - - _builderTwo.Start(); - } - - private void GivenIdentityServerSigningEnvironmentalVariablesAreSet() - { - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", "idsrv3test.pfx"); - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", "idsrv3test"); - } - - private void WhenIGetUrlOnTheSecondOcelot(string url) - { - _httpClientTwo.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - _response = _httpClientTwo.GetAsync(url).Result; - } - - private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) - { - var json = JsonConvert.SerializeObject(updatedConfiguration); - var content = new StringContent(json); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - _response = _httpClient.PostAsync(url, content).Result; - } - - private void ThenTheResponseShouldBe(List expected) - { - var content = _response.Content.ReadAsStringAsync().Result; - var result = JsonConvert.DeserializeObject(content); - result.Value.ShouldBe(expected); - } - - private void ThenTheResponseShouldBe(FileConfiguration expecteds) - { - var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); - - response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); - response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); - response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); - - for (var i = 0; i < response.ReRoutes.Count; i++) - { - for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) - { - var result = response.ReRoutes[i].DownstreamHostAndPorts[j]; - var expected = expecteds.ReRoutes[i].DownstreamHostAndPorts[j]; - result.Host.ShouldBe(expected.Host); - result.Port.ShouldBe(expected.Port); - } - - response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].DownstreamPathTemplate); - response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.ReRoutes[i].DownstreamScheme); - response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].UpstreamPathTemplate); - response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.ReRoutes[i].UpstreamHttpMethod); - } - } - - private void GivenIHaveAddedATokenToMyRequest() - { - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - } - - private void GivenIHaveAnOcelotToken(string adminPath) - { - var tokenUrl = $"{adminPath}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "admin"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "admin"), - new KeyValuePair("grant_type", "client_credentials") - }; - var content = new FormUrlEncodedContent(formData); - - var response = _httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - var configPath = $"{adminPath}/.well-known/openid-configuration"; - response = _httpClient.GetAsync(configPath).Result; - response.EnsureSuccessStatusCode(); - } - - private void GivenOcelotIsRunningWithIdentityServerSettings(Action configOptions) - { - _webHostBuilder = new WebHostBuilder() - .UseUrls(_ocelotBaseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => { - x.AddSingleton(_webHostBuilder); - x.AddOcelot() - .AddCacheManager(c => - { - c.WithDictionaryHandle(); - }) - .AddAdministration("/administration", configOptions); - }) - .Configure(app => { - app.UseOcelot().Wait(); - }); - - _builder = _webHostBuilder.Build(); - - _builder.Start(); - } - - private void GivenOcelotIsRunning() - { - _webHostBuilder = new WebHostBuilder() - .UseUrls(_ocelotBaseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => - { - Action settings = (s) => - { - s.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithDictionaryHandle(); - }; - - x.AddOcelot() - .AddCacheManager(settings) - .AddAdministration("/administration", "secret"); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _builder = _webHostBuilder.Build(); - - _builder.Start(); - } - - private void GivenOcelotIsRunningWithNoWebHostBuilder(string baseUrl) - { - _webHostBuilder = new WebHostBuilder() - .UseUrls(_ocelotBaseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => { - x.AddSingleton(_webHostBuilder); - x.AddOcelot() - .AddCacheManager(c => - { - c.WithDictionaryHandle(); - }) - .AddAdministration("/administration", "secret"); - }) - .Configure(app => { - app.UseOcelot().Wait(); - }); - - _builder = _webHostBuilder.Build(); - - _builder.Start(); - } - - private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) - { - var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; - - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - var text = File.ReadAllText(configurationPath); - - configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - text = File.ReadAllText(configurationPath); - } - - private void WhenIGetUrlOnTheApiGateway(string url) - { - _response = _httpClient.GetAsync(url).Result; - } - - private void WhenIDeleteOnTheApiGateway(string url) - { - _response = _httpClient.DeleteAsync(url).Result; - } - - private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) - { - _response.StatusCode.ShouldBe(expectedHttpStatusCode); - } - - public void Dispose() - { - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", ""); - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", ""); - _builder?.Dispose(); - _httpClient?.Dispose(); - _identityServerBuilder?.Dispose(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using CacheManager.Core; +using IdentityServer4.AccessTokenValidation; +using IdentityServer4.Models; +using IdentityServer4.Test; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.Cache; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] +namespace Ocelot.IntegrationTests +{ + public class AdministrationTests : IDisposable + { + private HttpClient _httpClient; + private readonly HttpClient _httpClientTwo; + private HttpResponseMessage _response; + private IWebHost _builder; + private IWebHostBuilder _webHostBuilder; + private string _ocelotBaseUrl; + private BearerToken _token; + private IWebHostBuilder _webHostBuilderTwo; + private IWebHost _builderTwo; + private IWebHost _identityServerBuilder; + private IWebHost _fooServiceBuilder; + private IWebHost _barServiceBuilder; + + public AdministrationTests() + { + _httpClient = new HttpClient(); + _httpClientTwo = new HttpClient(); + _ocelotBaseUrl = "http://localhost:5000"; + _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); + } + + [Fact] + public void should_return_response_401_with_call_re_routes_controller() + { + var configuration = new FileConfiguration(); + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_call_re_routes_controller() + { + var configuration = new FileConfiguration(); + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_call_re_routes_controller_using_base_url_added_in_file_config() + { + _httpClient = new HttpClient(); + _ocelotBaseUrl = "http://localhost:5011"; + _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + BaseUrl = _ocelotBaseUrl + } + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithNoWebHostBuilder(_ocelotBaseUrl)) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_be_able_to_use_token_from_ocelot_a_on_ocelot_b() + { + var configuration = new FileConfiguration(); + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenIdentityServerSigningEnvironmentalVariablesAreSet()) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenAnotherOcelotIsRunning("http://localhost:5007")) + .When(x => WhenIGetUrlOnTheSecondOcelot("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_file_configuration() + { + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + RequestIdKey = "RequestId", + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "127.0.0.1", + } + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10, + Region = "Geoff" + } + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/test", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10, + Region = "Dave" + } + } + } + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(configuration)) + .BDDfy(); + } + + [Fact] + public void should_get_file_configuration_edit_and_post_updated_version() + { + var initialConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/test" + } + } + }; + + var updatedConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "123.123.123", + Port = 443, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "post" }, + UpstreamPathTemplate = "/test" + } + } + }; + + this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .When(x => WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(updatedConfiguration)) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .And(x => ThenTheResponseShouldBe(updatedConfiguration)) + .BDDfy(); + } + + [Fact] + public void should_get_file_configuration_edit_and_post_updated_version_redirecting_reroute() + { + var fooPort = 47689; + var barPort = 47690; + + var initialConfiguration = new FileConfiguration + { + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = fooPort, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/foo", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/foo" + } + } + }; + + var updatedConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = barPort, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/bar", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/foo" + } + } + }; + + this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) + .And(x => GivenThereIsAFooServiceRunningOn($"http://localhost:{fooPort}")) + .And(x => GivenThereIsABarServiceRunningOn($"http://localhost:{barPort}")) + .And(x => GivenOcelotIsRunning()) + .And(x => WhenIGetUrlOnTheApiGateway("/foo")) + .Then(x => ThenTheResponseBodyShouldBe("foo")) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(updatedConfiguration)) + .And(x => WhenIGetUrlOnTheApiGateway("/foo")) + .Then(x => ThenTheResponseBodyShouldBe("bar")) + .When(x => WhenIPostOnTheApiGateway("/administration/configuration", initialConfiguration)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(initialConfiguration)) + .And(x => WhenIGetUrlOnTheApiGateway("/foo")) + .Then(x => ThenTheResponseBodyShouldBe("foo")) + .BDDfy(); + } + + [Fact] + public void should_clear_region() + { + var initialConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10 + } + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/test", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10 + } + } + } + }; + + var regionToClear = "gettest"; + + this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIDeleteOnTheApiGateway($"/administration/outputcache/{regionToClear}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NoContent)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_call_re_routes_controller_when_using_own_identity_server_to_secure_admin_area() + { + var configuration = new FileConfiguration(); + + var identityServerRootUrl = "http://localhost:5123"; + + Action options = o => { + o.Authority = identityServerRootUrl; + o.ApiName = "api"; + o.RequireHttpsMetadata = false; + o.SupportedTokens = SupportedTokens.Both; + o.ApiSecret = "secret"; + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenThereIsAnIdentityServerOn(identityServerRootUrl, "api")) + .And(x => GivenOcelotIsRunningWithIdentityServerSettings(options)) + .And(x => GivenIHaveAToken(identityServerRootUrl)) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private void GivenIHaveAToken(string url) + { + var formData = new List> + { + new KeyValuePair("client_id", "api"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "api"), + new KeyValuePair("username", "test"), + new KeyValuePair("password", "test"), + new KeyValuePair("grant_type", "password") + }; + var content = new FormUrlEncodedContent(formData); + + using (var httpClient = new HttpClient()) + { + var response = httpClient.PostAsync($"{url}/connect/token", content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } + } + + private void GivenThereIsAnIdentityServerOn(string url, string apiName) + { + _identityServerBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureServices(services => + { + services.AddLogging(); + services.AddIdentityServer() + .AddDeveloperSigningCredential() + .AddInMemoryApiResources(new List + { + new ApiResource + { + Name = apiName, + Description = apiName, + Enabled = true, + DisplayName = apiName, + Scopes = new List() + { + new Scope(apiName) + } + } + }) + .AddInMemoryClients(new List + { + new Client + { + ClientId = apiName, + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new Secret("secret".Sha256())}, + AllowedScopes = new List { apiName }, + AccessTokenType = AccessTokenType.Jwt, + Enabled = true + } + }) + .AddTestUsers(new List + { + new TestUser + { + Username = "test", + Password = "test", + SubjectId = "1231231" + } + }); + }) + .Configure(app => + { + app.UseIdentityServer(); + }) + .Build(); + + _identityServerBuilder.Start(); + + using (var httpClient = new HttpClient()) + { + var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; + response.EnsureSuccessStatusCode(); + } + } + + private void GivenAnotherOcelotIsRunning(string baseUrl) + { + _httpClientTwo.BaseAddress = new Uri(baseUrl); + + _webHostBuilderTwo = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => + { + Action settings = (s) => + { + s.WithMicrosoftLogging(log => + { + log.AddConsole(LogLevel.Debug); + }) + .WithDictionaryHandle(); + }; + + x.AddOcelot() + .AddCacheManager(settings) + .AddAdministration("/administration", "secret"); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + _builderTwo = _webHostBuilderTwo.Build(); + + _builderTwo.Start(); + } + + private void GivenIdentityServerSigningEnvironmentalVariablesAreSet() + { + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", "idsrv3test.pfx"); + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", "idsrv3test"); + } + + private void WhenIGetUrlOnTheSecondOcelot(string url) + { + _httpClientTwo.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + _response = _httpClientTwo.GetAsync(url).Result; + } + + private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) + { + var json = JsonConvert.SerializeObject(updatedConfiguration); + var content = new StringContent(json); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + _response = _httpClient.PostAsync(url, content).Result; + } + + private void ThenTheResponseShouldBe(List expected) + { + var content = _response.Content.ReadAsStringAsync().Result; + var result = JsonConvert.DeserializeObject(content); + result.Value.ShouldBe(expected); + } + + private void ThenTheResponseBodyShouldBe(string expected) + { + var content = _response.Content.ReadAsStringAsync().Result; + content.ShouldBe(expected); + } + + private void ThenTheResponseShouldBe(FileConfiguration expecteds) + { + var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); + + response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); + response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); + response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); + + for (var i = 0; i < response.ReRoutes.Count; i++) + { + for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) + { + var result = response.ReRoutes[i].DownstreamHostAndPorts[j]; + var expected = expecteds.ReRoutes[i].DownstreamHostAndPorts[j]; + result.Host.ShouldBe(expected.Host); + result.Port.ShouldBe(expected.Port); + } + + response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].DownstreamPathTemplate); + response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.ReRoutes[i].DownstreamScheme); + response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].UpstreamPathTemplate); + response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.ReRoutes[i].UpstreamHttpMethod); + } + } + + private void GivenIHaveAddedATokenToMyRequest() + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } + + private void GivenIHaveAnOcelotToken(string adminPath) + { + var tokenUrl = $"{adminPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "admin"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "admin"), + new KeyValuePair("grant_type", "client_credentials") + }; + var content = new FormUrlEncodedContent(formData); + + var response = _httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + var configPath = $"{adminPath}/.well-known/openid-configuration"; + response = _httpClient.GetAsync(configPath).Result; + response.EnsureSuccessStatusCode(); + } + + private void GivenOcelotIsRunningWithIdentityServerSettings(Action configOptions) + { + _webHostBuilder = new WebHostBuilder() + .UseUrls(_ocelotBaseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => { + x.AddSingleton(_webHostBuilder); + x.AddOcelot() + .AddCacheManager(c => + { + c.WithDictionaryHandle(); + }) + .AddAdministration("/administration", configOptions); + }) + .Configure(app => { + app.UseOcelot().Wait(); + }); + + _builder = _webHostBuilder.Build(); + + _builder.Start(); + } + + private void GivenOcelotIsRunning() + { + _webHostBuilder = new WebHostBuilder() + .UseUrls(_ocelotBaseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => + { + Action settings = (s) => + { + s.WithMicrosoftLogging(log => + { + log.AddConsole(LogLevel.Debug); + }) + .WithDictionaryHandle(); + }; + + x.AddOcelot() + .AddCacheManager(settings) + .AddAdministration("/administration", "secret"); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + _builder = _webHostBuilder.Build(); + + _builder.Start(); + } + + private void GivenOcelotIsRunningWithNoWebHostBuilder(string baseUrl) + { + _webHostBuilder = new WebHostBuilder() + .UseUrls(_ocelotBaseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => { + x.AddSingleton(_webHostBuilder); + x.AddOcelot() + .AddCacheManager(c => + { + c.WithDictionaryHandle(); + }) + .AddAdministration("/administration", "secret"); + }) + .Configure(app => { + app.UseOcelot().Wait(); + }); + + _builder = _webHostBuilder.Build(); + + _builder.Start(); + } + + private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + var text = File.ReadAllText(configurationPath); + + configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + text = File.ReadAllText(configurationPath); + } + + private void WhenIGetUrlOnTheApiGateway(string url) + { + _response = _httpClient.GetAsync(url).Result; + } + + private void WhenIDeleteOnTheApiGateway(string url) + { + _response = _httpClient.DeleteAsync(url).Result; + } + + private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) + { + _response.StatusCode.ShouldBe(expectedHttpStatusCode); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", ""); + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", ""); + _builder?.Dispose(); + _httpClient?.Dispose(); + _identityServerBuilder?.Dispose(); + } + + private void GivenThereIsAFooServiceRunningOn(string baseUrl) + { + _fooServiceBuilder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase("/foo"); + app.Run(async context => + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync("foo"); + }); + }) + .Build(); + + _fooServiceBuilder.Start(); + } + + private void GivenThereIsABarServiceRunningOn(string baseUrl) + { + _barServiceBuilder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase("/bar"); + app.Run(async context => + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync("bar"); + }); + }) + .Build(); + + _barServiceBuilder.Start(); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs index 459976f3..79111e2e 100644 --- a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs @@ -199,7 +199,7 @@ .WithDownstreamScheme("http") .WithUpstreamHttpMethod(new List() {"Get"}) .WithDownstreamAddresses(new List() {new DownstreamHostAndPort("localhost", 51878)}) - .WithLoadBalancerKey("/laura|Get") + .WithLoadBalancerKey("/laura|Get|localhost:51878") .Build(); var lauraReRoute = new ReRouteBuilder() @@ -218,7 +218,7 @@ .WithDownstreamScheme("http") .WithUpstreamHttpMethod(new List() { "Get" }) .WithDownstreamAddresses(new List() { new DownstreamHostAndPort("localhost", 51878) }) - .WithLoadBalancerKey("/tom|Get") + .WithLoadBalancerKey("/tom|Get|localhost:51880") .Build(); var tomReRoute = new ReRouteBuilder() @@ -409,7 +409,7 @@ .WithDownstreamPathTemplate("/products/{productId}") .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod(new List {"Get"}) - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|127.0.0.1:0") .Build(); this.Given(x => x.GivenTheConfigIs(new FileConfiguration @@ -461,7 +461,7 @@ .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod(new List {"Get"}) .WithDelegatingHandlers(handlers) - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); this.Given(x => x.GivenTheConfigIs(new FileConfiguration @@ -506,7 +506,7 @@ .WithUpstreamHttpMethod(new List {"Get"}) .WithUseServiceDiscovery(true) .WithServiceName("ProductService") - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); this.Given(x => x.GivenTheConfigIs(new FileConfiguration @@ -557,7 +557,7 @@ .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod(new List {"Get"}) .WithUseServiceDiscovery(false) - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); this.Given(x => x.GivenTheConfigIs(new FileConfiguration @@ -600,7 +600,7 @@ .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod(new List {"Get"}) .WithUpstreamTemplatePattern(new UpstreamPathTemplate("(?i)/api/products/.*/$", 1)) - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); this.Given(x => x.GivenTheConfigIs(new FileConfiguration @@ -645,7 +645,7 @@ .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod(new List {"Get"}) .WithRequestIdKey("blahhhh") - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); this.Given(x => x.GivenTheConfigIs(new FileConfiguration @@ -740,7 +740,7 @@ { new ClaimToThing("CustomerId", "CustomerId", "", 0), }) - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); var expected = new List @@ -783,7 +783,7 @@ .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod(new List {"Get"}) .WithAuthenticationOptions(authenticationOptions) - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); var expected = new List From b60d26e1c61db5a67c200bfcd32fef0819d5b621 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Fri, 22 Jun 2018 07:01:10 +0100 Subject: [PATCH 23/24] Feature/few tweaks (#422) * #419 Incorrect routing when UpstreamHost is specified and UpstreamHttpMethod is empty * few tweaks to make seperate test as I got confused having one...there is too much setup in these tests * added another test case for route matching * set cake coveralls addin to v0.0.7 so build works... --- .../DownstreamRouteFinderTests.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index b1ea2c81..b99bc865 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -607,6 +607,71 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .BDDfy(); } + [Fact] + public void should_not_return_route_when_host_doesnt_match_with_empty_upstream_http_method() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) + .And(x => GivenTheUpstreamHostIs("DONTMATCH")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamPathTemplate("someUpstreamPath") + .WithUpstreamHttpMethod(new List()) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("someUpstreamPath", 1)) + .WithUpstreamHost("MATCH") + .Build()) + .WithUpstreamPathTemplate("someUpstreamPath") + .WithUpstreamHttpMethod(new List()) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("someUpstreamPath", 1)) + .WithUpstreamHost("MATCH") + .Build() + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then(x => x.ThenAnErrorResponseIsReturned()) + .And(x => x.ThenTheUrlMatcherIsNotCalled()) + .BDDfy(); + } + + [Fact] + public void should_return_route_when_host_does_match_with_empty_upstream_http_method() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) + .And(x => GivenTheUpstreamHostIs("MATCH")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamPathTemplate("someUpstreamPath") + .WithUpstreamHttpMethod(new List()) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("someUpstreamPath", 1)) + .WithUpstreamHost("MATCH") + .Build()) + .WithUpstreamPathTemplate("someUpstreamPath") + .WithUpstreamHttpMethod(new List()) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("someUpstreamPath", 1)) + .WithUpstreamHost("MATCH") + .Build() + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1)) + .BDDfy(); + } + [Fact] public void should_return_route_when_host_matches_but_null_host_on_same_path_first() { From 9db4273f18e93d442bdd40ea5a7b798ccff72bcf Mon Sep 17 00:00:00 2001 From: Marco Antonio Araujo Date: Fri, 22 Jun 2018 16:35:21 +0100 Subject: [PATCH 24/24] Fix catch all route on UpstreamTemplatePatternCreator regex to match everything (#407) (#411) --- .../Creator/UpstreamTemplatePatternCreator.cs | 4 ++-- .../UpstreamTemplatePatternCreatorTests.cs | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs b/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs index dbf6a2d1..27328213 100644 --- a/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs +++ b/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs @@ -6,7 +6,7 @@ namespace Ocelot.Configuration.Creator { public class UpstreamTemplatePatternCreator : IUpstreamTemplatePatternCreator { - private const string RegExMatchEverything = "[0-9a-zA-Z].*"; + private const string RegExMatchOneOrMoreOfEverything = ".+"; private const string RegExMatchEndString = "$"; private const string RegExIgnoreCase = "(?i)"; private const string RegExForwardSlashOnly = "^/$"; @@ -37,7 +37,7 @@ namespace Ocelot.Configuration.Creator foreach (var placeholder in placeholders) { - upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchEverything); + upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchOneOrMoreOfEverything); } if (upstreamTemplate == "/") diff --git a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs index c70bd440..36986932 100644 --- a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs @@ -1,4 +1,3 @@ -using System; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Values; @@ -30,7 +29,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^(?i)/orders/[0-9a-zA-Z].*$")) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/orders/.+$")) .And(x => ThenThePriorityIs(0)) .BDDfy(); } @@ -62,7 +61,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^(?i)/PRODUCTS/[0-9a-zA-Z].*$")) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/PRODUCTS/.+$")) .And(x => ThenThePriorityIs(1)) .BDDfy(); } @@ -93,7 +92,7 @@ namespace Ocelot.UnitTests.Configuration }; this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/PRODUCTS/[0-9a-zA-Z].*$")) + .Then(x => x.ThenTheFollowingIsReturned("^/PRODUCTS/.+$")) .And(x => ThenThePriorityIs(1)) .BDDfy(); } @@ -109,7 +108,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/api/products/[0-9a-zA-Z].*$")) + .Then(x => x.ThenTheFollowingIsReturned("^/api/products/.+$")) .And(x => ThenThePriorityIs(1)) .BDDfy(); } @@ -125,7 +124,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/api/products/[0-9a-zA-Z].*/variants/[0-9a-zA-Z].*$")) + .Then(x => x.ThenTheFollowingIsReturned("^/api/products/.+/variants/.+$")) .And(x => ThenThePriorityIs(1)) .BDDfy(); } @@ -141,7 +140,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/api/products/[0-9a-zA-Z].*/variants/[0-9a-zA-Z].*(/|)$")) + .Then(x => x.ThenTheFollowingIsReturned("^/api/products/.+/variants/.+(/|)$")) .And(x => ThenThePriorityIs(1)) .BDDfy(); } @@ -187,7 +186,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/[0-9a-zA-Z].*/products/variants/[0-9a-zA-Z].*(/|)$")) + .Then(x => x.ThenTheFollowingIsReturned("^/.+/products/variants/.+(/|)$")) .And(x => ThenThePriorityIs(1)) .BDDfy(); }