Support adding custom plaintext headers to downstream requests (#314)

This commit is contained in:
Felix Boers 2018-04-14 07:41:12 +02:00 committed by Tom Pallister
parent b46ef1945d
commit fa09e4cf7a
14 changed files with 376 additions and 183 deletions

View File

@ -1,15 +1,31 @@
Headers Transformation
=====================
======================
Ocelot allows the user to transform headers pre and post downstream request. At the moment Ocelot only supports find and replace. This feature was requested `GitHub #190 <https://github.com/TomPallister/Ocelot/issues/190>`_ and I decided that it was going to be useful in various ways.
Add to Request
^^^^^^^^^^^^^^
This feature was requestes in `GitHub #313 <https://github.com/ThreeMammals/Ocelot/issues/313>`_.
If you want to add a header to your upstream request please add the following to a ReRoute in your configuration.json:
.. code-block:: json
"UpstreamHeaderTransform": {
"Uncle": "Bob"
}
In the example above a header with the key Uncle and value Bob would be send to to the upstream service.
Placeholders are supported too (see below).
Add to Response
^^^^^^^^^^^^^^^
This feature was requested in `GitHub #280 <https://github.com/TomPallister/Ocelot/issues/280>`_. I have only implemented
for responses but could add for requests in the future.
This feature was requested in `GitHub #280 <https://github.com/TomPallister/Ocelot/issues/280>`_.
If you want to add a header to your downstream response please add the following to a ReRoute in configuration.json..
If you want to add a header to your downstream response please add the following to a ReRoute in configuration.json.
.. code-block:: json
@ -50,7 +66,7 @@ Add the following to a ReRoute in configuration.json in order to replace http://
},
Post Downstream Request
^^^^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^^^^
Add the following to a ReRoute in configuration.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This transformation will take place after Ocelot has received the response from the downstream service.

View File

@ -39,12 +39,14 @@ namespace Ocelot.Configuration.Builder
private string _key;
private List<string> _delegatingHandlers;
private List<AddHeader> _addHeadersToDownstream;
private List<AddHeader> _addHeadersToUpstream;
public DownstreamReRouteBuilder()
{
_downstreamAddresses = new List<DownstreamHostAndPort>();
_delegatingHandlers = new List<string>();
_addHeadersToDownstream = new List<AddHeader>();
_addHeadersToUpstream = new List<AddHeader>();
}
public DownstreamReRouteBuilder WithDownstreamAddresses(List<DownstreamHostAndPort> downstreamAddresses)
@ -233,6 +235,12 @@ namespace Ocelot.Configuration.Builder
return this;
}
public DownstreamReRouteBuilder WithAddHeadersToUpstream(List<AddHeader> addHeadersToUpstream)
{
_addHeadersToUpstream = addHeadersToUpstream;
return this;
}
public DownstreamReRoute Build()
{
return new DownstreamReRoute(
@ -263,7 +271,8 @@ namespace Ocelot.Configuration.Builder
new PathTemplate(_downstreamPathTemplate),
_reRouteKey,
_delegatingHandlers,
_addHeadersToDownstream);
_addHeadersToDownstream,
_addHeadersToUpstream);
}
}
}

View File

@ -215,6 +215,7 @@ namespace Ocelot.Configuration.Creator
.WithUpstreamHost(fileReRoute.UpstreamHost)
.WithDelegatingHandlers(fileReRoute.DelegatingHandlers)
.WithAddHeadersToDownstream(hAndRs.AddHeadersToDownstream)
.WithAddHeadersToUpstream(hAndRs.AddHeadersToUpstream)
.Build();
return reRoute;

View File

