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"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</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> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 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 : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 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 : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 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 : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@ -462,4 +462,7 @@
<data name="Http2ErrorConnectionSpecificHeaderField" xml:space="preserve"> <data name="Http2ErrorConnectionSpecificHeaderField" xml:space="preserve">
<value>Request headers contain connection-specific header field.</value> <value>Request headers contain connection-specific header field.</value>
</data> </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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -18,10 +18,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
internal class AddressBinder internal class AddressBinder
{ {
public static async Task BindAsync(IServerAddressesFeature addresses, public static async Task BindAsync(IServerAddressesFeature addresses,
List<ListenOptions> listenOptions, KestrelServerOptions serverOptions,
ILogger logger, ILogger logger,
IDefaultHttpsProvider defaultHttpsProvider,
Func<ListenOptions, Task> createBinding) Func<ListenOptions, Task> createBinding)
{ {
var listenOptions = serverOptions.ListenOptions;
var strategy = CreateStrategy( var strategy = CreateStrategy(
listenOptions.ToArray(), listenOptions.ToArray(),
addresses.Addresses.ToArray(), addresses.Addresses.ToArray(),
@ -31,7 +33,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{ {
Addresses = addresses.Addresses, Addresses = addresses.Addresses,
ListenOptions = listenOptions, ListenOptions = listenOptions,
ServerOptions = serverOptions,
Logger = logger, Logger = logger,
DefaultHttpsProvider = defaultHttpsProvider ?? UnconfiguredDefaultHttpsProvider.Instance,
CreateBinding = createBinding CreateBinding = createBinding
}; };
@ -47,7 +51,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{ {
public ICollection<string> Addresses { get; set; } public ICollection<string> Addresses { get; set; }
public List<ListenOptions> ListenOptions { get; set; } public List<ListenOptions> ListenOptions { get; set; }
public KestrelServerOptions ServerOptions { get; set; }
public ILogger Logger { get; set; } public ILogger Logger { get; set; }
public IDefaultHttpsProvider DefaultHttpsProvider { get; set; }
public Func<ListenOptions, Task> CreateBinding { get; set; } public Func<ListenOptions, Task> CreateBinding { get; set; }
} }
@ -120,7 +126,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
context.ListenOptions.Add(endpoint); 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) if (address.Port == 0)
{ {
@ -131,7 +137,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
try 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)) catch (Exception ex) when (!(ex is IOException))
{ {
@ -141,7 +154,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
try 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)) 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) private static async Task BindAddressAsync(string address, AddressBindContext context)
{ {
var parsedAddress = ServerAddress.FromUrl(address); var parsedAddress = ServerAddress.FromUrl(address);
var https = false;
if (parsedAddress.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) 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)) 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()")); throw new InvalidOperationException(CoreStrings.FormatConfigurePathBaseFromMethodCall($"{nameof(IApplicationBuilder)}.UsePathBase()"));
} }
ListenOptions options = null;
if (parsedAddress.IsUnixPipe) if (parsedAddress.IsUnixPipe)
{ {
var endPoint = new ListenOptions(parsedAddress.UnixPipePath); options = new ListenOptions(parsedAddress.UnixPipePath);
await BindEndpointAsync(endPoint, context).ConfigureAwait(false); await BindEndpointAsync(options, context).ConfigureAwait(false);
context.Addresses.Add(endPoint.GetDisplayName()); context.Addresses.Add(options.GetDisplayName());
} }
else if (string.Equals(parsedAddress.Host, "localhost", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(parsedAddress.Host, "localhost", StringComparison.OrdinalIgnoreCase))
{ {
// "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint. // "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 else
{ {
ListenOptions options;
if (TryCreateIPEndPoint(parsedAddress, out var endpoint)) if (TryCreateIPEndPoint(parsedAddress, out var endpoint))
{ {
options = new ListenOptions(endpoint); options = new ListenOptions(endpoint);
@ -216,6 +237,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
context.Addresses.Add(options.GetDisplayName()); context.Addresses.Add(options.GetDisplayName());
} }
if (https && options != null)
{
options.KestrelServerOptions = context.ServerOptions;
context.DefaultHttpsProvider.ConfigureHttps(options);
}
} }
private interface IStrategy private interface IStrategy
@ -229,7 +256,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{ {
context.Logger.LogDebug(CoreStrings.BindingToDefaultAddress, Constants.DefaultServerAddress); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal 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 List<ITransport> _transports = new List<ITransport>();
private readonly Heartbeat _heartbeat; private readonly Heartbeat _heartbeat;
private readonly IServerAddressesFeature _serverAddresses; private readonly IServerAddressesFeature _serverAddresses;
private readonly IDefaultHttpsProvider _defaultHttpsProvider;
private readonly ITransportFactory _transportFactory; private readonly ITransportFactory _transportFactory;
private bool _hasStarted; 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 // For testing
internal KestrelServer(ITransportFactory transportFactory, ServiceContext serviceContext) internal KestrelServer(ITransportFactory transportFactory, ServiceContext serviceContext)
{ {
@ -152,7 +159,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
await transport.BindAsync().ConfigureAwait(false); 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) catch (Exception ex)
{ {

View File

@ -1620,6 +1620,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatHttp2ErrorConnectionSpecificHeaderField() internal static string FormatHttp2ErrorConnectionSpecificHeaderField()
=> GetString("Http2ErrorConnectionSpecificHeaderField"); => 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Hosting namespace Microsoft.AspNetCore.Hosting
{ {
/// <summary> /// <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> /// </summary>
public static class ListenOptionsHttpsExtensions 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> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Hosting" /> <PackageReference Include="Microsoft.AspNetCore.Hosting" />
<PackageReference Include="Microsoft.AspNetCore.Certificates.Generation.Sources" />
<PackageReference Include="System.Security.Cryptography.Cng" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Kestrel.Core\Kestrel.Core.csproj" /> <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 --> <!-- 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.Libuv\Kestrel.Transport.Libuv.csproj" />
<ProjectReference Include="..\Kestrel.Transport.Sockets\Kestrel.Transport.Sockets.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.Hosting.Server;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; 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.Abstractions.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -33,6 +34,7 @@ namespace Microsoft.AspNetCore.Hosting
services.AddTransient<IConfigureOptions<KestrelServerOptions>, KestrelServerOptionsSetup>(); services.AddTransient<IConfigureOptions<KestrelServerOptions>, KestrelServerOptionsSetup>();
services.AddSingleton<IServer, KestrelServer>(); services.AddSingleton<IServer, KestrelServer>();
services.AddSingleton<IDefaultHttpsProvider, DefaultHttpsProvider>();
}); });
} }

View File

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

View File

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -37,20 +38,56 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
} }
[Fact] [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"); server.Features.Get<IServerAddressesFeature>().Addresses.Add("https://127.0.0.1:0");
var exception = Assert.Throws<InvalidOperationException>(() => StartDummyApplication(server)); StartDummyApplication(server);
Assert.Equal( mockDefaultHttpsProvider.Verify(provider => provider.ConfigureHttps(It.IsAny<ListenOptions>()), Times.Once);
$"HTTPS endpoints can only be configured using {nameof(KestrelServerOptions)}.{nameof(KestrelServerOptions.Listen)}().", }
exception.Message); }
Assert.Equal(1, testLogger.CriticalErrorsLogged);
[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) })); 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) private static void StartDummyApplication(IServer server)
{ {
server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None).GetAwaiter().GetResult(); server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None).GetAwaiter().GetResult();

View File

@ -511,8 +511,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
} }
[Theory] [Theory]
[InlineData("https://localhost")]
[InlineData("ftp://localhost")] [InlineData("ftp://localhost")]
[InlineData("ssh://localhost")]
public void ThrowsForUnsupportedAddressFromHosting(string addr) public void ThrowsForUnsupportedAddressFromHosting(string addr)
{ {
var hostBuilder = TransportSelector.GetWebHostBuilder() 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -10,8 +10,8 @@ namespace Microsoft.AspNetCore.Testing
{ {
private readonly ILogger _testLogger; private readonly ILogger _testLogger;
public KestrelTestLoggerProvider() public KestrelTestLoggerProvider(bool throwOnCriticalErrors = true)
: this(new TestApplicationErrorLogger()) : this(new TestApplicationErrorLogger() { ThrowOnCriticalErrors = throwOnCriticalErrors })
{ {
} }
@ -30,4 +30,4 @@ namespace Microsoft.AspNetCore.Testing
throw new NotImplementedException(); throw new NotImplementedException();
} }
} }
} }