Merge branch 'pitming-feature/MethodTransformer'

This commit is contained in:
TomPallister 2020-02-09 16:55:58 +00:00
commit 689f04d011
17 changed files with 293 additions and 27 deletions

View File

@ -24,6 +24,7 @@ Here is an example ReRoute configuration, You don't need to set all of these thi
"UpstreamHttpMethod": [ "UpstreamHttpMethod": [
"Get" "Get"
], ],
"DownstreamHttpMethod": "",
"AddHeadersToRequest": {}, "AddHeadersToRequest": {},
"AddClaimsToRequest": {}, "AddClaimsToRequest": {},
"RouteClaimsRequirement": {}, "RouteClaimsRequirement": {},

View File

@ -0,0 +1,28 @@
HTTP Method Transformation
==========================
Ocelot allows the user to change the HTTP request method that will be used when making a request to a downstream service.
This achieved by setting the following ReRoute configuration:
.. code-block:: json
{
"DownstreamPathTemplate": "/{url}",
"UpstreamPathTemplate": "/{url}",
"UpstreamHttpMethod": [
"Get"
],
"DownstreamHttpMethod": "POST",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 53271
}
],
}
The key property here is DownstreamHttpMethod which is set as POST and the ReRoute will only match on GET as set by UpstreamHttpMethod.
This feature can be useful when interacting with downstream apis that only support POST and you want to present some kind of RESTful interface.

View File

@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Ocelot.DependencyInjection; using Ocelot.DependencyInjection;
using Ocelot.Middleware; using Ocelot.Middleware;
using Ocelot.Provider.Kubernetes; using Ocelot.Provider.Kubernetes;
@ -18,7 +19,7 @@ namespace ApiGateway
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env) public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{ {
if (env.IsDevelopment()) if (env.IsDevelopment())
{ {

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -28,7 +29,7 @@ namespace DownstreamService
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env) public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{ {
if (env.IsDevelopment()) if (env.IsDevelopment())
{ {

View File

@ -41,6 +41,7 @@ namespace Ocelot.Configuration.Builder
private List<AddHeader> _addHeadersToUpstream; private List<AddHeader> _addHeadersToUpstream;
private bool _dangerousAcceptAnyServerCertificateValidator; private bool _dangerousAcceptAnyServerCertificateValidator;
private SecurityOptions _securityOptions; private SecurityOptions _securityOptions;
private string _downstreamHttpMethod;
public DownstreamReRouteBuilder() public DownstreamReRouteBuilder()
{ {
@ -56,6 +57,12 @@ namespace Ocelot.Configuration.Builder
return this; return this;
} }
public DownstreamReRouteBuilder WithDownStreamHttpMethod(string method)
{
_downstreamHttpMethod = method;
return this;
}
public DownstreamReRouteBuilder WithLoadBalancerOptions(LoadBalancerOptions loadBalancerOptions) public DownstreamReRouteBuilder WithLoadBalancerOptions(LoadBalancerOptions loadBalancerOptions)
{ {
_loadBalancerOptions = loadBalancerOptions; _loadBalancerOptions = loadBalancerOptions;
@ -282,7 +289,8 @@ namespace Ocelot.Configuration.Builder
_addHeadersToDownstream, _addHeadersToDownstream,
_addHeadersToUpstream, _addHeadersToUpstream,
_dangerousAcceptAnyServerCertificateValidator, _dangerousAcceptAnyServerCertificateValidator,
_securityOptions); _securityOptions,
_downstreamHttpMethod);
} }
} }
} }

View File

@ -138,6 +138,7 @@ namespace Ocelot.Configuration.Creator
.WithAddHeadersToUpstream(hAndRs.AddHeadersToUpstream) .WithAddHeadersToUpstream(hAndRs.AddHeadersToUpstream)
.WithDangerousAcceptAnyServerCertificateValidator(fileReRoute.DangerousAcceptAnyServerCertificateValidator) .WithDangerousAcceptAnyServerCertificateValidator(fileReRoute.DangerousAcceptAnyServerCertificateValidator)
.WithSecurityOptions(securityOptions) .WithSecurityOptions(securityOptions)
.WithDownStreamHttpMethod(fileReRoute.DownstreamHttpMethod)
.Build(); .Build();
return reRoute; return reRoute;

View File

@ -38,7 +38,8 @@ namespace Ocelot.Configuration
List<AddHeader> addHeadersToDownstream, List<AddHeader> addHeadersToDownstream,
List<AddHeader> addHeadersToUpstream, List<AddHeader> addHeadersToUpstream,
bool dangerousAcceptAnyServerCertificateValidator, bool dangerousAcceptAnyServerCertificateValidator,
SecurityOptions securityOptions) SecurityOptions securityOptions,
string downstreamHttpMethod)
{ {
DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator;
AddHeadersToDownstream = addHeadersToDownstream; AddHeadersToDownstream = addHeadersToDownstream;
@ -72,6 +73,7 @@ namespace Ocelot.Configuration
LoadBalancerKey = loadBalancerKey; LoadBalancerKey = loadBalancerKey;
AddHeadersToUpstream = addHeadersToUpstream; AddHeadersToUpstream = addHeadersToUpstream;
SecurityOptions = securityOptions; SecurityOptions = securityOptions;
DownstreamHttpMethod = downstreamHttpMethod;
} }
public string Key { get; } public string Key { get; }
@ -106,5 +108,6 @@ namespace Ocelot.Configuration
public List<AddHeader> AddHeadersToUpstream { get; } public List<AddHeader> AddHeadersToUpstream { get; }
public bool DangerousAcceptAnyServerCertificateValidator { get; } public bool DangerousAcceptAnyServerCertificateValidator { get; }
public SecurityOptions SecurityOptions { get; } public SecurityOptions SecurityOptions { get; }
public string DownstreamHttpMethod { get; }
} }
} }