@ -22,17 +22,25 @@ namespace Ocelot.Configuration.Creator
public HeaderTransformations Create(FileReRoute fileReRoute)
{
var upstream = new List<HeaderFindAndReplace>();
var addHeadersToUpstream = new List<AddHeader>();
foreach(var input in fileReRoute.UpstreamHeaderTransform)
{
var hAndr = Map(input);
if(!hAndr.IsError)
if (input.Value.Contains(","))
{
upstream.Add(hAndr.Data);
var hAndr = Map(input);
if (!hAndr.IsError)
{
upstream.Add(hAndr.Data);
}
else
{
_logger.LogWarning($"Unable to add UpstreamHeaderTransform {input.Key}: {input.Value}");
}
}
else
{
_logger.LogWarning($"Unable to add UpstreamHeaderTransform {input.Key}: {input.Value}");
addHeadersToUpstream.Add(new AddHeader(input.Key, input.Value));
}
}
@ -59,7 +67,7 @@ namespace Ocelot.Configuration.Creator
}
}
return new HeaderTransformations(upstream, downstream, addHeadersToDownstream);
return new HeaderTransformations(upstream, downstream, addHeadersToDownstream, addHeadersToUpstream);
}
private Response<HeaderFindAndReplace> Map(KeyValuePair<string,string> input)

View File

@ -7,9 +7,11 @@ namespace Ocelot.Configuration.Creator
public HeaderTransformations(
List<HeaderFindAndReplace> upstream,
List<HeaderFindAndReplace> downstream,
List<AddHeader> addHeader)
List<AddHeader> addHeaderToDownstream,
List<AddHeader> addHeaderToUpstream)
{
AddHeadersToDownstream = addHeader;
AddHeadersToDownstream = addHeaderToDownstream;
AddHeadersToUpstream = addHeaderToUpstream;
Upstream = upstream;
Downstream = downstream;
}
@ -19,5 +21,6 @@ namespace Ocelot.Configuration.Creator
public List<HeaderFindAndReplace> Downstream { get; }
public List<AddHeader> AddHeadersToDownstream { get; }
public List<AddHeader> AddHeadersToUpstream { get; }
}
}

View File

@ -34,7 +34,8 @@ namespace Ocelot.Configuration
PathTemplate downstreamPathTemplate,
string reRouteKey,
List<string> delegatingHandlers,
List<AddHeader> addHeadersToDownstream)
List<AddHeader> addHeadersToDownstream,
List<AddHeader> addHeadersToUpstream)
{
AddHeadersToDownstream = addHeadersToDownstream;
DelegatingHandlers = delegatingHandlers;
@ -64,6 +65,7 @@ namespace Ocelot.Configuration
AuthenticationOptions = authenticationOptions;
DownstreamPathTemplate = downstreamPathTemplate;
ReRouteKey = reRouteKey;
AddHeadersToUpstream = addHeadersToUpstream;
}
public string Key { get; private set; }
@ -94,5 +96,6 @@ namespace Ocelot.Configuration
public string ReRouteKey { get; private set; }
public List<string> DelegatingHandlers {get;private set;}
public List<AddHeader> AddHeadersToDownstream {get;private set;}
public List<AddHeader> AddHeadersToUpstream { get; private set; }
}
}

View File

@ -4,6 +4,7 @@ using Ocelot.Configuration;
using Ocelot.Infrastructure.Claims.Parser;
using Ocelot.Responses;
using System.Net.Http;
using Microsoft.AspNetCore.Http;
using Ocelot.Configuration.Creator;
using Ocelot.Request.Middleware;
@ -41,5 +42,19 @@ namespace Ocelot.Headers
return new OkResponse();
}
public void SetHeadersOnDownstreamRequest(IEnumerable<AddHeader> headers, HttpContext context)
{
var requestHeader = context.Request.Headers;
foreach (var header in headers)
{
if (requestHeader.ContainsKey(header.Key))
{
requestHeader.Remove(header.Key);
}
requestHeader.Add(header.Key, header.Value);
}
}
}
}

View File

