diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln index 104064bd3..e295c7e7c 100644 --- a/WireMock.Net Solution.sln +++ b/WireMock.Net Solution.sln @@ -71,6 +71,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.WebApplication EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.WebApplication.NETCore3", "examples\WireMock.Net.WebApplication.NETCore3\WireMock.Net.WebApplication.NETCore3.csproj", "{E1C56967-3DC7-46CB-A1DF-B13167A0D9D4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Console.NETCoreApp3WithCertificate", "examples\WireMock.Net.Console.NETCoreApp3WithCertificate\WireMock.Net.Console.NETCoreApp3WithCertificate.csproj", "{925E421A-1B3F-4202-B48F-734743573A4B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -173,6 +175,10 @@ Global {E1C56967-3DC7-46CB-A1DF-B13167A0D9D4}.Debug|Any CPU.Build.0 = Debug|Any CPU {E1C56967-3DC7-46CB-A1DF-B13167A0D9D4}.Release|Any CPU.ActiveCfg = Release|Any CPU {E1C56967-3DC7-46CB-A1DF-B13167A0D9D4}.Release|Any CPU.Build.0 = Release|Any CPU + {925E421A-1B3F-4202-B48F-734743573A4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {925E421A-1B3F-4202-B48F-734743573A4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {925E421A-1B3F-4202-B48F-734743573A4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {925E421A-1B3F-4202-B48F-734743573A4B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -202,6 +208,7 @@ Global {B6269AAC-170A-4346-8B9A-579DED3D9A95} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} {6F38CB3A-6DA1-408A-AECD-E434523C2838} = {985E0ADB-D4B4-473A-AA40-567E279B7946} {E1C56967-3DC7-46CB-A1DF-B13167A0D9D4} = {985E0ADB-D4B4-473A-AA40-567E279B7946} + {925E421A-1B3F-4202-B48F-734743573A4B} = {985E0ADB-D4B4-473A-AA40-567E279B7946} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458} diff --git a/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/Program.cs b/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/Program.cs new file mode 100644 index 000000000..54878ddc2 --- /dev/null +++ b/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/Program.cs @@ -0,0 +1,36 @@ +using WireMock.Logging; +using WireMock.Server; +using WireMock.Settings; + +namespace WireMock.Net.Console.NETCoreApp3WithCertificate +{ + class Program + { + static void Main(string[] args) + { + string url = "https://localhost:8433/"; + + var server = WireMockServer.Start(new WireMockServerSettings + { + Urls = new[] { url }, + StartAdminInterface = true, + Logger = new WireMockConsoleLogger(), + CertificateSettings = new WireMockCertificateSettings + { + X509StoreName = "My", + X509StoreLocation = "CurrentUser", + X509StoreThumbprintOrSubjectName = "FE16586076A8B3F3E2F1466803A6C4C7CA35455B" + + // X509CertificateFilePath = "example.pfx", + // X509CertificatePassword = "wiremock" + } + + }); + System.Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls)); + + System.Console.WriteLine("Press any key to stop the server"); + System.Console.ReadKey(); + server.Stop(); + } + } +} \ No newline at end of file diff --git a/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/WireMock.Net.Console.NETCoreApp3WithCertificate.csproj b/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/WireMock.Net.Console.NETCoreApp3WithCertificate.csproj new file mode 100644 index 000000000..3e04aa7e3 --- /dev/null +++ b/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/WireMock.Net.Console.NETCoreApp3WithCertificate.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + PreserveNewest + + + + diff --git a/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/base64_encoded.cer b/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/base64_encoded.cer new file mode 100644 index 000000000..4826bb012 --- /dev/null +++ b/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/base64_encoded.cer @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEsDCCApigAwIBAgIQJbH6hSGKdoFI0B7qCIOK7jANBgkqhkiG9w0BAQUFADAU +MRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMjAxMDMwMjMwMDAwWhcNMzAxMTA2MjMw +MDAwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCl5fQSrRgT3Q6WoULR98Y+rrDWtTTgVpbLU04G0hLZ4yUeP7Wa +yuVbvx7zX8XT4lA8Hu5T/GG91U077JcSSEjnPBFsh4hE7FkRoSYIEW6BFG7D7eUG +dGHnDV8UkSRQ97LJPyjXuHVDJzNDJ9xQGMzOZ4n8vQ7SEKBw9hRG2ugkP5b2jVIN +e1E549tq2jnIVpKCZ4+prf64ZLsaokX7VHe+b/CW3GoAqUUaUjdTpAQ7LpypJuFz +415enOrKQe+UEBdqhGlgcC/O/Bw0uq4qVk+NNe5DEINVwoYs9XjNdzxuIkkAtcCt +avTEzhHf8zWYLb5Nt2DIOcRGVELvRhsBX4um5f7dOGzMbXzBfUdjkP2O4hi6crhm +Hba5bNkj4Zw2EHR9Xua3nadGCj22z0vpMKP2gXdFVnxFqQlaUWBLtwwN9p6tCQHl +kU7wypvOHUsMa2Ojg5eZP4RpYFvZG3kkc9zTZCSakgw2n0ampBbvxPP11/AYIXtz +HKu3CKcpjVQ+lE0DAU/Mm77QJ24TMbXmAydwCf1UCdFbDUZhdM9lspHvA0J9eiCv +LOE94BrpVKuZ6TrAW0LZjAmBnkqYQAewhTW7GSgARE+QQcwfyu03Ck7id3Zt4FeQ +sQDo0NNj7zQOy3Y1GK0ZYAVZv/GUeHMkxpClSWPoub/f5SJ4YzD5Il0cQQIDAQAB +MA0GCSqGSIb3DQEBBQUAA4ICAQBd91xfUepnWcKwmupie2h1CAAQZEunyW78i++t +evABfBu0TgV4s6Xe0umFv9V4r+O+rrF3ddSudbSOPBEb0Ooe+e3YGlNk1JrI1EEn +fhb0YI8bMfBNpl85yNqxgByra7JF2mG4qbAnjrCs/PZkXo/34N29SY6dyZ7mffR3 +r/l01Rdm3ogRwGkiMUeKb3iGwLUy1T55svuI3Zc13N+NJT1s9NqpwWeK/jFK/WRN +5Hi9W3DmlGCYAwFPCyBaQagxpGuGIpNsU0hKp86W5EvJpBpmCihfwlydH8ZbkHJ9 +jx2UDgTCaDzmaiKysiTP2HHDBsReL4tjakBksa9jkTfy5ajB53F3aUVs4jvTA46L +w8wcAJlRPBz5siBrv4CH/0lBMyNeYzuqmDY3ulF4IMKNb5Kk9Ye4Pt0474z50A4v +fSah+9iwI/mubaJ5tK522AtWtUoOIAswIwpDQyNeJPOggyzT2Y2OYZdGuFAoMYuq +ZD58k4Yo+vky9K88l8NuzNJJvtgTKtT+/9qfMucxFmnvwbKEEULP3sw1FUKkPtM4 +f242FIV/XnOeloDmhGGeTB7aODB+gGCvgmOH92njjUEIv+SnYQkflQaRhhyNIACi +ZvWlP/96H+X4fUG5kVNBHY021ZWmurUDqVxWUaswg63+DfsZcYtt6wgxiAN4ssXG +wLnLPw== +-----END CERTIFICATE----- diff --git a/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/base64_encoded.privatekey b/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/base64_encoded.privatekey new file mode 100644 index 000000000..3e6949a59 Binary files /dev/null and b/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/base64_encoded.privatekey differ diff --git a/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/example.pfx b/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/example.pfx new file mode 100644 index 000000000..434ba1321 Binary files /dev/null and b/examples/WireMock.Net.Console.NETCoreApp3WithCertificate/example.pfx differ diff --git a/src/WireMock.Net/Http/HttpClientHelper.cs b/src/WireMock.Net/Http/HttpClientHelper.cs index 1eb163e41..eab1ac385 100644 --- a/src/WireMock.Net/Http/HttpClientHelper.cs +++ b/src/WireMock.Net/Http/HttpClientHelper.cs @@ -41,7 +41,7 @@ public static HttpClient CreateHttpClient(IProxyAndRecordSettings settings) { handler.ClientCertificateOptions = ClientCertificateOption.Manual; - var x509Certificate2 = ClientCertificateHelper.GetCertificate(settings.ClientX509Certificate2ThumbprintOrSubjectName); + var x509Certificate2 = CertificateLoader.LoadCertificate(settings.ClientX509Certificate2ThumbprintOrSubjectName); handler.ClientCertificates.Add(x509Certificate2); } diff --git a/src/WireMock.Net/HttpsCertificate/CertificateLoader.cs b/src/WireMock.Net/HttpsCertificate/CertificateLoader.cs new file mode 100644 index 000000000..409bbb6c6 --- /dev/null +++ b/src/WireMock.Net/HttpsCertificate/CertificateLoader.cs @@ -0,0 +1,100 @@ +using System; +using System.IO; +using System.Security.Cryptography.X509Certificates; + +namespace WireMock.HttpsCertificate +{ + internal static class CertificateLoader + { + /// + /// Used by the WireMock.Net server + /// + public static X509Certificate2 LoadCertificate( + string storeName, + string storeLocation, + string thumbprintOrSubjectName, + string filePath, + string password, + string host) + { + if (!string.IsNullOrEmpty(storeName) && !string.IsNullOrEmpty(storeLocation)) + { + var thumbprintOrSubjectNameOrHost = thumbprintOrSubjectName ?? host; + + var certStore = new X509Store((StoreName)Enum.Parse(typeof(StoreName), storeName), (StoreLocation)Enum.Parse(typeof(StoreLocation), storeLocation)); + try + { + certStore.Open(OpenFlags.ReadOnly); + + // Attempt to find by Thumbprint first + var matchingCertificates = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprintOrSubjectNameOrHost, false); + if (matchingCertificates.Count == 0) + { + // Fallback to SubjectName + matchingCertificates = certStore.Certificates.Find(X509FindType.FindBySubjectName, thumbprintOrSubjectNameOrHost, false); + if (matchingCertificates.Count == 0) + { + // No certificates matched the search criteria. + throw new FileNotFoundException($"No Certificate found with in store '{storeName}', location '{storeLocation}' for Thumbprint or SubjectName '{thumbprintOrSubjectNameOrHost}'."); + } + } + + // Use the first matching certificate. + return matchingCertificates[0]; + } + finally + { +#if NETSTANDARD || NET46 + certStore.Dispose(); +#else + certStore.Close(); +#endif + } + } + + if (!string.IsNullOrEmpty(filePath) && !string.IsNullOrEmpty(password)) + { + return new X509Certificate2(filePath, password); + } + + throw new InvalidOperationException("X509StoreName and X509StoreLocation OR X509CertificateFilePath and X509CertificatePassword are mandatory."); + } + + /// + /// Used for Proxy + /// + public static X509Certificate2 LoadCertificate(string thumbprintOrSubjectName) + { + var 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 SubjectName + matchingCertificates = certStore.Certificates.Find(X509FindType.FindBySubjectName, thumbprintOrSubjectName, false); + if (matchingCertificates.Count == 0) + { + // No certificates matched the search criteria. + throw new FileNotFoundException("No certificate found with specified Thumbprint or SubjectName.", thumbprintOrSubjectName); + } + } + + // Use the first matching certificate. + return matchingCertificates[0]; + } + finally + { +#if NETSTANDARD || NET46 + certStore.Dispose(); +#else + certStore.Close(); +#endif + } + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/HttpsCertificate/ClientCertificateHelper.cs b/src/WireMock.Net/HttpsCertificate/ClientCertificateHelper.cs deleted file mode 100644 index bd81e4146..000000000 --- a/src/WireMock.Net/HttpsCertificate/ClientCertificateHelper.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.IO; -using System.Security.Cryptography.X509Certificates; - -namespace WireMock.HttpsCertificate -{ - internal static class ClientCertificateHelper - { - 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 FileNotFoundException("No certificate found with specified Thumbprint or SubjectName.", thumbprintOrSubjectName); - } - } - - // Use the first matching certificate. - return matchingCertificates[0]; - } - finally - { -#if NETSTANDARD || NET46 - certStore.Dispose(); -#else - certStore.Close(); -#endif - } - } - } -} \ No newline at end of file diff --git a/src/WireMock.Net/Owin/AspNetCoreSelfHost.NETStandard.cs b/src/WireMock.Net/Owin/AspNetCoreSelfHost.NETStandard.cs index 19b9a15a0..aefc35e59 100644 --- a/src/WireMock.Net/Owin/AspNetCoreSelfHost.NETStandard.cs +++ b/src/WireMock.Net/Owin/AspNetCoreSelfHost.NETStandard.cs @@ -1,10 +1,10 @@ #if USE_ASPNETCORE && !NETSTANDARD1_3 -using System; using System.Collections.Generic; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using WireMock.HttpsCertificate; namespace WireMock.Owin { @@ -18,20 +18,34 @@ private static void SetKestrelOptionsLimits(KestrelServerOptions options) options.Limits.MaxResponseBufferSize = null; } - private static void SetHttpsAndUrls(KestrelServerOptions options, ICollection<(string Url, int Port)> urlDetails) + private static void SetHttpsAndUrls(KestrelServerOptions kestrelOptions, IWireMockMiddlewareOptions wireMockMiddlewareOptions, IEnumerable urlDetails) { - foreach (var detail in urlDetails) + foreach (var urlDetail in urlDetails) { - if (detail.Url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + if (urlDetail.IsHttps) { - options.Listen(System.Net.IPAddress.Any, detail.Port, listenOptions => + kestrelOptions.Listen(System.Net.IPAddress.Any, urlDetail.Port, listenOptions => { - listenOptions.UseHttps(); + if (wireMockMiddlewareOptions.CustomCertificateDefined) + { + listenOptions.UseHttps(CertificateLoader.LoadCertificate( + wireMockMiddlewareOptions.X509StoreName, + wireMockMiddlewareOptions.X509StoreLocation, + wireMockMiddlewareOptions.X509ThumbprintOrSubjectName, + wireMockMiddlewareOptions.X509CertificateFilePath, + wireMockMiddlewareOptions.X509CertificatePassword, + urlDetail.Host) + ); + } + else + { + listenOptions.UseHttps(); + } }); } else { - options.Listen(System.Net.IPAddress.Any, detail.Port); + kestrelOptions.Listen(System.Net.IPAddress.Any, urlDetail.Port); } } } diff --git a/src/WireMock.Net/Owin/AspNetCoreSelfHost.NETStandard13.cs b/src/WireMock.Net/Owin/AspNetCoreSelfHost.NETStandard13.cs index 80798cb7a..0d2bf3512 100644 --- a/src/WireMock.Net/Owin/AspNetCoreSelfHost.NETStandard13.cs +++ b/src/WireMock.Net/Owin/AspNetCoreSelfHost.NETStandard13.cs @@ -1,7 +1,5 @@ #if USE_ASPNETCORE && NETSTANDARD1_3 -using System; using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.Extensions.Configuration; @@ -19,12 +17,28 @@ private static void SetKestrelOptionsLimits(KestrelServerOptions options) options.Limits.MaxResponseBufferSize = null; } - private static void SetHttpsAndUrls(KestrelServerOptions options, ICollection<(string Url, int Port)> urlDetails) + private static void SetHttpsAndUrls(KestrelServerOptions options, IWireMockMiddlewareOptions wireMockMiddlewareOptions, IEnumerable urlDetails) { - var urls = urlDetails.Select(u => u.Url); - if (urls.Any(u => u.StartsWith("https://", StringComparison.OrdinalIgnoreCase))) + foreach (var urlDetail in urlDetails) { - options.UseHttps(PublicCertificateHelper.GetX509Certificate2()); + if (urlDetail.IsHttps) + { + if (wireMockMiddlewareOptions.CustomCertificateDefined) + { + options.UseHttps(CertificateLoader.LoadCertificate( + wireMockMiddlewareOptions.X509StoreName, + wireMockMiddlewareOptions.X509StoreLocation, + wireMockMiddlewareOptions.X509ThumbprintOrSubjectName, + wireMockMiddlewareOptions.X509CertificateFilePath, + wireMockMiddlewareOptions.X509CertificatePassword, + urlDetail.Host) + ); + } + else + { + options.UseHttps(PublicCertificateHelper.GetX509Certificate2()); + } + } } } } diff --git a/src/WireMock.Net/Owin/AspNetCoreSelfHost.cs b/src/WireMock.Net/Owin/AspNetCoreSelfHost.cs index 651831591..717200d69 100644 --- a/src/WireMock.Net/Owin/AspNetCoreSelfHost.cs +++ b/src/WireMock.Net/Owin/AspNetCoreSelfHost.cs @@ -18,7 +18,7 @@ namespace WireMock.Owin internal partial class AspNetCoreSelfHost : IOwinSelfHost { private readonly CancellationTokenSource _cts = new CancellationTokenSource(); - private readonly IWireMockMiddlewareOptions _options; + private readonly IWireMockMiddlewareOptions _wireMockMiddlewareOptions; private readonly IWireMockLogger _logger; private readonly HostUrlOptions _urlOptions; @@ -33,14 +33,14 @@ internal partial class AspNetCoreSelfHost : IOwinSelfHost public Exception RunningException => _runningException; - public AspNetCoreSelfHost([NotNull] IWireMockMiddlewareOptions options, [NotNull] HostUrlOptions urlOptions) + public AspNetCoreSelfHost([NotNull] IWireMockMiddlewareOptions wireMockMiddlewareOptions, [NotNull] HostUrlOptions urlOptions) { - Check.NotNull(options, nameof(options)); + Check.NotNull(wireMockMiddlewareOptions, nameof(wireMockMiddlewareOptions)); Check.NotNull(urlOptions, nameof(urlOptions)); - _logger = options.Logger ?? new WireMockConsoleLogger(); + _logger = wireMockMiddlewareOptions.Logger ?? new WireMockConsoleLogger(); - _options = options; + _wireMockMiddlewareOptions = wireMockMiddlewareOptions; _urlOptions = urlOptions; } @@ -61,7 +61,7 @@ public Task StartAsync() .ConfigureAppConfigurationUsingEnvironmentVariables() .ConfigureServices(services => { - services.AddSingleton(_options); + services.AddSingleton(_wireMockMiddlewareOptions); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -70,17 +70,17 @@ public Task StartAsync() { appBuilder.UseMiddleware(); - _options.PreWireMockMiddlewareInit?.Invoke(appBuilder); + _wireMockMiddlewareOptions.PreWireMockMiddlewareInit?.Invoke(appBuilder); appBuilder.UseMiddleware(); - _options.PostWireMockMiddlewareInit?.Invoke(appBuilder); + _wireMockMiddlewareOptions.PostWireMockMiddlewareInit?.Invoke(appBuilder); }) .UseKestrel(options => { SetKestrelOptionsLimits(options); - SetHttpsAndUrls(options, _urlOptions.GetDetails()); + SetHttpsAndUrls(options, _wireMockMiddlewareOptions, _urlOptions.GetDetails()); }) .ConfigureKestrelServerOptions() @@ -107,7 +107,7 @@ private Task RunHost(CancellationToken token) { Urls.Add(address.Replace("0.0.0.0", "localhost")); - PortUtils.TryExtract(address, out string protocol, out string host, out int port); + PortUtils.TryExtract(address, out bool isHttps, out string protocol, out string host, out int port); Ports.Add(port); } diff --git a/src/WireMock.Net/Owin/HostUrlDetails.cs b/src/WireMock.Net/Owin/HostUrlDetails.cs new file mode 100644 index 000000000..6bd8826ca --- /dev/null +++ b/src/WireMock.Net/Owin/HostUrlDetails.cs @@ -0,0 +1,15 @@ +namespace WireMock.Owin +{ + internal class HostUrlDetails + { + public bool IsHttps { get; set; } + + public string Url { get; set; } + + public string Protocol { get; set; } + + public string Host { get; set; } + + public int Port { get; set; } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Owin/HostUrlOptions.cs b/src/WireMock.Net/Owin/HostUrlOptions.cs index e85c03245..dd94ce331 100644 --- a/src/WireMock.Net/Owin/HostUrlOptions.cs +++ b/src/WireMock.Net/Owin/HostUrlOptions.cs @@ -5,26 +5,29 @@ namespace WireMock.Owin { internal class HostUrlOptions { + private const string LOCALHOST = "localhost"; + public ICollection Urls { get; set; } public int? Port { get; set; } public bool UseSSL { get; set; } - public ICollection<(string Url, int Port)> GetDetails() + public ICollection GetDetails() { - var list = new List<(string Url, int Port)>(); + var list = new List(); if (Urls == null) { int port = Port > 0 ? Port.Value : FindFreeTcpPort(); - list.Add(($"{(UseSSL ? "https" : "http")}://localhost:{port}", port)); + string protocol = UseSSL ? "https" : "http"; + list.Add(new HostUrlDetails { IsHttps = UseSSL, Url = $"{protocol}://{LOCALHOST}:{port}", Protocol = protocol, Host = LOCALHOST, Port = port }); } else { foreach (string url in Urls) { - PortUtils.TryExtract(url, out string protocol, out string host, out int port); - list.Add((url, port)); + PortUtils.TryExtract(url, out bool isHttps, out string protocol, out string host, out int port); + list.Add(new HostUrlDetails { IsHttps = isHttps, Url = url, Protocol = protocol, Host = host, Port = port }); } } diff --git a/src/WireMock.Net/Owin/IWireMockMiddlewareOptions.cs b/src/WireMock.Net/Owin/IWireMockMiddlewareOptions.cs index 44f900e2f..f56eb877c 100644 --- a/src/WireMock.Net/Owin/IWireMockMiddlewareOptions.cs +++ b/src/WireMock.Net/Owin/IWireMockMiddlewareOptions.cs @@ -47,5 +47,17 @@ internal interface IWireMockMiddlewareOptions bool? DisableRequestBodyDecompressing { get; set; } bool? HandleRequestsSynchronously { get; set; } + + string X509StoreName { get; set; } + + string X509StoreLocation { get; set; } + + string X509ThumbprintOrSubjectName { get; set; } + + string X509CertificateFilePath { get; set; } + + string X509CertificatePassword { get; set; } + + bool CustomCertificateDefined { get; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Owin/WireMockMiddlewareOptions.cs b/src/WireMock.Net/Owin/WireMockMiddlewareOptions.cs index e6b1b138b..8b9698b15 100644 --- a/src/WireMock.Net/Owin/WireMockMiddlewareOptions.cs +++ b/src/WireMock.Net/Owin/WireMockMiddlewareOptions.cs @@ -53,5 +53,25 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions /// public bool? HandleRequestsSynchronously { get; set; } + + /// + public string X509StoreName { get; set; } + + /// + public string X509StoreLocation { get; set; } + + /// + public string X509ThumbprintOrSubjectName { get; set; } + + /// + public string X509CertificateFilePath { get; set; } + + /// + public string X509CertificatePassword { get; set; } + + /// + public bool CustomCertificateDefined => + !string.IsNullOrEmpty(X509StoreName) && !string.IsNullOrEmpty(X509StoreLocation) || + !string.IsNullOrEmpty(X509CertificateFilePath) && !string.IsNullOrEmpty(X509CertificatePassword); } } \ No newline at end of file diff --git a/src/WireMock.Net/Server/WireMockServer.cs b/src/WireMock.Net/Server/WireMockServer.cs index 8702c338f..a58adde89 100644 --- a/src/WireMock.Net/Server/WireMockServer.cs +++ b/src/WireMock.Net/Server/WireMockServer.cs @@ -230,6 +230,15 @@ protected WireMockServer(IWireMockServerSettings settings) _options.DisableJsonBodyParsing = _settings.DisableJsonBodyParsing; _options.HandleRequestsSynchronously = settings.HandleRequestsSynchronously; + if (settings.CustomCertificateDefined) + { + _options.X509StoreName = settings.CertificateSettings.X509StoreName; + _options.X509StoreLocation = settings.CertificateSettings.X509StoreLocation; + _options.X509ThumbprintOrSubjectName = settings.CertificateSettings.X509StoreThumbprintOrSubjectName; + _options.X509CertificateFilePath = settings.CertificateSettings.X509CertificateFilePath; + _options.X509CertificatePassword = settings.CertificateSettings.X509CertificatePassword; + } + _matcherMapper = new MatcherMapper(_settings); _mappingConverter = new MappingConverter(_matcherMapper); diff --git a/src/WireMock.Net/Settings/IWireMockCertificateSettings.cs b/src/WireMock.Net/Settings/IWireMockCertificateSettings.cs new file mode 100644 index 000000000..d2d476bb4 --- /dev/null +++ b/src/WireMock.Net/Settings/IWireMockCertificateSettings.cs @@ -0,0 +1,44 @@ +namespace WireMock.Settings +{ + /// + /// If https is used, these settings can be used to configure the CertificateSettings in case a custom certificate instead the default .NET certificate should be used. + /// + /// X509StoreName and X509StoreLocation should be defined + /// OR + /// X509CertificateFilePath and X509CertificatePassword should be defined + /// + public interface IWireMockCertificateSettings + { + /// + /// X509 StoreName (AddressBook, AuthRoot, CertificateAuthority, My, Root, TrustedPeople or TrustedPublisher) + /// + string X509StoreName { get; set; } + + /// + /// X509 StoreLocation (CurrentUser or LocalMachine) + /// + string X509StoreLocation { get; set; } + + /// + /// X509 Thumbprint or SubjectName (if not defined, the 'host' is used) + /// + string X509StoreThumbprintOrSubjectName { get; set; } + + /// + /// X509Certificate FilePath + /// + string X509CertificateFilePath { get; set; } + + /// + /// X509Certificate Password + /// + string X509CertificatePassword { get; set; } + + /// + /// X509StoreName and X509StoreLocation should be defined + /// OR + /// X509CertificateFilePath and X509CertificatePassword should be defined + /// + bool IsDefined { get; } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Settings/IWireMockServerSettings.cs b/src/WireMock.Net/Settings/IWireMockServerSettings.cs index 65ce9a30c..ac511676d 100644 --- a/src/WireMock.Net/Settings/IWireMockServerSettings.cs +++ b/src/WireMock.Net/Settings/IWireMockServerSettings.cs @@ -170,5 +170,21 @@ public interface IWireMockServerSettings /// [PublicAPI] bool? ThrowExceptionWhenMatcherFails { get; set; } + + /// + /// If https is used, these settings can be used to configure the CertificateSettings in case a custom certificate instead the default .NET certificate should be used. + /// + /// X509StoreName and X509StoreLocation should be defined + /// OR + /// X509CertificateFilePath and X509CertificatePassword should be defined + /// + [PublicAPI] + IWireMockCertificateSettings CertificateSettings { get; set; } + + /// + /// Defines if custom CertificateSettings are defined + /// + [PublicAPI] + bool CustomCertificateDefined { get; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/WireMockCertificateSettings.cs b/src/WireMock.Net/Settings/WireMockCertificateSettings.cs new file mode 100644 index 000000000..a9234c308 --- /dev/null +++ b/src/WireMock.Net/Settings/WireMockCertificateSettings.cs @@ -0,0 +1,36 @@ +using JetBrains.Annotations; + +namespace WireMock.Settings +{ + /// + /// + /// + public class WireMockCertificateSettings : IWireMockCertificateSettings + { + /// + [PublicAPI] + public string X509StoreName { get; set; } + + /// + [PublicAPI] + public string X509StoreLocation { get; set; } + + /// + [PublicAPI] + public string X509StoreThumbprintOrSubjectName { get; set; } + + /// + [PublicAPI] + public string X509CertificateFilePath { get; set; } + + /// + [PublicAPI] + public string X509CertificatePassword { get; set; } + + /// + [PublicAPI] + public bool IsDefined => + !string.IsNullOrEmpty(X509StoreName) && !string.IsNullOrEmpty(X509StoreLocation) || + !string.IsNullOrEmpty(X509CertificateFilePath) && !string.IsNullOrEmpty(X509CertificatePassword); + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Settings/WireMockServerSettings.cs b/src/WireMock.Net/Settings/WireMockServerSettings.cs index ac1beb14c..1f655c4cf 100644 --- a/src/WireMock.Net/Settings/WireMockServerSettings.cs +++ b/src/WireMock.Net/Settings/WireMockServerSettings.cs @@ -1,6 +1,6 @@ -using HandlebarsDotNet; +using System; +using HandlebarsDotNet; using JetBrains.Annotations; -using System; using Newtonsoft.Json; using WireMock.Handlers; using WireMock.Logging; @@ -121,5 +121,13 @@ public class WireMockServerSettings : IWireMockServerSettings /// [PublicAPI] public bool? ThrowExceptionWhenMatcherFails { get; set; } + + /// + [PublicAPI] + public IWireMockCertificateSettings CertificateSettings { get; set; } + + /// + [PublicAPI] + public bool CustomCertificateDefined => CertificateSettings?.IsDefined == true; } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs b/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs index be16b68a3..ff00bac5b 100644 --- a/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs +++ b/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs @@ -60,12 +60,12 @@ public static IWireMockServerSettings ParseArguments([NotNull] string[] args, [C settings.Urls = parser.GetValues("Urls", new[] { "http://*:9091/" }); } - string proxyURL = parser.GetStringValue("ProxyURL"); - if (!string.IsNullOrEmpty(proxyURL)) + string proxyUrl = parser.GetStringValue("ProxyURL") ?? parser.GetStringValue("ProxyUrl"); + if (!string.IsNullOrEmpty(proxyUrl)) { settings.ProxyAndRecordSettings = new ProxyAndRecordSettings { - Url = proxyURL, + Url = proxyUrl, SaveMapping = parser.GetBoolValue("SaveMapping"), SaveMappingToFile = parser.GetBoolValue("SaveMappingToFile"), SaveMappingForStatusCodePattern = parser.GetStringValue("SaveMappingForStatusCodePattern"), @@ -87,6 +87,19 @@ public static IWireMockServerSettings ParseArguments([NotNull] string[] args, [C } } + var certificateSettings = new WireMockCertificateSettings + { + X509StoreName = parser.GetStringValue("X509StoreName"), + X509StoreLocation = parser.GetStringValue("X509StoreLocation"), + X509StoreThumbprintOrSubjectName = parser.GetStringValue("X509StoreThumbprintOrSubjectName"), + X509CertificateFilePath = parser.GetStringValue("X509CertificateFilePath"), + X509CertificatePassword = parser.GetStringValue("X509CertificatePassword") + }; + if (certificateSettings.IsDefined) + { + settings.CertificateSettings = certificateSettings; + } + return settings; } } diff --git a/src/WireMock.Net/Util/PortUtils.cs b/src/WireMock.Net/Util/PortUtils.cs index 83ffe39f6..6bb189984 100644 --- a/src/WireMock.Net/Util/PortUtils.cs +++ b/src/WireMock.Net/Util/PortUtils.cs @@ -1,4 +1,5 @@ -using System.Net; +using System; +using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; @@ -32,21 +33,23 @@ public static int FindFreeTcpPort() } /// - /// Extract the protocol, host and port from a URL. + /// Extract the if-isHttps, protocol, host and port from a URL. /// - public static bool TryExtract(string url, out string protocol, out string host, out int port) + public static bool TryExtract(string url, out bool isHttps, out string protocol, out string host, out int port) { + isHttps = false; protocol = null; host = null; - port = default(int); + port = default; - Match m = UrlDetailsRegex.Match(url); - if (m.Success) + var match = UrlDetailsRegex.Match(url); + if (match.Success) { - protocol = m.Groups["proto"].Value; - host = m.Groups["host"].Value; + protocol = match.Groups["proto"].Value; + isHttps = protocol.StartsWith("https", StringComparison.OrdinalIgnoreCase); + host = match.Groups["host"].Value; - return int.TryParse(m.Groups["port"].Value, out port); + return int.TryParse(match.Groups["port"].Value, out port); } return false; diff --git a/test/WireMock.Net.Tests/Util/PortUtilsTests.cs b/test/WireMock.Net.Tests/Util/PortUtilsTests.cs index 27f10df8f..af3c18efc 100644 --- a/test/WireMock.Net.Tests/Util/PortUtilsTests.cs +++ b/test/WireMock.Net.Tests/Util/PortUtilsTests.cs @@ -1,3 +1,4 @@ +using FluentAssertions; using NFluent; using WireMock.Util; using Xunit; @@ -13,13 +14,14 @@ public void PortUtils_TryExtract_InvalidUrl_Returns_False() string url = "test"; // Act - bool result = PortUtils.TryExtract(url, out string proto, out string host, out int port); + bool result = PortUtils.TryExtract(url, out bool isHttps, out string proto, out string host, out int port); // Assert - Check.That(result).IsFalse(); - Check.That(proto).IsNull(); - Check.That(host).IsNull(); - Check.That(port).IsEqualTo(default(int)); + result.Should().BeFalse(); + isHttps.Should().BeFalse(); + proto.Should().BeNull(); + host.Should().BeNull(); + port.Should().Be(default(int)); } [Fact] @@ -29,39 +31,58 @@ public void PortUtils_TryExtract_UrlIsMissingPort_Returns_False() string url = "http://0.0.0.0"; // Act - bool result = PortUtils.TryExtract(url, out string proto, out string host, out int port); + bool result = PortUtils.TryExtract(url, out bool isHttps, out string proto, out string host, out int port); // Assert - Check.That(result).IsFalse(); - Check.That(proto).IsNull(); - Check.That(host).IsNull(); - Check.That(port).IsEqualTo(default(int)); + result.Should().BeFalse(); + isHttps.Should().BeFalse(); + proto.Should().BeNull(); + host.Should().BeNull(); + port.Should().Be(default(int)); } [Fact] - public void PortUtils_TryExtract_ValidUrl1_Returns_True() + public void PortUtils_TryExtract_Http_Returns_True() + { + // Assign + string url = "http://wiremock.net:1234"; + + // Act + bool result = PortUtils.TryExtract(url, out bool isHttps, out string proto, out string host, out int port); + + // Assert + result.Should().BeTrue(); + isHttps.Should().BeFalse(); + proto.Should().Be("http"); + host.Should().Be("wiremock.net"); + port.Should().Be(1234); + } + + [Fact] + public void PortUtils_TryExtract_Https_Returns_True() { // Assign string url = "https://wiremock.net:5000"; // Act - bool result = PortUtils.TryExtract(url, out string proto, out string host, out int port); + bool result = PortUtils.TryExtract(url, out bool isHttps, out string proto, out string host, out int port); // Assert - Check.That(result).IsTrue(); - Check.That(proto).IsEqualTo("https"); - Check.That(host).IsEqualTo("wiremock.net"); - Check.That(port).IsEqualTo(5000); + result.Should().BeTrue(); + isHttps.Should().BeTrue(); + proto.Should().Be("https"); + host.Should().Be("wiremock.net"); + port.Should().Be(5000); } [Fact] - public void PortUtils_TryExtract_ValidUrl2_Returns_True() + public void PortUtils_TryExtract_Https0_0_0_0_Returns_True() { // Assign string url = "https://0.0.0.0:5000"; // Act - bool result = PortUtils.TryExtract(url, out string proto, out string host, out int port); + bool result = PortUtils.TryExtract(url, out bool isHttps, out string proto, out string host, out int port); // Assert Check.That(result).IsTrue();