View File

@ -29,6 +29,7 @@ namespace Ocelot.Configuration.File
public string DownstreamPathTemplate { get; set; } public string DownstreamPathTemplate { get; set; }
public string UpstreamPathTemplate { get; set; } public string UpstreamPathTemplate { get; set; }
public List<string> UpstreamHttpMethod { get; set; } public List<string> UpstreamHttpMethod { get; set; }
public string DownstreamHttpMethod { get; set; }
public Dictionary<string, string> AddHeadersToRequest { get; set; } public Dictionary<string, string> AddHeadersToRequest { get; set; }
public Dictionary<string, string> UpstreamHeaderTransform { get; set; } public Dictionary<string, string> UpstreamHeaderTransform { get; set; }
public Dictionary<string, string> DownstreamHeaderTransform { get; set; } public Dictionary<string, string> DownstreamHeaderTransform { get; set; }

View File

@ -1,12 +1,13 @@
namespace Ocelot.Request.Mapper namespace Ocelot.Request.Mapper
{ {
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Ocelot.Configuration;
using Ocelot.Responses; using Ocelot.Responses;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
public interface IRequestMapper public interface IRequestMapper
{ {
Task<Response<HttpRequestMessage>> Map(HttpRequest request); Task<Response<HttpRequestMessage>> Map(HttpRequest request, DownstreamReRoute downstreamReRoute);
} }
} }

View File

