diff --git a/src/WireMock.Net.Minimal/HttpsCertificate/CertificateLoader.cs b/src/WireMock.Net.Minimal/HttpsCertificate/CertificateLoader.cs index 3e0a97d6..0dcc4b18 100644 --- a/src/WireMock.Net.Minimal/HttpsCertificate/CertificateLoader.cs +++ b/src/WireMock.Net.Minimal/HttpsCertificate/CertificateLoader.cs @@ -3,6 +3,7 @@ using System; using System.IO; using System.Security.Cryptography.X509Certificates; +using WireMock.Owin; namespace WireMock.HttpsCertificate; @@ -13,19 +14,13 @@ internal static class CertificateLoader /// /// Used by the WireMock.Net server /// - public static X509Certificate2 LoadCertificate( - string? storeName, - string? storeLocation, - string? thumbprintOrSubjectName, - string? filePath, - string? passwordOrKey, - string host) + public static X509Certificate2 LoadCertificate(IWireMockMiddlewareOptions options, string host) { - if (!string.IsNullOrEmpty(storeName) && !string.IsNullOrEmpty(storeLocation)) + if (!string.IsNullOrEmpty(options.X509StoreName) && !string.IsNullOrEmpty(options.X509StoreLocation)) { - var thumbprintOrSubjectNameOrHost = thumbprintOrSubjectName ?? host; + var thumbprintOrSubjectNameOrHost = options.X509ThumbprintOrSubjectName ?? host; - var certStore = new X509Store((StoreName)Enum.Parse(typeof(StoreName), storeName), (StoreLocation)Enum.Parse(typeof(StoreLocation), storeLocation)); + var certStore = new X509Store((StoreName)Enum.Parse(typeof(StoreName), options.X509StoreName!), (StoreLocation)Enum.Parse(typeof(StoreLocation), options.X509StoreLocation!)); try { certStore.Open(OpenFlags.ReadOnly); @@ -39,7 +34,7 @@ internal static class CertificateLoader 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}'."); + throw new FileNotFoundException($"No Certificate found with in store '{options.X509StoreName}', location '{options.X509StoreLocation}' for Thumbprint or SubjectName '{thumbprintOrSubjectNameOrHost}'."); } } @@ -56,27 +51,28 @@ internal static class CertificateLoader } } - if (!string.IsNullOrEmpty(filePath)) + if (!string.IsNullOrEmpty(options.X509CertificateFilePath)) { - if (filePath!.EndsWith(ExtensionPem, StringComparison.OrdinalIgnoreCase)) + if (options.X509CertificateFilePath.EndsWith(ExtensionPem, StringComparison.OrdinalIgnoreCase)) { // PEM logic based on: https://www.scottbrady91.com/c-sharp/pem-loading-in-dotnet-core-and-dotnet #if NET5_0_OR_GREATER - if (!string.IsNullOrEmpty(passwordOrKey)) + if (!string.IsNullOrEmpty(options.X509CertificatePassword)) { - var certPem = File.ReadAllText(filePath); - var cert = X509Certificate2.CreateFromPem(certPem, passwordOrKey); + var certPem = File.ReadAllText(options.X509CertificateFilePath); + var cert = X509Certificate2.CreateFromPem(certPem, options.X509CertificatePassword); const string defaultPasswordPem = "WireMock.Net"; + return new X509Certificate2(cert.Export(X509ContentType.Pfx, defaultPasswordPem), defaultPasswordPem); } - return X509Certificate2.CreateFromPemFile(filePath); + return X509Certificate2.CreateFromPemFile(options.X509CertificateFilePath); #elif NETCOREAPP3_1 - var cert = new X509Certificate2(filePath); - if (!string.IsNullOrEmpty(passwordOrKey)) + var cert = new X509Certificate2(options.X509CertificateFilePath); + if (!string.IsNullOrEmpty(options.X509CertificatePassword)) { var key = System.Security.Cryptography.ECDsa.Create()!; - key.ImportECPrivateKey(System.Text.Encoding.UTF8.GetBytes(passwordOrKey), out _); + key.ImportECPrivateKey(System.Text.Encoding.UTF8.GetBytes(options.X509CertificatePassword), out _); return cert.CopyWithPrivateKey(key); } return cert; @@ -85,10 +81,17 @@ internal static class CertificateLoader #endif } - return !string.IsNullOrEmpty(passwordOrKey) ? new X509Certificate2(filePath, passwordOrKey) : new X509Certificate2(filePath); + return !string.IsNullOrEmpty(options.X509CertificatePassword) ? + new X509Certificate2(options.X509CertificateFilePath, options.X509CertificatePassword) : + new X509Certificate2(options.X509CertificateFilePath); } - throw new InvalidOperationException("X509StoreName and X509StoreLocation OR X509CertificateFilePath are mandatory. Note that X509CertificatePassword is optional."); + if (options.X509Certificate != null) + { + return options.X509Certificate; + } + + throw new InvalidOperationException("X509StoreName and X509StoreLocation OR X509CertificateFilePath OR X509Certificate are mandatory. Note that X509CertificatePassword is optional."); } /// diff --git a/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.NETStandard.cs b/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.NETStandard.cs index 510bd9ea..981ef04e 100644 --- a/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.NETStandard.cs +++ b/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.NETStandard.cs @@ -11,113 +11,105 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using CertificateLoader = WireMock.HttpsCertificate.CertificateLoader; -namespace WireMock.Owin +namespace WireMock.Owin; + +internal partial class AspNetCoreSelfHost { - internal partial class AspNetCoreSelfHost + private static void SetKestrelOptionsLimits(KestrelServerOptions options) { - private static void SetKestrelOptionsLimits(KestrelServerOptions options) - { - options.Limits.MaxRequestBodySize = null; // https://stackoverflow.com/questions/46738364/increase-upload-request-length-limit-in-kestrel - options.Limits.MaxRequestBufferSize = null; - options.Limits.MaxRequestHeaderCount = 100; - options.Limits.MaxResponseBufferSize = null; - } + options.Limits.MaxRequestBodySize = null; // https://stackoverflow.com/questions/46738364/increase-upload-request-length-limit-in-kestrel + options.Limits.MaxRequestBufferSize = null; + options.Limits.MaxRequestHeaderCount = 100; + options.Limits.MaxResponseBufferSize = null; + } - private static void SetHttpsAndUrls(KestrelServerOptions kestrelOptions, IWireMockMiddlewareOptions wireMockMiddlewareOptions, IEnumerable urlDetails) + private static void SetHttpsAndUrls(KestrelServerOptions kestrelOptions, IWireMockMiddlewareOptions wireMockMiddlewareOptions, IEnumerable urlDetails) + { + foreach (var urlDetail in urlDetails) { - foreach (var urlDetail in urlDetails) + if (urlDetail.IsHttps) { - if (urlDetail.IsHttps) + Listen(kestrelOptions, urlDetail, listenOptions => { - Listen(kestrelOptions, urlDetail, listenOptions => + listenOptions.UseHttps(options => { - listenOptions.UseHttps(options => + if (wireMockMiddlewareOptions.CustomCertificateDefined) { - if (wireMockMiddlewareOptions.CustomCertificateDefined) - { - options.ServerCertificate = CertificateLoader.LoadCertificate( - wireMockMiddlewareOptions.X509StoreName, - wireMockMiddlewareOptions.X509StoreLocation, - wireMockMiddlewareOptions.X509ThumbprintOrSubjectName, - wireMockMiddlewareOptions.X509CertificateFilePath, - wireMockMiddlewareOptions.X509CertificatePassword, - urlDetail.Host - ); - } + options.ServerCertificate = CertificateLoader.LoadCertificate(wireMockMiddlewareOptions, urlDetail.Host); + } - options.ClientCertificateMode = (ClientCertificateMode)wireMockMiddlewareOptions.ClientCertificateMode; - if (wireMockMiddlewareOptions.AcceptAnyClientCertificate) - { - options.ClientCertificateValidation = (_, _, _) => true; - } - }); - - if (urlDetail.IsHttp2) + options.ClientCertificateMode = (ClientCertificateMode)wireMockMiddlewareOptions.ClientCertificateMode; + if (wireMockMiddlewareOptions.AcceptAnyClientCertificate) { - listenOptions.Protocols = HttpProtocols.Http2; + options.ClientCertificateValidation = (_, _, _) => true; } }); - continue; - } - if (urlDetail.IsHttp2) - { - Listen(kestrelOptions, urlDetail, listenOptions => + if (urlDetail.IsHttp2) { listenOptions.Protocols = HttpProtocols.Http2; - }); - continue; - } - - Listen(kestrelOptions, urlDetail, _ => { }); + } + }); + continue; } - } - private static void Listen(KestrelServerOptions kestrelOptions, HostUrlDetails urlDetail, Action configure) - { - // Listens on any IP with the given port. - if (urlDetail is { Port: > 0, Host: "0.0.0.0" }) + if (urlDetail.IsHttp2) { - kestrelOptions.ListenAnyIP(urlDetail.Port, configure); - return; - } - - // Listens on ::1 and 127.0.0.1 with the given port. - if (urlDetail is { Port: > 0, Host: "localhost" or "127.0.0.1" or "::1" }) - { - kestrelOptions.ListenLocalhost(urlDetail.Port, configure); - return; + Listen(kestrelOptions, urlDetail, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + continue; } - // Try to parse the host as a valid IP address and bind to the given IP address and port. - if (IPAddress.TryParse(urlDetail.Host, out var ipAddress)) - { - kestrelOptions.Listen(ipAddress, urlDetail.Port, configure); - return; - } - - // Otherwise, listen on all IPs. - kestrelOptions.ListenAnyIP(urlDetail.Port, configure); + Listen(kestrelOptions, urlDetail, _ => { }); } } - internal static class IWebHostBuilderExtensions + private static void Listen(KestrelServerOptions kestrelOptions, HostUrlDetails urlDetail, Action configure) { - internal static IWebHostBuilder ConfigureAppConfigurationUsingEnvironmentVariables(this IWebHostBuilder builder) + // Listens on any IP with the given port. + if (urlDetail is { Port: > 0, Host: "0.0.0.0" }) { - return builder.ConfigureAppConfiguration(config => - { - config.AddEnvironmentVariables(); - }); + kestrelOptions.ListenAnyIP(urlDetail.Port, configure); + return; } - internal static IWebHostBuilder ConfigureKestrelServerOptions(this IWebHostBuilder builder) + // Listens on ::1 and 127.0.0.1 with the given port. + if (urlDetail is { Port: > 0, Host: "localhost" or "127.0.0.1" or "::1" }) { - return builder.ConfigureServices((context, services) => - { - services.Configure(context.Configuration.GetSection("Kestrel")); - }); + kestrelOptions.ListenLocalhost(urlDetail.Port, configure); + return; } + + // Try to parse the host as a valid IP address and bind to the given IP address and port. + if (IPAddress.TryParse(urlDetail.Host, out var ipAddress)) + { + kestrelOptions.Listen(ipAddress, urlDetail.Port, configure); + return; + } + + // Otherwise, listen on all IPs. + kestrelOptions.ListenAnyIP(urlDetail.Port, configure); } } -#endif + +internal static class IWebHostBuilderExtensions +{ + internal static IWebHostBuilder ConfigureAppConfigurationUsingEnvironmentVariables(this IWebHostBuilder builder) + { + return builder.ConfigureAppConfiguration(config => + { + config.AddEnvironmentVariables(); + }); + } + + internal static IWebHostBuilder ConfigureKestrelServerOptions(this IWebHostBuilder builder) + { + return builder.ConfigureServices((context, services) => + { + services.Configure(context.Configuration.GetSection("Kestrel")); + }); + } +} +#endif \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.NETStandard13.cs b/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.NETStandard13.cs index 32821624..9e04420c 100644 --- a/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.NETStandard13.cs +++ b/src/WireMock.Net.Minimal/Owin/AspNetCoreSelfHost.NETStandard13.cs @@ -28,19 +28,9 @@ internal partial class AspNetCoreSelfHost { options.UseHttps(new HttpsConnectionFilterOptions { - ServerCertificate = wireMockMiddlewareOptions.CustomCertificateDefined - ? CertificateLoader.LoadCertificate( - wireMockMiddlewareOptions.X509StoreName, - wireMockMiddlewareOptions.X509StoreLocation, - wireMockMiddlewareOptions.X509ThumbprintOrSubjectName, - wireMockMiddlewareOptions.X509CertificateFilePath, - wireMockMiddlewareOptions.X509CertificatePassword, - urlDetail.Host) - : PublicCertificateHelper.GetX509Certificate2(), + ServerCertificate = wireMockMiddlewareOptions.CustomCertificateDefined ? CertificateLoader.LoadCertificate(wireMockMiddlewareOptions, urlDetail.Host) : PublicCertificateHelper.GetX509Certificate2(), ClientCertificateMode = (ClientCertificateMode) wireMockMiddlewareOptions.ClientCertificateMode, - ClientCertificateValidation = wireMockMiddlewareOptions.AcceptAnyClientCertificate - ? (_, _, _) => true - : null, + ClientCertificateValidation = wireMockMiddlewareOptions.AcceptAnyClientCertificate ? (_, _, _) => true : null }); } } diff --git a/src/WireMock.Net.Minimal/Owin/IWireMockMiddlewareOptions.cs b/src/WireMock.Net.Minimal/Owin/IWireMockMiddlewareOptions.cs index cd4c4d8d..d6f4c7ee 100644 --- a/src/WireMock.Net.Minimal/Owin/IWireMockMiddlewareOptions.cs +++ b/src/WireMock.Net.Minimal/Owin/IWireMockMiddlewareOptions.cs @@ -7,6 +7,10 @@ using WireMock.Logging; using WireMock.Matchers; using WireMock.Types; using WireMock.Util; +using System.Security.Cryptography.X509Certificates; + +using JetBrains.Annotations; + #if !USE_ASPNETCORE using Owin; #else @@ -70,6 +74,11 @@ internal interface IWireMockMiddlewareOptions string? X509CertificateFilePath { get; set; } + /// + /// A X.509 certificate instance. + /// + public X509Certificate2? X509Certificate { get; set; } + string? X509CertificatePassword { get; set; } bool CustomCertificateDefined { get; } diff --git a/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptions.cs b/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptions.cs index eff4c4e5..6f675459 100644 --- a/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptions.cs +++ b/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptions.cs @@ -7,6 +7,8 @@ using WireMock.Logging; using WireMock.Matchers; using WireMock.Types; using WireMock.Util; +using System.Security.Cryptography.X509Certificates; + #if !USE_ASPNETCORE using Owin; #else @@ -81,13 +83,17 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions /// public string? X509CertificateFilePath { get; set; } + /// + public X509Certificate2? X509Certificate { get; set; } + /// public string? X509CertificatePassword { get; set; } /// public bool CustomCertificateDefined => !string.IsNullOrEmpty(X509StoreName) && !string.IsNullOrEmpty(X509StoreLocation) || - !string.IsNullOrEmpty(X509CertificateFilePath); + !string.IsNullOrEmpty(X509CertificateFilePath) || + X509Certificate != null; /// public bool? SaveUnmatchedRequests { get; set; } diff --git a/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptionsHelper.cs b/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptionsHelper.cs index 7ba92b5f..a5dbd144 100644 --- a/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptionsHelper.cs +++ b/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareOptionsHelper.cs @@ -39,6 +39,7 @@ internal static class WireMockMiddlewareOptionsHelper options.X509StoreName = settings.CertificateSettings!.X509StoreName; options.X509StoreLocation = settings.CertificateSettings.X509StoreLocation; options.X509ThumbprintOrSubjectName = settings.CertificateSettings.X509StoreThumbprintOrSubjectName; + options.X509Certificate = settings.CertificateSettings.X509Certificate; options.X509CertificateFilePath = settings.CertificateSettings.X509CertificateFilePath; options.X509CertificatePassword = settings.CertificateSettings.X509CertificatePassword; } diff --git a/src/WireMock.Net.Minimal/Settings/WireMockCertificateSettings.cs b/src/WireMock.Net.Minimal/Settings/WireMockCertificateSettings.cs index c9ae39ef..b24fbcf0 100644 --- a/src/WireMock.Net.Minimal/Settings/WireMockCertificateSettings.cs +++ b/src/WireMock.Net.Minimal/Settings/WireMockCertificateSettings.cs @@ -1,5 +1,6 @@ // Copyright © WireMock.Net +using System.Security.Cryptography.X509Certificates; using JetBrains.Annotations; namespace WireMock.Settings; @@ -9,36 +10,44 @@ namespace WireMock.Settings; /// /// X509StoreName and X509StoreLocation should be defined /// OR -/// X509CertificateFilePath and X509CertificatePassword should be defined +/// X509CertificateFilePath should be defined +/// OR +/// X509Certificate should be defined /// public class WireMockCertificateSettings { /// - /// X509 StoreName (AddressBook, AuthRoot, CertificateAuthority, My, Root, TrustedPeople or TrustedPublisher) + /// X.509 certificate StoreName (AddressBook, AuthRoot, CertificateAuthority, My, Root, TrustedPeople or TrustedPublisher) /// [PublicAPI] public string? X509StoreName { get; set; } /// - /// X509 StoreLocation (CurrentUser or LocalMachine) + /// X.509 certificate StoreLocation (CurrentUser or LocalMachine) /// [PublicAPI] public string? X509StoreLocation { get; set; } /// - /// X509 Thumbprint or SubjectName (if not defined, the 'host' is used) + /// X.509 certificate Thumbprint or SubjectName (if not defined, the 'host' is used) /// [PublicAPI] public string? X509StoreThumbprintOrSubjectName { get; set; } /// - /// X509Certificate FilePath + /// X.509 certificate FilePath /// [PublicAPI] public string? X509CertificateFilePath { get; set; } /// - /// X509Certificate Password + /// A X.509 certificate instance. + /// + [PublicAPI] + public X509Certificate2? X509Certificate { get; set; } + + /// + /// X.509 certificate Password /// [PublicAPI] public string? X509CertificatePassword { get; set; } @@ -46,10 +55,13 @@ public class WireMockCertificateSettings /// /// X509StoreName and X509StoreLocation should be defined /// OR - /// X509CertificateFilePath and X509CertificatePassword should be defined + /// X509CertificateFilePath should be defined + /// OR + /// X509Certificate should be defined /// [PublicAPI] public bool IsDefined => !string.IsNullOrEmpty(X509StoreName) && !string.IsNullOrEmpty(X509StoreLocation) || - !string.IsNullOrEmpty(X509CertificateFilePath); + !string.IsNullOrEmpty(X509CertificateFilePath) || + X509Certificate != null; } \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/Settings/WireMockServerSettings.cs b/src/WireMock.Net.Minimal/Settings/WireMockServerSettings.cs index 1066ac6b..8fb01e22 100644 --- a/src/WireMock.Net.Minimal/Settings/WireMockServerSettings.cs +++ b/src/WireMock.Net.Minimal/Settings/WireMockServerSettings.cs @@ -235,7 +235,9 @@ public class WireMockServerSettings /// /// X509StoreName and X509StoreLocation should be defined /// OR - /// X509CertificateFilePath and X509CertificatePassword should be defined + /// X509CertificateFilePath should be defined + /// OR + /// X509Certificate should be defined /// [PublicAPI] public WireMockCertificateSettings? CertificateSettings { get; set; }