Skip to content

Commit 45e6571

Browse files
author
John Luo
authored
Downgrade or throw when HTTP/2 over TLS is configured on older Windows versions (#22859)
HTTP/2 over TLS is not compatible with Windows versions strictly older than Windows 10 or Windows Server 2016. Update kestrel to: - Downgrade to HTTP/1.1 when Http1AndHttp2 is configured. - Throw NotSupportedException when Http2 is configured. - Allow HTTP/2 over TLS to be enabled if AppContext switch Microsoft.AspNetCore.Server.Kestrel.EnableWindows81Http2 is set. This allows users who have configured cipher suites on Windows 8.1 and Windows Server 2012 R2 to continue using HTTP/2 over TLS.
1 parent f48f558 commit 45e6571

File tree

4 files changed

+137
-17
lines changed

4 files changed

+137
-17
lines changed

src/Servers/Kestrel/Core/src/CoreStrings.resx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -563,12 +563,9 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
563563
<data name="RequestTrailersNotAvailable" xml:space="preserve">
564564
<value>The request trailers are not available yet. They may not be available until the full request body is read.</value>
565565
</data>
566-
<data name="HTTP2NoTlsOsx" xml:space="preserve">
566+
<data name="Http2NoTlsOsx" xml:space="preserve">
567567
<value>HTTP/2 over TLS is not supported on macOS due to missing ALPN support.</value>
568568
</data>
569-
<data name="HTTP2NoTlsWin7" xml:space="preserve">
570-
<value>HTTP/2 over TLS is not supported on Windows 7 due to missing ALPN support.</value>
571-
</data>
572569
<data name="Http2StreamResetByApplication" xml:space="preserve">
573570
<value>The HTTP/2 stream was reset by the application with error code {errorCode}.</value>
574571
</data>
@@ -605,6 +602,12 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
605602
<data name="HttpsConnectionEstablished" xml:space="preserve">
606603
<value>Connection "{connectionId}" established using the following protocol: {protocol}</value>
607604
</data>
605+
<data name="Http2DefaultCiphersInsufficient" xml:space="preserve">
606+
<value>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.</value>
607+
</data>
608+
<data name="Http2NoTlsWin81" xml:space="preserve">
609+
<value>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.</value>
610+
</data>
608611
<data name="Http2ErrorKeepAliveTimeout" xml:space="preserve">
609612
<value>Timeout while waiting for incoming HTTP/2 frames after a keep alive ping.</value>
610613
</data>

src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
2525
{
2626
internal class HttpsConnectionMiddleware
2727
{
28+
private const string EnableWindows81Http2 = "Microsoft.AspNetCore.Server.Kestrel.EnableWindows81Http2";
2829
private readonly ConnectionDelegate _next;
2930
private readonly HttpsConnectionAdapterOptions _options;
3031
private readonly ILogger _logger;
@@ -43,18 +44,26 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter
4344
throw new ArgumentNullException(nameof(options));
4445
}
4546

47+
_options = options;
48+
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
49+
4650
// This configuration will always fail per-request, preemptively fail it here. See HttpConnection.SelectProtocol().
4751
if (options.HttpProtocols == HttpProtocols.Http2)
4852
{
4953
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
5054
{
51-
throw new NotSupportedException(CoreStrings.HTTP2NoTlsOsx);
55+
throw new NotSupportedException(CoreStrings.Http2NoTlsOsx);
5256
}
53-
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version < new Version(6, 2))
57+
else if (IsWindowsVersionIncompatible())
5458
{
55-
throw new NotSupportedException(CoreStrings.HTTP2NoTlsWin7);
59+
throw new NotSupportedException(CoreStrings.Http2NoTlsWin81);
5660
}
5761
}
62+
else if (options.HttpProtocols == HttpProtocols.Http1AndHttp2 && IsWindowsVersionIncompatible())
63+
{
64+
_logger.Http2DefaultCiphersInsufficient();
65+
options.HttpProtocols = HttpProtocols.Http1;
66+
}
5867

5968
_next = next;
6069
// capture the certificate now so it can't be switched after validation
@@ -75,9 +84,6 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter
7584
{
7685
EnsureCertificateIsAllowedForServerAuth(_serverCertificate);
7786
}
78-
79-
_options = options;
80-
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
8187
}
8288