@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Ocelot.Configuration;
using Ocelot.Responses; using Ocelot.Responses;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -15,14 +16,14 @@
{ {
private readonly string[] _unsupportedHeaders = { "host" }; private readonly string[] _unsupportedHeaders = { "host" };
public async Task<Response<HttpRequestMessage>> Map(HttpRequest request) public async Task<Response<HttpRequestMessage>> Map(HttpRequest request, DownstreamReRoute downstreamReRoute)
{ {
try try
{ {
var requestMessage = new HttpRequestMessage() var requestMessage = new HttpRequestMessage()
{ {
Content = await MapContent(request), Content = await MapContent(request),
Method = MapMethod(request), Method = MapMethod(request, downstreamReRoute),
RequestUri = MapUri(request) RequestUri = MapUri(request)
}; };
@ -71,8 +72,13 @@
} }
} }
private HttpMethod MapMethod(HttpRequest request) private HttpMethod MapMethod(HttpRequest request, DownstreamReRoute downstreamReRoute)
{ {
if (!string.IsNullOrEmpty(downstreamReRoute?.DownstreamHttpMethod))
{
return new HttpMethod(downstreamReRoute.DownstreamHttpMethod);
}
return new HttpMethod(request.Method); return new HttpMethod(request.Method);
} }

View File

@ -52,6 +52,7 @@ namespace Ocelot.Request.Middleware
}; };
_request.RequestUri = uriBuilder.Uri; _request.RequestUri = uriBuilder.Uri;
_request.Method = new HttpMethod(Method);
return _request; return _request;
} }

View File

@ -24,7 +24,7 @@ namespace Ocelot.Request.Middleware
public async Task Invoke(DownstreamContext context) public async Task Invoke(DownstreamContext context)
{ {
var downstreamRequest = await _requestMapper.Map(context.HttpContext.Request); var downstreamRequest = await _requestMapper.Map(context.HttpContext.Request, context.DownstreamReRoute);
if (downstreamRequest.IsError) if (downstreamRequest.IsError)
{ {

View File

@ -1,4 +1,3 @@
using Microsoft.AspNetCore.Builder;
using Ocelot.Middleware.Pipeline; using Ocelot.Middleware.Pipeline;
namespace Ocelot.Request.Middleware namespace Ocelot.Request.Middleware

View File

@ -0,0 +1,158 @@
namespace Ocelot.AcceptanceTests
{
using Microsoft.AspNetCore.Http;
using Ocelot.Configuration.File;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using TestStack.BDDfy;
using Xunit;
public class MethodTests : IDisposable
{
private readonly Steps _steps;
private readonly ServiceHandler _serviceHandler;
public MethodTests()
{
_serviceHandler = new ServiceHandler();
_steps = new Steps();
}
[Fact]
public void should_return_response_200_when_get_converted_to_post()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/{url}",
DownstreamScheme = "http",
UpstreamPathTemplate = "/{url}",
UpstreamHttpMethod = new List<string> { "Get" },
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 53171,
},
},
DownstreamHttpMethod = "POST",
},
},
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:53171/", "/", "POST"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.BDDfy();
}
[Fact]
public void should_return_response_200_when_get_converted_to_post_with_content()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/{url}",
DownstreamScheme = "http",
UpstreamPathTemplate = "/{url}",
UpstreamHttpMethod = new List<string> { "Get" },
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 53271,
},
},
DownstreamHttpMethod = "POST",
},
},
};
const string expected = "here is some content";
var httpContent = new StringContent(expected);
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:53271/", "/", "POST"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/", httpContent))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(_ => _steps.ThenTheResponseBodyShouldBe(expected))
.BDDfy();
}
[Fact]
public void should_return_response_200_when_get_converted_to_get_with_content()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/{url}",
DownstreamScheme = "http",
UpstreamPathTemplate = "/{url}",
UpstreamHttpMethod = new List<string> { "Post" },
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 53272,
},
},
DownstreamHttpMethod = "GET",
},
},
};
const string expected = "here is some content";
var httpContent = new StringContent(expected);
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:53272/", "/", "GET"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIPostUrlOnTheApiGateway("/", httpContent))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(_ => _steps.ThenTheResponseBodyShouldBe(expected))
.BDDfy();
}
private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string expected)
{
_serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context =>
{
if (context.Request.Method == expected)
{
context.Response.StatusCode = 200;
var reader = new StreamReader(context.Request.Body);
var body = await reader.ReadToEndAsync();
await context.Response.WriteAsync(body);
}
else
{
context.Response.StatusCode = 500;
}
});
}
public void Dispose()
{
_serviceHandler.Dispose();
_steps.Dispose();
}
}
}

