From f572d1b0cad2173a21263ebdbccdd40934620f95 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Sat, 27 Jan 2018 20:03:49 +0000 Subject: [PATCH] downstreambaseurl placeholder for multiple location value redirects (#207) --- docs/features/headerstransformation.rst | 31 +++- .../Headers/HttpResponseHeaderReplacer.cs | 39 ++++- .../Headers/IHttpResponseHeaderReplacer.cs | 2 +- .../HttpHeadersTransformationMiddleware.cs | 2 +- .../Extensions/StringExtensions.cs | 9 ++ test/Ocelot.AcceptanceTests/HeaderTests.cs | 36 +++++ ...ttpHeadersTransformationMiddlewareTests.cs | 10 +- .../HttpResponseHeaderReplacerTests.cs | 146 +++++++++++++++++- 8 files changed, 265 insertions(+), 10 deletions(-) diff --git a/docs/features/headerstransformation.rst b/docs/features/headerstransformation.rst index e6785cec9..01436cb97 100644 --- a/docs/features/headerstransformation.rst +++ b/docs/features/headerstransformation.rst @@ -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 ^^^^^^^^^^^^^^^^^^^^^^ @@ -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!! \ No newline at end of file diff --git a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs index acd9af306..ef8a4d20e 100644 --- a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs +++ b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs @@ -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 fAndRs) + private Dictionary> _placeholders; + + public HttpResponseHeaderReplacer() + { + _placeholders = new Dictionary>(); + _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 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); + } } } diff --git a/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs index e3056ca66..3a3753180 100644 --- a/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs +++ b/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs @@ -7,6 +7,6 @@ namespace Ocelot.Headers { public interface IHttpResponseHeaderReplacer { - Response Replace(HttpResponseMessage response, List fAndRs); + Response Replace(HttpResponseMessage response, List fAndRs, HttpRequestMessage httpRequestMessage); } } \ No newline at end of file diff --git a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs index 302efd18d..3cf30f3f1 100644 --- a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs +++ b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs @@ -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); } } } \ No newline at end of file diff --git a/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs b/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs index d74583811..2f7d2768c 100644 --- a/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs +++ b/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs @@ -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}/"; + } } } \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/HeaderTests.cs b/test/Ocelot.AcceptanceTests/HeaderTests.cs index 99320cd3f..70fdeb908 100644 --- a/test/Ocelot.AcceptanceTests/HeaderTests.cs +++ b/test/Ocelot.AcceptanceTests/HeaderTests.cs @@ -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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 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(); + } + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey) { diff --git a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs index 25be507b9..39471bc37 100644 --- a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs @@ -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()) @@ -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(request); + ScopedRepository.Setup(x => x.Get("DownstreamRequest")).Returns(response); + } + private void GivenTheHttpResponseMessageIs() { var httpResponseMessage = new HttpResponseMessage(); @@ -68,7 +76,7 @@ private void ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly() private void ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly() { - _postReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>()), Times.Once); + _postReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>(), It.IsAny()), Times.Once); } private void GivenTheFollowingRequest() diff --git a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs index 8d7c5a226..1de6afb4b 100644 --- a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs @@ -16,6 +16,7 @@ public class HttpResponseHeaderReplacerTests private HttpResponseHeaderReplacer _replacer; private List _headerFindAndReplaces; private Response _result; + private HttpRequestMessage _request; public HttpResponseHeaderReplacerTests() { @@ -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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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() { @@ -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()