Skip to content

Commit

Permalink
downstreambaseurl placeholder for multiple location value redirects (T…
Browse files Browse the repository at this point in the history
  • Loading branch information
TomPallister authored Jan 27, 2018
1 parent d0eee70 commit f572d1b
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 10 deletions.
31 changes: 29 additions & 2 deletions docs/features/headerstransformation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ Add the following to a ReRoute in configuration.json in order to replace http://
Placeholders
^^^^^^^^^^^^

Ocelot allows placeholders that can be used in header transformation. At the moment there is only one placeholder.
Ocelot allows placeholders that can be used in header transformation.

{BaseUrl} - This will use Ocelot's base url e.g. http://localhost:5000 as its value.
{DownstreamBaseUrl} - This will use the downstream services base url e.g. http://localhost:5000 as its value. This only works for DownstreamHeaderTransform at the moment.

Handling 302 Redirects
^^^^^^^^^^^^^^^^^^^^^^
Expand All @@ -67,4 +68,30 @@ or you could use the BaseUrl placeholder.
"AllowAutoRedirect": false,
},
Ocelot will not try and replace the location header returned by the downstream service with its own URL.
finally if you are using a load balancer with Ocelot you will get multiple downstream base urls so the above would not work. In this case you can do the following.

.. code-block:: json
"DownstreamHeaderTransform": {
"Location": "{DownstreamBaseUrl}, {BaseUrl}"
},
"HttpHandlerOptions": {
"AllowAutoRedirect": false,
},
Future
^^^^^^

Ideally this feature would be able to support the fact that a header can have multiple values. At the moment it just assumes one.
It would also be nice if it could multi find and replace e.g.

.. code-block:: json
"DownstreamHeaderTransform": {
"Location": "[{one,one},{two,two}"
},
"HttpHandlerOptions": {
"AllowAutoRedirect": false,
},
If anyone wants to have a go at this please help yourself!!
39 changes: 35 additions & 4 deletions src/Ocelot/Headers/HttpResponseHeaderReplacer.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Ocelot.Configuration;
using Ocelot.Infrastructure.Extensions;
using Ocelot.Responses;