View File

@ -909,6 +909,18 @@ namespace Ocelot.AcceptanceTests
_response = _ocelotClient.GetAsync(url).Result; _response = _ocelotClient.GetAsync(url).Result;
} }
public void WhenIGetUrlOnTheApiGateway(string url, HttpContent content)
{
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url) {Content = content};
_response = _ocelotClient.SendAsync(httpRequestMessage).Result;
}
public void WhenIPostUrlOnTheApiGateway(string url, HttpContent content)
{
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
_response = _ocelotClient.SendAsync(httpRequestMessage).Result;
}
public void WhenIGetUrlOnTheApiGateway(string url, string cookie, string value) public void WhenIGetUrlOnTheApiGateway(string url, string cookie, string value)
{ {
var request = _ocelotServer.CreateRequest(url); var request = _ocelotServer.CreateRequest(url);

View File

@ -1,6 +1,4 @@
using Ocelot.Middleware; namespace Ocelot.UnitTests.Request
namespace Ocelot.UnitTests.Request
{ {
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Moq; using Moq;
@ -9,9 +7,12 @@ namespace Ocelot.UnitTests.Request
using Ocelot.Request.Creator; using Ocelot.Request.Creator;
using Ocelot.Request.Mapper; using Ocelot.Request.Mapper;
using Ocelot.Request.Middleware; using Ocelot.Request.Middleware;
using Ocelot.Configuration.Builder;
using Ocelot.Middleware;
using Ocelot.Responses; using Ocelot.Responses;
using Shouldly; using Shouldly;
using System.Net.Http; using System.Net.Http;
using Ocelot.Configuration;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
@ -65,6 +66,20 @@ namespace Ocelot.UnitTests.Request
.Then(_ => ThenTheContexRequestIsMappedToADownstreamRequest()) .Then(_ => ThenTheContexRequestIsMappedToADownstreamRequest())
.And(_ => ThenTheDownstreamRequestIsStored()) .And(_ => ThenTheDownstreamRequestIsStored())
.And(_ => ThenTheNextMiddlewareIsInvoked()) .And(_ => ThenTheNextMiddlewareIsInvoked())
.And(_ => ThenTheDownstreamRequestMethodIs("GET"))
.BDDfy();
}
[Fact]
public void Should_map_downstream_reroute_method_to_downstream_request()
{
this.Given(_ => GivenTheHttpContextContainsARequest())
.And(_ => GivenTheMapperWillReturnAMappedRequest())
.When(_ => WhenTheMiddlewareIsInvoked())
.Then(_ => ThenTheContexRequestIsMappedToADownstreamRequest())
.And(_ => ThenTheDownstreamRequestIsStored())
.And(_ => ThenTheNextMiddlewareIsInvoked())
.And(_ => ThenTheDownstreamRequestMethodIs("GET"))
.BDDfy(); .BDDfy();
} }
@ -80,6 +95,11 @@ namespace Ocelot.UnitTests.Request
.BDDfy(); .BDDfy();
} }
private void ThenTheDownstreamRequestMethodIs(string expected)
{
_downstreamContext.DownstreamRequest.Method.ShouldBe(expected);
}
private void GivenTheHttpContextContainsARequest() private void GivenTheHttpContextContainsARequest()
{ {
_httpContext _httpContext
@ -92,7 +112,7 @@ namespace Ocelot.UnitTests.Request
_mappedRequest = new OkResponse<HttpRequestMessage>(new HttpRequestMessage(HttpMethod.Get, "http://www.bbc.co.uk")); _mappedRequest = new OkResponse<HttpRequestMessage>(new HttpRequestMessage(HttpMethod.Get, "http://www.bbc.co.uk"));
_requestMapper _requestMapper
.Setup(rm => rm.Map(It.IsAny<HttpRequest>())) .Setup(rm => rm.Map(It.IsAny<HttpRequest>(), It.IsAny<DownstreamReRoute>()))
.ReturnsAsync(_mappedRequest); .ReturnsAsync(_mappedRequest);
} }
@ -101,7 +121,7 @@ namespace Ocelot.UnitTests.Request
_mappedRequest = new ErrorResponse<HttpRequestMessage>(new UnmappableRequestError(new System.Exception("boooom!"))); _mappedRequest = new ErrorResponse<HttpRequestMessage>(new UnmappableRequestError(new System.Exception("boooom!")));
_requestMapper _requestMapper
.Setup(rm => rm.Map(It.IsAny<HttpRequest>())) .Setup(rm => rm.Map(It.IsAny<HttpRequest>(), It.IsAny<DownstreamReRoute>()))
.ReturnsAsync(_mappedRequest); .ReturnsAsync(_mappedRequest);
} }
@ -112,7 +132,7 @@ namespace Ocelot.UnitTests.Request
private void ThenTheContexRequestIsMappedToADownstreamRequest() private void ThenTheContexRequestIsMappedToADownstreamRequest()
{ {
_requestMapper.Verify(rm => rm.Map(_httpRequest.Object), Times.Once); _requestMapper.Verify(rm => rm.Map(_httpRequest.Object, _downstreamContext.DownstreamReRoute), Times.Once);
} }
private void ThenTheDownstreamRequestIsStored() private void ThenTheDownstreamRequestIsStored()

