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());