diff --git a/examples/WireMock.Net.Console.Record.NETCoreApp/Program.cs b/examples/WireMock.Net.Console.Record.NETCoreApp/Program.cs index 94943df5d..55dc79bf1 100644 --- a/examples/WireMock.Net.Console.Record.NETCoreApp/Program.cs +++ b/examples/WireMock.Net.Console.Record.NETCoreApp/Program.cs @@ -10,11 +10,12 @@ static void Main(params string[] args) { 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 91bfc9125..07f2c27be 100644 --- a/examples/WireMock.Net.ConsoleApplication/MainApp.cs +++ b/examples/WireMock.Net.ConsoleApplication/MainApp.cs @@ -96,17 +96,23 @@ public static void Run() .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 2d75efa3c..a4d3a9d1a 100644 --- a/src/WireMock.Net.StandAlone/StandAloneApp.cs +++ b/src/WireMock.Net.StandAlone/StandAloneApp.cs @@ -35,8 +35,8 @@ private class Options [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 @@ public static FluentMockServer Start([NotNull] string[] args) { 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 bd12064a1..21b955425 100644 --- a/src/WireMock.Net/Admin/Mappings/ResponseModel.cs +++ b/src/WireMock.Net/Admin/Mappings/ResponseModel.cs @@ -84,5 +84,10 @@ public class ResponseModel /// /// 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 000000000..66211225a --- /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 0ca2915b3..d30fb101f 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 @@ private static HttpClient CreateHttpClient(string clientX509Certificate2Filename 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 @@ private static HttpClient CreateHttpClient(string clientX509Certificate2Filename 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 @@ public static async Task SendAsync(RequestMessage requestMessag } // 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) + + // 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) { - responseMessage.AddHeader(header.Key, header.Value.FirstOrDefault()); + 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 820ee030e..bb5a7d812 100644 --- a/src/WireMock.Net/ResponseBuilders/IProxyResponseBuilder.cs +++ b/src/WireMock.Net/ResponseBuilders/IProxyResponseBuilder.cs @@ -18,8 +18,8 @@ public interface IProxyResponseBuilder : IStatusCodeResponseBuilder /// 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 8c7529116..08b7cb3ad 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -35,9 +35,9 @@ public class Response : IResponseBuilder 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 @@ public IResponseBuilder WithDelay(int milliseconds) /// 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 @@ public IResponseBuilder WithProxy(string proxyUrl) /// 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 @@ public async Task ProvideResponseAsync(RequestMessage requestMe 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 1e8c94c6e..59800ea2c 100644 --- a/src/WireMock.Net/Server/FluentMockServer.Admin.cs +++ b/src/WireMock.Net/Server/FluentMockServer.Admin.cs @@ -129,7 +129,11 @@ private void InitProxyAndRecord(ProxyAndRecordSettings settings) 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 @@ private ResponseMessage SettingsGet(RequestMessage requestMessage) var model = new SettingsModel { AllowPartialMapping = _options.AllowPartialMapping, - GlobalProcessingDelay = (int?) _options.RequestProcessingDelay?.TotalMilliseconds + GlobalProcessingDelay = (int?)_options.RequestProcessingDelay?.TotalMilliseconds }; return ToJson(model); @@ -513,7 +517,13 @@ private IResponseBuilder InitResponseBuilder(ResponseModel responseModel) 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 b3ed33f12..2c58136a6 100644 --- a/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs +++ b/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs @@ -16,8 +16,8 @@ public class ProxyAndRecordSettings 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 6781eda75..4a383b319 100644 --- a/test/WireMock.Net.Tests/FluentMockServerTests.cs +++ b/test/WireMock.Net.Tests/FluentMockServerTests.cs @@ -363,12 +363,29 @@ public async Task Should_proxy_responses() .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() {