View File

@ -13,6 +13,8 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
@ -27,6 +29,8 @@
private List<KeyValuePair<string, StringValues>> _inputHeaders = null; private List<KeyValuePair<string, StringValues>> _inputHeaders = null;
private DownstreamReRoute _downstreamReRoute;
public RequestMapperTests() public RequestMapperTests()
{ {
_httpContext = new DefaultHttpContext(); _httpContext = new DefaultHttpContext();
@ -82,6 +86,21 @@
.BDDfy(); .BDDfy();
} }
[Theory]
[InlineData("", "GET")]
[InlineData(null, "GET")]
[InlineData("POST", "POST")]
public void Should_use_downstream_reroute_method_if_set(string input, string expected)
{
this.Given(_ => GivenTheInputRequestHasMethod("GET"))
.And(_ => GivenTheDownstreamReRouteMethodIs(input))
.And(_ => GivenTheInputRequestHasAValidUri())
.When(_ => WhenMapped())
.Then(_ => ThenNoErrorIsReturned())
.And(_ => ThenTheMappedRequestHasMethod(expected))
.BDDfy();
}
[Fact] [Fact]
public void Should_map_all_headers() public void Should_map_all_headers()
{ {
@ -154,16 +173,6 @@
.BDDfy(); .BDDfy();
} }
private void GivenTheInputRequestHasNoContentLength()
{
_inputRequest.ContentLength = null;
}
private void GivenTheInputRequestHasNoContentType()
{
_inputRequest.ContentType = null;
}
[Fact] [Fact]
public void Should_map_content_headers() public void Should_map_content_headers()
{ {
@ -212,6 +221,22 @@
.BDDfy(); .BDDfy();
} }
private void GivenTheDownstreamReRouteMethodIs(string input)
{
_downstreamReRoute = new DownstreamReRouteBuilder().WithDownStreamHttpMethod(input).Build();
}
private void GivenTheInputRequestHasNoContentLength()
{
_inputRequest.ContentLength = null;
}
private void GivenTheInputRequestHasNoContentType()
{
_inputRequest.ContentType = null;
}
private void ThenTheContentHeadersAreNotAddedToNonContentHeaders() private void ThenTheContentHeadersAreNotAddedToNonContentHeaders()
{ {
_mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-Disposition"); _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-Disposition");
@ -380,7 +405,7 @@
private async Task WhenMapped() private async Task WhenMapped()
{ {
_mappedRequest = await _requestMapper.Map(_inputRequest); _mappedRequest = await _requestMapper.Map(_inputRequest, _downstreamReRoute);
} }
private void ThenNoErrorIsReturned() private void ThenNoErrorIsReturned()