From cbd5b2f5bd9f4236c0088ecf04c99c9feb192811 Mon Sep 17 00:00:00 2001 From: Oleksandr Liakhevych Date: Sat, 28 Oct 2017 18:26:17 +0300 Subject: [PATCH 1/2] Fix proxy headers handling --- src/WireMock.Net/Http/HttpClientHelper.cs | 78 ++++++++--- src/WireMock.Net/ResponseBuilders/Response.cs | 8 +- .../Server/FluentMockServer.Admin.cs | 6 +- .../FluentMockServerTests.cs | 127 +++++++++++++++++- 4 files changed, 195 insertions(+), 24 deletions(-) diff --git a/src/WireMock.Net/Http/HttpClientHelper.cs b/src/WireMock.Net/Http/HttpClientHelper.cs index 9a9794d42..e2289cbe4 100644 --- a/src/WireMock.Net/Http/HttpClientHelper.cs +++ b/src/WireMock.Net/Http/HttpClientHelper.cs @@ -2,17 +2,24 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using WireMock.Validation; namespace WireMock.Http { internal static class HttpClientHelper { - private static HttpClient CreateHttpClient(string clientX509Certificate2ThumbprintOrSubjectName = null) + public static HttpClient CreateHttpClient(string clientX509Certificate2ThumbprintOrSubjectName = null) { - if (!string.IsNullOrEmpty(clientX509Certificate2ThumbprintOrSubjectName)) + HttpClientHandler handler; + + if (string.IsNullOrEmpty(clientX509Certificate2ThumbprintOrSubjectName)) + { + handler = new HttpClientHandler(); + } + else { #if NETSTANDARD || NET46 - var handler = new HttpClientHandler + handler = new HttpClientHandler { ClientCertificateOptions = ClientCertificateOption.Manual, SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls, @@ -22,7 +29,8 @@ private static HttpClient CreateHttpClient(string clientX509Certificate2Thumbpri var x509Certificate2 = CertificateUtil.GetCertificate(clientX509Certificate2ThumbprintOrSubjectName); handler.ClientCertificates.Add(x509Certificate2); #else - var handler = new WebRequestHandler + + var webRequestHandler = new WebRequestHandler { ClientCertificateOptions = ClientCertificateOption.Manual, ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true, @@ -30,38 +38,51 @@ private static HttpClient CreateHttpClient(string clientX509Certificate2Thumbpri }; var x509Certificate2 = CertificateUtil.GetCertificate(clientX509Certificate2ThumbprintOrSubjectName); - handler.ClientCertificates.Add(x509Certificate2); - return new HttpClient(handler); + webRequestHandler.ClientCertificates.Add(x509Certificate2); + handler = webRequestHandler; #endif } - return new HttpClient(); + // For proxy we shouldn't follow auto redirects + handler.AllowAutoRedirect = false; + // If UseCookies enabled, httpClient ignores Cookie header + handler.UseCookies = false; + + return new HttpClient(handler); } - public static async Task SendAsync(RequestMessage requestMessage, string url, string clientX509Certificate2ThumbprintOrSubjectName = null) + public static async Task SendAsync(HttpClient client, RequestMessage requestMessage, string url) { - var client = CreateHttpClient(clientX509Certificate2ThumbprintOrSubjectName); + Check.NotNull(client, nameof(client)); + + var originalUri = new Uri(requestMessage.Url); + var requiredUri = new Uri(url); var httpRequestMessage = new HttpRequestMessage(new HttpMethod(requestMessage.Method), url); + // Set Body if present + if (requestMessage.BodyAsBytes != null && requestMessage.BodyAsBytes.Length > 0) + { + httpRequestMessage.Content = new ByteArrayContent(requestMessage.BodyAsBytes); + } + // Overwrite the host header - httpRequestMessage.Headers.Host = new Uri(url).Authority; + httpRequestMessage.Headers.Host = requiredUri.Authority; // Set headers if present if (requestMessage.Headers != null) { - foreach (var headerName in requestMessage.Headers.Keys.Where(k => k.ToUpper() != "HOST")) + foreach (var header in requestMessage.Headers.Where(header => !string.Equals(header.Key, "HOST", StringComparison.OrdinalIgnoreCase))) { - httpRequestMessage.Headers.TryAddWithoutValidation(headerName, requestMessage.Headers[headerName]); + // Try to add to request headers. If failed - try to add to content headers + if (!httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value) + && httpRequestMessage.Content != null) + { + httpRequestMessage.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); + } } } - // Set Body if present - if (requestMessage.BodyAsBytes != null && requestMessage.BodyAsBytes.Length > 0) - { - httpRequestMessage.Content = new ByteArrayContent(requestMessage.BodyAsBytes); - } - // Call the URL var httpResponseMessage = await client.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseContentRead); @@ -74,9 +95,26 @@ public static async Task SendAsync(RequestMessage requestMessag Body = await httpResponseMessage.Content.ReadAsStringAsync() }; - foreach (var header in httpResponseMessage.Headers) + // Set both content and response headers, replacing URLs in values + var headers = httpResponseMessage.Content != null + ? httpResponseMessage.Content.Headers.Union(httpResponseMessage.Headers) + : httpResponseMessage.Headers; + + foreach (var header in headers) { - responseMessage.AddHeader(header.Key, header.Value.FirstOrDefault()); + // if Location header contains absolute redirect URL, and base URL is one that we proxy to, + // we need to replace it to original one + if (string.Equals(header.Key, "Location", StringComparison.OrdinalIgnoreCase) + && Uri.TryCreate(header.Value.First(), UriKind.Absolute, out Uri absoluteLocationUri) + && string.Equals(absoluteLocationUri.Host, requiredUri.Host, StringComparison.OrdinalIgnoreCase)) + { + var replacedLocationUri = new Uri(originalUri, absoluteLocationUri.PathAndQuery); + responseMessage.AddHeader(header.Key, replacedLocationUri.ToString()); + } + else + { + responseMessage.AddHeader(header.Key, header.Value.ToArray()); + } } return responseMessage; diff --git a/src/WireMock.Net/ResponseBuilders/Response.cs b/src/WireMock.Net/ResponseBuilders/Response.cs index 53f7152d4..34db3f53e 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; @@ -19,6 +20,8 @@ namespace WireMock.ResponseBuilders /// public class Response : IResponseBuilder { + private HttpClient httpClientForProxy; + /// /// The delay /// @@ -317,6 +320,7 @@ public IResponseBuilder WithProxy(string proxyUrl, string clientX509Certificate2 ProxyUrl = proxyUrl; X509Certificate2ThumbprintOrSubjectName = clientX509Certificate2ThumbprintOrSubjectName; + httpClientForProxy = HttpClientHelper.CreateHttpClient(clientX509Certificate2ThumbprintOrSubjectName); return this; } @@ -332,12 +336,12 @@ public async Task ProvideResponseAsync(RequestMessage requestMe if (Delay != null) await Task.Delay(Delay.Value); - if (ProxyUrl != null) + if (ProxyUrl != null && httpClientForProxy != null) { var requestUri = new Uri(requestMessage.Url); var proxyUri = new Uri(ProxyUrl); var proxyUriWithRequestPathAndQuery = new Uri(proxyUri, requestUri.PathAndQuery); - return await HttpClientHelper.SendAsync(requestMessage, proxyUriWithRequestPathAndQuery.AbsoluteUri, X509Certificate2ThumbprintOrSubjectName); + return await HttpClientHelper.SendAsync(httpClientForProxy, requestMessage, proxyUriWithRequestPathAndQuery.AbsoluteUri); } if (UseTransformer) diff --git a/src/WireMock.Net/Server/FluentMockServer.Admin.cs b/src/WireMock.Net/Server/FluentMockServer.Admin.cs index 5081c4e8c..a4bf2ad28 100644 --- a/src/WireMock.Net/Server/FluentMockServer.Admin.cs +++ b/src/WireMock.Net/Server/FluentMockServer.Admin.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; @@ -129,8 +130,11 @@ private void InitAdmin() } #region Proxy and Record + private HttpClient httpClientForProxy; + private void InitProxyAndRecord(ProxyAndRecordSettings settings) { + httpClientForProxy = HttpClientHelper.CreateHttpClient(settings.X509Certificate2ThumbprintOrSubjectName); Given(Request.Create().WithPath("/*").UsingAnyVerb()).RespondWith(new ProxyAsyncResponseProvider(ProxyAndRecordAsync, settings)); } @@ -140,7 +144,7 @@ private async Task ProxyAndRecordAsync(RequestMessage requestMe var proxyUri = new Uri(settings.Url); var proxyUriWithRequestPathAndQuery = new Uri(proxyUri, requestUri.PathAndQuery); - var responseMessage = await HttpClientHelper.SendAsync(requestMessage, proxyUriWithRequestPathAndQuery.AbsoluteUri, settings.X509Certificate2ThumbprintOrSubjectName); + var responseMessage = await HttpClientHelper.SendAsync(httpClientForProxy, requestMessage, proxyUriWithRequestPathAndQuery.AbsoluteUri); if (settings.SaveMapping) { diff --git a/test/WireMock.Net.Tests/FluentMockServerTests.cs b/test/WireMock.Net.Tests/FluentMockServerTests.cs index cb8b31ae5..28d688674 100644 --- a/test/WireMock.Net.Tests/FluentMockServerTests.cs +++ b/test/WireMock.Net.Tests/FluentMockServerTests.cs @@ -4,13 +4,14 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Threading.Tasks; using NFluent; -using Xunit; using WireMock.Matchers; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; +using Xunit; namespace WireMock.Net.Tests { @@ -19,6 +20,7 @@ namespace WireMock.Net.Tests public class FluentMockServerTests : IDisposable { private FluentMockServer _server; + private FluentMockServer _serverForProxyForwarding; // For for AppVeyor + OpenCover private string GetCurrentFolder() @@ -386,6 +388,128 @@ public async Task Should_proxy_responses() Check.That(result).Contains("google"); } + [Fact] + public async Task Should_preserve_content_header_in_proxied_request() + { + // given + _serverForProxyForwarding = FluentMockServer.Start(); + _serverForProxyForwarding + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create()); + + _server = FluentMockServer.Start(); + _server + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create().WithProxy(_serverForProxyForwarding.Urls[0])); + + // when + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri(_server.Urls[0]), + Content = new StringContent("stringContent") + }; + requestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + await new HttpClient().SendAsync(requestMessage); + + // then + var receivedRequest = _serverForProxyForwarding.LogEntries.First().RequestMessage; + Check.That(receivedRequest.Body).IsEqualTo("stringContent"); + Check.That(receivedRequest.Headers).ContainsKey("Content-Type"); + Check.That(receivedRequest.Headers["Content-Type"]).ContainsExactly(new[] { "text/plain" }); + } + + [Fact] + public async Task Should_preserve_content_header_in_proxied_response() + { + // given + _serverForProxyForwarding = FluentMockServer.Start(); + _serverForProxyForwarding + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create() + .WithBody("body") + .WithHeader("Content-Type", "text/plain")); + + _server = FluentMockServer.Start(); + _server + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create().WithProxy(_serverForProxyForwarding.Urls[0])); + + // when + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(_server.Urls[0]) + }; + var response = await new HttpClient().SendAsync(requestMessage); + + // then + Check.That(await response.Content.ReadAsStringAsync()).IsEqualTo("body"); + Check.That(response.Content.Headers.Contains("Content-Type")).IsTrue(); + Check.That(response.Content.Headers.GetValues("Content-Type")).ContainsExactly(new[] { "text/plain" }); + } + + [Fact] + public async Task Should_change_absolute_location_header_in_proxied_response() + { + // given + _serverForProxyForwarding = FluentMockServer.Start(); + _serverForProxyForwarding + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create() + .WithStatusCode(HttpStatusCode.Redirect) + .WithHeader("Location", _serverForProxyForwarding.Urls[0] + "testpath")); + + _server = FluentMockServer.Start(); + _server + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create().WithProxy(_serverForProxyForwarding.Urls[0])); + + // when + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(_server.Urls[0]) + }; + var httpClientHandler = new HttpClientHandler { AllowAutoRedirect = false }; + var response = await new HttpClient(httpClientHandler).SendAsync(requestMessage); + + // then + Check.That(response.Headers.Contains("Location")).IsTrue(); + Check.That(response.Headers.GetValues("Location")).ContainsExactly(new[] { _server.Urls[0] + "testpath" }); + } + + [Fact] + public async Task Should_preserve_cookie_header_in_proxied_request() + { + // given + _serverForProxyForwarding = FluentMockServer.Start(); + _serverForProxyForwarding + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create()); + + _server = FluentMockServer.Start(); + _server + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create().WithProxy(_serverForProxyForwarding.Urls[0])); + + // when + var requestUri = new Uri(_server.Urls[0]); + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = requestUri + }; + var clientHandler = new HttpClientHandler(); + clientHandler.CookieContainer.Add(requestUri, new Cookie("name", "value")); + await new HttpClient(clientHandler).SendAsync(requestMessage); + + // then + var receivedRequest = _serverForProxyForwarding.LogEntries.First().RequestMessage; + Check.That(receivedRequest.Cookies).IsNotNull(); + Check.That(receivedRequest.Cookies).ContainsPair("name", "value"); + } + //Leaving commented as this requires an actual certificate with password, along with a service that expects a client certificate //[Fact] //public async Task Should_proxy_responses_with_client_certificate() @@ -429,6 +553,7 @@ public async Task FluentMockServer_Logging_SetMaxRequestLogCount() public void Dispose() { _server?.Stop(); + _serverForProxyForwarding?.Stop(); } } } \ No newline at end of file From 9789b98304ddb7b51d4ecf2e47a6400c14418ebf Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 28 Oct 2017 18:59:11 +0200 Subject: [PATCH 2/2] Small refactor and moved proxy tests to new test-file. --- WireMock.Net Solution.sln | 5 +- ...eMock.Net.Console.Record.NETCoreApp.csproj | 4 + .../791a3f31-6946-4ce7-8e6f-0237c7443275.json | 36 ---- .../ab38efae-4e4d-4f20-8afe-635533ec2535.json | 41 ----- src/WireMock.Net/Http/HttpClientHelper.cs | 12 +- .../FluentMockServerTests.Proxy.cs | 157 +++++++++++++++++ .../FluentMockServerTests.cs | 164 ++---------------- 7 files changed, 181 insertions(+), 238 deletions(-) delete mode 100644 examples/WireMock.Net.Console.Record.NETCoreApp/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json delete mode 100644 examples/WireMock.Net.Console.Record.NETCoreApp/__admin/mappings/ab38efae-4e4d-4f20-8afe-635533ec2535.json create mode 100644 test/WireMock.Net.Tests/FluentMockServerTests.Proxy.cs diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln index 60f552d1a..4c4c83599 100644 --- a/WireMock.Net Solution.sln +++ b/WireMock.Net Solution.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27004.2002 +VisualStudioVersion = 15.0.27004.2005 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EF242EDF-7133-4277-9A0C-18744DE08707}" EndProject @@ -14,6 +14,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{F0C22C47-DF71-463C-9B04-B4E0F3B8708A}" + ProjectSection(SolutionItems) = preProject + examples\WireMock.Net.Console.Record.NETCoreApp\__admin\mappings\ab38efae-4e4d-4f20-8afe-635533ec2535.json = examples\WireMock.Net.Console.Record.NETCoreApp\__admin\mappings\ab38efae-4e4d-4f20-8afe-635533ec2535.json + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{890A1DED-C229-4FA1-969E-AAC3BBFC05E5}" EndProject diff --git a/examples/WireMock.Net.Console.Record.NETCoreApp/WireMock.Net.Console.Record.NETCoreApp.csproj b/examples/WireMock.Net.Console.Record.NETCoreApp/WireMock.Net.Console.Record.NETCoreApp.csproj index 61a866e77..7e9abd17e 100644 --- a/examples/WireMock.Net.Console.Record.NETCoreApp/WireMock.Net.Console.Record.NETCoreApp.csproj +++ b/examples/WireMock.Net.Console.Record.NETCoreApp/WireMock.Net.Console.Record.NETCoreApp.csproj @@ -15,4 +15,8 @@ + + + + \ No newline at end of file diff --git a/examples/WireMock.Net.Console.Record.NETCoreApp/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json b/examples/WireMock.Net.Console.Record.NETCoreApp/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json deleted file mode 100644 index f360d25de..000000000 --- a/examples/WireMock.Net.Console.Record.NETCoreApp/__admin/mappings/791a3f31-6946-4ce7-8e6f-0237c7443275.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "Guid": "791a3f31-6946-4ce7-8e6f-0237c7443275", - "Title": "", - "Priority": 0, - "Request": { - "Path": { - "Matchers": [ - { - "Name": "WildcardMatcher", - "Pattern": "/proxy-google-test-post" - } - ] - }, - "Methods": [ - "post" - ], - "Body": {} - }, - "Response": { - "StatusCode": 404, - "Body": "\n\n \n \n Error 404 (Not Found)!!1\n \n \n

404. That’s an error.\n

The requested URL /proxy-google-test-post was not found on this server. That’s all we know.\n", - "BodyAsBytes": "PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ZW4+CiAgPG1ldGEgY2hhcnNldD11dGYtOD4KICA8bWV0YSBuYW1lPXZpZXdwb3J0IGNvbnRlbnQ9ImluaXRpYWwtc2NhbGU9MSwgbWluaW11bS1zY2FsZT0xLCB3aWR0aD1kZXZpY2Utd2lkdGgiPgogIDx0aXRsZT5FcnJvciA0MDQgKE5vdCBGb3VuZCkhITE8L3RpdGxlPgogIDxzdHlsZT4KICAgICp7bWFyZ2luOjA7cGFkZGluZzowfWh0bWwsY29kZXtmb250OjE1cHgvMjJweCBhcmlhbCxzYW5zLXNlcmlmfWh0bWx7YmFja2dyb3VuZDojZmZmO2NvbG9yOiMyMjI7cGFkZGluZzoxNXB4fWJvZHl7bWFyZ2luOjclIGF1dG8gMDttYXgtd2lkdGg6MzkwcHg7bWluLWhlaWdodDoxODBweDtwYWRkaW5nOjMwcHggMCAxNXB4fSogPiBib2R5e2JhY2tncm91bmQ6dXJsKC8vd3d3Lmdvb2dsZS5jb20vaW1hZ2VzL2Vycm9ycy9yb2JvdC5wbmcpIDEwMCUgNXB4IG5vLXJlcGVhdDtwYWRkaW5nLXJpZ2h0OjIwNXB4fXB7bWFyZ2luOjExcHggMCAyMnB4O292ZXJmbG93OmhpZGRlbn1pbnN7Y29sb3I6Izc3Nzt0ZXh0LWRlY29yYXRpb246bm9uZX1hIGltZ3tib3JkZXI6MH1AbWVkaWEgc2NyZWVuIGFuZCAobWF4LXdpZHRoOjc3MnB4KXtib2R5e2JhY2tncm91bmQ6bm9uZTttYXJnaW4tdG9wOjA7bWF4LXdpZHRoOm5vbmU7cGFkZGluZy1yaWdodDowfX0jbG9nb3tiYWNrZ3JvdW5kOnVybCgvL3d3dy5nb29nbGUuY29tL2ltYWdlcy9icmFuZGluZy9nb29nbGVsb2dvLzF4L2dvb2dsZWxvZ29fY29sb3JfMTUweDU0ZHAucG5nKSBuby1yZXBlYXQ7bWFyZ2luLWxlZnQ6LTVweH1AbWVkaWEgb25seSBzY3JlZW4gYW5kIChtaW4tcmVzb2x1dGlvbjoxOTJkcGkpeyNsb2dve2JhY2tncm91bmQ6dXJsKC8vd3d3Lmdvb2dsZS5jb20vaW1hZ2VzL2JyYW5kaW5nL2dvb2dsZWxvZ28vMngvZ29vZ2xlbG9nb19jb2xvcl8xNTB4NTRkcC5wbmcpIG5vLXJlcGVhdCAwJSAwJS8xMDAlIDEwMCU7LW1vei1ib3JkZXItaW1hZ2U6dXJsKC8vd3d3Lmdvb2dsZS5jb20vaW1hZ2VzL2JyYW5kaW5nL2dvb2dsZWxvZ28vMngvZ29vZ2xlbG9nb19jb2xvcl8xNTB4NTRkcC5wbmcpIDB9fUBtZWRpYSBvbmx5IHNjcmVlbiBhbmQgKC13ZWJraXQtbWluLWRldmljZS1waXhlbC1yYXRpbzoyKXsjbG9nb3tiYWNrZ3JvdW5kOnVybCgvL3d3dy5nb29nbGUuY29tL2ltYWdlcy9icmFuZGluZy9nb29nbGVsb2dvLzJ4L2dvb2dsZWxvZ29fY29sb3JfMTUweDU0ZHAucG5nKSBuby1yZXBlYXQ7LXdlYmtpdC1iYWNrZ3JvdW5kLXNpemU6MTAwJSAxMDAlfX0jbG9nb3tkaXNwbGF5OmlubGluZS1ibG9jaztoZWlnaHQ6NTRweDt3aWR0aDoxNTBweH0KICA8L3N0eWxlPgogIDxhIGhyZWY9Ly93d3cuZ29vZ2xlLmNvbS8+PHNwYW4gaWQ9bG9nbyBhcmlhLWxhYmVsPUdvb2dsZT48L3NwYW4+PC9hPgogIDxwPjxiPjQwNC48L2I+IDxpbnM+VGhhdOKAmXMgYW4gZXJyb3IuPC9pbnM+CiAgPHA+VGhlIHJlcXVlc3RlZCBVUkwgPGNvZGU+L3Byb3h5LWdvb2dsZS10ZXN0LXBvc3Q8L2NvZGU+IHdhcyBub3QgZm91bmQgb24gdGhpcyBzZXJ2ZXIuICA8aW5zPlRoYXTigJlzIGFsbCB3ZSBrbm93LjwvaW5zPgo=", - "BodyEncoding": { - "CodePage": 65001, - "EncodingName": "Unicode (UTF-8)", - "WebName": "utf-8" - }, - "UseTransformer": false, - "Headers": { - "Date": "Wed, 25 Oct 2017 18:57:40 GMT", - "Alt-Svc": "quic=\":443\"; ma=2592000; v=\"39,38,37,35\"", - "Referrer-Policy": "no-referrer", - "Connection": "close" - } - } -} \ No newline at end of file diff --git a/examples/WireMock.Net.Console.Record.NETCoreApp/__admin/mappings/ab38efae-4e4d-4f20-8afe-635533ec2535.json b/examples/WireMock.Net.Console.Record.NETCoreApp/__admin/mappings/ab38efae-4e4d-4f20-8afe-635533ec2535.json deleted file mode 100644 index 8f3d57112..000000000 --- a/examples/WireMock.Net.Console.Record.NETCoreApp/__admin/mappings/ab38efae-4e4d-4f20-8afe-635533ec2535.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "Guid": "ab38efae-4e4d-4f20-8afe-635533ec2535", - "Title": "", - "Priority": 0, - "Request": { - "Path": { - "Matchers": [ - { - "Name": "WildcardMatcher", - "Pattern": "/earth/story/20170510-terrifying-20m-tall-rogue-waves-are-actually-real" - } - ] - }, - "Methods": [ - "get" - ] - }, - "Response": { - "StatusCode": 200, - "Body": " BBC - Homepage \n\n\n\n\n\n\n \n\n\n\n\n \n \n\n\n\n \n\n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n\n \n \n \n\n \n \n \n \n \n \n \n\n \r\n\r\n\r\n \n \n

\n

Accessibility links

\n\n \n Notifications\n \n \n \n \n
Search\n\n
\n
\n \n \n \n \n \n
\n
\n\n\n
\n\n\n\n\n

BBC Homepage

\n\n
\n \n
\n

 

\n

\n News\n

\n

\n Sport\n

\n
\r\n
\r\n

\n Editor’s Picks\n

\n

\n Featured video\n

\n
\n \n
\n
\n
\"'Attention!
\n\n \n \n
\n\n

\n \n 'Attention! You are next to 15 great white sharks' \n

\n\n

\n A sheriff's helicopter crew warns paddle-boarders they are swimming with sharks off the California coast.

\n \n US\n \n
\n\n \n 'Attention! You are next to 15 great white sharks' \n
\n\n \n
\r\n

\n More from around the BBC\n

\n

\n BBC in other languages\n

\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n \n\n \n ", - "BodyEncoding": { - "CodePage": 65001, - "EncodingName": "Unicode (UTF-8)", - "WebName": "utf-8" - }, - "UseTransformer": false, - "Headers": { - "X-LB-NoCache": "true", - "X-Cache-Age": "0", - "ETag": "\"07931511980b88249a64e043d5f5341d\"", - "Connection": "keep-alive", - "Cache-Control": "max-age=60, private", - "X-PAL-Host": "pal128.back.live.telhc.local:80", - "Date": "Fri, 12 May 2017 20:20:25 GMT", - "Server": "Apache", - "Vary": "X-CDN", - "Set-Cookie": "BBC-UID=c58921a6d13960c8991318b121db82a089b7948f332826838beba493b8b8c4880PostmanRuntime/3.0.11-hotfix.2; expires=Tue, 11-May-21 20:20:25 GMT; path=/; domain=.bbc.com", - "X-Cache-Action": "MISS" - } - } -} \ No newline at end of file diff --git a/src/WireMock.Net/Http/HttpClientHelper.cs b/src/WireMock.Net/Http/HttpClientHelper.cs index e2289cbe4..224410cb0 100644 --- a/src/WireMock.Net/Http/HttpClientHelper.cs +++ b/src/WireMock.Net/Http/HttpClientHelper.cs @@ -45,6 +45,7 @@ public static HttpClient CreateHttpClient(string clientX509Certificate2Thumbprin // For proxy we shouldn't follow auto redirects handler.AllowAutoRedirect = false; + // If UseCookies enabled, httpClient ignores Cookie header handler.UseCookies = false; @@ -75,10 +76,9 @@ public static async Task SendAsync(HttpClient client, RequestMe foreach (var header in requestMessage.Headers.Where(header => !string.Equals(header.Key, "HOST", StringComparison.OrdinalIgnoreCase))) { // Try to add to request headers. If failed - try to add to content headers - if (!httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value) - && httpRequestMessage.Content != null) + if (!httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value)) { - httpRequestMessage.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); + httpRequestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value); } } } @@ -96,14 +96,12 @@ public static async Task SendAsync(HttpClient client, RequestMe }; // Set both content and response headers, replacing URLs in values - var headers = httpResponseMessage.Content != null - ? httpResponseMessage.Content.Headers.Union(httpResponseMessage.Headers) - : httpResponseMessage.Headers; + var headers = httpResponseMessage.Content?.Headers.Union(httpResponseMessage.Headers); foreach (var header in headers) { // if Location header contains absolute redirect URL, and base URL is one that we proxy to, - // we need to replace it to original one + // we need to replace it to original one. if (string.Equals(header.Key, "Location", StringComparison.OrdinalIgnoreCase) && Uri.TryCreate(header.Value.First(), UriKind.Absolute, out Uri absoluteLocationUri) && string.Equals(absoluteLocationUri.Host, requiredUri.Host, StringComparison.OrdinalIgnoreCase)) diff --git a/test/WireMock.Net.Tests/FluentMockServerTests.Proxy.cs b/test/WireMock.Net.Tests/FluentMockServerTests.Proxy.cs new file mode 100644 index 000000000..ab63b69db --- /dev/null +++ b/test/WireMock.Net.Tests/FluentMockServerTests.Proxy.cs @@ -0,0 +1,157 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using NFluent; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; +using Xunit; + +namespace WireMock.Net.Tests +{ + public partial class FluentMockServerTests + { + private FluentMockServer _serverForProxyForwarding; + + [Fact] + public async Task FluentMockServer_Should_proxy_responses() + { + // given + _server = FluentMockServer.Start(); + _server + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create().WithProxy("http://www.google.com")); + + // when + var result = await new HttpClient().GetStringAsync("http://localhost:" + _server.Ports[0] + "/search?q=test"); + + // then + Check.That(result).Contains("google"); + } + + [Fact] + public async Task FluentMockServer_Should_preserve_content_header_in_proxied_request() + { + // given + _serverForProxyForwarding = FluentMockServer.Start(); + _serverForProxyForwarding + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create()); + + _server = FluentMockServer.Start(); + _server + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create().WithProxy(_serverForProxyForwarding.Urls[0])); + + // when + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri(_server.Urls[0]), + Content = new StringContent("stringContent") + }; + requestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); + await new HttpClient().SendAsync(requestMessage); + + // then + var receivedRequest = _serverForProxyForwarding.LogEntries.First().RequestMessage; + Check.That(receivedRequest.Body).IsEqualTo("stringContent"); + Check.That(receivedRequest.Headers).ContainsKey("Content-Type"); + Check.That(receivedRequest.Headers["Content-Type"]).ContainsExactly("text/plain"); + } + + [Fact] + public async Task FluentMockServer_Should_preserve_content_header_in_proxied_response() + { + // given + _serverForProxyForwarding = FluentMockServer.Start(); + _serverForProxyForwarding + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create() + .WithBody("body") + .WithHeader("Content-Type", "text/plain")); + + _server = FluentMockServer.Start(); + _server + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create().WithProxy(_serverForProxyForwarding.Urls[0])); + + // when + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(_server.Urls[0]) + }; + var response = await new HttpClient().SendAsync(requestMessage); + + // then + Check.That(await response.Content.ReadAsStringAsync()).IsEqualTo("body"); + Check.That(response.Content.Headers.Contains("Content-Type")).IsTrue(); + Check.That(response.Content.Headers.GetValues("Content-Type")).ContainsExactly("text/plain"); + } + + [Fact] + public async Task FluentMockServer_Should_change_absolute_location_header_in_proxied_response() + { + // given + _serverForProxyForwarding = FluentMockServer.Start(); + _serverForProxyForwarding + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create() + .WithStatusCode(HttpStatusCode.Redirect) + .WithHeader("Location", _serverForProxyForwarding.Urls[0] + "testpath")); + + _server = FluentMockServer.Start(); + _server + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create().WithProxy(_serverForProxyForwarding.Urls[0])); + + // when + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = new Uri(_server.Urls[0]) + }; + var httpClientHandler = new HttpClientHandler { AllowAutoRedirect = false }; + var response = await new HttpClient(httpClientHandler).SendAsync(requestMessage); + + // then + Check.That(response.Headers.Contains("Location")).IsTrue(); + Check.That(response.Headers.GetValues("Location")).ContainsExactly(_server.Urls[0] + "testpath"); + } + + [Fact] + public async Task FluentMockServer_Should_preserve_cookie_header_in_proxied_request() + { + // given + _serverForProxyForwarding = FluentMockServer.Start(); + _serverForProxyForwarding + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create()); + + _server = FluentMockServer.Start(); + _server + .Given(Request.Create().WithPath("/*")) + .RespondWith(Response.Create().WithProxy(_serverForProxyForwarding.Urls[0])); + + // when + var requestUri = new Uri(_server.Urls[0]); + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Get, + RequestUri = requestUri + }; + var clientHandler = new HttpClientHandler(); + clientHandler.CookieContainer.Add(requestUri, new Cookie("name", "value")); + await new HttpClient(clientHandler).SendAsync(requestMessage); + + // then + var receivedRequest = _serverForProxyForwarding.LogEntries.First().RequestMessage; + Check.That(receivedRequest.Cookies).IsNotNull(); + Check.That(receivedRequest.Cookies).ContainsPair("name", "value"); + } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/FluentMockServerTests.cs b/test/WireMock.Net.Tests/FluentMockServerTests.cs index 28d688674..343256d0e 100644 --- a/test/WireMock.Net.Tests/FluentMockServerTests.cs +++ b/test/WireMock.Net.Tests/FluentMockServerTests.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading.Tasks; using NFluent; using WireMock.Matchers; @@ -15,12 +14,9 @@ namespace WireMock.Net.Tests { - //[TestFixture] - //[Timeout(5000)] - public class FluentMockServerTests : IDisposable + public partial class FluentMockServerTests : IDisposable { private FluentMockServer _server; - private FluentMockServer _serverForProxyForwarding; // For for AppVeyor + OpenCover private string GetCurrentFolder() @@ -180,7 +176,7 @@ public async Task FluentMockServer_Admin_Requests_Get() } [Fact] - public async Task Should_respond_to_request_bodyAsString() + public async Task FluentMockServer_Should_respond_to_request_bodyAsString() { // given _server = FluentMockServer.Start(); @@ -201,7 +197,7 @@ public async Task Should_respond_to_request_bodyAsString() } [Fact] - public async Task Should_respond_to_request_bodyAsBase64() + public async Task FluentMockServer_Should_respond_to_request_bodyAsBase64() { // given _server = FluentMockServer.Start(); @@ -216,7 +212,7 @@ public async Task Should_respond_to_request_bodyAsBase64() } [Fact] - public async Task Should_respond_to_request_bodyAsBytes() + public async Task FluentMockServer_Should_respond_to_request_bodyAsBytes() { // given _server = FluentMockServer.Start(); @@ -233,7 +229,7 @@ public async Task Should_respond_to_request_bodyAsBytes() } [Fact] - public async Task Should_respond_404_for_unexpected_request() + public async Task FluentMockServer_Should_respond_404_for_unexpected_request() { // given _server = FluentMockServer.Start(); @@ -247,7 +243,7 @@ public async Task Should_respond_404_for_unexpected_request() } [Fact] - public async Task Should_find_a_request_satisfying_a_request_spec() + public async Task FluentMockServer_Should_find_a_request_satisfying_a_request_spec() { // given _server = FluentMockServer.Start(); @@ -266,7 +262,7 @@ public async Task Should_find_a_request_satisfying_a_request_spec() } [Fact] - public async Task Should_reset_requestlogs() + public async Task FluentMockServer_Should_reset_requestlogs() { // given _server = FluentMockServer.Start(); @@ -280,7 +276,7 @@ public async Task Should_reset_requestlogs() } [Fact] - public void Should_reset_mappings() + public void FluentMockServer_Should_reset_mappings() { // given _server = FluentMockServer.Start(); @@ -302,7 +298,7 @@ public void Should_reset_mappings() } [Fact] - public async Task Should_respond_a_redirect_without_body() + public async Task FluentMockServer_Should_respond_a_redirect_without_body() { // given _server = FluentMockServer.Start(); @@ -330,7 +326,7 @@ public async Task Should_respond_a_redirect_without_body() } [Fact] - public async Task Should_delay_responses_for_a_given_route() + public async Task FluentMockServer_Should_delay_responses_for_a_given_route() { // given _server = FluentMockServer.Start(); @@ -353,7 +349,7 @@ public async Task Should_delay_responses_for_a_given_route() } [Fact] - public async Task Should_delay_responses() + public async Task FluentMockServer_Should_delay_responses() { // given _server = FluentMockServer.Start(); @@ -372,144 +368,6 @@ public async Task Should_delay_responses() Check.That(watch.ElapsedMilliseconds).IsStrictlyGreaterThan(200); } - [Fact] - public async Task Should_proxy_responses() - { - // given - _server = FluentMockServer.Start(); - _server - .Given(Request.Create().WithPath("/*")) - .RespondWith(Response.Create().WithProxy("http://www.google.com")); - - // when - var result = await new HttpClient().GetStringAsync("http://localhost:" + _server.Ports[0] + "/search?q=test"); - - // then - Check.That(result).Contains("google"); - } - - [Fact] - public async Task Should_preserve_content_header_in_proxied_request() - { - // given - _serverForProxyForwarding = FluentMockServer.Start(); - _serverForProxyForwarding - .Given(Request.Create().WithPath("/*")) - .RespondWith(Response.Create()); - - _server = FluentMockServer.Start(); - _server - .Given(Request.Create().WithPath("/*")) - .RespondWith(Response.Create().WithProxy(_serverForProxyForwarding.Urls[0])); - - // when - var requestMessage = new HttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri(_server.Urls[0]), - Content = new StringContent("stringContent") - }; - requestMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); - await new HttpClient().SendAsync(requestMessage); - - // then - var receivedRequest = _serverForProxyForwarding.LogEntries.First().RequestMessage; - Check.That(receivedRequest.Body).IsEqualTo("stringContent"); - Check.That(receivedRequest.Headers).ContainsKey("Content-Type"); - Check.That(receivedRequest.Headers["Content-Type"]).ContainsExactly(new[] { "text/plain" }); - } - - [Fact] - public async Task Should_preserve_content_header_in_proxied_response() - { - // given - _serverForProxyForwarding = FluentMockServer.Start(); - _serverForProxyForwarding - .Given(Request.Create().WithPath("/*")) - .RespondWith(Response.Create() - .WithBody("body") - .WithHeader("Content-Type", "text/plain")); - - _server = FluentMockServer.Start(); - _server - .Given(Request.Create().WithPath("/*")) - .RespondWith(Response.Create().WithProxy(_serverForProxyForwarding.Urls[0])); - - // when - var requestMessage = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri(_server.Urls[0]) - }; - var response = await new HttpClient().SendAsync(requestMessage); - - // then - Check.That(await response.Content.ReadAsStringAsync()).IsEqualTo("body"); - Check.That(response.Content.Headers.Contains("Content-Type")).IsTrue(); - Check.That(response.Content.Headers.GetValues("Content-Type")).ContainsExactly(new[] { "text/plain" }); - } - - [Fact] - public async Task Should_change_absolute_location_header_in_proxied_response() - { - // given - _serverForProxyForwarding = FluentMockServer.Start(); - _serverForProxyForwarding - .Given(Request.Create().WithPath("/*")) - .RespondWith(Response.Create() - .WithStatusCode(HttpStatusCode.Redirect) - .WithHeader("Location", _serverForProxyForwarding.Urls[0] + "testpath")); - - _server = FluentMockServer.Start(); - _server - .Given(Request.Create().WithPath("/*")) - .RespondWith(Response.Create().WithProxy(_serverForProxyForwarding.Urls[0])); - - // when - var requestMessage = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = new Uri(_server.Urls[0]) - }; - var httpClientHandler = new HttpClientHandler { AllowAutoRedirect = false }; - var response = await new HttpClient(httpClientHandler).SendAsync(requestMessage); - - // then - Check.That(response.Headers.Contains("Location")).IsTrue(); - Check.That(response.Headers.GetValues("Location")).ContainsExactly(new[] { _server.Urls[0] + "testpath" }); - } - - [Fact] - public async Task Should_preserve_cookie_header_in_proxied_request() - { - // given - _serverForProxyForwarding = FluentMockServer.Start(); - _serverForProxyForwarding - .Given(Request.Create().WithPath("/*")) - .RespondWith(Response.Create()); - - _server = FluentMockServer.Start(); - _server - .Given(Request.Create().WithPath("/*")) - .RespondWith(Response.Create().WithProxy(_serverForProxyForwarding.Urls[0])); - - // when - var requestUri = new Uri(_server.Urls[0]); - var requestMessage = new HttpRequestMessage - { - Method = HttpMethod.Get, - RequestUri = requestUri - }; - var clientHandler = new HttpClientHandler(); - clientHandler.CookieContainer.Add(requestUri, new Cookie("name", "value")); - await new HttpClient(clientHandler).SendAsync(requestMessage); - - // then - var receivedRequest = _serverForProxyForwarding.LogEntries.First().RequestMessage; - Check.That(receivedRequest.Cookies).IsNotNull(); - Check.That(receivedRequest.Cookies).ContainsPair("name", "value"); - } - //Leaving commented as this requires an actual certificate with password, along with a service that expects a client certificate //[Fact] //public async Task Should_proxy_responses_with_client_certificate()