Add "zero config" HTTPS support using local development certificate. (#2093)

This commit is contained in:
Cesar Blum Silveira 2017-10-25 13:59:09 -07:00 committed by Andrew Stanton-Nurse
parent c3ba875d12
commit 8c4bdbcf6b
17 changed files with 422 additions and 62 deletions

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -462,4 +462,7 @@
<data name="Http2ErrorConnectionSpecificHeaderField" xml:space="preserve">
<value>Request headers contain connection-specific header field.</value>
</data>
</root>
<data name="UnableToConfigureHttpsBindings" xml:space="preserve">
<value>Unable to configure default https bindings because no IDefaultHttpsProvider service was provided.</value>
</data>
</root>

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -18,10 +18,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
internal class AddressBinder
{
public static async Task BindAsync(IServerAddressesFeature addresses,
List<ListenOptions> listenOptions,
KestrelServerOptions serverOptions,
ILogger logger,
IDefaultHttpsProvider defaultHttpsProvider,
Func<ListenOptions, Task> createBinding)
{
var listenOptions = serverOptions.ListenOptions;
var strategy = CreateStrategy(
listenOptions.ToArray(),
addresses.Addresses.ToArray(),
@ -31,7 +33,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
Addresses = addresses.Addresses,
ListenOptions = listenOptions,
ServerOptions = serverOptions,
Logger = logger,
DefaultHttpsProvider = defaultHttpsProvider ?? UnconfiguredDefaultHttpsProvider.Instance,
CreateBinding = createBinding
};
@ -47,7 +51,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
public ICollection<string> Addresses { get; set; }
public List<ListenOptions> ListenOptions { get; set; }
public KestrelServerOptions ServerOptions { get; set; }
public ILogger Logger { get; set; }
public IDefaultHttpsProvider DefaultHttpsProvider { get; set; }
public Func<ListenOptions, Task> CreateBinding { get; set; }
}
@ -120,7 +126,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
context.ListenOptions.Add(endpoint);
}
private static async Task BindLocalhostAsync(ServerAddress address, AddressBindContext context)
private static async Task BindLocalhostAsync(ServerAddress address, AddressBindContext context, bool https)
{
if (address.Port == 0)
{
@ -131,7 +137,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
try
{
await BindEndpointAsync(new IPEndPoint(IPAddress.Loopback, address.Port), context).ConfigureAwait(false);
var options = new ListenOptions(new IPEndPoint(IPAddress.Loopback, address.Port));
await BindEndpointAsync(options, context).ConfigureAwait(false);
if (https)
{
options.KestrelServerOptions = context.ServerOptions;
context.DefaultHttpsProvider.ConfigureHttps(options);
}
}
catch (Exception ex) when (!(ex is IOException))
{
@ -141,7 +154,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
try
{
await BindEndpointAsync(new IPEndPoint(IPAddress.IPv6Loopback, address.Port), context).ConfigureAwait(false);
var options = new ListenOptions(new IPEndPoint(IPAddress.IPv6Loopback, address.Port));
await BindEndpointAsync(options, context).ConfigureAwait(false);
if (https)
{
options.KestrelServerOptions = context.ServerOptions;
context.DefaultHttpsProvider.ConfigureHttps(options);
}
}
catch (Exception ex) when (!(ex is IOException))
{
@ -162,10 +182,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private static async Task BindAddressAsync(string address, AddressBindContext context)
{
var parsedAddress = ServerAddress.FromUrl(address);
var https = false;
if (parsedAddress.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(CoreStrings.FormatConfigureHttpsFromMethodCall($"{nameof(KestrelServerOptions)}.{nameof(KestrelServerOptions.Listen)}()"));
https = true;
}
else if (!parsedAddress.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase))
{
@ -177,20 +198,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
throw new InvalidOperationException(CoreStrings.FormatConfigurePathBaseFromMethodCall($"{nameof(IApplicationBuilder)}.UsePathBase()"));
}
ListenOptions options = null;
if (parsedAddress.IsUnixPipe)
{
var endPoint = new ListenOptions(parsedAddress.UnixPipePath);
await BindEndpointAsync(endPoint, context).ConfigureAwait(false);
context.Addresses.Add(endPoint.GetDisplayName());
options = new ListenOptions(parsedAddress.UnixPipePath);
await BindEndpointAsync(options, context).ConfigureAwait(false);
context.Addresses.Add(options.GetDisplayName());
}
else if (string.Equals(parsedAddress.Host, "localhost", StringComparison.OrdinalIgnoreCase))
{
// "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint.
await BindLocalhostAsync(parsedAddress, context).ConfigureAwait(false);
await BindLocalhostAsync(parsedAddress, context, https).ConfigureAwait(false);
}
else
{
ListenOptions options;
if (TryCreateIPEndPoint(parsedAddress, out var endpoint))
{
options = new ListenOptions(endpoint);
@ -216,6 +237,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
context.Addresses.Add(options.GetDisplayName());
}
if (https && options != null)
{
options.KestrelServerOptions = context.ServerOptions;
context.DefaultHttpsProvider.ConfigureHttps(options);
}
}
private interface IStrategy
@ -229,7 +256,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
context.Logger.LogDebug(CoreStrings.BindingToDefaultAddress, Constants.DefaultServerAddress);
await BindLocalhostAsync(ServerAddress.FromUrl(Constants.DefaultServerAddress), context).ConfigureAwait(false);
await BindLocalhostAsync(ServerAddress.FromUrl(Constants.DefaultServerAddress), context, https: false).ConfigureAwait(false);
}
}
@ -305,5 +332,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
}
}
}
private class UnconfiguredDefaultHttpsProvider : IDefaultHttpsProvider
{
public static readonly UnconfiguredDefaultHttpsProvider Instance = new UnconfiguredDefaultHttpsProvider();
private UnconfiguredDefaultHttpsProvider()
{
}
public void ConfigureHttps(ListenOptions listenOptions)
{
// We have to throw here. If this is called, it's because the user asked for "https" binding but for some
// reason didn't provide a certificate and didn't use the "DefaultHttpsProvider". This means if we no-op,
// we'll silently downgrade to HTTP, which is bad.
throw new InvalidOperationException(CoreStrings.UnableToConfigureHttpsBindings);
}
}
}
}

