From e636cefdb1a2e2c6a703b4355f74d6de94c9d5c7 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Wed, 20 Jun 2018 20:44:38 +0100 Subject: [PATCH] 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 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 V%$88{h~I%@Xjg7x^oQR@_8&Ry9S;Bi literal 0 HcmV?d00001