namespace Ocelot.Headers
{
public class HttpResponseHeaderReplacer : IHttpResponseHeaderReplacer
{
public Response Replace(HttpResponseMessage response, List<HeaderFindAndReplace> fAndRs)
private Dictionary<string, Func<HttpRequestMessage, string>> _placeholders;

public HttpResponseHeaderReplacer()
{
_placeholders = new Dictionary<string, Func<HttpRequestMessage, string>>();
_placeholders.Add("{DownstreamBaseUrl}", x => {
var downstreamUrl = $"{x.RequestUri.Scheme}://{x.RequestUri.Host}";

if(x.RequestUri.Port != 80 && x.RequestUri.Port != 443)
{
downstreamUrl = $"{downstreamUrl}:{x.RequestUri.Port}";
}

return $"{downstreamUrl}/";
});
}
public Response Replace(HttpResponseMessage response, List<HeaderFindAndReplace> fAndRs, HttpRequestMessage httpRequestMessage)
{
foreach (var f in fAndRs)
{
//if the response headers contain a matching find and replace
if(response.Headers.TryGetValues(f.Key, out var values))
{
var replaced = values.ToList()[f.Index].Replace(f.Find, f.Replace);
response.Headers.Remove(f.Key);
response.Headers.Add(f.Key, replaced);
//check to see if it is a placeholder in the find...
if(_placeholders.TryGetValue(f.Find, out var replacePlaceholder))
{
//if it is we need to get the value of the placeholder
var find = replacePlaceholder(httpRequestMessage);
var replaced = values.ToList()[f.Index].Replace(find, f.Replace.LastCharAsForwardSlash());
response.Headers.Remove(f.Key);
response.Headers.Add(f.Key, replaced);
}
else
{
var replaced = values.ToList()[f.Index].Replace(f.Find, f.Replace);
response.Headers.Remove(f.Key);
response.Headers.Add(f.Key, replaced);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ namespace Ocelot.Headers
{
public interface IHttpResponseHeaderReplacer
{
Response Replace(HttpResponseMessage response, List<HeaderFindAndReplace> fAndRs);
Response Replace(HttpResponseMessage response, List<HeaderFindAndReplace> fAndRs, HttpRequestMessage httpRequestMessage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public async Task Invoke(HttpContext context)

var postFAndRs = this.DownstreamRoute.ReRoute.DownstreamHeadersFindAndReplace;

_postReplacer.Replace(HttpResponseMessage, postFAndRs);
_postReplacer.Replace(HttpResponseMessage, postFAndRs, DownstreamRequest);
}
}
}
9 changes: 9 additions & 0 deletions src/Ocelot/Infrastructure/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,14 @@ public static string TrimStart(this string source, string trim, StringComparison
return s;
}

public static string LastCharAsForwardSlash(this string source)
{
if(source.EndsWith('/'))
{
return source;
}

return $"{source}/";
}
}
}
36 changes: 36 additions & 0 deletions test/Ocelot.AcceptanceTests/HeaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,42 @@ public void should_fix_issue_190()
.BDDfy();
}

[Fact]
public void should_fix_issue_205()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHost = "localhost",
DownstreamPort = 6773,
UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Get" },
DownstreamHeaderTransform = new Dictionary<string,string>
{
{"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();
}


private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public HttpHeadersTransformationMiddlewareTests()
public void should_call_pre_and_post_header_transforms()
{
this.Given(x => GivenTheFollowingRequest())
.And(x => GivenTheDownstreamRequestIs())
.And(x => GivenTheReRouteHasPreFindAndReplaceSetUp())
.And(x => GivenTheHttpResponseMessageIs())
.When(x => WhenICallTheMiddleware())
Expand All @@ -45,6 +46,13 @@ public void should_call_pre_and_post_header_transforms()
.BDDfy();
}

private void GivenTheDownstreamRequestIs()
{
var request = new HttpRequestMessage();
var response = new OkResponse<HttpRequestMessage>(request);
ScopedRepository.Setup(x => x.Get<HttpRequestMessage>("DownstreamRequest")).Returns(response);
}

private void GivenTheHttpResponseMessageIs()
{
var httpResponseMessage = new HttpResponseMessage();
Expand All @@ -68,7 +76,7 @@ private void ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly()

private void ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly()
{
_postReplacer.Verify(x => x.Replace(It.IsAny<HttpResponseMessage>(), It.IsAny<List<HeaderFindAndReplace>>()), Times.Once);
_postReplacer.Verify(x => x.Replace(It.IsAny<HttpResponseMessage>(), It.IsAny<List<HeaderFindAndReplace>>(), It.IsAny<HttpRequestMessage>()), Times.Once);
}

private void GivenTheFollowingRequest()
Expand Down
146 changes: 145 additions & 1 deletion test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class HttpResponseHeaderReplacerTests
private HttpResponseHeaderReplacer _replacer;
private List<HeaderFindAndReplace> _headerFindAndReplaces;
private Response _result;
private HttpRequestMessage _request;

public HttpResponseHeaderReplacerTests()
{
Expand Down Expand Up @@ -52,6 +53,143 @@ public void should_not_replace_headers()
.BDDfy();
}

[Fact]
public void should_replace_downstream_base_url_with_ocelot_base_url()
{
var downstreamUrl = "http://downstream.com/";

var request = new HttpRequestMessage();
request.RequestUri = new System.Uri(downstreamUrl);

var response = new HttpResponseMessage();
response.Headers.Add("Location", downstreamUrl);

var fAndRs = new List<HeaderFindAndReplace>();
fAndRs.Add(new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0));

this.Given(x => GivenTheHttpResponse(response))
.And(x => GivenTheRequestIs(request))
.And(x => GivenTheFollowingHeaderReplacements(fAndRs))
.When(x => WhenICallTheReplacer())
.Then(x => ThenTheHeaderShouldBe("Location", "http://ocelot.com/"))
.BDDfy();
}

[Fact]
public void should_replace_downstream_base_url_with_ocelot_base_url_with_port()
{
var downstreamUrl = "http://downstream.com/";

var request = new HttpRequestMessage();
request.RequestUri = new System.Uri(downstreamUrl);

var response = new HttpResponseMessage();
response.Headers.Add("Location", downstreamUrl);

var fAndRs = new List<HeaderFindAndReplace>();
fAndRs.Add(new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com:123/", 0));

this.Given(x => GivenTheHttpResponse(response))
.And(x => GivenTheRequestIs(request))
.And(x => GivenTheFollowingHeaderReplacements(fAndRs))
.When(x => WhenICallTheReplacer())
.Then(x => ThenTheHeaderShouldBe("Location", "http://ocelot.com:123/"))
.BDDfy();
}


[Fact]
public void should_replace_downstream_base_url_with_ocelot_base_url_and_path()
{
var downstreamUrl = "http://downstream.com/test/product";

var request = new HttpRequestMessage();
request.RequestUri = new System.Uri(downstreamUrl);

var response = new HttpResponseMessage();
response.Headers.Add("Location", downstreamUrl);

var fAndRs = new List<HeaderFindAndReplace>();
fAndRs.Add(new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0));

this.Given(x => GivenTheHttpResponse(response))
.And(x => GivenTheRequestIs(request))
.And(x => GivenTheFollowingHeaderReplacements(fAndRs))
.When(x => WhenICallTheReplacer())
.Then(x => ThenTheHeaderShouldBe("Location", "http://ocelot.com/test/product"))
.BDDfy();
}

[Fact]
public void should_replace_downstream_base_url_with_ocelot_base_url_with_path_and_port()
{
var downstreamUrl = "http://downstream.com/test/product";

var request = new HttpRequestMessage();
request.RequestUri = new System.Uri(downstreamUrl);

var response = new HttpResponseMessage();
response.Headers.Add("Location", downstreamUrl);

var fAndRs = new List<HeaderFindAndReplace>();
fAndRs.Add(new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com:123/", 0));

this.Given(x => GivenTheHttpResponse(response))
.And(x => GivenTheRequestIs(request))
.And(x => GivenTheFollowingHeaderReplacements(fAndRs))
.When(x => WhenICallTheReplacer())
.Then(x => ThenTheHeaderShouldBe("Location", "http://ocelot.com:123/test/product"))
.BDDfy();
}

[Fact]
public void should_replace_downstream_base_url_and_port_with_ocelot_base_url()
{
var downstreamUrl = "http://downstream.com:123/test/product";

var request = new HttpRequestMessage();
request.RequestUri = new System.Uri(downstreamUrl);

var response = new HttpResponseMessage();
response.Headers.Add("Location", downstreamUrl);

var fAndRs = new List<HeaderFindAndReplace>();
fAndRs.Add(new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0));

this.Given(x => GivenTheHttpResponse(response))
.And(x => GivenTheRequestIs(request))
.And(x => GivenTheFollowingHeaderReplacements(fAndRs))
.When(x => WhenICallTheReplacer())
.Then(x => ThenTheHeaderShouldBe("Location", "http://ocelot.com/test/product"))
.BDDfy();
}

[Fact]
public void should_replace_downstream_base_url_and_port_with_ocelot_base_url_and_port()
{
var downstreamUrl = "http://downstream.com:123/test/product";

var request = new HttpRequestMessage();
request.RequestUri = new System.Uri(downstreamUrl);

var response = new HttpResponseMessage();
response.Headers.Add("Location", downstreamUrl);

var fAndRs = new List<HeaderFindAndReplace>();
fAndRs.Add(new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com:321/", 0));

this.Given(x => GivenTheHttpResponse(response))
.And(x => GivenTheRequestIs(request))
.And(x => GivenTheFollowingHeaderReplacements(fAndRs))
.When(x => WhenICallTheReplacer())
.Then(x => ThenTheHeaderShouldBe("Location", "http://ocelot.com:321/test/product"))
.BDDfy();
}

private void GivenTheRequestIs(HttpRequestMessage request)
{
_request = request;
}

private void ThenTheHeadersAreNotReplaced()
{
Expand All @@ -75,7 +213,13 @@ private void GivenTheHttpResponse(HttpResponseMessage response)

private void WhenICallTheReplacer()
{
_result = _replacer.Replace(_response, _headerFindAndReplaces);
_result = _replacer.Replace(_response, _headerFindAndReplaces, _request);
}

private void ThenTheHeaderShouldBe(string key, string value)
{
var test = _response.Headers.GetValues(key);
test.First().ShouldBe(value);
}

private void ThenTheHeadersAreReplaced()
Expand Down

0 comments on commit f572d1b

Please sign in to comment.