8389
public async Task OnConnectionAsync(ConnectionContext context)
@@ -214,7 +220,7 @@ public async Task OnConnectionAsync(ConnectionContext context)
214220
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
215221
KestrelEventSource.Log.TlsHandshakeStop(context, null);
216222

217-
_logger.LogDebug(2, CoreStrings.AuthenticationTimedOut);
223+
_logger.AuthenticationTimedOut();
218224
await sslStream.DisposeAsync();
219225
return;
220226
}
@@ -223,7 +229,7 @@ public async Task OnConnectionAsync(ConnectionContext context)
223229
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
224230
KestrelEventSource.Log.TlsHandshakeStop(context, null);
225231

226-
_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
232+
_logger.AuthenticationFailed(ex);
227233
await sslStream.DisposeAsync();
228234
return;
229235
}
@@ -232,7 +238,7 @@ public async Task OnConnectionAsync(ConnectionContext context)
232238
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
233239
KestrelEventSource.Log.TlsHandshakeStop(context, null);
234240

235-
_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
241+
_logger.AuthenticationFailed(ex);
236242

237243
await sslStream.DisposeAsync();
238244
return;
@@ -252,7 +258,7 @@ public async Task OnConnectionAsync(ConnectionContext context)
252258

253259
KestrelEventSource.Log.TlsHandshakeStop(context, feature);
254260

255-
_logger.LogDebug(3, CoreStrings.HttpsConnectionEstablished, context.ConnectionId, sslStream.SslProtocol);
261+
_logger.HttpsConnectionEstablished(context.ConnectionId, sslStream.SslProtocol);
256262

257263
var originalTransport = context.Transport;
258264

@@ -298,5 +304,57 @@ private static X509Certificate2 ConvertToX509Certificate2(X509Certificate certif
298304

299305
return new X509Certificate2(certificate);
300306
}
307+
308+
private static bool IsWindowsVersionIncompatible()
309+
{
310+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
311+
{
312+
var enableHttp2OnWindows81 = AppContext.TryGetSwitch(EnableWindows81Http2, out var enabled) && enabled;
313+
if (Environment.OSVersion.Version < new Version(6, 3) // Missing ALPN support
314+
// Win8.1 and 2012 R2 don't support the right cipher configuration by default.
315+
|| (Environment.OSVersion.Version < new Version(10, 0) && !enableHttp2OnWindows81))
316+
{
317+
return true;
318+
}
319+
}
320+
321+
return false;
322+
}
323+
}
324+
325+
internal static class HttpsConnectionMiddlewareLoggerExtensions
326+
{
327+
328+
private static readonly Action<ILogger, Exception> _authenticationFailed =
329+
LoggerMessage.Define(
330+
logLevel: LogLevel.Debug,
331+
eventId: new EventId(1, "AuthenticationFailed"),
332+
formatString: CoreStrings.AuthenticationFailed);
333+
334+
private static readonly Action<ILogger, Exception> _authenticationTimedOut =
335+
LoggerMessage.Define(
336+
logLevel: LogLevel.Debug,
337+
eventId: new EventId(2, "AuthenticationTimedOut"),
338+
formatString: CoreStrings.AuthenticationTimedOut);
339+
340+
private static readonly Action<ILogger, string, SslProtocols, Exception> _httpsConnectionEstablished =
341+
LoggerMessage.Define<string, SslProtocols>(
342+
logLevel: LogLevel.Debug,
343+
eventId: new EventId(3, "HttpsConnectionEstablished"),
344+
formatString: CoreStrings.HttpsConnectionEstablished);
345+
346+
private static readonly Action<ILogger, Exception> _http2DefaultCiphersInsufficient =
347+
LoggerMessage.Define(
348+
logLevel: LogLevel.Information,
349+
eventId: new EventId(4, "Http2DefaultCiphersInsufficient"),
350+
formatString: CoreStrings.Http2DefaultCiphersInsufficient);
351+
352+
public static void AuthenticationFailed(this ILogger logger, Exception exception) => _authenticationFailed(logger, exception);
353+
354+
public static void AuthenticationTimedOut(this ILogger logger) => _authenticationTimedOut(logger, null);
355+
356+
public static void HttpsConnectionEstablished(this ILogger logger, string connectionId, SslProtocols sslProtocol) => _httpsConnectionEstablished(logger, connectionId, sslProtocol, null);
357+
358+
public static void Http2DefaultCiphersInsufficient(this ILogger logger) => _http2DefaultCiphersInsufficient(logger, null);
301359
}
302360
}

