Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Add support for client certificate password and test with real services that require client certificate auth #32

Merged
merged 13 commits into from
Jun 16, 2017
Merged
3 changes: 2 additions & 1 deletion examples/WireMock.Net.Console.Record.NETCoreApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
});
Expand Down
20 changes: 13 additions & 7 deletions examples/WireMock.Net.ConsoleApplication/MainApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
6 changes: 3 additions & 3 deletions src/WireMock.Net.StandAlone/StandAloneApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}

/// <summary>
Expand Down Expand Up @@ -91,7 +91,7 @@ public static FluentMockServer Start([NotNull] string[] args)
{
Url = options.ProxyURL,
SaveMapping = options.SaveMapping,
X509Certificate2Filename = options.X509Certificate2Filename
X509Certificate2ThumbprintOrSubjectName = options.X509Certificate2ThumbprintOrSubjectName
};
}

Expand Down
5 changes: 5 additions & 0 deletions src/WireMock.Net/Admin/Mappings/ResponseModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,10 @@ public class ResponseModel
/// </summary>
/// <value>ProxyUrl</value>
public string ProxyUrl { get; set; }

/// <summary>
/// The client X509Certificate2 Thumbprint or SubjectName to use.
/// </summary>
public string X509Certificate2ThumbprintOrSubjectName { get; set; }
}
}
44 changes: 44 additions & 0 deletions src/WireMock.Net/Http/CertificateUtil.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
}
54 changes: 35 additions & 19 deletions src/WireMock.Net/Http/HttpClientHelper.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,26 +23,30 @@ 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
}

return new HttpClient();
}

public static async Task<ResponseMessage> SendAsync(RequestMessage requestMessage, string url, string clientX509Certificate2Filename = null)
public static async Task<ResponseMessage> 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);

Expand All @@ -61,21 +69,29 @@ public static async Task<ResponseMessage> 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;
}
}
}
4 changes: 2 additions & 2 deletions src/WireMock.Net/ResponseBuilders/IProxyResponseBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public interface IProxyResponseBuilder : IStatusCodeResponseBuilder
/// With Proxy URL using X509Certificate2.
/// </summary>
/// <param name="proxyUrl">The proxy url.</param>
/// <param name="clientX509Certificate2Filename">The X509Certificate2 file to use for client authentication.</param>
/// <param name="clientX509Certificate2ThumbprintOrSubjectName">The X509Certificate2 file to use for client authentication.</param>
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithProxy([NotNull] string proxyUrl, [CanBeNull] string clientX509Certificate2Filename);
IResponseBuilder WithProxy([NotNull] string proxyUrl, [CanBeNull] string clientX509Certificate2ThumbprintOrSubjectName);
}
}
19 changes: 11 additions & 8 deletions src/WireMock.Net/ResponseBuilders/Response.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ public class Response : IResponseBuilder
public string ProxyUrl { get; private set; }

/// <summary>
/// The client X509Certificate2Filename to use.
/// The client X509Certificate2 Thumbprint or SubjectName to use.
/// </summary>
public string X509Certificate2Filename { get; private set; } = null;
public string X509Certificate2ThumbprintOrSubjectName { get; private set; }

/// <summary>
/// Gets the response message.
Expand Down Expand Up @@ -247,7 +247,7 @@ public IResponseBuilder WithDelay(int milliseconds)
/// <param name="proxyUrl">The proxy url.</param>
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
[PublicAPI]
public IResponseBuilder WithProxy(string proxyUrl)
public IResponseBuilder WithProxy([NotNull] string proxyUrl)
{
Check.NotEmpty(proxyUrl, nameof(proxyUrl));

Expand All @@ -259,15 +259,15 @@ public IResponseBuilder WithProxy(string proxyUrl)
/// With Proxy URL.
/// </summary>
/// <param name="proxyUrl">The proxy url.</param>
/// <param name="clientX509Certificate2Filename">The X509Certificate2 file to use for client authentication.</param>
/// <param name="clientX509Certificate2ThumbprintOrSubjectName">The X509Certificate2 file to use for client authentication.</param>
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
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;
}

Expand All @@ -285,7 +285,10 @@ public async Task<ResponseMessage> ProvideResponseAsync(RequestMessage requestMe

if (ProxyUrl != null)
{
return await HttpClientHelper.SendAsync(requestMessage, ProxyUrl, X509Certificate2Filename);
var requestUri = new Uri(requestMessage.Url);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

Why not just add the new parameter (X509Certificate2Password) to the method?

var proxyUri = new Uri(ProxyUrl);
var proxyUriWithRequestPathAndQuery = new Uri(proxyUri, requestUri.PathAndQuery);
return await HttpClientHelper.SendAsync(requestMessage, proxyUriWithRequestPathAndQuery.AbsoluteUri, X509Certificate2ThumbprintOrSubjectName);
}

if (UseTransformer)
Expand Down
16 changes: 13 additions & 3 deletions src/WireMock.Net/Server/FluentMockServer.Admin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,11 @@ private void InitProxyAndRecord(ProxyAndRecordSettings settings)

private async Task<ResponseMessage> ProxyAndRecordAsync(RequestMessage requestMessage, ProxyAndRecordSettings settings)
{
var responseMessage = await HttpClientHelper.SendAsync(requestMessage, settings.Url);
var requestUri = new Uri(requestMessage.Url);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, please explain why this needed.

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)
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/WireMock.Net/Settings/ProxyAndRecordSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public class ProxyAndRecordSettings
public bool SaveMapping { get; set; } = true;

/// <summary>
/// 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""
/// </summary>
public string X509Certificate2Filename { get; set; }
public string X509Certificate2ThumbprintOrSubjectName { get; set; }
}
}
19 changes: 18 additions & 1 deletion test/WireMock.Net.Tests/FluentMockServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down