diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx
index 1327210b5a..96f6e49969 100644
--- a/src/Servers/Kestrel/Core/src/CoreStrings.resx
+++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx
@@ -563,12 +563,9 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
The request trailers are not available yet. They may not be available until the full request body is read.
-
+
HTTP/2 over TLS is not supported on macOS due to missing ALPN support.
-
- HTTP/2 over TLS is not supported on Windows 7 due to missing ALPN support.
-
The HTTP/2 stream was reset by the application with error code {errorCode}.
@@ -605,6 +602,12 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
Connection "{connectionId}" established using the following protocol: {protocol}
+
+ HTTP/2 over TLS is not supported on Windows versions older than Windows 10 and Windows Server 2016 due to incompatible ciphers or missing ALPN support. Falling back to HTTP/1.1 instead.
+
+
+ HTTP/2 over TLS is not supported on Windows versions earlier than Windows 10 and Windows Server 2016 due to incompatible ciphers or missing ALPN support.
+
Timeout while waiting for incoming HTTP/2 frames after a keep alive ping.
diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
index 473579f210..3f908091fc 100644
--- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
+++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs
@@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
{
internal class HttpsConnectionMiddleware
{
+ private const string EnableWindows81Http2 = "Microsoft.AspNetCore.Server.Kestrel.EnableWindows81Http2";
private readonly ConnectionDelegate _next;
private readonly HttpsConnectionAdapterOptions _options;
private readonly ILogger _logger;
@@ -43,18 +44,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
throw new ArgumentNullException(nameof(options));
}
+ _options = options;
+ _logger = loggerFactory.CreateLogger();
+
// This configuration will always fail per-request, preemptively fail it here. See HttpConnection.SelectProtocol().
if (options.HttpProtocols == HttpProtocols.Http2)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
- throw new NotSupportedException(CoreStrings.HTTP2NoTlsOsx);
+ throw new NotSupportedException(CoreStrings.Http2NoTlsOsx);
}
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version < new Version(6, 2))
+ else if (IsWindowsVersionIncompatible())
{
- throw new NotSupportedException(CoreStrings.HTTP2NoTlsWin7);
+ throw new NotSupportedException(CoreStrings.Http2NoTlsWin81);
}
}
+ else if (options.HttpProtocols == HttpProtocols.Http1AndHttp2 && IsWindowsVersionIncompatible())
+ {
+ _logger.Http2DefaultCiphersInsufficient();
+ options.HttpProtocols = HttpProtocols.Http1;
+ }
_next = next;
// capture the certificate now so it can't be switched after validation
@@ -75,9 +84,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
{
EnsureCertificateIsAllowedForServerAuth(_serverCertificate);
}
-
- _options = options;
- _logger = loggerFactory.CreateLogger();
}
public async Task OnConnectionAsync(ConnectionContext context)
@@ -214,7 +220,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
KestrelEventSource.Log.TlsHandshakeStop(context, null);
- _logger.LogDebug(2, CoreStrings.AuthenticationTimedOut);
+ _logger.AuthenticationTimedOut();
await sslStream.DisposeAsync();
return;
}
@@ -223,7 +229,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
KestrelEventSource.Log.TlsHandshakeStop(context, null);
- _logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
+ _logger.AuthenticationFailed(ex);
await sslStream.DisposeAsync();
return;
}
@@ -232,7 +238,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
KestrelEventSource.Log.TlsHandshakeStop(context, null);
- _logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
+ _logger.AuthenticationFailed(ex);
await sslStream.DisposeAsync();
return;
@@ -252,7 +258,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
KestrelEventSource.Log.TlsHandshakeStop(context, feature);
- _logger.LogDebug(3, CoreStrings.HttpsConnectionEstablished, context.ConnectionId, sslStream.SslProtocol);
+ _logger.HttpsConnectionEstablished(context.ConnectionId, sslStream.SslProtocol);
var originalTransport = context.Transport;
@@ -298,5 +304,57 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
return new X509Certificate2(certificate);
}
+
+ private static bool IsWindowsVersionIncompatible()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ var enableHttp2OnWindows81 = AppContext.TryGetSwitch(EnableWindows81Http2, out var enabled) && enabled;
+ if (Environment.OSVersion.Version < new Version(6, 3) // Missing ALPN support
+ // Win8.1 and 2012 R2 don't support the right cipher configuration by default.
+ || (Environment.OSVersion.Version < new Version(10, 0) && !enableHttp2OnWindows81))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ internal static class HttpsConnectionMiddlewareLoggerExtensions
+ {
+
+ private static readonly Action _authenticationFailed =
+ LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: new EventId(1, "AuthenticationFailed"),
+ formatString: CoreStrings.AuthenticationFailed);
+
+ private static readonly Action _authenticationTimedOut =
+ LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: new EventId(2, "AuthenticationTimedOut"),
+ formatString: CoreStrings.AuthenticationTimedOut);
+
+ private static readonly Action _httpsConnectionEstablished =
+ LoggerMessage.Define(
+ logLevel: LogLevel.Debug,
+ eventId: new EventId(3, "HttpsConnectionEstablished"),
+ formatString: CoreStrings.HttpsConnectionEstablished);
+
+ private static readonly Action _http2DefaultCiphersInsufficient =
+ LoggerMessage.Define(
+ logLevel: LogLevel.Information,
+ eventId: new EventId(4, "Http2DefaultCiphersInsufficient"),
+ formatString: CoreStrings.Http2DefaultCiphersInsufficient);
+
+ public static void AuthenticationFailed(this ILogger logger, Exception exception) => _authenticationFailed(logger, exception);
+
+ public static void AuthenticationTimedOut(this ILogger logger) => _authenticationTimedOut(logger, null);
+
+ public static void HttpsConnectionEstablished(this ILogger logger, string connectionId, SslProtocols sslProtocol) => _httpsConnectionEstablished(logger, connectionId, sslProtocol, null);
+
+ public static void Http2DefaultCiphersInsufficient(this ILogger logger) => _http2DefaultCiphersInsufficient(logger, null);
}
}
diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/HandshakeTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/HandshakeTests.cs
index 7ffd9b2bc0..c4a89e9fcf 100644
--- a/src/Servers/Kestrel/test/FunctionalTests/Http2/HandshakeTests.cs
+++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/HandshakeTests.cs
@@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/10428", Queues = "Debian.8.Amd64;Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2
- [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
+ [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public async Task TlsAlpnHandshakeSelectsHttp2From1and2()
{
using (var server = new TestServer(context =>
@@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/10428", Queues = "Debian.8.Amd64;Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2
- [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
+ [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public async Task TlsAlpnHandshakeSelectsHttp2()
{
using (var server = new TestServer(context =>
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs
index 8ea1a7752e..44b8b50039 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs
@@ -594,7 +594,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
[InlineData(HttpProtocols.Http1AndHttp2)]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/10428", Queues = "Debian.8.Amd64;Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2
- [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
+ [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public async Task ListenOptionsProtolsCanBeSetAfterUseHttps(HttpProtocols httpProtocols)
{
void ConfigureListenOptions(ListenOptions listenOptions)
@@ -623,6 +623,65 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
stream.NegotiatedApplicationProtocol);
}
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Downgrade logic only applies on Windows")]
+ [MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
+ public void Http1AndHttp2DowngradeToHttp1ForHttpsOnIncompatibleWindowsVersions()
+ {
+ var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
+ {
+ ServerCertificate = _x509Certificate2,
+ HttpProtocols = HttpProtocols.Http1AndHttp2
+ };
+ new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);
+
+ Assert.Equal(HttpProtocols.Http1, httpConnectionAdapterOptions.HttpProtocols);
+ }
+
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Downgrade logic only applies on Windows")]
+ [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
+ public void Http1AndHttp2DoesNotDowngradeOnCompatibleWindowsVersions()
+ {
+ var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
+ {
+ ServerCertificate = _x509Certificate2,
+ HttpProtocols = HttpProtocols.Http1AndHttp2
+ };
+ new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);
+
+ Assert.Equal(HttpProtocols.Http1AndHttp2, httpConnectionAdapterOptions.HttpProtocols);
+ }
+
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Error logic only applies on Windows")]
+ [MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
+ public void Http2ThrowsOnIncompatibleWindowsVersions()
+ {
+ var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
+ {
+ ServerCertificate = _x509Certificate2,
+ HttpProtocols = HttpProtocols.Http2
+ };
+
+ Assert.Throws(() => new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions));
+ }
+
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Error logic only applies on Windows")]
+ [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
+ public void Http2DoesNotThrowOnCompatibleWindowsVersions()
+ {
+ var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
+ {
+ ServerCertificate = _x509Certificate2,
+ HttpProtocols = HttpProtocols.Http2
+ };
+
+ // Does not throw
+ new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);
+ }
+
private static async Task App(HttpContext httpContext)
{
var request = httpContext.Request;