From 0747bb9891bd27179be20cff5d8f78b62f5a259a Mon Sep 17 00:00:00 2001 From: Andrew Browne Date: Sun, 21 Sep 2014 00:29:04 +1000 Subject: [PATCH 1/2] Support for X-Forwarded headers Support has been added for X-Forwarded-Port and X-Forwarded-Proto. This will enable reverse proxy servers to feed the public port and protocol to eventstore. These values will be used in generating URLs in responses. --- .../EventStore.Core.Tests.csproj | 1 + .../Services/Transport/Http/proxy_headers.cs | 55 +++++++++++++++++++ .../Controllers/ClusterWebUIController.cs | 2 +- .../EntityManagement/HttpEntity.cs | 27 ++++++++- .../EventStore.Transport.Http.csproj | 1 + src/EventStore.Transport.Http/ProxyHeaders.cs | 14 +++++ 6 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/EventStore.Core.Tests/Services/Transport/Http/proxy_headers.cs create mode 100644 src/EventStore.Transport.Http/ProxyHeaders.cs diff --git a/src/EventStore.Core.Tests/EventStore.Core.Tests.csproj b/src/EventStore.Core.Tests/EventStore.Core.Tests.csproj index c6cdeecf44b..961f6da4eec 100644 --- a/src/EventStore.Core.Tests/EventStore.Core.Tests.csproj +++ b/src/EventStore.Core.Tests/EventStore.Core.Tests.csproj @@ -417,6 +417,7 @@ + diff --git a/src/EventStore.Core.Tests/Services/Transport/Http/proxy_headers.cs b/src/EventStore.Core.Tests/Services/Transport/Http/proxy_headers.cs new file mode 100644 index 00000000000..46895101cb3 --- /dev/null +++ b/src/EventStore.Core.Tests/Services/Transport/Http/proxy_headers.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Specialized; +using EventStore.Transport.Http.EntityManagement; +using NUnit.Framework; + +namespace EventStore.Core.Tests.Services.Transport.Http +{ + [TestFixture] + class proxy_headers + { + [Test] + public void with_no_headers_uri_is_unchanged() + { + var inputUri = new Uri("http://www.example.com:1234/path/?key=value#anchor"); + var requestedUri = + HttpEntity.BuildRequestedUrl(inputUri, + new NameValueCollection()); + + Assert.AreEqual(inputUri, requestedUri); + } + + [Test] + public void with_port_forward_header_only_port_is_changed() + { + var inputUri = new Uri("http://www.example.com:1234/path/?key=value#anchor"); + var headers = new NameValueCollection { { "X-Forwarded-Port", "4321" } }; + var requestedUri = + HttpEntity.BuildRequestedUrl(inputUri, headers); + + Assert.AreEqual(new Uri("http://www.example.com:4321/path/?key=value#anchor"), requestedUri); + } + + [Test] + public void non_integer_port_forward_header_is_ignored() + { + var inputUri = new Uri("http://www.example.com:1234/path/?key=value#anchor"); + var headers = new NameValueCollection { { "X-Forwarded-Port", "abc" } }; + var requestedUri = + HttpEntity.BuildRequestedUrl(inputUri, headers); + + Assert.AreEqual(inputUri, requestedUri); + } + + [Test] + public void with_proto_forward_header_only_scheme_is_changed() + { + var inputUri = new Uri("http://www.example.com:1234/path/?key=value#anchor"); + var headers = new NameValueCollection { { "X-Forwarded-Proto", "https" } }; + var requestedUri = + HttpEntity.BuildRequestedUrl(inputUri, headers); + + Assert.AreEqual(new Uri("https://www.example.com:1234/path/?key=value#anchor"), requestedUri); + } + } +} diff --git a/src/EventStore.Core/Services/Transport/Http/Controllers/ClusterWebUIController.cs b/src/EventStore.Core/Services/Transport/Http/Controllers/ClusterWebUIController.cs index 8cfe8f9942a..57429a91887 100644 --- a/src/EventStore.Core/Services/Transport/Http/Controllers/ClusterWebUIController.cs +++ b/src/EventStore.Core/Services/Transport/Http/Controllers/ClusterWebUIController.cs @@ -64,7 +64,7 @@ private static void RegisterRedirectAction(IHttpService service, string fromUrl, new[] { new KeyValuePair( - "Location", new Uri(match.BaseUri, toUrl).AbsoluteUri) + "Location", new Uri(http.HttpEntity.RequestedUrl, toUrl).AbsoluteUri) }, Console.WriteLine)); } } diff --git a/src/EventStore.Transport.Http/EntityManagement/HttpEntity.cs b/src/EventStore.Transport.Http/EntityManagement/HttpEntity.cs index 664bc3e7331..92f491eb802 100644 --- a/src/EventStore.Transport.Http/EntityManagement/HttpEntity.cs +++ b/src/EventStore.Transport.Http/EntityManagement/HttpEntity.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Specialized; using System.Net; using System.Security.Principal; using EventStore.Common.Utils; @@ -19,12 +20,36 @@ public HttpEntity(HttpListenerRequest request, HttpListenerResponse response, IP Ensure.NotNull(request, "request"); Ensure.NotNull(response, "response"); - RequestedUrl = request.Url; + RequestedUrl = BuildRequestedUrl(request.Url, request.Headers); Request = request; Response = response; User = user; } + public static Uri BuildRequestedUrl(Uri requestUrl, NameValueCollection requestHeaders) + { + var uriBuilder = new UriBuilder(requestUrl); + + var forwardedPortHeaderValue = requestHeaders[ProxyHeaders.XForwardedPort]; + + if (!string.IsNullOrEmpty(forwardedPortHeaderValue)) + { + int requestPort; + if (Int32.TryParse(forwardedPortHeaderValue, out requestPort)) + { + uriBuilder.Port = requestPort; + } + } + + var forwardedProtoHeaderValue = requestHeaders[ProxyHeaders.XForwardedProto]; + if (!string.IsNullOrEmpty(forwardedProtoHeaderValue)) + { + uriBuilder.Scheme = forwardedProtoHeaderValue; + } + + return uriBuilder.Uri; + } + private HttpEntity(IPrincipal user) { RequestedUrl = null; diff --git a/src/EventStore.Transport.Http/EventStore.Transport.Http.csproj b/src/EventStore.Transport.Http/EventStore.Transport.Http.csproj index 81746f0035a..404add9f4ad 100644 --- a/src/EventStore.Transport.Http/EventStore.Transport.Http.csproj +++ b/src/EventStore.Transport.Http/EventStore.Transport.Http.csproj @@ -68,6 +68,7 @@ + diff --git a/src/EventStore.Transport.Http/ProxyHeaders.cs b/src/EventStore.Transport.Http/ProxyHeaders.cs new file mode 100644 index 00000000000..404041b8b10 --- /dev/null +++ b/src/EventStore.Transport.Http/ProxyHeaders.cs @@ -0,0 +1,14 @@ +namespace EventStore.Transport.Http +{ + public static class ProxyHeaders + { + public const string XForwardedPort = "X-Forwarded-Port"; + public const string XForwardedProto = "X-Forwarded-Proto"; + } + + public static class ProxyHeaderValues + { + public const string XForwardedProtoHttp = "http"; + public const string XForwardedProtoHttps = "https"; + } +} \ No newline at end of file From 2fd3b2b66eabc13ed3872121c1e7337f96308884 Mon Sep 17 00:00:00 2001 From: Andrew Browne Date: Sun, 21 Sep 2014 10:48:21 +1000 Subject: [PATCH 2/2] Use requestedUrl for ProjectionsController links Was using the UriTemplateMatch which contained the local Uri. --- .../Services/Http/ProjectionsController.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/EventStore.Projections.Core/Services/Http/ProjectionsController.cs b/src/EventStore.Projections.Core/Services/Http/ProjectionsController.cs index a8c906ca9c0..aac17e73f8d 100644 --- a/src/EventStore.Projections.Core/Services/Http/ProjectionsController.cs +++ b/src/EventStore.Projections.Core/Services/Http/ProjectionsController.cs @@ -235,7 +235,7 @@ private void OnProjectionStatusGet(HttpEntityManager http, UriTemplateMatch matc new SendToHttpWithConversionEnvelope ( _networkSendQueue, http, DefaultFormatter, OkNoCacheResponseConfigurator, - status => new ProjectionStatisticsHttpFormatted(status.Projections[0], s => MakeUrl(match, s)), + status => new ProjectionStatisticsHttpFormatted(status.Projections[0], s => MakeUrl(http, s)), ErrorsEnvelope(http)); Publish(new ProjectionManagementMessage.Command.GetStatistics(envelope, null, match.BoundVariables["name"], true)); } @@ -263,7 +263,7 @@ private void OnProjectionStatisticsGet(HttpEntityManager http, UriTemplateMatch new SendToHttpWithConversionEnvelope ( _networkSendQueue, http, DefaultFormatter, OkNoCacheResponseConfigurator, - status => new ProjectionsStatisticsHttpFormatted(status, s => MakeUrl(match, s)), + status => new ProjectionsStatisticsHttpFormatted(status, s => MakeUrl(http, s)), ErrorsEnvelope(http)); Publish(new ProjectionManagementMessage.Command.GetStatistics(envelope, null, match.BoundVariables["name"], true)); } @@ -335,7 +335,7 @@ private void ProjectionsGet(HttpEntityManager http, UriTemplateMatch match, Proj var envelope = new SendToHttpWithConversionEnvelope( _networkSendQueue, http, DefaultFormatter, OkNoCacheResponseConfigurator, - status => new ProjectionsStatisticsHttpFormatted(status, s => MakeUrl(match, s)), + status => new ProjectionsStatisticsHttpFormatted(status, s => MakeUrl(http, s)), ErrorsEnvelope(http)); Publish(new ProjectionManagementMessage.Command.GetStatistics(envelope, mode, null, true)); } @@ -349,7 +349,7 @@ private void ProjectionsPost(HttpEntityManager http, UriTemplateMatch match, Pro _networkSendQueue, http, DefaultFormatter, (codec, message) => { var localPath = string.Format("/projection/{0}", message.Name); - var url = MakeUrl(match, localPath); + var url = MakeUrl(http, localPath); return new ResponseConfiguration( 201, "Created", codec.ContentType, codec.Encoding, new KeyValuePair("Location", url)); }, ErrorsEnvelope(http)); @@ -544,11 +544,6 @@ private string ConflictFormatter(ICodec codec, ProjectionManagementMessage.Opera return message.Reason; } - private static string MakeUrl(UriTemplateMatch match, string localPath) - { - return new Uri(match.BaseUri, localPath).AbsoluteUri; - } - private static string DefaultFormatter(ICodec codec, T message) { return codec.To(message);