@ -1,4 +1,6 @@
namespace Ocelot.Headers
using Microsoft.AspNetCore.Http;
namespace Ocelot.Headers
{
using System.Collections.Generic;
using System.Net.Http;
@ -12,5 +14,6 @@
public interface IAddHeadersToRequest
{
Response SetHeadersOnDownstreamRequest(List<ClaimToThing> claimsToThings, IEnumerable<System.Security.Claims.Claim> claims, DownstreamRequest downstreamRequest);
void SetHeadersOnDownstreamRequest(IEnumerable<AddHeader> headers, HttpContext context);
}
}

View File

@ -9,16 +9,19 @@ namespace Ocelot.Headers.Middleware
private readonly OcelotRequestDelegate _next;
private readonly IHttpContextRequestHeaderReplacer _preReplacer;
private readonly IHttpResponseHeaderReplacer _postReplacer;
private readonly IAddHeadersToResponse _addHeaders;
private readonly IAddHeadersToResponse _addHeadersToResponse;
private readonly IAddHeadersToRequest _addHeadersToRequest;
public HttpHeadersTransformationMiddleware(OcelotRequestDelegate next,
IOcelotLoggerFactory loggerFactory,
IHttpContextRequestHeaderReplacer preReplacer,
IHttpResponseHeaderReplacer postReplacer,
IAddHeadersToResponse addHeaders)
IAddHeadersToResponse addHeadersToResponse,
IAddHeadersToRequest addHeadersToRequest)
:base(loggerFactory.CreateLogger<HttpHeadersTransformationMiddleware>())
{
_addHeaders = addHeaders;
_addHeadersToResponse = addHeadersToResponse;
_addHeadersToRequest = addHeadersToRequest;
_next = next;
_postReplacer = postReplacer;
_preReplacer = preReplacer;
@ -31,13 +34,15 @@ namespace Ocelot.Headers.Middleware
//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);
_addHeaders.Add(context.DownstreamReRoute.AddHeadersToDownstream, context.DownstreamResponse);
_addHeadersToResponse.Add(context.DownstreamReRoute.AddHeadersToDownstream, context.DownstreamResponse);
}
}
}

View File

@ -829,6 +829,7 @@ namespace Ocelot.UnitTests.Configuration
result.DownstreamReRoute[0].RequestIdKey.ShouldBe(expected.DownstreamReRoute[0].RequestIdKey);
result.DownstreamReRoute[0].DelegatingHandlers.ShouldBe(expected.DownstreamReRoute[0].DelegatingHandlers);
result.DownstreamReRoute[0].AddHeadersToDownstream.ShouldBe(expected.DownstreamReRoute[0].AddHeadersToDownstream);
result.DownstreamReRoute[0].AddHeadersToUpstream.ShouldBe(expected.DownstreamReRoute[0].AddHeadersToUpstream, "AddHeadersToUpstream should be set");
}
}
@ -911,7 +912,7 @@ namespace Ocelot.UnitTests.Configuration
private void GivenTheHeaderFindAndReplaceCreatorReturns()
{
_headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny<FileReRoute>())).Returns(new HeaderTransformations(new List<HeaderFindAndReplace>(), new List<HeaderFindAndReplace>(), new List<AddHeader>()));
_headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny<FileReRoute>())).Returns(new HeaderTransformations(new List<HeaderFindAndReplace>(), new List<HeaderFindAndReplace>(), new List<AddHeader>(), new List<AddHeader>()));
}
private void GivenTheFollowingIsReturned(ServiceProviderConfiguration serviceProviderConfiguration)

View File

