From 49ce2f0dfb3dd29a0af634bc49d9795571de9f49 Mon Sep 17 00:00:00 2001 From: phillee007 Date: Fri, 16 Jun 2017 21:36:10 +1200 Subject: [PATCH] [Feature] Add support for client certificate password and test with real services that require client certificate auth (#32) * Add support for client certificate password and test with some real services that require client certificates. Also update so when proxying, the path and query will be passed to the proxied api. * Add correct param * Update to read certificate from store instead of file * Add support for client certificate password and test with some real services that require client certificates. Also update so when proxying, the path and query will be passed to the proxied api. * Add correct param * Fixup PR issues * Add support for client certificate password and test with some real services that require client certificates. Also update so when proxying, the path and query will be passed to the proxied api. * Add correct param * Fixup PR issues --- .../Program.cs | 3 +- .../MainApp.cs | 20 ++++--- src/WireMock.Net.StandAlone/StandAloneApp.cs | 6 +- .../Admin/Mappings/ResponseModel.cs | 5 ++ src/WireMock.Net/Http/CertificateUtil.cs | 44 +++++++++++++++ src/WireMock.Net/Http/HttpClientHelper.cs | 56 ++++++++++++------- .../ResponseBuilders/IProxyResponseBuilder.cs | 4 +- src/WireMock.Net/ResponseBuilders/Response.cs | 19 ++++--- .../Server/FluentMockServer.Admin.cs | 16 +++++- .../Settings/ProxyAndRecordSettings.cs | 4 +- .../FluentMockServerTests.cs | 19 ++++++- 11 files changed, 149 insertions(+), 47 deletions(-) create mode 100644 src/WireMock.Net/Http/CertificateUtil.cs diff --git a/examples/WireMock.Net.Console.Record.NETCoreApp/Program.cs b/examples/WireMock.Net.Console.Record.NETCoreApp/Program.cs index 94943df5..55dc79bf 100644 --- a/examples/WireMock.Net.Console.Record.NETCoreApp/Program.cs +++ b/examples/WireMock.Net.Console.Record.NETCoreApp/Program.cs @@ -10,11 +10,12 @@ namespace WireMock.Net.Console.Record.NETCoreApp { var server = FluentMockServer.Start(new FluentMockServerSettings { - Urls = new[] { "http://localhost:9095/", "https://localhost:9096/" }, + Urls = new[] { "http://localhost:9090/", "https://localhost:9096/" }, StartAdminInterface = true, ProxyAndRecordSettings = new ProxyAndRecordSettings { Url = "https://www.msn.com", + X509Certificate2ThumbprintOrSubjectName = "x3bwbapi-dev.nzlb.service.dev", SaveMapping = true } }); diff --git a/examples/WireMock.Net.ConsoleApplication/MainApp.cs b/examples/WireMock.Net.ConsoleApplication/MainApp.cs index 91bfc912..07f2c27b 100644 --- a/examples/WireMock.Net.ConsoleApplication/MainApp.cs +++ b/examples/WireMock.Net.ConsoleApplication/MainApp.cs @@ -96,17 +96,23 @@ namespace WireMock.Net.ConsoleApplication .RespondWith(Response.Create().WithStatusCode(200).WithBody("partial = 200")); // http://localhost:8080/any/any?start=1000&stop=1&stop=2 + //server + // .Given(Request.Create().WithPath("/*").UsingGet()) + // .WithGuid("90356dba-b36c-469a-a17e-669cd84f1f05") + // .AtPriority(server.Mappings.Count() + 1) + // .RespondWith(Response.Create() + // .WithStatusCode(200) + // .WithHeader("Content-Type", "application/json") + // .WithHeader("Transformed-Postman-Token", "token is {{request.headers.Postman-Token}}") + // .WithBody(@"{""msg"": ""Hello world CATCH-ALL on /*, {{request.path}}, bykey={{request.query.start}}, bykey={{request.query.stop}}, byidx0={{request.query.stop.[0]}}, byidx1={{request.query.stop.[1]}}"" }") + // .WithTransformer() + // .WithDelay(TimeSpan.FromMilliseconds(100)) + // ); server .Given(Request.Create().WithPath("/*").UsingGet()) .WithGuid("90356dba-b36c-469a-a17e-669cd84f1f05") - .AtPriority(server.Mappings.Count() + 1) .RespondWith(Response.Create() - .WithStatusCode(200) - .WithHeader("Content-Type", "application/json") - .WithHeader("Transformed-Postman-Token", "token is {{request.headers.Postman-Token}}") - .WithBody(@"{""msg"": ""Hello world CATCH-ALL on /*, {{request.path}}, bykey={{request.query.start}}, bykey={{request.query.stop}}, byidx0={{request.query.stop.[0]}}, byidx1={{request.query.stop.[1]}}"" }") - .WithTransformer() - .WithDelay(TimeSpan.FromMilliseconds(100)) + .WithProxy("https://semhub-test.lbtest.anznb.co.nz:5200", "D2DBF134A8D06ACCD0E1FAD9B8B28678DF7A9816") ); System.Console.WriteLine("Press any key to stop the server"); diff --git a/src/WireMock.Net.StandAlone/StandAloneApp.cs b/src/WireMock.Net.StandAlone/StandAloneApp.cs index 2d75efa3..a4d3a9d1 100644 --- a/src/WireMock.Net.StandAlone/StandAloneApp.cs +++ b/src/WireMock.Net.StandAlone/StandAloneApp.cs @@ -35,8 +35,8 @@ namespace WireMock.Net.StandAlone [SwitchArgument("SaveProxyMapping", true, Description = "Save the proxied request and response mapping files in ./__admin/mappings. (default set to true).", Optional = true)] public bool SaveMapping { get; set; } - [ValueArgument(typeof(string), "X509Certificate2", Description = "The X509Certificate2 Filename to use.", Optional = true)] - public string X509Certificate2Filename { get; set; } + [ValueArgument(typeof(string), "X509Certificate2ThumbprintOrSubjectName", Description = "The X509Certificate2 Thumbprint or SubjectName to use.", Optional = true)] + public string X509Certificate2ThumbprintOrSubjectName { get; set; } } /// @@ -91,7 +91,7 @@ namespace WireMock.Net.StandAlone { Url = options.ProxyURL, SaveMapping = options.SaveMapping, - X509Certificate2Filename = options.X509Certificate2Filename + X509Certificate2ThumbprintOrSubjectName = options.X509Certificate2ThumbprintOrSubjectName }; } diff --git a/src/WireMock.Net/Admin/Mappings/ResponseModel.cs b/src/WireMock.Net/Admin/Mappings/ResponseModel.cs index bd12064a..21b95542 100644 --- a/src/WireMock.Net/Admin/Mappings/ResponseModel.cs +++ b/src/WireMock.Net/Admin/Mappings/ResponseModel.cs @@ -84,5 +84,10 @@ namespace WireMock.Admin.Mappings /// /// ProxyUrl public string ProxyUrl { get; set; } + + /// + /// The client X509Certificate2 Thumbprint or SubjectName to use. + /// + public string X509Certificate2ThumbprintOrSubjectName { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Http/CertificateUtil.cs b/src/WireMock.Net/Http/CertificateUtil.cs new file mode 100644 index 00000000..66211225 --- /dev/null +++ b/src/WireMock.Net/Http/CertificateUtil.cs @@ -0,0 +1,44 @@ +using System; +using System.Security.Cryptography.X509Certificates; + +namespace WireMock.Http +{ + internal static class CertificateUtil + { + public static X509Certificate2 GetCertificate(string thumbprintOrSubjectName) + { + X509Store certStore = new X509Store(StoreName.My, StoreLocation.LocalMachine); + try + { + //Certificate must be in the local machine store + certStore.Open(OpenFlags.ReadOnly); + + //Attempt to find by thumbprint first + var matchingCertificates = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprintOrSubjectName, false); + if (matchingCertificates.Count == 0) + { + //Fallback to subject name + matchingCertificates = certStore.Certificates.Find(X509FindType.FindBySubjectName, thumbprintOrSubjectName, false); + if (matchingCertificates.Count == 0) + { + // No certificates matched the search criteria. + throw new Exception($"No certificate found with Thumbprint or SubjectName '{thumbprintOrSubjectName}'"); + } + } + // Use the first matching certificate. + return matchingCertificates[0]; + } + finally + { + if (certStore != null) + { +#if NETSTANDARD || NET46 + certStore.Dispose(); +#else + certStore.Close(); +#endif + } + } + } + } +} diff --git a/src/WireMock.Net/Http/HttpClientHelper.cs b/src/WireMock.Net/Http/HttpClientHelper.cs index 0ca2915b..d30fb101 100644 --- a/src/WireMock.Net/Http/HttpClientHelper.cs +++ b/src/WireMock.Net/Http/HttpClientHelper.cs @@ -1,15 +1,19 @@ using System; using System.Linq; +using System.Net; using System.Net.Http; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; namespace WireMock.Http { internal static class HttpClientHelper { - private static HttpClient CreateHttpClient(string clientX509Certificate2Filename = null) + + private static HttpClient CreateHttpClient(string clientX509Certificate2ThumbprintOrSubjectName = null) { - if (!string.IsNullOrEmpty(clientX509Certificate2Filename)) + if (!string.IsNullOrEmpty(clientX509Certificate2ThumbprintOrSubjectName)) { #if NETSTANDARD || NET46 var handler = new HttpClientHandler @@ -19,16 +23,20 @@ namespace WireMock.Http ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true }; - handler.ClientCertificates.Add(new System.Security.Cryptography.X509Certificates.X509Certificate2(clientX509Certificate2Filename)); - return new HttpClient(handler); + var x509Certificate2 = CertificateUtil.GetCertificate(clientX509Certificate2ThumbprintOrSubjectName); + handler.ClientCertificates.Add(x509Certificate2); + + #else var handler = new WebRequestHandler { ClientCertificateOptions = ClientCertificateOption.Manual, - ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true + ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true, + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }; - handler.ClientCertificates.Add(new System.Security.Cryptography.X509Certificates.X509Certificate2(clientX509Certificate2Filename)); + var x509Certificate2 = CertificateUtil.GetCertificate(clientX509Certificate2ThumbprintOrSubjectName); + handler.ClientCertificates.Add(x509Certificate2); return new HttpClient(handler); #endif } @@ -36,9 +44,9 @@ namespace WireMock.Http return new HttpClient(); } - public static async Task SendAsync(RequestMessage requestMessage, string url, string clientX509Certificate2Filename = null) + public static async Task SendAsync(RequestMessage requestMessage, string url, string clientX509Certificate2ThumbprintOrSubjectName = null) { - var client = CreateHttpClient(clientX509Certificate2Filename); + var client = CreateHttpClient(clientX509Certificate2ThumbprintOrSubjectName); var httpRequestMessage = new HttpRequestMessage(new HttpMethod(requestMessage.Method), url); @@ -61,21 +69,29 @@ namespace WireMock.Http } // Call the URL - var httpResponseMessage = await client.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseContentRead); - - // Transform response - var responseMessage = new ResponseMessage + try { - StatusCode = (int)httpResponseMessage.StatusCode, - Body = await httpResponseMessage.Content.ReadAsStringAsync() - }; + var httpResponseMessage = await client.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseContentRead); - foreach (var header in httpResponseMessage.Headers) - { - responseMessage.AddHeader(header.Key, header.Value.FirstOrDefault()); + + // Transform response + var responseMessage = new ResponseMessage + { + StatusCode = (int)httpResponseMessage.StatusCode, + Body = await httpResponseMessage.Content.ReadAsStringAsync() + }; + + foreach (var header in httpResponseMessage.Headers) + { + responseMessage.AddHeader(header.Key, header.Value.FirstOrDefault()); + } + + return responseMessage; + } + catch(Exception ex) + { + throw ex; } - - return responseMessage; } } } \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/IProxyResponseBuilder.cs b/src/WireMock.Net/ResponseBuilders/IProxyResponseBuilder.cs index 820ee030..bb5a7d81 100644 --- a/src/WireMock.Net/ResponseBuilders/IProxyResponseBuilder.cs +++ b/src/WireMock.Net/ResponseBuilders/IProxyResponseBuilder.cs @@ -18,8 +18,8 @@ namespace WireMock.ResponseBuilders /// With Proxy URL using X509Certificate2. /// /// The proxy url. - /// The X509Certificate2 file to use for client authentication. + /// The X509Certificate2 file to use for client authentication. /// A . - IResponseBuilder WithProxy([NotNull] string proxyUrl, [CanBeNull] string clientX509Certificate2Filename); + IResponseBuilder WithProxy([NotNull] string proxyUrl, [CanBeNull] string clientX509Certificate2ThumbprintOrSubjectName); } } \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/Response.cs b/src/WireMock.Net/ResponseBuilders/Response.cs index 8c752911..08b7cb3a 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -35,9 +35,9 @@ namespace WireMock.ResponseBuilders public string ProxyUrl { get; private set; } /// - /// The client X509Certificate2Filename to use. + /// The client X509Certificate2 Thumbprint or SubjectName to use. /// - public string X509Certificate2Filename { get; private set; } = null; + public string X509Certificate2ThumbprintOrSubjectName { get; private set; } /// /// Gets the response message. @@ -247,7 +247,7 @@ namespace WireMock.ResponseBuilders /// The proxy url. /// A . [PublicAPI] - public IResponseBuilder WithProxy(string proxyUrl) + public IResponseBuilder WithProxy([NotNull] string proxyUrl) { Check.NotEmpty(proxyUrl, nameof(proxyUrl)); @@ -259,15 +259,15 @@ namespace WireMock.ResponseBuilders /// With Proxy URL. /// /// The proxy url. - /// The X509Certificate2 file to use for client authentication. + /// The X509Certificate2 file to use for client authentication. /// A . - public IResponseBuilder WithProxy(string proxyUrl, string clientX509Certificate2Filename) + public IResponseBuilder WithProxy([NotNull] string proxyUrl, [NotNull] string clientX509Certificate2ThumbprintOrSubjectName) { Check.NotEmpty(proxyUrl, nameof(proxyUrl)); - Check.NotEmpty(clientX509Certificate2Filename, nameof(clientX509Certificate2Filename)); + Check.NotEmpty(clientX509Certificate2ThumbprintOrSubjectName, nameof(clientX509Certificate2ThumbprintOrSubjectName)); ProxyUrl = proxyUrl; - X509Certificate2Filename = clientX509Certificate2Filename; + X509Certificate2ThumbprintOrSubjectName = clientX509Certificate2ThumbprintOrSubjectName; return this; } @@ -285,7 +285,10 @@ namespace WireMock.ResponseBuilders if (ProxyUrl != null) { - return await HttpClientHelper.SendAsync(requestMessage, ProxyUrl, X509Certificate2Filename); + 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); } if (UseTransformer) diff --git a/src/WireMock.Net/Server/FluentMockServer.Admin.cs b/src/WireMock.Net/Server/FluentMockServer.Admin.cs index 1e8c94c6..59800ea2 100644 --- a/src/WireMock.Net/Server/FluentMockServer.Admin.cs +++ b/src/WireMock.Net/Server/FluentMockServer.Admin.cs @@ -129,7 +129,11 @@ namespace WireMock.Server private async Task ProxyAndRecordAsync(RequestMessage requestMessage, ProxyAndRecordSettings settings) { - var responseMessage = await HttpClientHelper.SendAsync(requestMessage, settings.Url); + var requestUri = new Uri(requestMessage.Url); + var proxyUri = new Uri(settings.Url); + var proxyUriWithRequestPathAndQuery = new Uri(proxyUri, requestUri.PathAndQuery); + + var responseMessage = await HttpClientHelper.SendAsync(requestMessage, proxyUriWithRequestPathAndQuery.AbsoluteUri, settings.X509Certificate2ThumbprintOrSubjectName); if (settings.SaveMapping) { @@ -158,7 +162,7 @@ namespace WireMock.Server var model = new SettingsModel { AllowPartialMapping = _options.AllowPartialMapping, - GlobalProcessingDelay = (int?) _options.RequestProcessingDelay?.TotalMilliseconds + GlobalProcessingDelay = (int?)_options.RequestProcessingDelay?.TotalMilliseconds }; return ToJson(model); @@ -513,7 +517,13 @@ namespace WireMock.Server if (!string.IsNullOrEmpty(responseModel.ProxyUrl)) { - return responseBuilder.WithProxy(responseModel.ProxyUrl); + if (string.IsNullOrEmpty(responseModel.X509Certificate2ThumbprintOrSubjectName)) + { + return responseBuilder.WithProxy(responseModel.ProxyUrl); + } + + return responseBuilder.WithProxy(responseModel.ProxyUrl, responseModel.X509Certificate2ThumbprintOrSubjectName); + } if (responseModel.StatusCode.HasValue) diff --git a/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs b/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs index b3ed33f1..2c58136a 100644 --- a/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs +++ b/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs @@ -16,8 +16,8 @@ public bool SaveMapping { get; set; } = true; /// - /// The clientCertificateFilename to use. Example : "C:\certificates\cert.pfx" + /// The clientCertificate thumbprint or subject name fragment to use. Example thumbprint : "D2DBF135A8D06ACCD0E1FAD9BFB28678DF7A9818". Example subject name: "www.google.com"" /// - public string X509Certificate2Filename { get; set; } + public string X509Certificate2ThumbprintOrSubjectName { get; set; } } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/FluentMockServerTests.cs b/test/WireMock.Net.Tests/FluentMockServerTests.cs index 6781eda7..4a383b31 100644 --- a/test/WireMock.Net.Tests/FluentMockServerTests.cs +++ b/test/WireMock.Net.Tests/FluentMockServerTests.cs @@ -363,12 +363,29 @@ namespace WireMock.Net.Tests .RespondWith(Response.Create().WithProxy("http://www.google.com")); // when - var result = await new HttpClient().GetStringAsync("http://localhost:" + _server.Ports[0] + "/foo"); + var result = await new HttpClient().GetStringAsync("http://localhost:" + _server.Ports[0] + "/search?q=test"); // then Check.That(result).Contains("google"); } + //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() + //{ + // // given + // _server = FluentMockServer.Start(); + // _server + // .Given(Request.Create().WithPath("/*")) + // .RespondWith(Response.Create().WithProxy("https://server-that-expects-a-client-certificate", @"\\yourclientcertificatecontainingprivatekey.pfx", "yourclientcertificatepassword")); + + // // when + // var result = await new HttpClient().GetStringAsync("http://localhost:" + _server.Ports[0] + "/someurl?someQuery=someValue"); + + // // then + // Check.That(result).Contains("google"); + //} + //[TearDown] public void Dispose() {