src/Servers/Kestrel/test/FunctionalTests/Http2/HandshakeTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public void TlsAndHttp2NotSupportedOnWin7()
8181
[ConditionalFact]
8282
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")]
8383
[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
84-
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
84+
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
8585
public async Task TlsAlpnHandshakeSelectsHttp2From1and2()
8686
{
8787
using (var server = new TestServer(context =>
@@ -112,7 +112,7 @@ public async Task TlsAlpnHandshakeSelectsHttp2From1and2()
112112
[ConditionalFact]
113113
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")]
114114
[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
115-
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
115+
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
116116
public async Task TlsAlpnHandshakeSelectsHttp2()
117117
{
118118
using (var server = new TestServer(context =>

src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ public void ThrowsForCertificatesMissingServerEku(string testCertName)
594594
[InlineData(HttpProtocols.Http1AndHttp2)]
595595
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")]
596596
[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
597-
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
597+
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
598598
public async Task ListenOptionsProtolsCanBeSetAfterUseHttps(HttpProtocols httpProtocols)
599599
{
600600
void ConfigureListenOptions(ListenOptions listenOptions)
@@ -623,6 +623,65 @@ void ConfigureListenOptions(ListenOptions listenOptions)
623623
stream.NegotiatedApplicationProtocol);
624624
}
625625

626+
[ConditionalFact]
627+
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Downgrade logic only applies on Windows")]
628+
[MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
629+
public void Http1AndHttp2DowngradeToHttp1ForHttpsOnIncompatibleWindowsVersions()
630+
{
631+
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
632+
{
633+
ServerCertificate = _x509Certificate2,
634+
HttpProtocols = HttpProtocols.Http1AndHttp2
635+
};
636+
new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);
637+
638+
Assert.Equal(HttpProtocols.Http1, httpConnectionAdapterOptions.HttpProtocols);
639+
}
640+
641+
[ConditionalFact]
642+
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Downgrade logic only applies on Windows")]
643+
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
644+
public void Http1AndHttp2DoesNotDowngradeOnCompatibleWindowsVersions()
645+
{
646+
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
647+
{
648+
ServerCertificate = _x509Certificate2,
649+
HttpProtocols = HttpProtocols.Http1AndHttp2
650+
};
651+
new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);
652+
653+
Assert.Equal(HttpProtocols.Http1AndHttp2, httpConnectionAdapterOptions.HttpProtocols);
654+
}
655+
656+
[ConditionalFact]
657+
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Error logic only applies on Windows")]
658+
[MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
659+
public void Http2ThrowsOnIncompatibleWindowsVersions()
660+
{
661+
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
662+
{
663+
ServerCertificate = _x509Certificate2,
664+
HttpProtocols = HttpProtocols.Http2
665+
};
666+
667+
Assert.Throws<NotSupportedException>(() => new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions));
668+
}
669+
670+
[ConditionalFact]
671+
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Error logic only applies on Windows")]
672+
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
673+
public void Http2DoesNotThrowOnCompatibleWindowsVersions()
674+
{
675+
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
676+
{
677+
ServerCertificate = _x509Certificate2,
678+
HttpProtocols = HttpProtocols.Http2
679+
};
680+
681+
// Does not throw
682+
new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);
683+
}
684+
626685
private static async Task App(HttpContext httpContext)
627686
{
628687
var request = httpContext.Request;

0 commit comments

Comments
 (0)