@ -149,7 +149,6 @@ namespace Ocelot.UnitTests.Configuration
.Then(x => ThenTheFollowingDownstreamIsReturned(downstream))
.BDDfy();
}
[Fact]
public void should_add_trace_id_header()
{
@ -166,7 +165,45 @@ namespace Ocelot.UnitTests.Configuration
this.Given(x => GivenTheReRoute(reRoute))
.And(x => GivenTheBaseUrlIs("http://ocelot.com/"))
.When(x => WhenICreate())
.Then(x => ThenTheFollowingAddHeaderIsReturned(expected))
.Then(x => ThenTheFollowingAddHeaderToDownstreamIsReturned(expected))
.BDDfy();
}
[Fact]
public void should_add_downstream_header_as_is_when_no_replacement_is_given()
{
var reRoute = new FileReRoute
{
DownstreamHeaderTransform = new Dictionary<string, string>
{
{"X-Custom-Header", "Value"},
}
};
var expected = new AddHeader("X-Custom-Header", "Value");
this.Given(x => GivenTheReRoute(reRoute))
.And(x => WhenICreate())
.Then(x => x.ThenTheFollowingAddHeaderToDownstreamIsReturned(expected))
.BDDfy();
}
[Fact]
public void should_add_upstream_header_as_is_when_no_replacement_is_given()
{
var reRoute = new FileReRoute
{
UpstreamHeaderTransform = new Dictionary<string, string>
{
{"X-Custom-Header", "Value"},
}
};
var expected = new AddHeader("X-Custom-Header", "Value");
this.Given(x => GivenTheReRoute(reRoute))
.And(x => WhenICreate())
.Then(x => x.ThenTheFollowingAddHeaderToUpstreamIsReturned(expected))
.BDDfy();
}
@ -180,12 +217,18 @@ namespace Ocelot.UnitTests.Configuration
_placeholders.Setup(x => x.Get(It.IsAny<string>())).Returns(new ErrorResponse<string>(new AnyError()));
}
private void ThenTheFollowingAddHeaderIsReturned(AddHeader addHeader)
private void ThenTheFollowingAddHeaderToDownstreamIsReturned(AddHeader addHeader)
{
_result.AddHeadersToDownstream[0].Key.ShouldBe(addHeader.Key);
_result.AddHeadersToDownstream[0].Value.ShouldBe(addHeader.Value);
}
private void ThenTheFollowingAddHeaderToUpstreamIsReturned(AddHeader addHeader)
{
_result.AddHeadersToUpstream[0].Key.ShouldBe(addHeader.Key);
_result.AddHeadersToUpstream[0].Value.ShouldBe(addHeader.Value);
}
private void ThenTheFollowingDownstreamIsReturned(List<HeaderFindAndReplace> downstream)
{
_result.Downstream.Count.ShouldBe(downstream.Count);

View File

@ -15,7 +15,7 @@ using Ocelot.Request.Middleware;
namespace Ocelot.UnitTests.Headers
{
public class AddHeadersToRequestTests
public class AddHeadersToRequestClaimToThingTests
{
private readonly AddHeadersToRequest _addHeadersToRequest;
private readonly Mock<IClaimsParser> _parser;
@ -25,7 +25,7 @@ namespace Ocelot.UnitTests.Headers
private Response _result;
private Response<string> _claimValue;
public AddHeadersToRequestTests()
public AddHeadersToRequestClaimToThingTests()
{
_parser = new Mock<IClaimsParser>();
_addHeadersToRequest = new AddHeadersToRequest(_parser.Object);

View File

@ -0,0 +1,75 @@
using Microsoft.AspNetCore.Http;
using Moq;
using Ocelot.Configuration.Creator;
using Ocelot.Headers;
using Ocelot.Infrastructure.Claims.Parser;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.Headers
{
public class AddHeadersToRequestPlainTests
{
private readonly AddHeadersToRequest _addHeadersToRequest;
private HttpContext _context;
private AddHeader _addedHeader;
public AddHeadersToRequestPlainTests()
{
_addHeadersToRequest = new AddHeadersToRequest(Mock.Of<IClaimsParser>());
}
[Fact]
public void should_add_plain_text_header_to_downstream_request()
{
this.Given(_ => GivenHttpRequestWithoutHeaders())
.When(_ => WhenAddingHeader("X-Custom-Header", "PlainValue"))
.Then(_ => ThenTheHeaderGetsTakenOverToTheRequestHeaders())
.BDDfy();
}
[Fact]
public void should_overwrite_existing_header_with_added_header()
{
this.Given(_ => GivenHttpRequestWithHeader("X-Custom-Header", "This should get overwritten"))
.When(_ => WhenAddingHeader("X-Custom-Header", "PlainValue"))
.Then(_ => ThenTheHeaderGetsTakenOverToTheRequestHeaders())
.BDDfy();
}
private void GivenHttpRequestWithoutHeaders()
{
_context = new DefaultHttpContext();
}
private void GivenHttpRequestWithHeader(string headerKey, string headerValue)
{
_context = new DefaultHttpContext
{
Request =
{
Headers =
{
{ headerKey, headerValue }
}
}
};
}
private void WhenAddingHeader(string headerKey, string headerValue)
{
_addedHeader = new AddHeader(headerKey, headerValue);
_addHeadersToRequest.SetHeadersOnDownstreamRequest(new[] { _addedHeader }, _context);
}
private void ThenTheHeaderGetsTakenOverToTheRequestHeaders()
{
var requestHeaders = _context.Request.Headers;
requestHeaders.ContainsKey(_addedHeader.Key).ShouldBeTrue($"Header {_addedHeader.Key} was expected but not there.");
var value = requestHeaders[_addedHeader.Key];
value.ShouldNotBeNull($"Value of header {_addedHeader.Key} was expected to not be null.");
value.ToString().ShouldBe(_addedHeader.Value);
}
}
}

View File

@ -27,7 +27,8 @@ namespace Ocelot.UnitTests.Headers
private readonly HttpHeadersTransformationMiddleware _middleware;
private readonly DownstreamContext _downstreamContext;
private OcelotRequestDelegate _next;
private readonly Mock<IAddHeadersToResponse> _addHeaders;
private readonly Mock<IAddHeadersToResponse> _addHeadersToResponse;
private readonly Mock<IAddHeadersToRequest> _addHeadersToRequest;
public HttpHeadersTransformationMiddlewareTests()
{
@ -38,8 +39,11 @@ namespace Ocelot.UnitTests.Headers
_logger = new Mock<IOcelotLogger>();
_loggerFactory.Setup(x => x.CreateLogger<AuthorisationMiddleware>()).Returns(_logger.Object);
_next = context => Task.CompletedTask;
_addHeaders = new Mock<IAddHeadersToResponse>();
_middleware = new HttpHeadersTransformationMiddleware(_next, _loggerFactory.Object, _preReplacer.Object, _postReplacer.Object, _addHeaders.Object);
_addHeadersToResponse = new Mock<IAddHeadersToResponse>();
_addHeadersToRequest = new Mock<IAddHeadersToRequest>();
_middleware = new HttpHeadersTransformationMiddleware(
_next, _loggerFactory.Object, _preReplacer.Object,
_postReplacer.Object, _addHeadersToResponse.Object, _addHeadersToRequest.Object);
}
[Fact]
@ -51,17 +55,24 @@ namespace Ocelot.UnitTests.Headers
.And(x => GivenTheHttpResponseMessageIs())
.When(x => WhenICallTheMiddleware())
.Then(x => ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly())
.Then(x => ThenAddHeadersToRequestIsCalledCorrectly())
.And(x => ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly())
.And(x => ThenAddHeadersIsCalledCorrectly())
.And(x => ThenAddHeadersToResponseIsCalledCorrectly())
.BDDfy();
}
private void ThenAddHeadersIsCalledCorrectly()
private void ThenAddHeadersToResponseIsCalledCorrectly()
{
_addHeaders
_addHeadersToResponse
.Verify(x => x.Add(_downstreamContext.DownstreamReRoute.AddHeadersToDownstream, _downstreamContext.DownstreamResponse), Times.Once);
}
private void ThenAddHeadersToRequestIsCalledCorrectly()
{
_addHeadersToRequest
.Verify(x => x.SetHeadersOnDownstreamRequest(_downstreamContext.DownstreamReRoute.AddHeadersToUpstream, _downstreamContext.HttpContext), Times.Once);
}
private void WhenICallTheMiddleware()
{
_middleware.Invoke(_downstreamContext).GetAwaiter().GetResult();