|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Primitives;
using Moq;
namespace Microsoft.AspNetCore.Server.Kestrel.Tests;
public class KestrelConfigurationLoaderTests
{
private KestrelServerOptions CreateServerOptions()
{
var serverOptions = new KestrelServerOptions();
var env = new MockHostingEnvironment { ApplicationName = "TestApplication", ContentRootPath = Directory.GetCurrentDirectory() };
serverOptions.ApplicationServices = new ServiceCollection()
.AddLogging()
.AddSingleton<IHostEnvironment>(env)
.AddSingleton(new KestrelMetrics(new TestMeterFactory()))
.AddSingleton<IHttpsConfigurationService, HttpsConfigurationService>()
.AddSingleton<HttpsConfigurationService.IInitializer, HttpsConfigurationService.Initializer>()
.BuildServiceProvider();
return serverOptions;
}
private static Mock<IConfiguration> CreateMockConfiguration() => CreateMockConfiguration(out _);
private static Mock<IConfiguration> CreateMockConfiguration(out Mock<IChangeToken> mockReloadToken)
{
var currentConfig = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:A:Url", "http://*:5000"),
new KeyValuePair<string, string>("Endpoints:B:Url", "http://*:5001"),
}).Build();
mockReloadToken = new Mock<IChangeToken>();
var mockConfig = new Mock<IConfiguration>();
mockConfig.Setup(c => c.GetSection(It.IsAny<string>())).Returns<string>(currentConfig.GetSection);
mockConfig.Setup(c => c.GetChildren()).Returns(currentConfig.GetChildren);
mockConfig.Setup(c => c.GetReloadToken()).Returns(mockReloadToken.Object);
return mockConfig;
}
[Fact]
public void ConfigureNamedEndpoint_OnlyRunForMatchingConfig()
{
var found = false;
var serverOptions = CreateServerOptions();
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:Found:Url", "http://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("Found", endpointOptions => found = true)
.Endpoint("NotFound", endpointOptions => throw new NotImplementedException())
.Load();
Assert.Single(serverOptions.GetListenOptions());
Assert.Equal(5001, serverOptions.ConfigurationBackedListenOptions[0].IPEndPoint.Port);
Assert.True(found);
}
[Fact]
public void ConfigureEndpoint_OnlyRunWhenBuildIsCalled()
{
var run = false;
var serverOptions = CreateServerOptions();
serverOptions.Configure()
.LocalhostEndpoint(5001, endpointOptions => run = true);
Assert.Empty(serverOptions.GetListenOptions());
serverOptions.ConfigurationLoader.Load();
Assert.Single(serverOptions.GetListenOptions());
Assert.Equal(5001, serverOptions.CodeBackedListenOptions[0].IPEndPoint.Port);
Assert.True(run);
}
[Fact]
public void CallBuildTwice_OnlyRunsOnce()
{
var serverOptions = CreateServerOptions();
var builder = serverOptions.Configure()
.LocalhostEndpoint(5001);
Assert.Empty(serverOptions.GetListenOptions());
Assert.Equal(builder, serverOptions.ConfigurationLoader);
builder.Load();
Assert.Single(serverOptions.GetListenOptions());
Assert.Equal(5001, serverOptions.CodeBackedListenOptions[0].IPEndPoint.Port);
Assert.NotNull(serverOptions.ConfigurationLoader);
builder.Load();
Assert.Single(serverOptions.GetListenOptions());
Assert.Equal(5001, serverOptions.CodeBackedListenOptions[0].IPEndPoint.Port);
Assert.NotNull(serverOptions.ConfigurationLoader);
}
[Fact]
public void Configure_IsReplaceable()
{
var run1 = false;
var serverOptions = CreateServerOptions();
var config1 = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
}).Build();
serverOptions.Configure(config1)
.LocalhostEndpoint(5001, endpointOptions => run1 = true);
Assert.Empty(serverOptions.GetListenOptions());
Assert.False(run1);
var run2 = false;
var config2 = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End2:Url", "http://*:5002"),
}).Build();
serverOptions.Configure(config2)
.LocalhostEndpoint(5003, endpointOptions => run2 = true);
serverOptions.ConfigurationLoader.Load();
Assert.Equal(2, serverOptions.GetListenOptions().Length);
Assert.Equal(5002, serverOptions.ConfigurationBackedListenOptions[0].IPEndPoint.Port);
Assert.Equal(5003, serverOptions.CodeBackedListenOptions[0].IPEndPoint.Port);
Assert.False(run1);
Assert.True(run2);
}
[Fact]
public void ConfigureDefaultsAppliesToNewConfigureEndpoints()
{
var serverOptions = CreateServerOptions();
serverOptions.ConfigureEndpointDefaults(opt =>
{
opt.Protocols = HttpProtocols.Http1;
});
serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();
opt.ServerCertificateChain = TestResources.GetTestChain();
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
var ran1 = false;
var ran2 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
ran1 = true;
Assert.True(opt.IsHttps);
Assert.NotNull(opt.HttpsOptions.ServerCertificate);
Assert.NotNull(opt.HttpsOptions.ServerCertificateChain);
Assert.Equal(2, opt.HttpsOptions.ServerCertificateChain.Count);
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode);
Assert.Equal(HttpProtocols.Http1, opt.ListenOptions.Protocols);
})
.LocalhostEndpoint(5002, opt =>
{
ran2 = true;
Assert.Equal(HttpProtocols.Http1, opt.Protocols);
})
.Load();
Assert.True(ran1);
Assert.True(ran2);
Assert.True(serverOptions.ConfigurationBackedListenOptions[0].IsTls);
Assert.False(serverOptions.CodeBackedListenOptions[0].IsTls);
}
[Fact]
public void ConfigureEndpointDefaultCanEnableHttps()
{
var serverOptions = CreateServerOptions();
serverOptions.ConfigureEndpointDefaults(opt =>
{
opt.UseHttps(TestResources.GetTestCertificate());
});
serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
var ran1 = false;
var ran2 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
ran1 = true;
Assert.True(opt.IsHttps);
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode);
})
.LocalhostEndpoint(5002, opt =>
{
ran2 = true;
})
.Load();
Assert.True(ran1);
Assert.True(ran2);
// You only get Https once per endpoint.
Assert.True(serverOptions.ConfigurationBackedListenOptions[0].IsTls);
Assert.True(serverOptions.CodeBackedListenOptions[0].IsTls);
}
[Fact]
// inherently flaky (writes to a well-known path)
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/48736")]
public void ConfigureEndpointDevelopmentCertificateGetsLoadedWhenPresent()
{
try
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
var bytes = certificate.Export(X509ContentType.Pkcs12, "1234");
var path = GetCertificatePath();
Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllBytes(path, bytes);
var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
}).Build();
serverOptions
.Configure(config)
.Endpoint("End1", opt =>
{
ran1 = true;
Assert.True(opt.IsHttps);
Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, certificate.SerialNumber);
}).Load();
Assert.True(ran1);
Assert.Null(serverOptions.DevelopmentCertificate); // Not used since configuration cert is present
}
finally
{
if (File.Exists(GetCertificatePath()))
{
File.Delete(GetCertificatePath());
}
}
}
[Fact]
public void DevelopmentCertificateCanBeRemoved()
{
try
{
var serverOptions = CreateServerOptions();
var devCert = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
var devCertBytes = devCert.Export(X509ContentType.Pkcs12, "1234");
var devCertPath = GetCertificatePath();
Directory.CreateDirectory(Path.GetDirectoryName(devCertPath));
File.WriteAllBytes(devCertPath, devCertBytes);
var defaultCertPath = TestResources.TestCertificatePath;
var defaultCert = TestResources.GetTestCertificate();
Assert.NotEqual(devCert.SerialNumber, defaultCert.SerialNumber); // Need to be able to distinguish them
var endpointConfig = new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
};
var devCertConfig = new[]
{
new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
};
var defaultCertConfig = new[]
{
new KeyValuePair<string, string>("Certificates:Default:path", defaultCertPath),
new KeyValuePair<string, string>("Certificates:Default:Password", "testPassword"),
};
var config = new ConfigurationBuilder().AddInMemoryCollection(endpointConfig.Concat(devCertConfig)).Build();
serverOptions.Configure(config).Load();
CheckCertificates(devCert);
// Add Default certificate
serverOptions.ConfigurationLoader.Configuration = new ConfigurationBuilder().AddInMemoryCollection(endpointConfig.Concat(devCertConfig).Concat(defaultCertConfig)).Build();
_ = serverOptions.ConfigurationLoader.Reload();
// Default is preferred to Development
CheckCertificates(defaultCert);
// Remove Default certificate
serverOptions.ConfigurationLoader.Configuration = new ConfigurationBuilder().AddInMemoryCollection(endpointConfig.Concat(devCertConfig)).Build();
_ = serverOptions.ConfigurationLoader.Reload();
// Back to Development
CheckCertificates(devCert);
// Remove Development certificate
serverOptions.ConfigurationLoader.Configuration = new ConfigurationBuilder().AddInMemoryCollection(endpointConfig).Build();
// With all of the configuration certs removed, the only place left to check is the CertificateManager.
// We don't want to depend on machine state, so we cheat and say we already looked.
serverOptions.IsDevelopmentCertificateLoaded = true;
Assert.Null(serverOptions.DevelopmentCertificate);
// Since there are no configuration certs and we bypassed the CertificateManager, there will be an
// exception about not finding any certs at all.
Assert.Throws<InvalidOperationException>(() => serverOptions.ConfigurationLoader.Reload());
Assert.Null(serverOptions.ConfigurationLoader.DefaultCertificate);
void CheckCertificates(X509Certificate2 expectedCert)
{
var httpsOptions = new HttpsConnectionAdapterOptions();
serverOptions.ApplyDefaultCertificate(httpsOptions);
Assert.Equal(expectedCert.SerialNumber, httpsOptions.ServerCertificate.SerialNumber);
Assert.Equal(expectedCert.SerialNumber, serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber);
}
}
finally
{
if (File.Exists(GetCertificatePath()))
{
File.Delete(GetCertificatePath());
}
}
}
[Fact]
public void ConfigureEndpoint_RecoverFromBadPassword()
{
var serverOptions = CreateServerOptions();
var configRoot = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Endpoints:End1:Certificate:Path", TestResources.TestCertificatePath),
new KeyValuePair<string, string>("Endpoints:End1:Certificate:Password", "testPassword")
}).Build();
var configProvider = configRoot.Providers.Single();
var testCertificate = TestResources.GetTestCertificate();
var otherCertificatePath = TestResources.GetCertPath("aspnetdevcert.pfx");
var otherCertificate = new X509Certificate2(otherCertificatePath, "testPassword");
serverOptions.Configure(configRoot).Load();
CheckListenOptions(testCertificate);
// Update cert but use incorrect password
configProvider.Set("Endpoints:End1:Certificate:Path", otherCertificatePath);
configProvider.Set("Endpoints:End1:Certificate:Password", "badPassword");
// Fails to load certificate because password is bad
Assert.ThrowsAny<CryptographicException>(() => serverOptions.ConfigurationLoader.Reload());
// ConfigurationBackedListenOptions still contains prior value
CheckListenOptions(testCertificate);
// Correct password
configProvider.Set("Endpoints:End1:Certificate:Password", "testPassword");
_ = serverOptions.ConfigurationLoader.Reload();
// ConfigurationBackedListenOptions contains new value
CheckListenOptions(otherCertificate);
void CheckListenOptions(X509Certificate2 expectedCert)
{
var listenOptions = Assert.Single(serverOptions.ConfigurationBackedListenOptions);
Assert.Equal(expectedCert.SerialNumber, listenOptions.HttpsOptions!.ServerCertificate.SerialNumber);
}
}
[Fact]
public void LoadDevelopmentCertificate_LoadBeforeUseHttps()
{
try
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
var bytes = certificate.Export(X509ContentType.Pkcs12, "1234");
var path = GetCertificatePath();
Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllBytes(path, bytes);
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
}).Build();
serverOptions.Configure(config);
Assert.Null(serverOptions.ConfigurationLoader.DefaultCertificate);
serverOptions.ConfigurationLoader.Load();
Assert.NotNull(serverOptions.ConfigurationLoader.DefaultCertificate);
Assert.Equal(serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber, certificate.SerialNumber);
var ran1 = false;
serverOptions.ListenAnyIP(4545, listenOptions =>
{
ran1 = true;
listenOptions.UseHttps();
});
Assert.True(ran1);
var listenOptions = serverOptions.CodeBackedListenOptions.Single();
listenOptions.Build();
Assert.Equal(listenOptions.HttpsOptions.ServerCertificate?.SerialNumber, certificate.SerialNumber);
}
finally
{
if (File.Exists(GetCertificatePath()))
{
File.Delete(GetCertificatePath());
}
}
}
[Fact]
public void LoadDevelopmentCertificate_UseHttpsBeforeLoad()
{
try
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
var bytes = certificate.Export(X509ContentType.Pkcs12, "1234");
var path = GetCertificatePath();
Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllBytes(path, bytes);
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
}).Build();
serverOptions.Configure(config);
Assert.Null(serverOptions.ConfigurationLoader.DefaultCertificate);
var ran1 = false;
serverOptions.ListenAnyIP(4545, listenOptions =>
{
ran1 = true;
listenOptions.UseHttps();
});
Assert.True(ran1);
// Use Https triggers a load, so the default cert is already set
Assert.NotNull(serverOptions.ConfigurationLoader.DefaultCertificate);
Assert.Equal(serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber, certificate.SerialNumber);
// This Load is a no-op (tested elsewhere)
serverOptions.ConfigurationLoader.Load();
var listenOptions = serverOptions.CodeBackedListenOptions.Single();
listenOptions.Build();
Assert.Equal(listenOptions.HttpsOptions.ServerCertificate?.SerialNumber, certificate.SerialNumber);
}
finally
{
if (File.Exists(GetCertificatePath()))
{
File.Delete(GetCertificatePath());
}
}
}
[Fact]
public void LoadDevelopmentCertificate_UseHttpsBeforeConfigure()
{
try
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
var bytes = certificate.Export(X509ContentType.Pkcs12, "1234");
var path = GetCertificatePath();
Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllBytes(path, bytes);
var defaultCertificate = TestResources.GetTestCertificate();
Assert.NotEqual(certificate.SerialNumber, defaultCertificate.SerialNumber);
serverOptions.TestOverrideDefaultCertificate = defaultCertificate;
var ran1 = false;
serverOptions.ListenAnyIP(4545, listenOptions =>
{
ran1 = true;
listenOptions.UseHttps();
});
Assert.True(ran1);
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
}).Build();
serverOptions.Configure(config);
Assert.Null(serverOptions.ConfigurationLoader.DefaultCertificate);
serverOptions.ConfigurationLoader.Load();
Assert.NotNull(serverOptions.ConfigurationLoader.DefaultCertificate);
Assert.Equal(serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber, certificate.SerialNumber);
var listenOptions = serverOptions.CodeBackedListenOptions.Single();
listenOptions.Build();
// In a perfect world, it would match certificate.SerialNumber, but there's no way for an eager UseHttps
// to do that before Configure is called.
Assert.Equal(listenOptions.HttpsOptions.ServerCertificate?.SerialNumber, defaultCertificate.SerialNumber);
}
finally
{
if (File.Exists(GetCertificatePath()))
{
File.Delete(GetCertificatePath());
}
}
}
[Fact]
public void ConfigureEndpoint_ThrowsWhen_The_PasswordIsMissing()
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("https-aspnet.crt"));
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-aspnet.crt")),
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-aspnet.key"))
}).Build();
var ex = Assert.Throws<ArgumentException>(() =>
{
serverOptions
.Configure(config)
.Endpoint("End1", opt =>
{
Assert.True(opt.IsHttps);
}).Load();
});
}
[Fact]
public void ConfigureEndpoint_ThrowsWhen_TheKeyDoesntMatchTheCertificateKey()
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("https-aspnet.crt"));
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-aspnet.crt")),
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-ecdsa.key")),
new KeyValuePair<string, string>("Certificates:Default:Password", "aspnetcore")
}).Build();
var ex = Assert.Throws<ArgumentException>(() =>
{
serverOptions
.Configure(config)
.Endpoint("End1", opt =>
{
Assert.True(opt.IsHttps);
}).Load();
});
}
[Fact]
public void ConfigureEndpoint_ThrowsWhen_The_PasswordIsIncorrect()
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("https-aspnet.crt"));
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-aspnet.crt")),
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-aspnet.key")),
new KeyValuePair<string, string>("Certificates:Default:Password", "abcde"),
}).Build();
var ex = Assert.Throws<CryptographicException>(() =>
{
serverOptions
.Configure(config)
.Endpoint("End1", opt =>
{
Assert.True(opt.IsHttps);
}).Load();
});
}
[Fact]
public void ConfigureEndpoint_ThrowsWhen_The_KeyIsPublic()
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("https-aspnet.crt"));
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", "https-aspnet.crt")),
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", "https-aspnet.pub")),
}).Build();
var ex = Assert.Throws<InvalidOperationException>(() =>
{
serverOptions
.Configure(config)
.Endpoint("End1", opt =>
{
Assert.True(opt.IsHttps);
}).Load();
});
Assert.StartsWith("Error getting private key from", ex.Message);
Assert.IsAssignableFrom<CryptographicException>(ex.InnerException);
}
[Theory]
[InlineData("https-rsa.pem", "https-rsa.key", null)]
[InlineData("https-rsa.pem", "https-rsa-protected.key", "aspnetcore")]
[InlineData("https-rsa.crt", "https-rsa.key", null)]
[InlineData("https-rsa.crt", "https-rsa-protected.key", "aspnetcore")]
[InlineData("https-ecdsa.pem", "https-ecdsa.key", null)]
[InlineData("https-ecdsa.pem", "https-ecdsa-protected.key", "aspnetcore")]
[InlineData("https-ecdsa.crt", "https-ecdsa.key", null)]
[InlineData("https-ecdsa.crt", "https-ecdsa-protected.key", "aspnetcore")]
[InlineData("https-dsa.pem", "https-dsa.key", null)]
[InlineData("https-dsa.pem", "https-dsa-protected.key", "test")]
[InlineData("https-dsa.crt", "https-dsa.key", null)]
[InlineData("https-dsa.crt", "https-dsa-protected.key", "test")]
public void ConfigureEndpoint_CanLoadPemCertificates(string certificateFile, string certificateKey, string password)
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath(Path.ChangeExtension(certificateFile, "crt")));
var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Certificates:Default:Path", Path.Combine("shared", "TestCertificates", certificateFile)),
new KeyValuePair<string, string>("Certificates:Default:KeyPath", Path.Combine("shared", "TestCertificates", certificateKey)),
}
.Concat(password != null ? new[] { new KeyValuePair<string, string>("Certificates:Default:Password", password) } : Array.Empty<KeyValuePair<string, string>>()))
.Build();
serverOptions
.Configure(config)
.Endpoint("End1", opt =>
{
ran1 = true;
Assert.True(opt.IsHttps);
Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, certificate.SerialNumber);
}).Load();
Assert.True(ran1);
Assert.Null(serverOptions.DevelopmentCertificate); // Not used since configuration cert is present
}
[Fact]
public void ConfigureEndpointDevelopmentCertificateGetsIgnoredIfPasswordIsNotCorrect()
{
try
{
var serverOptions = CreateServerOptions();
var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
var bytes = certificate.Export(X509ContentType.Pkcs12, "1234");
var path = GetCertificatePath();
Directory.CreateDirectory(Path.GetDirectoryName(path));
File.WriteAllBytes(path, bytes);
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Certificates:Development:Password", "12341234"),
}).Build();
serverOptions
.Configure(config)
.Load();
Assert.Null(serverOptions.DevelopmentCertificate);
}
finally
{
if (File.Exists(GetCertificatePath()))
{
File.Delete(GetCertificatePath());
}
}
}
[Fact]
public void ConfigureEndpointDevelopmentCertificateGetsIgnoredIfPfxFileDoesNotExist()
{
try
{
var serverOptions = CreateServerOptions();
if (File.Exists(GetCertificatePath()))
{
File.Delete(GetCertificatePath());
}
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Certificates:Development:Password", "12341234")
}).Build();
serverOptions
.Configure(config)
.Load();
Assert.Null(serverOptions.DevelopmentCertificate);
}
finally
{
if (File.Exists(GetCertificatePath()))
{
File.Delete(GetCertificatePath());
}
}
}
[Fact]
public void ConfigureEndpoint_ThrowsWhen_HttpsConfigIsDeclaredInNonHttpsEndpoints()
{
var serverOptions = CreateServerOptions();
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
// We shouldn't need to specify a real cert, because KestrelConfigurationLoader should check whether the endpoint requires a cert before trying to load it.
new KeyValuePair<string, string>("Endpoints:End1:Certificate:Path", "fakecert.pfx"),
}).Build();
var ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "Certificate"), ex.Message);
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
new KeyValuePair<string, string>("Endpoints:End1:Certificate:Subject", "example.org"),
}).Build();
ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "Certificate"), ex.Message);
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
new KeyValuePair<string, string>("Endpoints:End1:ClientCertificateMode", ClientCertificateMode.RequireCertificate.ToString()),
}).Build();
ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "ClientCertificateMode"), ex.Message);
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
new KeyValuePair<string, string>("Endpoints:End1:SslProtocols:0", SslProtocols.Tls13.ToString()),
}).Build();
ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "SslProtocols"), ex.Message);
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
new KeyValuePair<string, string>("Endpoints:End1:Sni:Protocols", HttpProtocols.Http1.ToString()),
}).Build();
ex = Assert.Throws<InvalidOperationException>(() => serverOptions.Configure(config).Load());
Assert.Equal(CoreStrings.FormatEndpointHasUnusedHttpsConfig("End1", "Sni"), ex.Message);
}
[Fact]
public void ConfigureEndpoint_DoesNotThrowWhen_HttpsConfigIsDeclaredInEndpointDefaults()
{
var serverOptions = CreateServerOptions();
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
new KeyValuePair<string, string>("EndpointDefaults:ClientCertificateMode", ClientCertificateMode.RequireCertificate.ToString()),
}).Build();
var (_, endpointsToStart) = serverOptions.Configure(config).Reload();
var end1 = Assert.Single(endpointsToStart);
Assert.NotNull(end1?.EndpointConfig);
Assert.Null(end1.EndpointConfig.ClientCertificateMode);
serverOptions = CreateServerOptions();
config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
new KeyValuePair<string, string>("EndpointDefaults:SslProtocols:0", SslProtocols.Tls13.ToString()),
}).Build();
(_, endpointsToStart) = serverOptions.Configure(config).Reload();
end1 = Assert.Single(endpointsToStart);
Assert.NotNull(end1?.EndpointConfig);
Assert.Null(end1.EndpointConfig.SslProtocols);
}
// On helix retry list - inherently flaky (FS events)
[Theory]
[InlineData(true)] // This might be flaky, since it depends on file system events (or polling)
[InlineData(false)] // This will be slow (1 seconds)
public async Task CertificateChangedOnDisk(bool reloadOnChange)
{
var certificatePath = GetCertificatePath();
try
{
var serverOptions = CreateServerOptions();
var certificatePassword = "1234";
var oldCertificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
var oldCertificateBytes = oldCertificate.Export(X509ContentType.Pkcs12, certificatePassword);
var newCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword", X509KeyStorageFlags.Exportable);
var newCertificateBytes = newCertificate.Export(X509ContentType.Pkcs12, certificatePassword);
Directory.CreateDirectory(Path.GetDirectoryName(certificatePath));
File.WriteAllBytes(certificatePath, oldCertificateBytes);
var endpointConfigurationCallCount = 0;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Endpoints:End1:Certificate:Path", certificatePath),
new KeyValuePair<string, string>("Endpoints:End1:Certificate:Password", certificatePassword),
}).Build();
var configLoader = serverOptions
.Configure(config, reloadOnChange)
.Endpoint("End1", opt =>
{
Assert.True(opt.IsHttps);
var expectedSerialNumber = endpointConfigurationCallCount == 0
? oldCertificate.SerialNumber
: newCertificate.SerialNumber;
Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, expectedSerialNumber);
endpointConfigurationCallCount++;
});
configLoader.Load();
var fileTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
if (reloadOnChange) // There's no reload token if !reloadOnChange
{
configLoader.GetReloadToken().RegisterChangeCallback(_ => fileTcs.SetResult(), state: null);
}
File.WriteAllBytes(certificatePath, newCertificateBytes);
if (reloadOnChange)
{
await fileTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10)); // Needs to be meaningfully longer than the polling period - 4 seconds
}
else
{
// We can't just check immediately that the callback hasn't fired - we might preempt it
await Task.Delay(TimeSpan.FromSeconds(1));
Assert.False(fileTcs.Task.IsCompleted);
}
Assert.Equal(1, endpointConfigurationCallCount);
if (reloadOnChange)
{
configLoader.Reload();
Assert.Equal(2, endpointConfigurationCallCount);
}
}
finally
{
if (File.Exists(certificatePath))
{
// Note: the watcher will see this event, but we ignore deletions, so it shouldn't matter
File.Delete(certificatePath);
}
}
}
// On helix retry list - inherently flaky (FS events)
[ConditionalFact]
[OSSkipCondition(OperatingSystems.Windows)] // Windows has poor support for directory symlinks (e.g. https://github.com/dotnet/runtime/issues/27826)
public async Task CertificateChangedOnDisk_Symlink()
{
var tempDir = Directory.CreateTempSubdirectory().FullName;
try
{
// temp/
// tls.key -> link/tls.key
// link/ -> old/
// old/
// tls.key
// new/
// tls.key
var oldDir = Directory.CreateDirectory(Path.Combine(tempDir, "old"));
var newDir = Directory.CreateDirectory(Path.Combine(tempDir, "new"));
var oldCertPath = Path.Combine(oldDir.FullName, "tls.key");
var newCertPath = Path.Combine(newDir.FullName, "tls.key");
var dirLink = Directory.CreateSymbolicLink(Path.Combine(tempDir, "link"), "./old");
var fileLink = File.CreateSymbolicLink(Path.Combine(tempDir, "tls.key"), "./link/tls.key");
var serverOptions = CreateServerOptions();
var certificatePassword = "1234";
var oldCertificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
var oldCertificateBytes = oldCertificate.Export(X509ContentType.Pkcs12, certificatePassword);
File.WriteAllBytes(oldCertPath, oldCertificateBytes);
var newCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword", X509KeyStorageFlags.Exportable);
var newCertificateBytes = newCertificate.Export(X509ContentType.Pkcs12, certificatePassword);
File.WriteAllBytes(newCertPath, newCertificateBytes);
var endpointConfigurationCallCount = 0;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Endpoints:End1:Certificate:Path", fileLink.FullName),
new KeyValuePair<string, string>("Endpoints:End1:Certificate:Password", certificatePassword),
}).Build();
var configLoader = serverOptions
.Configure(config, reloadOnChange: true)
.Endpoint("End1", opt =>
{
Assert.True(opt.IsHttps);
var expectedSerialNumber = endpointConfigurationCallCount == 0
? oldCertificate.SerialNumber
: newCertificate.SerialNumber;
Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, expectedSerialNumber);
endpointConfigurationCallCount++;
});
configLoader.Load();
var fileTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
configLoader.GetReloadToken().RegisterChangeCallback(_ => fileTcs.SetResult(), state: null);
// Clobber link/ directory symlink - this will effectively cause the cert to be updated.
// Unfortunately, it throws (file exists) if we don't delete the old one first so it's not a single, clean FS operation.
dirLink.Delete();
dirLink = Directory.CreateSymbolicLink(Path.Combine(tempDir, "link"), "./new");
// This can fail in local runs where the timeout is 5 seconds and polling period is 4 seconds - just re-run
await fileTcs.Task.DefaultTimeout();
Assert.Equal(1, endpointConfigurationCallCount);
configLoader.Reload();
Assert.Equal(2, endpointConfigurationCallCount);
}
finally
{
if (Directory.Exists(tempDir))
{
// Note: the watcher will see this event, but we ignore deletions, so it shouldn't matter
Directory.Delete(tempDir, recursive: true);
}
}
}
[ConditionalTheory]
[InlineData("http1", HttpProtocols.Http1)]
// [InlineData("http2", HttpProtocols.Http2)] // Not supported due to missing ALPN support. https://github.com/dotnet/corefx/issues/33016
[InlineData("http1AndHttp2", HttpProtocols.Http1AndHttp2)] // Gracefully falls back to HTTP/1
[OSSkipCondition(OperatingSystems.Linux)]
[MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win7)]
public void DefaultConfigSectionCanSetProtocols_MacAndWin7(string input, HttpProtocols expected)
=> DefaultConfigSectionCanSetProtocols(input, expected);
[ConditionalTheory]
[InlineData("http1", HttpProtocols.Http1)]
[InlineData("http2", HttpProtocols.Http2)]
[InlineData("http1AndHttp2", HttpProtocols.Http1AndHttp2)]
// [InlineData("http1AndHttp2andHttp3", HttpProtocols.Http1AndHttp2AndHttp3)] // HTTP/3 not currently supported on macOS
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
public void DefaultConfigSectionCanSetProtocols_NonWin7(string input, HttpProtocols expected)
=> DefaultConfigSectionCanSetProtocols(input, expected);
[ConditionalTheory]
[InlineData("http1", HttpProtocols.Http1)]
[InlineData("http2", HttpProtocols.Http2)]
[InlineData("http1AndHttp2", HttpProtocols.Http1AndHttp2)]
[InlineData("http1AndHttp2andHttp3", HttpProtocols.Http1AndHttp2AndHttp3)]
[OSSkipCondition(OperatingSystems.MacOSX)]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
public void DefaultConfigSectionCanSetProtocols_NonMacAndWin7(string input, HttpProtocols expected)
=> DefaultConfigSectionCanSetProtocols(input, expected);
private void DefaultConfigSectionCanSetProtocols(string input, HttpProtocols expected)
{
var serverOptions = CreateServerOptions();
var ranDefault = false;
serverOptions.ConfigureEndpointDefaults(opt =>
{
Assert.Equal(expected, opt.Protocols);
ranDefault = true;
});
serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
var ran1 = false;
var ran2 = false;
var ran3 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("EndpointDefaults:Protocols", input),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
Assert.True(opt.IsHttps);
Assert.NotNull(opt.HttpsOptions.ServerCertificate);
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode);
Assert.Equal(expected, opt.ListenOptions.Protocols);
ran1 = true;
})
.LocalhostEndpoint(5002, opt =>
{
Assert.Equal(expected, opt.Protocols);
ran2 = true;
})
.Load();
serverOptions.ListenAnyIP(0, opt =>
{
Assert.Equal(expected, opt.Protocols);
ran3 = true;
});
Assert.True(ranDefault);
Assert.True(ran1);
Assert.True(ran2);
Assert.True(ran3);
}
[ConditionalTheory]
[InlineData("http1", HttpProtocols.Http1)]
// [InlineData("http2", HttpProtocols.Http2)] // Not supported due to missing ALPN support. https://github.com/dotnet/corefx/issues/33016
[InlineData("http1AndHttp2", HttpProtocols.Http1AndHttp2)] // Gracefully falls back to HTTP/1
[OSSkipCondition(OperatingSystems.Linux)]
[MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win7)]
public void EndpointConfigSectionCanSetProtocols_MacAndWin7(string input, HttpProtocols expected) =>
EndpointConfigSectionCanSetProtocols(input, expected);
[ConditionalTheory]
[InlineData("http1", HttpProtocols.Http1)]
[InlineData("http2", HttpProtocols.Http2)]
[InlineData("http1AndHttp2", HttpProtocols.Http1AndHttp2)]
[OSSkipCondition(OperatingSystems.MacOSX)]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
public void EndpointConfigSectionCanSetProtocols_NonMacAndWin7(string input, HttpProtocols expected) =>
EndpointConfigSectionCanSetProtocols(input, expected);
private void EndpointConfigSectionCanSetProtocols(string input, HttpProtocols expected)
{
var serverOptions = CreateServerOptions();
var ranDefault = false;
serverOptions.ConfigureEndpointDefaults(opt =>
{
// Kestrel default.
Assert.Equal(ListenOptions.DefaultHttpProtocols, opt.Protocols);
ranDefault = true;
});
serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
var ran1 = false;
var ran2 = false;
var ran3 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Protocols", input),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
Assert.True(opt.IsHttps);
Assert.NotNull(opt.HttpsOptions.ServerCertificate);
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode);
Assert.Equal(expected, opt.ListenOptions.Protocols);
ran1 = true;
})
.LocalhostEndpoint(5002, opt =>
{
// Kestrel default.
Assert.Equal(ListenOptions.DefaultHttpProtocols, opt.Protocols);
ran2 = true;
})
.Load();
serverOptions.ListenAnyIP(0, opt =>
{
// Kestrel default.
Assert.Equal(ListenOptions.DefaultHttpProtocols, opt.Protocols);
ran3 = true;
});
Assert.True(ranDefault);
Assert.True(ran1);
Assert.True(ran2);
Assert.True(ran3);
}
[Fact]
public void EndpointConfigureSection_CanSetSslProtocol()
{
var serverOptions = CreateServerOptions();
var ranDefault = false;
serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();
// Kestrel default
Assert.Equal(SslProtocols.None, opt.SslProtocols);
ranDefault = true;
});
var ran1 = false;
var ran2 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:SslProtocols:0", "Tls11"),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
Assert.Equal(SslProtocols.Tls11, opt.HttpsOptions.SslProtocols);
#pragma warning restore SYSLIB0039
ran1 = true;
})
.Load();
serverOptions.ListenAnyIP(0, opt =>
{
opt.UseHttps(httpsOptions =>
{
// Kestrel default.
Assert.Equal(SslProtocols.None, httpsOptions.SslProtocols);
ran2 = true;
});
});
Assert.True(ranDefault);
Assert.True(ran1);
Assert.True(ran2);
}
[Fact]
public void EndpointConfigureSection_CanOverrideSslProtocolsFromConfigureHttpsDefaults()
{
var serverOptions = CreateServerOptions();
serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();
opt.SslProtocols = SslProtocols.Tls12;
});
var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:SslProtocols:0", "Tls11"),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
Assert.Equal(SslProtocols.Tls11, opt.HttpsOptions.SslProtocols);
#pragma warning restore SYSLIB0039
ran1 = true;
})
.Load();
Assert.True(ran1);
}
[Fact]
public void DefaultEndpointConfigureSection_CanSetSslProtocols()
{
var serverOptions = CreateServerOptions();
serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();
});
var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("EndpointDefaults:SslProtocols:0", "Tls11"),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
Assert.Equal(SslProtocols.Tls11, opt.HttpsOptions.SslProtocols);
#pragma warning restore SYSLIB0039
ran1 = true;
})
.Load();
Assert.True(ran1);
}
[Fact]
public void DefaultEndpointConfigureSection_ConfigureHttpsDefaultsCanOverrideSslProtocols()
{
var serverOptions = CreateServerOptions();
serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
Assert.Equal(SslProtocols.Tls11, opt.SslProtocols);
#pragma warning restore SYSLIB0039
opt.SslProtocols = SslProtocols.Tls12;
});
var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("EndpointDefaults:SslProtocols:0", "Tls11"),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
Assert.Equal(SslProtocols.Tls12, opt.HttpsOptions.SslProtocols);
ran1 = true;
})
.Load();
Assert.True(ran1);
}
[Fact]
public void EndpointConfigureSection_CanSetClientCertificateMode()
{
var serverOptions = CreateServerOptions();
var ranDefault = false;
serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();
// Kestrel default
Assert.Equal(ClientCertificateMode.NoCertificate, opt.ClientCertificateMode);
ranDefault = true;
});
var ran1 = false;
var ran2 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:ClientCertificateMode", "AllowCertificate"),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
Assert.Equal(ClientCertificateMode.AllowCertificate, opt.HttpsOptions.ClientCertificateMode);
ran1 = true;
})
.Load();
serverOptions.ListenAnyIP(0, opt =>
{
opt.UseHttps(httpsOptions =>
{
// Kestrel default.
Assert.Equal(ClientCertificateMode.NoCertificate, httpsOptions.ClientCertificateMode);
ran2 = true;
});
});
Assert.True(ranDefault);
Assert.True(ran1);
Assert.True(ran2);
}
[Fact]
public void EndpointConfigureSection_CanConfigureSni()
{
var serverOptions = CreateServerOptions();
var certPath = Path.Combine("shared", "TestCertificates", "https-ecdsa.pem");
var keyPath = Path.Combine("shared", "TestCertificates", "https-ecdsa.key");
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:Protocols", HttpProtocols.None.ToString()),
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:SslProtocols:0", SslProtocols.Tls13.ToString()),
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:ClientCertificateMode", ClientCertificateMode.RequireCertificate.ToString()),
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:Certificate:Path", certPath),
new KeyValuePair<string, string>("Endpoints:End1:Sni:*.example.org:Certificate:KeyPath", keyPath),
}).Build();
var (_, endpointsToStart) = serverOptions.Configure(config).Reload();
var end1 = Assert.Single(endpointsToStart);
var (name, sniConfig) = Assert.Single(end1?.EndpointConfig?.Sni);
Assert.Equal("*.example.org", name);
Assert.Equal(HttpProtocols.None, sniConfig.Protocols);
Assert.Equal(SslProtocols.Tls13, sniConfig.SslProtocols);
Assert.Equal(ClientCertificateMode.RequireCertificate, sniConfig.ClientCertificateMode);
Assert.Equal(certPath, sniConfig.Certificate.Path);
Assert.Equal(keyPath, sniConfig.Certificate.KeyPath);
}
[Fact]
public void EndpointConfigureSection_CanOverrideClientCertificateModeFromConfigureHttpsDefaults()
{
var serverOptions = CreateServerOptions();
serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:ClientCertificateMode", "AllowCertificate"),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
Assert.Equal(ClientCertificateMode.AllowCertificate, opt.HttpsOptions.ClientCertificateMode);
ran1 = true;
})
.Load();
Assert.True(ran1);
}
[Fact]
public void DefaultEndpointConfigureSection_CanSetClientCertificateMode()
{
var serverOptions = CreateServerOptions();
serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();
});
var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("EndpointDefaults:ClientCertificateMode", "AllowCertificate"),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
Assert.Equal(ClientCertificateMode.AllowCertificate, opt.HttpsOptions.ClientCertificateMode);
ran1 = true;
})
.Load();
Assert.True(ran1);
}
[Fact]
public void DefaultEndpointConfigureSection_ConfigureHttpsDefaultsCanOverrideClientCertificateMode()
{
var serverOptions = CreateServerOptions();
serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();
Assert.Equal(ClientCertificateMode.AllowCertificate, opt.ClientCertificateMode);
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("EndpointDefaults:ClientCertificateMode", "AllowCertificate"),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode);
ran1 = true;
})
.Load();
Assert.True(ran1);
}
[Fact]
public void Reload_IdentifiesEndpointsToStartAndStop()
{
var serverOptions = CreateServerOptions();
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:A:Url", "http://*:5000"),
new KeyValuePair<string, string>("Endpoints:B:Url", "http://*:5001"),
}).Build();
serverOptions.Configure(config).Load();
Assert.Equal(2, serverOptions.ConfigurationBackedListenOptions.Count);
Assert.Equal(5000, serverOptions.ConfigurationBackedListenOptions[0].IPEndPoint.Port);
Assert.Equal(5001, serverOptions.ConfigurationBackedListenOptions[1].IPEndPoint.Port);
serverOptions.ConfigurationLoader.Configuration = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:A:Url", "http://*:5000"),
new KeyValuePair<string, string>("Endpoints:B:Url", "http://*:5002"),
new KeyValuePair<string, string>("Endpoints:C:Url", "http://*:5003"),
}).Build();
var (endpointsToStop, endpointsToStart) = serverOptions.ConfigurationLoader.Reload();
Assert.Single(endpointsToStop);
Assert.Equal(5001, endpointsToStop[0].IPEndPoint.Port);
Assert.Equal(2, endpointsToStart.Count);
Assert.Equal(5002, endpointsToStart[0].IPEndPoint.Port);
Assert.Equal(5003, endpointsToStart[1].IPEndPoint.Port);
Assert.Equal(3, serverOptions.ConfigurationBackedListenOptions.Count);
Assert.Equal(5000, serverOptions.ConfigurationBackedListenOptions[0].IPEndPoint.Port);
Assert.Same(endpointsToStart[0], serverOptions.ConfigurationBackedListenOptions[1]);
Assert.Same(endpointsToStart[1], serverOptions.ConfigurationBackedListenOptions[2]);
}
[Fact]
public void Reload_IdentifiesEndpointsWithChangedDefaults()
{
var serverOptions = CreateServerOptions();
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:DefaultProtocol:Url", "http://*:5000"),
new KeyValuePair<string, string>("Endpoints:NonDefaultProtocol:Url", "http://*:5001"),
new KeyValuePair<string, string>("Endpoints:NonDefaultProtocol:Protocols", "Http1AndHttp2"),
}).Build();
serverOptions.Configure(config).Load();
serverOptions.ConfigurationLoader.Configuration = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:DefaultProtocol:Url", "http://*:5000"),
new KeyValuePair<string, string>("Endpoints:NonDefaultProtocol:Url", "http://*:5001"),
new KeyValuePair<string, string>("Endpoints:NonDefaultProtocol:Protocols", "Http1AndHttp2"),
new KeyValuePair<string, string>("EndpointDefaults:Protocols", "Http1"),
}).Build();
var (endpointsToStop, endpointsToStart) = serverOptions.ConfigurationLoader.Reload();
// NonDefaultProtocol is unchanged and doesn't need to be stopped/started
var stopEndpoint = Assert.Single(endpointsToStop);
var startEndpoint = Assert.Single(endpointsToStart);
Assert.Equal(5000, stopEndpoint.IPEndPoint.Port);
Assert.Equal(ListenOptions.DefaultHttpProtocols, stopEndpoint.Protocols);
Assert.Equal(5000, startEndpoint.IPEndPoint.Port);
Assert.Equal(HttpProtocols.Http1, startEndpoint.Protocols);
}
[Fact]
public void Reload_RerunsNamedEndpointConfigurationOnChange()
{
var foundChangedCount = 0;
var foundUnchangedCount = 0;
var serverOptions = CreateServerOptions();
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:Changed:Url", "http://*:5001"),
new KeyValuePair<string, string>("Endpoints:Unchanged:Url", "http://*:5000"),
}).Build();
serverOptions.Configure(config)
.Endpoint("Changed", endpointOptions => foundChangedCount++)
.Endpoint("Unchanged", endpointOptions => foundUnchangedCount++)
.Endpoint("NotFound", endpointOptions => throw new NotImplementedException())
.Load();
Assert.Equal(1, foundChangedCount);
Assert.Equal(1, foundUnchangedCount);
serverOptions.ConfigurationLoader.Configuration = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:Changed:Url", "http://*:5002"),
new KeyValuePair<string, string>("Endpoints:Unchanged:Url", "http://*:5000"),
}).Build();
serverOptions.ConfigurationLoader.Reload();
Assert.Equal(2, foundChangedCount);
Assert.Equal(1, foundUnchangedCount);
}
[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public void MultipleLoads_Consecutive(bool loadInternal, bool reloadOnChange)
{
var serverOptions = CreateServerOptions();
var mockConfig = CreateMockConfiguration();
serverOptions.Configure(mockConfig.Object, reloadOnChange);
Action load = loadInternal ? serverOptions.ConfigurationLoader.LoadInternal : serverOptions.ConfigurationLoader.Load;
load();
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), Times.AtLeastOnce);
mockConfig.Invocations.Clear();
load();
// In any case, nothing has changed, so nothing is read
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), Times.Never);
}
[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public void MultipleLoads_ConfigureBetween(bool loadInternal, bool reloadOnChange)
{
var serverOptions = CreateServerOptions();
var mockConfig = CreateMockConfiguration();
serverOptions.Configure(mockConfig.Object, reloadOnChange);
var oldConfigurationLoader = serverOptions.ConfigurationLoader;
if (loadInternal)
{
serverOptions.ConfigurationLoader.LoadInternal();
}
else
{
serverOptions.ConfigurationLoader.Load();
}
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), Times.AtLeastOnce);
mockConfig.Invocations.Clear();
serverOptions.Configure(mockConfig.Object, reloadOnChange: false);
var newConfigurationLoader = serverOptions.ConfigurationLoader;
Assert.NotSame(oldConfigurationLoader, newConfigurationLoader);
if (loadInternal)
{
serverOptions.ConfigurationLoader.LoadInternal();
}
else
{
serverOptions.ConfigurationLoader.Load();
}
// In any case, the configuration loader has been replaced, so this is a "first" load
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), Times.AtLeastOnce);
}
[Theory]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public void MultipleLoadInternals_ConfigurationChanges(bool loadInternal, bool reloadOnChange)
{
var serverOptions = CreateServerOptions();
var mockConfig = CreateMockConfiguration(out var mockReloadToken);
serverOptions.Configure(mockConfig.Object, reloadOnChange);
Action load = loadInternal ? serverOptions.ConfigurationLoader.LoadInternal : serverOptions.ConfigurationLoader.Load;
load();
mockReloadToken.VerifyGet(t => t.HasChanged, Times.Never);
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), Times.AtLeastOnce);
mockReloadToken.SetupGet(t => t.HasChanged).Returns(true);
mockReloadToken.Invocations.Clear();
mockConfig.Invocations.Clear();
load();
Func<Times> reloadTimes = loadInternal && reloadOnChange ? Times.AtLeastOnce : Times.Never;
mockReloadToken.VerifyGet(t => t.HasChanged, reloadTimes);
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), reloadTimes);
}
[Fact]
public void LoadInternalBeforeLoad()
{
var serverOptions = CreateServerOptions();
var mockConfig = CreateMockConfiguration(out var mockReloadToken);
serverOptions.Configure(mockConfig.Object, reloadOnChange: true);
serverOptions.ConfigurationLoader.LoadInternal();
mockReloadToken.VerifyGet(t => t.HasChanged, Times.Never);
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), Times.AtLeastOnce);
mockReloadToken.SetupGet(t => t.HasChanged).Returns(true);
mockReloadToken.Invocations.Clear();
mockConfig.Invocations.Clear();
serverOptions.ConfigurationLoader.LocalhostEndpoint(5000);
serverOptions.ConfigurationLoader.Load();
mockReloadToken.VerifyGet(t => t.HasChanged, Times.Never);
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), Times.Never);
Assert.Single(serverOptions.CodeBackedListenOptions); // Still have to process endpoints
}
[Fact]
public void LoadInternalAfterLoad()
{
var serverOptions = CreateServerOptions();
var mockConfig = CreateMockConfiguration(out var mockReloadToken);
serverOptions.Configure(mockConfig.Object, reloadOnChange: true);
serverOptions.ConfigurationLoader.Load();
mockReloadToken.VerifyGet(t => t.HasChanged, Times.Never);
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), Times.AtLeastOnce);
mockReloadToken.SetupGet(t => t.HasChanged).Returns(true);
mockReloadToken.Invocations.Clear();
mockConfig.Invocations.Clear();
serverOptions.ConfigurationLoader.LoadInternal();
mockReloadToken.VerifyGet(t => t.HasChanged, Times.AtLeastOnce);
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), Times.AtLeastOnce);
}
[Fact]
public void ProcessEndpointsToAdd()
{
int numEndpointsToAdd = 3;
int numEndpointsAdded = 0;
var serverOptions = CreateServerOptions();
serverOptions.Configure();
for (int i = 0; i < numEndpointsToAdd; i++)
{
serverOptions.ConfigurationLoader.LocalhostEndpoint(5000 + i, _ => numEndpointsAdded++);
}
serverOptions.ConfigurationLoader.ProcessEndpointsToAdd();
Assert.Equal(numEndpointsToAdd, numEndpointsAdded);
Assert.Equal(numEndpointsToAdd, serverOptions.CodeBackedListenOptions.Count);
Assert.Empty(serverOptions.ConfigurationBackedListenOptions);
// Adding more endpoints and calling again has no effect
for (int i = 0; i < numEndpointsToAdd; i++)
{
serverOptions.ConfigurationLoader.LocalhostEndpoint(6000 + i, _ => numEndpointsAdded++);
}
serverOptions.ConfigurationLoader.ProcessEndpointsToAdd();
Assert.Equal(numEndpointsToAdd, numEndpointsAdded);
Assert.Equal(numEndpointsToAdd, serverOptions.CodeBackedListenOptions.Count);
Assert.Empty(serverOptions.ConfigurationBackedListenOptions);
}
[Fact]
public void ProcessEndpointsToAdd_CallbackThrows()
{
int numEndpointsAdded = 0;
var serverOptions = CreateServerOptions();
serverOptions.Configure();
serverOptions.ConfigurationLoader.LocalhostEndpoint(5000, _ => numEndpointsAdded++);
serverOptions.ConfigurationLoader.LocalhostEndpoint(5001, _ => throw new InvalidOperationException());
serverOptions.ConfigurationLoader.LocalhostEndpoint(5002, _ => numEndpointsAdded++);
Assert.Throws<InvalidOperationException>(serverOptions.ConfigurationLoader.ProcessEndpointsToAdd);
Assert.Equal(1, numEndpointsAdded);
Assert.Single(serverOptions.CodeBackedListenOptions);
Assert.Empty(serverOptions.ConfigurationBackedListenOptions);
serverOptions.ConfigurationLoader.ProcessEndpointsToAdd();
// As in success scenarios, the second call has no effect
Assert.Equal(1, numEndpointsAdded);
Assert.Single(serverOptions.CodeBackedListenOptions);
Assert.Empty(serverOptions.ConfigurationBackedListenOptions);
}
[Fact]
public void ProcessEndpointsToAddBeforeLoad()
{
var serverOptions = CreateServerOptions();
var mockConfig = CreateMockConfiguration();
serverOptions.Configure(mockConfig.Object);
serverOptions.ConfigurationLoader.LocalhostEndpoint(5000);
serverOptions.ConfigurationLoader.ProcessEndpointsToAdd();
Assert.Single(serverOptions.CodeBackedListenOptions);
mockConfig.Verify(c => c.GetSection(It.IsNotIn("EndpointDefaults")), Times.Never); // It does read the EndpointDefaults sections
mockConfig.Invocations.Clear();
serverOptions.ConfigurationLoader.LocalhostEndpoint(7000, _ => Assert.Fail("New endpoints should not be added after ProcessEndpointsToAdd"));
serverOptions.ConfigurationLoader.Load();
Assert.Single(serverOptions.CodeBackedListenOptions);
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), Times.AtLeastOnce); // Still need to load, even if endpoints have been processed
}
[Fact]
public void ProcessEndpointsToAddAfterLoad()
{
var serverOptions = CreateServerOptions();
var mockConfig = CreateMockConfiguration();
serverOptions.Configure(mockConfig.Object);
serverOptions.ConfigurationLoader.LocalhostEndpoint(5000);
serverOptions.ConfigurationLoader.Load();
Assert.Single(serverOptions.CodeBackedListenOptions);
mockConfig.Invocations.Clear();
serverOptions.ConfigurationLoader.LocalhostEndpoint(7000, _ => Assert.Fail("New endpoints should not be added after Load"));
Assert.Single(serverOptions.CodeBackedListenOptions);
serverOptions.ConfigurationLoader.ProcessEndpointsToAdd();
}
[Fact]
public void LoadInternalDoesNotAddEndpoints()
{
var serverOptions = CreateServerOptions();
serverOptions.Configure();
serverOptions.ConfigurationLoader.LocalhostEndpoint(7000, _ => Assert.Fail("New endpoints should not be added by LoadInternal"));
serverOptions.ConfigurationLoader.LoadInternal();
}
[Fact]
public void ReloadDoesNotAddEndpoints()
{
var serverOptions = CreateServerOptions();
serverOptions.Configure();
serverOptions.ConfigurationLoader.Load();
serverOptions.ConfigurationLoader.LocalhostEndpoint(7000, _ => Assert.Fail("New endpoints should not be added by Reload"));
_ = serverOptions.ConfigurationLoader.Reload();
}
[Fact]
public void AddNamedPipeEndpoint()
{
var serverOptions = CreateServerOptions();
var builder = serverOptions.Configure()
.NamedPipeEndpoint("abc");
Assert.Empty(serverOptions.GetListenOptions());
Assert.Equal(builder, serverOptions.ConfigurationLoader);
builder.Load();
Assert.Single(serverOptions.GetListenOptions());
Assert.Equal("abc", serverOptions.CodeBackedListenOptions[0].PipeName);
Assert.NotNull(serverOptions.ConfigurationLoader);
}
private static string GetCertificatePath()
{
var appData = Environment.GetEnvironmentVariable("APPDATA");
var home = Environment.GetEnvironmentVariable("HOME");
var basePath = appData != null ? Path.Combine(appData, "ASP.NET", "https") : null;
basePath = basePath ?? (home != null ? Path.Combine(home, ".aspnet", "https") : null);
return Path.Combine(basePath, $"TestApplication.pfx");
}
private class MockHostingEnvironment : IHostEnvironment
{
public string ApplicationName { get; set; }
public string EnvironmentName { get; set; }
public string ContentRootPath { get; set; }
public IFileProvider ContentRootFileProvider { get; set; }
}
}
|