View File

@ -0,0 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
public interface IDefaultHttpsProvider
{
void ConfigureHttps(ListenOptions listenOptions);
}
}

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
private readonly List<ITransport> _transports = new List<ITransport>();
private readonly Heartbeat _heartbeat;
private readonly IServerAddressesFeature _serverAddresses;
private readonly IDefaultHttpsProvider _defaultHttpsProvider;
private readonly ITransportFactory _transportFactory;
private bool _hasStarted;
@ -33,6 +34,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
{
}
public KestrelServer(IOptions<KestrelServerOptions> options, ITransportFactory transportFactory, ILoggerFactory loggerFactory, IDefaultHttpsProvider defaultHttpsProvider)
: this(transportFactory, CreateServiceContext(options, loggerFactory))
{
_defaultHttpsProvider = defaultHttpsProvider;
}
// For testing
internal KestrelServer(ITransportFactory transportFactory, ServiceContext serviceContext)
{
@ -152,7 +159,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
await transport.BindAsync().ConfigureAwait(false);
}
await AddressBinder.BindAsync(_serverAddresses, Options.ListenOptions, Trace, OnBind).ConfigureAwait(false);
await AddressBinder.BindAsync(_serverAddresses, Options, Trace, _defaultHttpsProvider, OnBind).ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@ -1620,6 +1620,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatHttp2ErrorConnectionSpecificHeaderField()
=> GetString("Http2ErrorConnectionSpecificHeaderField");
/// <summary>
/// Unable to configure default https bindings because no IDefaultHttpsProvider service was provided.
/// </summary>
internal static string UnableToConfigureHttpsBindings
{
get => GetString("UnableToConfigureHttpsBindings");
}
/// <summary>
/// Unable to configure default https bindings because no IDefaultHttpsProvider service was provided.
/// </summary>
internal static string FormatUnableToConfigureHttpsBindings()
=> GetString("UnableToConfigureHttpsBindings");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Hosting
{
/// <summary>
/// Extension methods fro <see cref="ListenOptions"/> that configure Kestrel to use HTTPS for a given endpoint.
/// Extension methods for <see cref="ListenOptions"/> that configure Kestrel to use HTTPS for a given endpoint.
/// </summary>
public static class ListenOptionsHttpsExtensions
{

View File

@ -0,0 +1,42 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Certificates.Generation;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal
{
public class DefaultHttpsProvider : IDefaultHttpsProvider
{
private static readonly CertificateManager _certificateManager = new CertificateManager();
private readonly ILogger<DefaultHttpsProvider> _logger;
public DefaultHttpsProvider(ILogger<DefaultHttpsProvider> logger)
{
_logger = logger;
}
public void ConfigureHttps(ListenOptions listenOptions)
{
var certificate = _certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true)
.FirstOrDefault();
if (certificate != null)
{
_logger.LocatedDevelopmentCertificate(certificate);
listenOptions.UseHttps(certificate);
}
else
{
_logger.UnableToLocateDevelopmentCertificate();
throw new InvalidOperationException(KestrelStrings.HttpsUrlProvidedButNoDevelopmentCertificateFound);
}
}
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal
{
internal static class LoggerExtensions
{
// Category: DefaultHttpsProvider
private static readonly Action<ILogger, string, string, Exception> _locatedDevelopmentCertificate =
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(0, nameof(LocatedDevelopmentCertificate)), "Using development certificate: {certificateSubjectName} (Thumbprint: {certificateThumbprint})");
private static readonly Action<ILogger, Exception> _unableToLocateDevelopmentCertificate =
LoggerMessage.Define(LogLevel.Error, new EventId(1, nameof(UnableToLocateDevelopmentCertificate)), "Unable to locate an appropriate development https certificate.");
public static void LocatedDevelopmentCertificate(this ILogger logger, X509Certificate2 certificate) => _locatedDevelopmentCertificate(logger, certificate.Subject, certificate.Thumbprint, null);
public static void UnableToLocateDevelopmentCertificate(this ILogger logger) => _unableToLocateDevelopmentCertificate(logger, null);
}
}

View File

@ -12,10 +12,14 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Hosting" />
<PackageReference Include="Microsoft.AspNetCore.Certificates.Generation.Sources" />
<PackageReference Include="System.Security.Cryptography.Cng" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Kestrel.Core\Kestrel.Core.csproj" />
<ProjectReference Include="..\Kestrel.Https\Kestrel.Https.csproj" />
<!-- Even though the Libuv transport is no longer used by default, it remains for back-compat -->
<ProjectReference Include="..\Kestrel.Transport.Libuv\Kestrel.Transport.Libuv.csproj" />
<ProjectReference Include="..\Kestrel.Transport.Sockets\Kestrel.Transport.Sockets.csproj" />

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="HttpsUrlProvidedButNoDevelopmentCertificateFound" xml:space="preserve">
<value>Unable to configure HTTPS endpoint. Try running 'dotnet developercertificates https -t' to setup a developer certificate for use with localhost. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054</value>
</data>
</root>

View File

@ -0,0 +1,44 @@
// <auto-generated />
namespace Microsoft.AspNetCore.Server.Kestrel
{
using System.Globalization;
using System.Reflection;
using System.Resources;
internal static class KestrelStrings
{
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.AspNetCore.Server.Kestrel.KestrelStrings", typeof(KestrelStrings).GetTypeInfo().Assembly);
/// <summary>
/// An 'https' URL was provided, but a development certificate could not be found.
/// </summary>
internal static string HttpsUrlProvidedButNoDevelopmentCertificateFound
{
get => GetString("HttpsUrlProvidedButNoDevelopmentCertificateFound");
}
/// <summary>
/// An 'https' URL was provided, but a development certificate could not be found.
/// </summary>
internal static string FormatHttpsUrlProvidedButNoDevelopmentCertificateFound()
=> GetString("HttpsUrlProvidedButNoDevelopmentCertificateFound");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
System.Diagnostics.Debug.Assert(value != null);
if (formatterNames != null)
{
for (var i = 0; i < formatterNames.Length; i++)
{
value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
}
}
return value;
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets;
using Microsoft.Extensions.DependencyInjection;
@ -33,6 +34,7 @@ namespace Microsoft.AspNetCore.Hosting
services.AddTransient<IConfigureOptions<KestrelServerOptions>, KestrelServerOptionsSetup>();
services.AddSingleton<IServer, KestrelServer>();
services.AddSingleton<IDefaultHttpsProvider, DefaultHttpsProvider>();
});
}

View File

@ -8,9 +8,9 @@ using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Protocols;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
@ -55,12 +55,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
var addresses = new ServerAddressesFeature();
addresses.Addresses.Add($"http://{host}");
var options = new List<ListenOptions>();
var options = new KestrelServerOptions();
var tcs = new TaskCompletionSource<ListenOptions>();
await AddressBinder.BindAsync(addresses,
options,
NullLogger.Instance,
Mock.Of<IDefaultHttpsProvider>(),
endpoint =>
{
tcs.TrySetResult(endpoint);
@ -75,13 +76,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
var addresses = new ServerAddressesFeature();
addresses.Addresses.Add("http://localhost:5000");
var options = new List<ListenOptions>();
var options = new KestrelServerOptions();
await Assert.ThrowsAsync<IOException>(() =>
AddressBinder.BindAsync(addresses,
options,
NullLogger.Instance,
endpoint => throw new AddressInUseException("already in use")));
options,
NullLogger.Instance,
Mock.Of<IDefaultHttpsProvider>(),
endpoint => throw new AddressInUseException("already in use")));
}
[Theory]
@ -93,7 +95,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var logger = new MockLogger();
var addresses = new ServerAddressesFeature();
addresses.Addresses.Add(address);
var options = new List<ListenOptions>();
var options = new KestrelServerOptions();
var ipV6Attempt = false;
var ipV4Attempt = false;
@ -101,6 +103,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await AddressBinder.BindAsync(addresses,
options,
logger,
Mock.Of<IDefaultHttpsProvider>(),
endpoint =>
{
if (endpoint.IPEndPoint.Address == IPAddress.IPv6Any)

View File

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging;
@ -37,20 +38,56 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
[Fact]
public void StartWithHttpsAddressThrows()
public void StartWithHttpsAddressConfiguresHttpsEndpoints()
{
var testLogger = new TestApplicationErrorLogger { ThrowOnCriticalErrors = false };
var mockDefaultHttpsProvider = new Mock<IDefaultHttpsProvider>();
using (var server = CreateServer(new KestrelServerOptions(), testLogger))
using (var server = CreateServer(new KestrelServerOptions(), mockDefaultHttpsProvider.Object))
{
server.Features.Get<IServerAddressesFeature>().Addresses.Add("https://127.0.0.1:0");
var exception = Assert.Throws<InvalidOperationException>(() => StartDummyApplication(server));
StartDummyApplication(server);
Assert.Equal(
$"HTTPS endpoints can only be configured using {nameof(KestrelServerOptions)}.{nameof(KestrelServerOptions.Listen)}().",
exception.Message);
Assert.Equal(1, testLogger.CriticalErrorsLogged);
mockDefaultHttpsProvider.Verify(provider => provider.ConfigureHttps(It.IsAny<ListenOptions>()), Times.Once);
}
}
[Fact]
public void KestrelServerThrowsUsefulExceptionIfDefaultHttpsProviderNotAdded()
{
using (var server = CreateServer(new KestrelServerOptions(), defaultHttpsProvider: null, throwOnCriticalErrors: false))
{
server.Features.Get<IServerAddressesFeature>().Addresses.Add("https://127.0.0.1:0");
var ex = Assert.Throws<InvalidOperationException>(() => StartDummyApplication(server));
Assert.Equal(CoreStrings.UnableToConfigureHttpsBindings, ex.Message);
}
}
[Fact]
public void KestrelServerDoesNotThrowIfNoDefaultHttpsProviderButNoHttpUrls()
{
using (var server = CreateServer(new KestrelServerOptions(), defaultHttpsProvider: null))
{
server.Features.Get<IServerAddressesFeature>().Addresses.Add("http://127.0.0.1:0");
StartDummyApplication(server);
}
}
[Fact]
public void KestrelServerDoesNotThrowIfNoDefaultHttpsProviderButManualListenOptions()
{
var mockDefaultHttpsProvider = new Mock<IDefaultHttpsProvider>();
var serverOptions = new KestrelServerOptions();
serverOptions.Listen(new IPEndPoint(IPAddress.Loopback, 0));
using (var server = CreateServer(serverOptions, defaultHttpsProvider: null))
{
server.Features.Get<IServerAddressesFeature>().Addresses.Add("https://127.0.0.1:0");
StartDummyApplication(server);
}
}
@ -274,6 +311,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
return new KestrelServer(Options.Create(options), new MockTransportFactory(), new LoggerFactory(new[] { new KestrelTestLoggerProvider(testLogger) }));
}
private static KestrelServer CreateServer(KestrelServerOptions options, IDefaultHttpsProvider defaultHttpsProvider, bool throwOnCriticalErrors = true)
{
return new KestrelServer(Options.Create(options), new MockTransportFactory(), new LoggerFactory(new[] { new KestrelTestLoggerProvider(throwOnCriticalErrors) }), defaultHttpsProvider);
}
private static void StartDummyApplication(IServer server)
{
server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None).GetAwaiter().GetResult();

View File

@ -511,8 +511,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
}
[Theory]
[InlineData("https://localhost")]
[InlineData("ftp://localhost")]
[InlineData("ssh://localhost")]
public void ThrowsForUnsupportedAddressFromHosting(string addr)
{
var hostBuilder = TransportSelector.GetWebHostBuilder()

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -10,8 +10,8 @@ namespace Microsoft.AspNetCore.Testing
{
private readonly ILogger _testLogger;
public KestrelTestLoggerProvider()
: this(new TestApplicationErrorLogger())
public KestrelTestLoggerProvider(bool throwOnCriticalErrors = true)
: this(new TestApplicationErrorLogger() { ThrowOnCriticalErrors = throwOnCriticalErrors })
{
}
@ -30,4 +30,4 @@ namespace Microsoft.AspNetCore.Testing
throw new NotImplementedException();
}
}
}
}