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;