diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx index eb0cff90a5..6fea320658 100644 --- a/src/Kestrel.Core/CoreStrings.resx +++ b/src/Kestrel.Core/CoreStrings.resx @@ -351,4 +351,13 @@ Timespan must be positive and finite. + + An endpoint must be configured to serve at least one protocol. + + + Using both HTTP/1.x and HTTP/2 on the same endpoint requires the use of TLS. + + + HTTP/2 over TLS was not negotiated on an HTTP/2-only endpoint. + \ No newline at end of file diff --git a/src/Kestrel.Core/HttpProtocols.cs b/src/Kestrel.Core/HttpProtocols.cs new file mode 100644 index 0000000000..09524bf156 --- /dev/null +++ b/src/Kestrel.Core/HttpProtocols.cs @@ -0,0 +1,16 @@ +// 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; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core +{ + [Flags] + public enum HttpProtocols + { + None = 0x0, + Http1 = 0x1, + Http2 = 0x2, + Http1AndHttp2 = Http1 | Http2, + } +} diff --git a/src/Kestrel.Core/Internal/HttpConnection.cs b/src/Kestrel.Core/Internal/HttpConnection.cs index 9ccca7d182..099446058b 100644 --- a/src/Kestrel.Core/Internal/HttpConnection.cs +++ b/src/Kestrel.Core/Internal/HttpConnection.cs @@ -10,6 +10,7 @@ using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; @@ -135,8 +136,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal adaptedPipelineTask = adaptedPipeline.RunAsync(stream); } - if (_http1Connection.ConnectionFeatures.Get()?.ApplicationProtocol == "h2" && - Interlocked.CompareExchange(ref _http2ConnectionState, Http2ConnectionStarted, Http2ConnectionNotStarted) == Http2ConnectionNotStarted) + var protocol = SelectProtocol(); + + if (protocol == HttpProtocols.None) + { + Abort(ex: null); + } + + // One of these has to run even if no protocol was selected so the abort propagates and everything completes properly + if (protocol == HttpProtocols.Http2 && Interlocked.CompareExchange(ref _http2ConnectionState, Http2ConnectionStarted, Http2ConnectionNotStarted) == Http2ConnectionNotStarted) { await _http2Connection.ProcessAsync(httpApplication); } @@ -207,6 +215,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public Task StopProcessingNextRequestAsync() { Debug.Assert(_http1Connection != null, $"{nameof(_http1Connection)} is null"); + Debug.Assert(_http2Connection != null, $"{nameof(_http2Connection)} is null"); if (Interlocked.Exchange(ref _http2ConnectionState, Http2ConnectionClosed) == Http2ConnectionStarted) { @@ -223,6 +232,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public void Abort(Exception ex) { Debug.Assert(_http1Connection != null, $"{nameof(_http1Connection)} is null"); + Debug.Assert(_http2Connection != null, $"{nameof(_http2Connection)} is null"); // Abort the connection (if not already aborted) if (Interlocked.Exchange(ref _http2ConnectionState, Http2ConnectionClosed) == Http2ConnectionStarted) @@ -245,6 +255,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public void SendTimeoutResponse() { Debug.Assert(_http1Connection != null, $"{nameof(_http1Connection)} is null"); + Debug.Assert(_http2Connection != null, $"{nameof(_http2Connection)} is null"); RequestTimedOut = true; _http1Connection.SendTimeoutResponse(); @@ -253,6 +264,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public void StopProcessingNextRequest() { Debug.Assert(_http1Connection != null, $"{nameof(_http1Connection)} is null"); + Debug.Assert(_http2Connection != null, $"{nameof(_http2Connection)} is null"); _http1Connection.StopProcessingNextRequest(); } @@ -260,10 +272,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal private async Task ApplyConnectionAdaptersAsync() { Debug.Assert(_http1Connection != null, $"{nameof(_http1Connection)} is null"); + Debug.Assert(_http2Connection != null, $"{nameof(_http2Connection)} is null"); var connectionAdapters = _context.ConnectionAdapters; var stream = new RawStream(_context.Transport.Input, _context.Transport.Output); - var adapterContext = new ConnectionAdapterContext(_http1Connection.ConnectionFeatures, stream); + var adapterContext = new ConnectionAdapterContext(_context.ConnectionFeatures, stream); _adaptedConnections = new List(connectionAdapters.Count); try @@ -272,7 +285,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { var adaptedConnection = await connectionAdapters[i].OnConnectionAsync(adapterContext); _adaptedConnections.Add(adaptedConnection); - adapterContext = new ConnectionAdapterContext(_http1Connection.ConnectionFeatures, adaptedConnection.ConnectionStream); + adapterContext = new ConnectionAdapterContext(_context.ConnectionFeatures, adaptedConnection.ConnectionStream); } } catch (Exception ex) @@ -290,16 +303,50 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal var adaptedConnections = _adaptedConnections; if (adaptedConnections != null) { - for (int i = adaptedConnections.Count - 1; i >= 0; i--) + for (var i = adaptedConnections.Count - 1; i >= 0; i--) { adaptedConnections[i].Dispose(); } } } + private HttpProtocols SelectProtocol() + { + var hasTls = _context.ConnectionFeatures.Get() != null; + var applicationProtocol = _context.ConnectionFeatures.Get()?.ApplicationProtocol; + var http1Enabled = (_context.Protocols & HttpProtocols.Http1) == HttpProtocols.Http1; + var http2Enabled = (_context.Protocols & HttpProtocols.Http2) == HttpProtocols.Http2; + + string error = null; + + if (_context.Protocols == HttpProtocols.None) + { + error = CoreStrings.EndPointRequiresAtLeastOneProtocol; + } + + if (!hasTls && http1Enabled && http2Enabled) + { + error = CoreStrings.EndPointRequiresTlsForHttp1AndHttp2; + } + + if (!http1Enabled && http2Enabled && hasTls && applicationProtocol != "h2") + { + error = CoreStrings.EndPointHttp2NotNegotiated; + } + + if (error != null) + { + Log.LogError(0, error); + return HttpProtocols.None; + } + + return http2Enabled && (!hasTls || applicationProtocol == "h2") ? HttpProtocols.Http2 : HttpProtocols.Http1; + } + public void Tick(DateTimeOffset now) { Debug.Assert(_http1Connection != null, $"{nameof(_http1Connection)} is null"); + Debug.Assert(_http2Connection != null, $"{nameof(_http2Connection)} is null"); var timestamp = now.Ticks; diff --git a/src/Kestrel.Core/Internal/HttpConnectionBuilderExtensions.cs b/src/Kestrel.Core/Internal/HttpConnectionBuilderExtensions.cs index ca3ca5c9ab..ed57343f27 100644 --- a/src/Kestrel.Core/Internal/HttpConnectionBuilderExtensions.cs +++ b/src/Kestrel.Core/Internal/HttpConnectionBuilderExtensions.cs @@ -1,6 +1,8 @@ -using System; +// 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.Collections.Generic; -using System.Text; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Protocols; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; @@ -9,14 +11,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { public static class HttpConnectionBuilderExtensions { - public static IConnectionBuilder UseHttpServer(this IConnectionBuilder builder, ServiceContext serviceContext, IHttpApplication application) + public static IConnectionBuilder UseHttpServer(this IConnectionBuilder builder, ServiceContext serviceContext, IHttpApplication application, HttpProtocols protocols) { - return builder.UseHttpServer(Array.Empty(), serviceContext, application); + return builder.UseHttpServer(Array.Empty(), serviceContext, application, protocols); } - public static IConnectionBuilder UseHttpServer(this IConnectionBuilder builder, IList adapters, ServiceContext serviceContext, IHttpApplication application) + public static IConnectionBuilder UseHttpServer(this IConnectionBuilder builder, IList adapters, ServiceContext serviceContext, IHttpApplication application, HttpProtocols protocols) { - var middleware = new HttpConnectionMiddleware(adapters, serviceContext, application); + var middleware = new HttpConnectionMiddleware(adapters, serviceContext, application, protocols); return builder.Use(next => { return middleware.OnConnectionAsync; diff --git a/src/Kestrel.Core/Internal/HttpConnectionContext.cs b/src/Kestrel.Core/Internal/HttpConnectionContext.cs index 9993070689..67298c0544 100644 --- a/src/Kestrel.Core/Internal/HttpConnectionContext.cs +++ b/src/Kestrel.Core/Internal/HttpConnectionContext.cs @@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { public string ConnectionId { get; set; } public long HttpConnectionId { get; set; } + public HttpProtocols Protocols { get; set; } public ServiceContext ServiceContext { get; set; } public IFeatureCollection ConnectionFeatures { get; set; } public IList ConnectionAdapters { get; set; } diff --git a/src/Kestrel.Core/Internal/HttpConnectionMiddleware.cs b/src/Kestrel.Core/Internal/HttpConnectionMiddleware.cs index 4b342f20c1..d2b0757fcb 100644 --- a/src/Kestrel.Core/Internal/HttpConnectionMiddleware.cs +++ b/src/Kestrel.Core/Internal/HttpConnectionMiddleware.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO.Pipelines; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -8,6 +9,8 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Protocols; using Microsoft.AspNetCore.Protocols.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { @@ -20,11 +23,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal private readonly IList _connectionAdapters; private readonly ServiceContext _serviceContext; private readonly IHttpApplication _application; + private readonly HttpProtocols _protocols; - public HttpConnectionMiddleware(IList adapters, ServiceContext serviceContext, IHttpApplication application) + public HttpConnectionMiddleware(IList adapters, ServiceContext serviceContext, IHttpApplication application, HttpProtocols protocols) { _serviceContext = serviceContext; _application = application; + _protocols = protocols; // Keeping these around for now so progress can be made without updating tests _connectionAdapters = adapters; @@ -42,6 +47,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { ConnectionId = connectionContext.ConnectionId, HttpConnectionId = httpConnectionId, + Protocols = _protocols, ServiceContext = _serviceContext, ConnectionFeatures = connectionContext.Features, PipeFactory = connectionContext.PipeFactory, diff --git a/src/Kestrel.Core/KestrelServer.cs b/src/Kestrel.Core/KestrelServer.cs index 8182c1e6c9..fac999d904 100644 --- a/src/Kestrel.Core/KestrelServer.cs +++ b/src/Kestrel.Core/KestrelServer.cs @@ -135,7 +135,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core async Task OnBind(ListenOptions endpoint) { // Add the HTTP middleware as the terminal connection middleware - endpoint.UseHttpServer(endpoint.ConnectionAdapters, ServiceContext, application); + endpoint.UseHttpServer(endpoint.ConnectionAdapters, ServiceContext, application, endpoint.Protocols); var connectionDelegate = endpoint.Build(); diff --git a/src/Kestrel.Core/ListenOptions.cs b/src/Kestrel.Core/ListenOptions.cs index f56b3c98be..19e94c43a6 100644 --- a/src/Kestrel.Core/ListenOptions.cs +++ b/src/Kestrel.Core/ListenOptions.cs @@ -118,6 +118,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// public bool NoDelay { get; set; } = true; + /// + /// The protocols enabled on this endpoint. + /// + /// Defaults to HTTP/1.x only. + public HttpProtocols Protocols { get; set; } = HttpProtocols.Http1; + /// /// Gets the that allows each connection /// to be intercepted and transformed. diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs index 0d7341ba08..2fa3c085eb 100644 --- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -1102,6 +1102,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatPositiveFiniteTimeSpanRequired() => GetString("PositiveFiniteTimeSpanRequired"); + /// + /// An endpoint must be configured to serve at least one protocol. + /// + internal static string EndPointRequiresAtLeastOneProtocol + { + get => GetString("EndPointRequiresAtLeastOneProtocol"); + } + + /// + /// An endpoint must be configured to serve at least one protocol. + /// + internal static string FormatEndPointRequiresAtLeastOneProtocol() + => GetString("EndPointRequiresAtLeastOneProtocol"); + + /// + /// Using both HTTP/1.x and HTTP/2 on the same endpoint requires the use of TLS. + /// + internal static string EndPointRequiresTlsForHttp1AndHttp2 + { + get => GetString("EndPointRequiresTlsForHttp1AndHttp2"); + } + + /// + /// Using both HTTP/1.x and HTTP/2 on the same endpoint requires the use of TLS. + /// + internal static string FormatEndPointRequiresTlsForHttp1AndHttp2() + => GetString("EndPointRequiresTlsForHttp1AndHttp2"); + + /// + /// HTTP/2 over TLS was not negotiated on an HTTP/2-only endpoint. + /// + internal static string EndPointHttp2NotNegotiated + { + get => GetString("EndPointHttp2NotNegotiated"); + } + + /// + /// HTTP/2 over TLS was not negotiated on an HTTP/2-only endpoint. + /// + internal static string FormatEndPointHttp2NotNegotiated() + => GetString("EndPointHttp2NotNegotiated"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Kestrel.Tls/ListenOptionsTlsExtensions.cs b/src/Kestrel.Tls/ListenOptionsTlsExtensions.cs index 85011e6227..cb2c459152 100644 --- a/src/Kestrel.Tls/ListenOptionsTlsExtensions.cs +++ b/src/Kestrel.Tls/ListenOptionsTlsExtensions.cs @@ -15,7 +15,8 @@ namespace Microsoft.AspNetCore.Hosting return listenOptions.UseTls(new TlsConnectionAdapterOptions { CertificatePath = certificatePath, - PrivateKeyPath = privateKeyPath + PrivateKeyPath = privateKeyPath, + Protocols = listenOptions.Protocols }); } diff --git a/src/Kestrel.Tls/TlsConnectionAdapter.cs b/src/Kestrel.Tls/TlsConnectionAdapter.cs index 0dae1b701c..539c8404f3 100644 --- a/src/Kestrel.Tls/TlsConnectionAdapter.cs +++ b/src/Kestrel.Tls/TlsConnectionAdapter.cs @@ -4,9 +4,9 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.Extensions.Logging; @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tls public class TlsConnectionAdapter : IConnectionAdapter { private static readonly ClosedAdaptedConnection _closedAdaptedConnection = new ClosedAdaptedConnection(); - private static readonly HashSet _serverProtocols = new HashSet(new[] { "h2", "http/1.1" }); + private static readonly List _serverProtocols = new List(); private readonly TlsConnectionAdapterOptions _options; private readonly ILogger _logger; @@ -47,6 +47,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tls _options = options; _logger = loggerFactory?.CreateLogger(nameof(TlsConnectionAdapter)); + + // Order is important. If HTTP/2 is enabled, we prefer it over HTTP/1.1. So add it first. + if ((options.Protocols & HttpProtocols.Http2) == HttpProtocols.Http2) + { + _serverProtocols.Add("h2"); + } + + if ((options.Protocols & HttpProtocols.Http1) == HttpProtocols.Http1) + { + _serverProtocols.Add("http/1.1"); + } } public bool IsHttps => true; diff --git a/src/Kestrel.Tls/TlsConnectionAdapterOptions.cs b/src/Kestrel.Tls/TlsConnectionAdapterOptions.cs index 0d49b62b69..88d107ffdd 100644 --- a/src/Kestrel.Tls/TlsConnectionAdapterOptions.cs +++ b/src/Kestrel.Tls/TlsConnectionAdapterOptions.cs @@ -1,6 +1,8 @@ // 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 Microsoft.AspNetCore.Server.Kestrel.Core; + namespace Microsoft.AspNetCore.Server.Kestrel.Tls { public class TlsConnectionAdapterOptions @@ -8,5 +10,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tls public string CertificatePath { get; set; } = string.Empty; public string PrivateKeyPath { get; set; } = string.Empty; + + public HttpProtocols Protocols { get; set; } } } diff --git a/test/Kestrel.Core.Tests/HttpConnectionTests.cs b/test/Kestrel.Core.Tests/HttpConnectionTests.cs index f1f887ddc2..4eebb7a7ae 100644 --- a/test/Kestrel.Core.Tests/HttpConnectionTests.cs +++ b/test/Kestrel.Core.Tests/HttpConnectionTests.cs @@ -56,6 +56,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests mockDebugger.SetupGet(g => g.IsAttached).Returns(true); _httpConnection.Debugger = mockDebugger.Object; _httpConnection.CreateHttp1Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.CreateHttp2Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); var now = DateTimeOffset.Now; _httpConnection.Tick(now); @@ -103,6 +104,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnectionContext.ServiceContext.Log = logger; _httpConnection.CreateHttp1Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.CreateHttp2Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); _httpConnection.Http1Connection.Reset(); // Initialize timestamp @@ -130,6 +132,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.CreateHttp1Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.CreateHttp2Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); _httpConnection.Http1Connection.Reset(); // Initialize timestamp @@ -172,6 +175,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.CreateHttp1Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.CreateHttp2Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); _httpConnection.Http1Connection.Reset(); // Initialize timestamp @@ -249,6 +253,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.CreateHttp1Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.CreateHttp2Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); _httpConnection.Http1Connection.Reset(); // Initialize timestamp @@ -317,6 +322,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.CreateHttp1Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.CreateHttp2Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); _httpConnection.Http1Connection.Reset(); // Initialize timestamp @@ -379,6 +385,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.CreateHttp1Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.CreateHttp2Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); _httpConnection.Http1Connection.Reset(); var startTime = systemClock.UtcNow; @@ -420,6 +427,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.CreateHttp1Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.CreateHttp2Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { @@ -454,6 +462,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.CreateHttp1Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.CreateHttp2Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { @@ -496,6 +505,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnectionContext.ServiceContext.Log = mockLogger.Object; _httpConnection.CreateHttp1Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); + _httpConnection.CreateHttp2Connection(new DummyApplication(), _httpConnectionContext.Transport, _httpConnectionContext.Application); _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { diff --git a/test/Kestrel.Core.Tests/ListenOptionsTests.cs b/test/Kestrel.Core.Tests/ListenOptionsTests.cs new file mode 100644 index 0000000000..89d495876f --- /dev/null +++ b/test/Kestrel.Core.Tests/ListenOptionsTests.cs @@ -0,0 +1,18 @@ +// 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.Net; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +{ + public class ListenOptionsTests + { + [Fact] + public void ProtocolsDefault() + { + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); + Assert.Equal(HttpProtocols.Http1, listenOptions.Protocols); + } + } +} diff --git a/test/Kestrel.FunctionalTests/HttpProtocolSelectionTests.cs b/test/Kestrel.FunctionalTests/HttpProtocolSelectionTests.cs new file mode 100644 index 0000000000..7b665d5ef1 --- /dev/null +++ b/test/Kestrel.FunctionalTests/HttpProtocolSelectionTests.cs @@ -0,0 +1,96 @@ +// 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.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class HttpProtocolSelectionTests + { + [Fact] + public Task Server_NoProtocols_Error() + { + return TestError(HttpProtocols.None, CoreStrings.EndPointRequiresAtLeastOneProtocol); + } + + [Fact] + public Task Server_Http1AndHttp2_Cleartext_Error() + { + return TestError(HttpProtocols.Http1AndHttp2, CoreStrings.EndPointRequiresTlsForHttp1AndHttp2); + } + + [Fact] + public Task Server_Http1Only_Cleartext_Success() + { + return TestSuccess(HttpProtocols.Http1, "GET / HTTP/1.1\r\nHost:\r\n\r\n", "HTTP/1.1 200 OK"); + } + + [Fact] + public Task Server_Http2Only_Cleartext_Success() + { + // Expect a SETTINGS frame (type 0x4) with no payload and no flags + return TestSuccess(HttpProtocols.Http2, Encoding.ASCII.GetString(Http2Connection.ClientPreface), "\x00\x00\x00\x04\x00\x00\x00\x00\x00"); + } + + private async Task TestSuccess(HttpProtocols serverProtocols, string request, string expectedResponse) + { + var builder = new WebHostBuilder() + .UseKestrel(options => + { + options.Listen(IPAddress.Loopback, 0, listenOptions => listenOptions.Protocols = serverProtocols); + }) + .Configure(app => app.Run(context => Task.CompletedTask)); + + using (var host = builder.Build()) + { + host.Start(); + + using (var connection = new TestConnection(host.GetPort())) + { + await connection.Send(request); + await connection.Receive(expectedResponse); + } + } + } + + private async Task TestError(HttpProtocols serverProtocols, string expectedErrorMessage) + where TException : Exception + { + var logger = new TestApplicationErrorLogger(); + var loggerProvider = new Mock(); + loggerProvider + .Setup(provider => provider.CreateLogger(It.IsAny())) + .Returns(logger); + + var builder = new WebHostBuilder() + .ConfigureLogging(loggingBuilder => loggingBuilder.AddProvider(loggerProvider.Object)) + .UseKestrel(options => options.Listen(IPAddress.Loopback, 0, listenOptions => listenOptions.Protocols = serverProtocols)) + .Configure(app => app.Run(context => Task.CompletedTask)); + + using (var host = builder.Build()) + { + host.Start(); + + using (var connection = new TestConnection(host.GetPort())) + { + await connection.WaitForConnectionClose().TimeoutAfter(TimeSpan.FromSeconds(30)); + } + } + + Assert.Single(logger.Messages, message => message.LogLevel == LogLevel.Error + && message.EventId.Id == 0 + && message.Message == expectedErrorMessage); + } + } +} diff --git a/test/Kestrel.Transport.Libuv.Tests/LibuvTransportTests.cs b/test/Kestrel.Transport.Libuv.Tests/LibuvTransportTests.cs index 6f3e9126a7..d3739ae07f 100644 --- a/test/Kestrel.Transport.Libuv.Tests/LibuvTransportTests.cs +++ b/test/Kestrel.Transport.Libuv.Tests/LibuvTransportTests.cs @@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests public async Task ConnectionCanReadAndWrite(ListenOptions listenOptions) { var serviceContext = new TestServiceContext(); - listenOptions.UseHttpServer(listenOptions.ConnectionAdapters, serviceContext, new DummyApplication(TestApp.EchoApp)); + listenOptions.UseHttpServer(listenOptions.ConnectionAdapters, serviceContext, new DummyApplication(TestApp.EchoApp), HttpProtocols.Http1); var transportContext = new TestLibuvTransportContext() { diff --git a/test/Kestrel.Transport.Libuv.Tests/ListenerPrimaryTests.cs b/test/Kestrel.Transport.Libuv.Tests/ListenerPrimaryTests.cs index f751fa2de6..41ee4a7f19 100644 --- a/test/Kestrel.Transport.Libuv.Tests/ListenerPrimaryTests.cs +++ b/test/Kestrel.Transport.Libuv.Tests/ListenerPrimaryTests.cs @@ -33,12 +33,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests var serviceContextPrimary = new TestServiceContext(); var transportContextPrimary = new TestLibuvTransportContext(); var builderPrimary = new ConnectionBuilder(); - builderPrimary.UseHttpServer(serviceContextPrimary, new DummyApplication(c => c.Response.WriteAsync("Primary"))); + builderPrimary.UseHttpServer(serviceContextPrimary, new DummyApplication(c => c.Response.WriteAsync("Primary")), HttpProtocols.Http1); transportContextPrimary.ConnectionHandler = new ConnectionHandler(serviceContextPrimary, builderPrimary.Build()); var serviceContextSecondary = new TestServiceContext(); var builderSecondary = new ConnectionBuilder(); - builderSecondary.UseHttpServer(serviceContextSecondary, new DummyApplication(c => c.Response.WriteAsync("Secondary"))); + builderSecondary.UseHttpServer(serviceContextSecondary, new DummyApplication(c => c.Response.WriteAsync("Secondary")), HttpProtocols.Http1); var transportContextSecondary = new TestLibuvTransportContext(); transportContextSecondary.ConnectionHandler = new ConnectionHandler(serviceContextSecondary, builderSecondary.Build()); @@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests var serviceContextPrimary = new TestServiceContext(); var builderPrimary = new ConnectionBuilder(); - builderPrimary.UseHttpServer(serviceContextPrimary, new DummyApplication(c => c.Response.WriteAsync("Primary"))); + builderPrimary.UseHttpServer(serviceContextPrimary, new DummyApplication(c => c.Response.WriteAsync("Primary")), HttpProtocols.Http1); var transportContextPrimary = new TestLibuvTransportContext() { Log = new LibuvTrace(logger) }; transportContextPrimary.ConnectionHandler = new ConnectionHandler(serviceContextPrimary, builderPrimary.Build()); @@ -113,7 +113,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests HttpParserFactory = serviceContextPrimary.HttpParserFactory, }; var builderSecondary = new ConnectionBuilder(); - builderSecondary.UseHttpServer(serviceContextSecondary, new DummyApplication(c => c.Response.WriteAsync("Secondary"))); + builderSecondary.UseHttpServer(serviceContextSecondary, new DummyApplication(c => c.Response.WriteAsync("Secondary")), HttpProtocols.Http1); var transportContextSecondary = new TestLibuvTransportContext(); transportContextSecondary.ConnectionHandler = new ConnectionHandler(serviceContextSecondary, builderSecondary.Build()); @@ -212,7 +212,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests var serviceContextPrimary = new TestServiceContext(); var builderPrimary = new ConnectionBuilder(); - builderPrimary.UseHttpServer(serviceContextPrimary, new DummyApplication(c => c.Response.WriteAsync("Primary"))); + builderPrimary.UseHttpServer(serviceContextPrimary, new DummyApplication(c => c.Response.WriteAsync("Primary")), HttpProtocols.Http1); var transportContextPrimary = new TestLibuvTransportContext() { Log = new LibuvTrace(logger) }; transportContextPrimary.ConnectionHandler = new ConnectionHandler(serviceContextPrimary, builderPrimary.Build()); @@ -224,7 +224,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests HttpParserFactory = serviceContextPrimary.HttpParserFactory, }; var builderSecondary = new ConnectionBuilder(); - builderSecondary.UseHttpServer(serviceContextSecondary, new DummyApplication(c => c.Response.WriteAsync("Secondary"))); + builderSecondary.UseHttpServer(serviceContextSecondary, new DummyApplication(c => c.Response.WriteAsync("Secondary")), HttpProtocols.Http1); var transportContextSecondary = new TestLibuvTransportContext(); transportContextSecondary.ConnectionHandler = new ConnectionHandler(serviceContextSecondary, builderSecondary.Build());