8 changed files with 9 additions and 734 deletions
@ -1,96 +0,0 @@
@@ -1,96 +0,0 @@
|
||||
#nullable enable |
||||
|
||||
using System.Security.Cryptography.X509Certificates; |
||||
using Bit.Core.Settings; |
||||
using Microsoft.Extensions.Hosting; |
||||
using Microsoft.Extensions.Logging; |
||||
using Microsoft.Extensions.Options; |
||||
|
||||
namespace Bit.Core.Platform.X509ChainCustomization; |
||||
|
||||
internal sealed class PostConfigureX509ChainOptions : IPostConfigureOptions<X509ChainOptions> |
||||
{ |
||||
const string CertificateSearchPattern = "*.crt"; |
||||
|
||||
private readonly ILogger<PostConfigureX509ChainOptions> _logger; |
||||
private readonly IHostEnvironment _hostEnvironment; |
||||
private readonly GlobalSettings _globalSettings; |
||||
|
||||
public PostConfigureX509ChainOptions( |
||||
ILogger<PostConfigureX509ChainOptions> logger, |
||||
IHostEnvironment hostEnvironment, |
||||
GlobalSettings globalSettings) |
||||
{ |
||||
_logger = logger; |
||||
_hostEnvironment = hostEnvironment; |
||||
_globalSettings = globalSettings; |
||||
} |
||||
|
||||
public void PostConfigure(string? name, X509ChainOptions options) |
||||
{ |
||||
// We don't register or request a named instance of these options, |
||||
// so don't customize it. |
||||
if (name != Options.DefaultName) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
// We only allow this setting to be configured on self host. |
||||
if (!_globalSettings.SelfHosted) |
||||
{ |
||||
options.AdditionalCustomTrustCertificatesDirectory = null; |
||||
return; |
||||
} |
||||
|
||||
if (options.AdditionalCustomTrustCertificates != null) |
||||
{ |
||||
// Additional certificates were added directly, this overwrites the need to |
||||
// read them from the directory. |
||||
_logger.LogInformation( |
||||
"Additional custom trust certificates were added directly, skipping loading them from '{Directory}'", |
||||
options.AdditionalCustomTrustCertificatesDirectory |
||||
); |
||||
return; |
||||
} |
||||
|
||||
if (string.IsNullOrEmpty(options.AdditionalCustomTrustCertificatesDirectory)) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
if (!Directory.Exists(options.AdditionalCustomTrustCertificatesDirectory)) |
||||
{ |
||||
// The default directory is volume mounted via the default Bitwarden setup process. |
||||
// If the directory doesn't exist it could indicate a error in configuration but this |
||||
// directory is never expected in a normal development environment so lower the log |
||||
// level in that case. |
||||
var logLevel = _hostEnvironment.IsDevelopment() |
||||
? LogLevel.Debug |
||||
: LogLevel.Warning; |
||||
_logger.Log( |
||||
logLevel, |
||||
"An additional custom trust certificate directory was given '{Directory}' but that directory does not exist.", |
||||
options.AdditionalCustomTrustCertificatesDirectory |
||||
); |
||||
return; |
||||
} |
||||
|
||||
var certificates = new List<X509Certificate2>(); |
||||
|
||||
foreach (var certFile in Directory.EnumerateFiles(options.AdditionalCustomTrustCertificatesDirectory, CertificateSearchPattern)) |
||||
{ |
||||
certificates.Add(new X509Certificate2(certFile)); |
||||
} |
||||
|
||||
if (options.AdditionalCustomTrustCertificatesDirectory != X509ChainOptions.DefaultAdditionalCustomTrustCertificatesDirectory && certificates.Count == 0) |
||||
{ |
||||
// They have intentionally given us a non-default directory but there weren't certificates, that is odd. |
||||
_logger.LogWarning( |
||||
"No additional custom trust certificates were found in '{Directory}'", |
||||
options.AdditionalCustomTrustCertificatesDirectory |
||||
); |
||||
} |
||||
|
||||
options.AdditionalCustomTrustCertificates = certificates; |
||||
} |
||||
} |
||||
@ -1,53 +0,0 @@
@@ -1,53 +0,0 @@
|
||||
using Bit.Core.Platform.X509ChainCustomization; |
||||
using Microsoft.Extensions.DependencyInjection.Extensions; |
||||
using Microsoft.Extensions.Options; |
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection; |
||||
|
||||
/// <summary> |
||||
/// Extension methods for setting up the ability to provide customization to how X509 chain validation works in an <see cref="IServiceCollection"/>. |
||||
/// </summary> |
||||
public static class X509ChainCustomizationServiceCollectionExtensions |
||||
{ |
||||
/// <summary> |
||||
/// Configures X509ChainPolicy customization through the root level <c>X509ChainOptions</c> configuration section |
||||
/// and configures the primary <see cref="HttpMessageHandler"/> to use custom certificate validation |
||||
/// when customized to do so. |
||||
/// </summary> |
||||
/// <param name="services">The <see cref="IServiceCollection"/>.</param> |
||||
/// <returns>The <see cref="IServiceCollection"/> for additional chaining.</returns> |
||||
public static IServiceCollection AddX509ChainCustomization(this IServiceCollection services) |
||||
{ |
||||
ArgumentNullException.ThrowIfNull(services); |
||||
|
||||
services.AddOptions<X509ChainOptions>() |
||||
.BindConfiguration(nameof(X509ChainOptions)); |
||||
|
||||
// Use TryAddEnumerable to make sure `PostConfigureX509ChainOptions` isn't added multiple |
||||
// times even if this method is called multiple times. |
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<X509ChainOptions>, PostConfigureX509ChainOptions>()); |
||||
|
||||
services.AddHttpClient() |
||||
.ConfigureHttpClientDefaults(builder => |
||||
{ |
||||
builder.ConfigurePrimaryHttpMessageHandler(sp => |
||||
{ |
||||
var x509ChainOptions = sp.GetRequiredService<IOptions<X509ChainOptions>>().Value; |
||||
|
||||
var handler = new HttpClientHandler(); |
||||
|
||||
if (x509ChainOptions.TryGetCustomRemoteCertificateValidationCallback(out var callback)) |
||||
{ |
||||
handler.ServerCertificateCustomValidationCallback = (sender, certificate, chain, errors) => |
||||
{ |
||||
return callback(certificate, chain, errors); |
||||
}; |
||||
} |
||||
|
||||
return handler; |
||||
}); |
||||
}); |
||||
|
||||
return services; |
||||
} |
||||
} |
||||
@ -1,81 +0,0 @@
@@ -1,81 +0,0 @@
|
||||
#nullable enable |
||||
|
||||
using System.Diagnostics.CodeAnalysis; |
||||
using System.Net.Security; |
||||
using System.Security.Cryptography.X509Certificates; |
||||
|
||||
namespace Bit.Core.Platform.X509ChainCustomization; |
||||
|
||||
/// <summary> |
||||
/// Allows for customization of the <see cref="X509ChainPolicy"/> and access to a custom server certificate validator |
||||
/// if customization has been made. |
||||
/// </summary> |
||||
public sealed class X509ChainOptions |
||||
{ |
||||
// This is the directory that we historically used to allow certificates be added inside our container |
||||
// and then on start of the container we would move them to `/usr/local/share/ca-certificates/` and call |
||||
// `update-ca-certificates` but since that operation requires root we can't do it in a rootless container. |
||||
// Ref: https://github.com/bitwarden/server/blob/67d7d685a619a5fc413f8532dacb09681ee5c956/src/Api/entrypoint.sh#L38-L41 |
||||
public const string DefaultAdditionalCustomTrustCertificatesDirectory = "/etc/bitwarden/ca-certificates/"; |
||||
|
||||
/// <summary> |
||||
/// A directory where additional certificates should be read from and included in <see cref="X509ChainPolicy.CustomTrustStore"/>. |
||||
/// </summary> |
||||
/// <remarks> |
||||
/// Only certificates suffixed with <c>*.crt</c> will be read. If <see cref="AdditionalCustomTrustCertificates"/> is |
||||
/// set, then this directory will not be read from. |
||||
/// </remarks> |
||||
public string? AdditionalCustomTrustCertificatesDirectory { get; set; } = DefaultAdditionalCustomTrustCertificatesDirectory; |
||||
|
||||
/// <summary> |
||||
/// A list of additional certificates that should be included in <see cref="X509ChainPolicy.CustomTrustStore"/>. |
||||
/// </summary> |
||||
/// <remarks> |
||||
/// If this value is set manually, then <see cref="AdditionalCustomTrustCertificatesDirectory"/> will be ignored. |
||||
/// </remarks> |
||||
public List<X509Certificate2>? AdditionalCustomTrustCertificates { get; set; } |
||||
|
||||
/// <summary> |
||||
/// Attempts to retrieve a custom remote certificate validation callback. |
||||
/// </summary> |
||||
/// <param name="callback"></param> |
||||
/// <returns>Returns <see langword="true"/> when we have custom remote certification that should be added, |
||||
/// <see langword="false"/> when no custom validation is needed and the default validation callback should |
||||
/// be used instead. |
||||
/// </returns> |
||||
[MemberNotNullWhen(true, nameof(AdditionalCustomTrustCertificates))] |
||||
public bool TryGetCustomRemoteCertificateValidationCallback( |
||||
[MaybeNullWhen(false)] out Func<X509Certificate2?, X509Chain?, SslPolicyErrors, bool> callback) |
||||
{ |
||||
callback = null; |
||||
if (AdditionalCustomTrustCertificates == null || AdditionalCustomTrustCertificates.Count == 0) |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
// Do this outside of the callback so that we aren't opening the root store every request. |
||||
using var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine, OpenFlags.ReadOnly); |
||||
var rootCertificates = store.Certificates; |
||||
|
||||
// Ref: https://github.com/dotnet/runtime/issues/39835#issuecomment-663020581 |
||||
callback = (certificate, chain, errors) => |
||||
{ |
||||
if (chain == null || certificate == null) |
||||
{ |
||||
return false; |
||||
} |
||||
|
||||
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; |
||||
|
||||
// We want our additional certificates to be in addition to the machines root store. |
||||
chain.ChainPolicy.CustomTrustStore.AddRange(rootCertificates); |
||||
|
||||
foreach (var additionalCertificate in AdditionalCustomTrustCertificates) |
||||
{ |
||||
chain.ChainPolicy.CustomTrustStore.Add(additionalCertificate); |
||||
} |
||||
return chain.Build(certificate); |
||||
}; |
||||
return true; |
||||
} |
||||
} |
||||
@ -1,359 +0,0 @@
@@ -1,359 +0,0 @@
|
||||
using System.Security.Authentication; |
||||
using System.Security.Cryptography; |
||||
using System.Security.Cryptography.X509Certificates; |
||||
using Bit.Core.Platform.X509ChainCustomization; |
||||
using Bit.Core.Settings; |
||||
using Microsoft.AspNetCore.Builder; |
||||
using Microsoft.AspNetCore.Hosting; |
||||
using Microsoft.AspNetCore.Server.Kestrel.Https; |
||||
using Microsoft.Extensions.Configuration; |
||||
using Microsoft.Extensions.DependencyInjection; |
||||
using Microsoft.Extensions.Hosting; |
||||
using Microsoft.Extensions.Logging; |
||||
using Microsoft.Extensions.Options; |
||||
using NSubstitute; |
||||
using Xunit; |
||||
|
||||
namespace Bit.Core.Test.Platform.X509ChainCustomization; |
||||
|
||||
public class X509ChainCustomizationServiceCollectionExtensionsTests |
||||
{ |
||||
private static X509Certificate2 CreateSelfSignedCert(string commonName) |
||||
{ |
||||
using var rsa = RSA.Create(2048); |
||||
var certRequest = new CertificateRequest($"CN={commonName}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); |
||||
return certRequest.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1)); |
||||
} |
||||
|
||||
[Fact] |
||||
public async Task OptionsPatternReturnsCachedValue() |
||||
{ |
||||
var tempDir = Directory.CreateTempSubdirectory("certs"); |
||||
|
||||
var tempCert = Path.Combine(tempDir.FullName, "test.crt"); |
||||
await File.WriteAllBytesAsync(tempCert, CreateSelfSignedCert("localhost").Export(X509ContentType.Cert)); |
||||
|
||||
var services = CreateServices((gs, environment, config) => |
||||
{ |
||||
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName; |
||||
}); |
||||
|
||||
// Create options once |
||||
var firstOptions = services.GetRequiredService<IOptions<X509ChainOptions>>().Value; |
||||
|
||||
Assert.NotNull(firstOptions.AdditionalCustomTrustCertificates); |
||||
var cert = Assert.Single(firstOptions.AdditionalCustomTrustCertificates); |
||||
Assert.Equal("CN=localhost", cert.Subject); |
||||
|
||||
// Since the second resolution should have cached values, deleting the file during operation |
||||
// should have no impact. |
||||
File.Delete(tempCert); |
||||
|
||||
// This is expected to be a cached version and doesn't actually need to go and read the file system |
||||
var secondOptions = services.GetRequiredService<IOptions<X509ChainOptions>>().Value; |
||||
Assert.Same(firstOptions, secondOptions); |
||||
|
||||
// This is the same reference as the first one so it shouldn't be different but just in case. |
||||
Assert.NotNull(secondOptions.AdditionalCustomTrustCertificates); |
||||
Assert.Single(secondOptions.AdditionalCustomTrustCertificates); |
||||
} |
||||
|
||||
[Fact] |
||||
public async Task DoesNotProvideCustomCallbackOnCloud() |
||||
{ |
||||
var tempDir = Directory.CreateTempSubdirectory("certs"); |
||||
|
||||
var tempCert = Path.Combine(tempDir.FullName, "test.crt"); |
||||
await File.WriteAllBytesAsync(tempCert, CreateSelfSignedCert("localhost").Export(X509ContentType.Cert)); |
||||
|
||||
var options = CreateOptions((gs, environment, config) => |
||||
{ |
||||
gs.SelfHosted = false; |
||||
|
||||
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName; |
||||
}); |
||||
|
||||
Assert.False(options.TryGetCustomRemoteCertificateValidationCallback(out _)); |
||||
} |
||||
|
||||
[Fact] |
||||
public async Task ManuallyAddingOptionsTakesPrecedence() |
||||
{ |
||||
var tempDir = Directory.CreateTempSubdirectory("certs"); |
||||
|
||||
var tempCert = Path.Combine(tempDir.FullName, "test.crt"); |
||||
await File.WriteAllBytesAsync(tempCert, CreateSelfSignedCert("localhost").Export(X509ContentType.Cert)); |
||||
|
||||
var services = CreateServices((gs, environment, config) => |
||||
{ |
||||
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName; |
||||
}, services => |
||||
{ |
||||
services.Configure<X509ChainOptions>(options => |
||||
{ |
||||
options.AdditionalCustomTrustCertificates = [CreateSelfSignedCert("example.com")]; |
||||
}); |
||||
}); |
||||
|
||||
var options = services.GetRequiredService<IOptions<X509ChainOptions>>().Value; |
||||
|
||||
Assert.True(options.TryGetCustomRemoteCertificateValidationCallback(out var callback)); |
||||
var cert = Assert.Single(options.AdditionalCustomTrustCertificates); |
||||
Assert.Equal("CN=example.com", cert.Subject); |
||||
|
||||
var fakeLogCollector = services.GetFakeLogCollector(); |
||||
|
||||
Assert.Contains(fakeLogCollector.GetSnapshot(), |
||||
r => r.Message == $"Additional custom trust certificates were added directly, skipping loading them from '{tempDir}'"); |
||||
} |
||||
|
||||
[Fact] |
||||
public void NullCustomDirectory_SkipsTryingToLoad() |
||||
{ |
||||
var services = CreateServices((gs, environment, config) => |
||||
{ |
||||
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = null; |
||||
}); |
||||
|
||||
var options = services.GetRequiredService<IOptions<X509ChainOptions>>().Value; |
||||
|
||||
Assert.False(options.TryGetCustomRemoteCertificateValidationCallback(out _)); |
||||
} |
||||
|
||||
[Theory] |
||||
[InlineData("Development", LogLevel.Debug)] |
||||
[InlineData("Production", LogLevel.Warning)] |
||||
public void CustomDirectoryDoesNotExist_Logs(string environment, LogLevel logLevel) |
||||
{ |
||||
var fakeDir = "/fake/dir/that/does/not/exist"; |
||||
var services = CreateServices((gs, hostEnvironment, config) => |
||||
{ |
||||
hostEnvironment.EnvironmentName = environment; |
||||
|
||||
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = fakeDir; |
||||
}); |
||||
|
||||
var options = services.GetRequiredService<IOptions<X509ChainOptions>>().Value; |
||||
|
||||
Assert.False(options.TryGetCustomRemoteCertificateValidationCallback(out _)); |
||||
|
||||
var fakeLogCollector = services.GetFakeLogCollector(); |
||||
|
||||
Assert.Contains(fakeLogCollector.GetSnapshot(), |
||||
r => r.Message == $"An additional custom trust certificate directory was given '{fakeDir}' but that directory does not exist." |
||||
&& r.Level == logLevel |
||||
); |
||||
} |
||||
|
||||
[Fact] |
||||
public async Task NamedOptions_NotConfiguredAsync() |
||||
{ |
||||
// To help make sure this fails for the right reason we should add certs to the directory |
||||
var tempDir = Directory.CreateTempSubdirectory("certs"); |
||||
|
||||
var tempCert = Path.Combine(tempDir.FullName, "test.crt"); |
||||
await File.WriteAllBytesAsync(tempCert, CreateSelfSignedCert("localhost").Export(X509ContentType.Cert)); |
||||
|
||||
var services = CreateServices((gs, environment, config) => |
||||
{ |
||||
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName; |
||||
}); |
||||
|
||||
var options = services.GetRequiredService<IOptionsMonitor<X509ChainOptions>>(); |
||||
|
||||
var namedOptions = options.Get("SomeName"); |
||||
|
||||
Assert.Null(namedOptions.AdditionalCustomTrustCertificates); |
||||
} |
||||
|
||||
[Fact] |
||||
public void CustomLocation_NoCertificates_Logs() |
||||
{ |
||||
var tempDir = Directory.CreateTempSubdirectory("certs"); |
||||
var services = CreateServices((gs, hostEnvironment, config) => |
||||
{ |
||||
config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName; |
||||
}); |
||||
|
||||
var options = services.GetRequiredService<IOptions<X509ChainOptions>>().Value; |
||||
|
||||
Assert.False(options.TryGetCustomRemoteCertificateValidationCallback(out _)); |
||||
|
||||
var fakeLogCollector = services.GetFakeLogCollector(); |
||||
|
||||
Assert.Contains(fakeLogCollector.GetSnapshot(), |
||||
r => r.Message == $"No additional custom trust certificates were found in '{tempDir.FullName}'" |
||||
); |
||||
} |
||||
|
||||
[Fact] |
||||
public async Task CallHttpWithSelfSignedCert_SelfSignedCertificateConfigured_Works() |
||||
{ |
||||
var selfSignedCertificate = CreateSelfSignedCert("localhost"); |
||||
await using var app = await CreateServerAsync(55555, options => |
||||
{ |
||||
options.ServerCertificate = selfSignedCertificate; |
||||
}); |
||||
|
||||
var services = CreateServices((gs, environment, config) => { }, services => |
||||
{ |
||||
services.Configure<X509ChainOptions>(options => |
||||
{ |
||||
options.AdditionalCustomTrustCertificates = [selfSignedCertificate]; |
||||
}); |
||||
}); |
||||
|
||||
var httpClient = services.GetRequiredService<IHttpClientFactory>().CreateClient(); |
||||
|
||||
var response = await httpClient.GetStringAsync("https://localhost:55555"); |
||||
Assert.Equal("Hi", response); |
||||
} |
||||
|
||||
[Fact] |
||||
public async Task CallHttpWithSelfSignedCert_SelfSignedCertificateNotConfigured_Throws() |
||||
{ |
||||
var selfSignedCertificate = CreateSelfSignedCert("localhost"); |
||||
await using var app = await CreateServerAsync(55556, options => |
||||
{ |
||||
options.ServerCertificate = selfSignedCertificate; |
||||
}); |
||||
|
||||
var services = CreateServices((gs, environment, config) => { }, services => |
||||
{ |
||||
services.Configure<X509ChainOptions>(options => |
||||
{ |
||||
options.AdditionalCustomTrustCertificates = [CreateSelfSignedCert("example.com")]; |
||||
}); |
||||
}); |
||||
|
||||
var httpClient = services.GetRequiredService<IHttpClientFactory>().CreateClient(); |
||||
|
||||
var requestException = await Assert.ThrowsAsync<HttpRequestException>(async () => await httpClient.GetStringAsync("https://localhost:55556")); |
||||
Assert.NotNull(requestException.InnerException); |
||||
var authenticationException = Assert.IsAssignableFrom<AuthenticationException>(requestException.InnerException); |
||||
Assert.Equal("The remote certificate was rejected by the provided RemoteCertificateValidationCallback.", authenticationException.Message); |
||||
} |
||||
|
||||
[Fact] |
||||
public async Task CallHttpWithSelfSignedCert_SelfSignedCertificateConfigured_WithExtraCert_Works() |
||||
{ |
||||
var selfSignedCertificate = CreateSelfSignedCert("localhost"); |
||||
await using var app = await CreateServerAsync(55557, options => |
||||
{ |
||||
options.ServerCertificate = selfSignedCertificate; |
||||
}); |
||||
|
||||
var services = CreateServices((gs, environment, config) => { }, services => |
||||
{ |
||||
services.Configure<X509ChainOptions>(options => |
||||
{ |
||||
options.AdditionalCustomTrustCertificates = [selfSignedCertificate, CreateSelfSignedCert("example.com")]; |
||||
}); |
||||
}); |
||||
|
||||
var httpClient = services.GetRequiredService<IHttpClientFactory>().CreateClient(); |
||||
|
||||
var response = await httpClient.GetStringAsync("https://localhost:55557"); |
||||
Assert.Equal("Hi", response); |
||||
} |
||||
|
||||
[Fact] |
||||
public async Task CallHttp_ReachingOutToServerTrustedThroughSystemCA() |
||||
{ |
||||
var services = CreateServices((gs, environment, config) => { }, services => |
||||
{ |
||||
services.Configure<X509ChainOptions>(options => |
||||
{ |
||||
options.AdditionalCustomTrustCertificates = []; |
||||
}); |
||||
}); |
||||
|
||||
var httpClient = services.GetRequiredService<IHttpClientFactory>().CreateClient(); |
||||
|
||||
var response = await httpClient.GetAsync("https://example.com"); |
||||
response.EnsureSuccessStatusCode(); |
||||
} |
||||
|
||||
[Fact] |
||||
public async Task CallHttpWithCustomTrustForSelfSigned_ReachingOutToServerTrustedThroughSystemCA() |
||||
{ |
||||
var selfSignedCertificate = CreateSelfSignedCert("localhost"); |
||||
var services = CreateServices((gs, environment, config) => { }, services => |
||||
{ |
||||
services.Configure<X509ChainOptions>(options => |
||||
{ |
||||
options.AdditionalCustomTrustCertificates = [selfSignedCertificate]; |
||||
}); |
||||
}); |
||||
|
||||
var httpClient = services.GetRequiredService<IHttpClientFactory>().CreateClient(); |
||||
|
||||
var response = await httpClient.GetAsync("https://example.com"); |
||||
response.EnsureSuccessStatusCode(); |
||||
} |
||||
|
||||
private static async Task<IAsyncDisposable> CreateServerAsync(int port, Action<HttpsConnectionAdapterOptions> configure) |
||||
{ |
||||
var builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions()); |
||||
builder.Services.AddRoutingCore(); |
||||
builder.WebHost.UseKestrelCore() |
||||
.ConfigureKestrel(options => |
||||
{ |
||||
options.ListenLocalhost(port, listenOptions => |
||||
{ |
||||
listenOptions.UseHttps(httpsOptions => |
||||
{ |
||||
configure(httpsOptions); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
var app = builder.Build(); |
||||
|
||||
app.MapGet("/", () => "Hi"); |
||||
|
||||
await app.StartAsync(); |
||||
|
||||
return app; |
||||
} |
||||
|
||||
private static X509ChainOptions CreateOptions(Action<GlobalSettings, IHostEnvironment, Dictionary<string, string>> configure, Action<IServiceCollection>? after = null) |
||||
{ |
||||
var services = CreateServices(configure, after); |
||||
return services.GetRequiredService<IOptions<X509ChainOptions>>().Value; |
||||
} |
||||
|
||||
private static IServiceProvider CreateServices(Action<GlobalSettings, IHostEnvironment, Dictionary<string, string>> configure, Action<IServiceCollection>? after = null) |
||||
{ |
||||
var globalSettings = new GlobalSettings |
||||
{ |
||||
// A solid default for these tests as these settings aren't allowed to work in cloud. |
||||
SelfHosted = true, |
||||
}; |
||||
var hostEnvironment = Substitute.For<IHostEnvironment>(); |
||||
hostEnvironment.EnvironmentName = "Development"; |
||||
var config = new Dictionary<string, string>(); |
||||
|
||||
configure(globalSettings, hostEnvironment, config); |
||||
|
||||
var services = new ServiceCollection(); |
||||
services.AddLogging(logging => |
||||
{ |
||||
logging.SetMinimumLevel(LogLevel.Debug); |
||||
logging.AddFakeLogging(); |
||||
}); |
||||
services.AddSingleton(globalSettings); |
||||
services.AddSingleton(hostEnvironment); |
||||
services.AddSingleton<IConfiguration>( |
||||
new ConfigurationBuilder() |
||||
.AddInMemoryCollection(config) |
||||
.Build() |
||||
); |
||||
|
||||
services.AddX509ChainCustomization(); |
||||
|
||||
after?.Invoke(services); |
||||
|
||||
return services.BuildServiceProvider(); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue