diff --git a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netcoreapp.cs b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netcoreapp.cs index b454acd516b6..75ed809b6d97 100644 --- a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netcoreapp.cs +++ b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netcoreapp.cs @@ -8,6 +8,19 @@ public partial class AddressInUseException : System.InvalidOperationException public AddressInUseException(string message) { } public AddressInUseException(string message, System.Exception inner) { } } + public abstract partial class BaseConnectionContext : System.IAsyncDisposable + { + protected BaseConnectionContext() { } + public virtual System.Threading.CancellationToken ConnectionClosed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public abstract string ConnectionId { get; set; } + public abstract Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { get; } + public abstract System.Collections.Generic.IDictionary Items { get; set; } + public virtual System.Net.EndPoint LocalEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public virtual System.Net.EndPoint RemoteEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public abstract void Abort(); + public abstract void Abort(Microsoft.AspNetCore.Connections.ConnectionAbortedException abortReason); + public virtual System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + } public partial class ConnectionAbortedException : System.OperationCanceledException { public ConnectionAbortedException() { } @@ -27,19 +40,12 @@ public static partial class ConnectionBuilderExtensions public static Microsoft.AspNetCore.Connections.IConnectionBuilder Use(this Microsoft.AspNetCore.Connections.IConnectionBuilder connectionBuilder, System.Func, System.Threading.Tasks.Task> middleware) { throw null; } public static Microsoft.AspNetCore.Connections.IConnectionBuilder UseConnectionHandler(this Microsoft.AspNetCore.Connections.IConnectionBuilder connectionBuilder) where TConnectionHandler : Microsoft.AspNetCore.Connections.ConnectionHandler { throw null; } } - public abstract partial class ConnectionContext : System.IAsyncDisposable + public abstract partial class ConnectionContext : Microsoft.AspNetCore.Connections.BaseConnectionContext, System.IAsyncDisposable { protected ConnectionContext() { } - public virtual System.Threading.CancellationToken ConnectionClosed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } - public abstract string ConnectionId { get; set; } - public abstract Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { get; } - public abstract System.Collections.Generic.IDictionary Items { get; set; } - public virtual System.Net.EndPoint LocalEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } - public virtual System.Net.EndPoint RemoteEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public abstract System.IO.Pipelines.IDuplexPipe Transport { get; set; } - public virtual void Abort() { } - public virtual void Abort(Microsoft.AspNetCore.Connections.ConnectionAbortedException abortReason) { } - public virtual System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + public override void Abort() { } + public override void Abort(Microsoft.AspNetCore.Connections.ConnectionAbortedException abortReason) { } } public delegate System.Threading.Tasks.Task ConnectionDelegate(Microsoft.AspNetCore.Connections.ConnectionContext connection); public abstract partial class ConnectionHandler @@ -123,9 +129,40 @@ public partial interface IConnectionListenerFactory { System.Threading.Tasks.ValueTask BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); } - public partial interface IMultiplexedConnectionListenerFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory + public partial interface IMultiplexedConnectionBuilder + { + System.IServiceProvider ApplicationServices { get; } + Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate Build(); + Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder Use(System.Func middleware); + } + public partial interface IMultiplexedConnectionFactory { + System.Threading.Tasks.ValueTask ConnectAsync(System.Net.EndPoint endpoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection features = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); } + public partial interface IMultiplexedConnectionListener : System.IAsyncDisposable + { + System.Net.EndPoint EndPoint { get; } + System.Threading.Tasks.ValueTask AcceptAsync(Microsoft.AspNetCore.Http.Features.IFeatureCollection features = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + System.Threading.Tasks.ValueTask UnbindAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + } + public partial interface IMultiplexedConnectionListenerFactory + { + System.Threading.Tasks.ValueTask BindAsync(System.Net.EndPoint endpoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection features = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + } + public partial class MultiplexedConnectionBuilder : Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder + { + public MultiplexedConnectionBuilder(System.IServiceProvider applicationServices) { } + public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } + public Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate Build() { throw null; } + public Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder Use(System.Func middleware) { throw null; } + } + public abstract partial class MultiplexedConnectionContext : Microsoft.AspNetCore.Connections.BaseConnectionContext, System.IAsyncDisposable + { + protected MultiplexedConnectionContext() { } + public abstract System.Threading.Tasks.ValueTask AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + public abstract System.Threading.Tasks.ValueTask ConnectAsync(Microsoft.AspNetCore.Http.Features.IFeatureCollection features = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + } + public delegate System.Threading.Tasks.Task MultiplexedConnectionDelegate(Microsoft.AspNetCore.Connections.MultiplexedConnectionContext connection); [System.FlagsAttribute] public enum TransferFormat { @@ -139,14 +176,6 @@ public UriEndPoint(System.Uri uri) { } public override string ToString() { throw null; } } } -namespace Microsoft.AspNetCore.Connections.Abstractions.Features -{ - public partial interface IQuicCreateStreamFeature - { - System.Threading.Tasks.ValueTask StartBidirectionalStreamAsync(); - System.Threading.Tasks.ValueTask StartUnidirectionalStreamAsync(); - } -} namespace Microsoft.AspNetCore.Connections.Features { public partial interface IConnectionCompleteFeature @@ -196,15 +225,18 @@ public partial interface IMemoryPoolFeature { System.Buffers.MemoryPool MemoryPool { get; } } - public partial interface IQuicStreamFeature + public partial interface IProtocolErrorCodeFeature + { + long Error { get; set; } + } + public partial interface IStreamDirectionFeature { bool CanRead { get; } bool CanWrite { get; } - long StreamId { get; } } - public partial interface IQuicStreamListenerFeature + public partial interface IStreamIdFeature { - System.Threading.Tasks.ValueTask AcceptAsync(); + long StreamId { get; } } public partial interface ITlsHandshakeFeature { diff --git a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs index b454acd516b6..75ed809b6d97 100644 --- a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs +++ b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs @@ -8,6 +8,19 @@ public partial class AddressInUseException : System.InvalidOperationException public AddressInUseException(string message) { } public AddressInUseException(string message, System.Exception inner) { } } + public abstract partial class BaseConnectionContext : System.IAsyncDisposable + { + protected BaseConnectionContext() { } + public virtual System.Threading.CancellationToken ConnectionClosed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public abstract string ConnectionId { get; set; } + public abstract Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { get; } + public abstract System.Collections.Generic.IDictionary Items { get; set; } + public virtual System.Net.EndPoint LocalEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public virtual System.Net.EndPoint RemoteEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public abstract void Abort(); + public abstract void Abort(Microsoft.AspNetCore.Connections.ConnectionAbortedException abortReason); + public virtual System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + } public partial class ConnectionAbortedException : System.OperationCanceledException { public ConnectionAbortedException() { } @@ -27,19 +40,12 @@ public static partial class ConnectionBuilderExtensions public static Microsoft.AspNetCore.Connections.IConnectionBuilder Use(this Microsoft.AspNetCore.Connections.IConnectionBuilder connectionBuilder, System.Func, System.Threading.Tasks.Task> middleware) { throw null; } public static Microsoft.AspNetCore.Connections.IConnectionBuilder UseConnectionHandler(this Microsoft.AspNetCore.Connections.IConnectionBuilder connectionBuilder) where TConnectionHandler : Microsoft.AspNetCore.Connections.ConnectionHandler { throw null; } } - public abstract partial class ConnectionContext : System.IAsyncDisposable + public abstract partial class ConnectionContext : Microsoft.AspNetCore.Connections.BaseConnectionContext, System.IAsyncDisposable { protected ConnectionContext() { } - public virtual System.Threading.CancellationToken ConnectionClosed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } - public abstract string ConnectionId { get; set; } - public abstract Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { get; } - public abstract System.Collections.Generic.IDictionary Items { get; set; } - public virtual System.Net.EndPoint LocalEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } - public virtual System.Net.EndPoint RemoteEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public abstract System.IO.Pipelines.IDuplexPipe Transport { get; set; } - public virtual void Abort() { } - public virtual void Abort(Microsoft.AspNetCore.Connections.ConnectionAbortedException abortReason) { } - public virtual System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + public override void Abort() { } + public override void Abort(Microsoft.AspNetCore.Connections.ConnectionAbortedException abortReason) { } } public delegate System.Threading.Tasks.Task ConnectionDelegate(Microsoft.AspNetCore.Connections.ConnectionContext connection); public abstract partial class ConnectionHandler @@ -123,9 +129,40 @@ public partial interface IConnectionListenerFactory { System.Threading.Tasks.ValueTask BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); } - public partial interface IMultiplexedConnectionListenerFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory + public partial interface IMultiplexedConnectionBuilder + { + System.IServiceProvider ApplicationServices { get; } + Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate Build(); + Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder Use(System.Func middleware); + } + public partial interface IMultiplexedConnectionFactory { + System.Threading.Tasks.ValueTask ConnectAsync(System.Net.EndPoint endpoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection features = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); } + public partial interface IMultiplexedConnectionListener : System.IAsyncDisposable + { + System.Net.EndPoint EndPoint { get; } + System.Threading.Tasks.ValueTask AcceptAsync(Microsoft.AspNetCore.Http.Features.IFeatureCollection features = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + System.Threading.Tasks.ValueTask UnbindAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + } + public partial interface IMultiplexedConnectionListenerFactory + { + System.Threading.Tasks.ValueTask BindAsync(System.Net.EndPoint endpoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection features = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + } + public partial class MultiplexedConnectionBuilder : Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder + { + public MultiplexedConnectionBuilder(System.IServiceProvider applicationServices) { } + public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } + public Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate Build() { throw null; } + public Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder Use(System.Func middleware) { throw null; } + } + public abstract partial class MultiplexedConnectionContext : Microsoft.AspNetCore.Connections.BaseConnectionContext, System.IAsyncDisposable + { + protected MultiplexedConnectionContext() { } + public abstract System.Threading.Tasks.ValueTask AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + public abstract System.Threading.Tasks.ValueTask ConnectAsync(Microsoft.AspNetCore.Http.Features.IFeatureCollection features = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + } + public delegate System.Threading.Tasks.Task MultiplexedConnectionDelegate(Microsoft.AspNetCore.Connections.MultiplexedConnectionContext connection); [System.FlagsAttribute] public enum TransferFormat { @@ -139,14 +176,6 @@ public UriEndPoint(System.Uri uri) { } public override string ToString() { throw null; } } } -namespace Microsoft.AspNetCore.Connections.Abstractions.Features -{ - public partial interface IQuicCreateStreamFeature - { - System.Threading.Tasks.ValueTask StartBidirectionalStreamAsync(); - System.Threading.Tasks.ValueTask StartUnidirectionalStreamAsync(); - } -} namespace Microsoft.AspNetCore.Connections.Features { public partial interface IConnectionCompleteFeature @@ -196,15 +225,18 @@ public partial interface IMemoryPoolFeature { System.Buffers.MemoryPool MemoryPool { get; } } - public partial interface IQuicStreamFeature + public partial interface IProtocolErrorCodeFeature + { + long Error { get; set; } + } + public partial interface IStreamDirectionFeature { bool CanRead { get; } bool CanWrite { get; } - long StreamId { get; } } - public partial interface IQuicStreamListenerFeature + public partial interface IStreamIdFeature { - System.Threading.Tasks.ValueTask AcceptAsync(); + long StreamId { get; } } public partial interface ITlsHandshakeFeature { diff --git a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.1.cs b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.1.cs index b454acd516b6..75ed809b6d97 100644 --- a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.1.cs +++ b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.1.cs @@ -8,6 +8,19 @@ public partial class AddressInUseException : System.InvalidOperationException public AddressInUseException(string message) { } public AddressInUseException(string message, System.Exception inner) { } } + public abstract partial class BaseConnectionContext : System.IAsyncDisposable + { + protected BaseConnectionContext() { } + public virtual System.Threading.CancellationToken ConnectionClosed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public abstract string ConnectionId { get; set; } + public abstract Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { get; } + public abstract System.Collections.Generic.IDictionary Items { get; set; } + public virtual System.Net.EndPoint LocalEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public virtual System.Net.EndPoint RemoteEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public abstract void Abort(); + public abstract void Abort(Microsoft.AspNetCore.Connections.ConnectionAbortedException abortReason); + public virtual System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + } public partial class ConnectionAbortedException : System.OperationCanceledException { public ConnectionAbortedException() { } @@ -27,19 +40,12 @@ public static partial class ConnectionBuilderExtensions public static Microsoft.AspNetCore.Connections.IConnectionBuilder Use(this Microsoft.AspNetCore.Connections.IConnectionBuilder connectionBuilder, System.Func, System.Threading.Tasks.Task> middleware) { throw null; } public static Microsoft.AspNetCore.Connections.IConnectionBuilder UseConnectionHandler(this Microsoft.AspNetCore.Connections.IConnectionBuilder connectionBuilder) where TConnectionHandler : Microsoft.AspNetCore.Connections.ConnectionHandler { throw null; } } - public abstract partial class ConnectionContext : System.IAsyncDisposable + public abstract partial class ConnectionContext : Microsoft.AspNetCore.Connections.BaseConnectionContext, System.IAsyncDisposable { protected ConnectionContext() { } - public virtual System.Threading.CancellationToken ConnectionClosed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } - public abstract string ConnectionId { get; set; } - public abstract Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { get; } - public abstract System.Collections.Generic.IDictionary Items { get; set; } - public virtual System.Net.EndPoint LocalEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } - public virtual System.Net.EndPoint RemoteEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public abstract System.IO.Pipelines.IDuplexPipe Transport { get; set; } - public virtual void Abort() { } - public virtual void Abort(Microsoft.AspNetCore.Connections.ConnectionAbortedException abortReason) { } - public virtual System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + public override void Abort() { } + public override void Abort(Microsoft.AspNetCore.Connections.ConnectionAbortedException abortReason) { } } public delegate System.Threading.Tasks.Task ConnectionDelegate(Microsoft.AspNetCore.Connections.ConnectionContext connection); public abstract partial class ConnectionHandler @@ -123,9 +129,40 @@ public partial interface IConnectionListenerFactory { System.Threading.Tasks.ValueTask BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); } - public partial interface IMultiplexedConnectionListenerFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory + public partial interface IMultiplexedConnectionBuilder + { + System.IServiceProvider ApplicationServices { get; } + Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate Build(); + Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder Use(System.Func middleware); + } + public partial interface IMultiplexedConnectionFactory { + System.Threading.Tasks.ValueTask ConnectAsync(System.Net.EndPoint endpoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection features = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); } + public partial interface IMultiplexedConnectionListener : System.IAsyncDisposable + { + System.Net.EndPoint EndPoint { get; } + System.Threading.Tasks.ValueTask AcceptAsync(Microsoft.AspNetCore.Http.Features.IFeatureCollection features = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + System.Threading.Tasks.ValueTask UnbindAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + } + public partial interface IMultiplexedConnectionListenerFactory + { + System.Threading.Tasks.ValueTask BindAsync(System.Net.EndPoint endpoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection features = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + } + public partial class MultiplexedConnectionBuilder : Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder + { + public MultiplexedConnectionBuilder(System.IServiceProvider applicationServices) { } + public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } + public Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate Build() { throw null; } + public Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder Use(System.Func middleware) { throw null; } + } + public abstract partial class MultiplexedConnectionContext : Microsoft.AspNetCore.Connections.BaseConnectionContext, System.IAsyncDisposable + { + protected MultiplexedConnectionContext() { } + public abstract System.Threading.Tasks.ValueTask AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + public abstract System.Threading.Tasks.ValueTask ConnectAsync(Microsoft.AspNetCore.Http.Features.IFeatureCollection features = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + } + public delegate System.Threading.Tasks.Task MultiplexedConnectionDelegate(Microsoft.AspNetCore.Connections.MultiplexedConnectionContext connection); [System.FlagsAttribute] public enum TransferFormat { @@ -139,14 +176,6 @@ public UriEndPoint(System.Uri uri) { } public override string ToString() { throw null; } } } -namespace Microsoft.AspNetCore.Connections.Abstractions.Features -{ - public partial interface IQuicCreateStreamFeature - { - System.Threading.Tasks.ValueTask StartBidirectionalStreamAsync(); - System.Threading.Tasks.ValueTask StartUnidirectionalStreamAsync(); - } -} namespace Microsoft.AspNetCore.Connections.Features { public partial interface IConnectionCompleteFeature @@ -196,15 +225,18 @@ public partial interface IMemoryPoolFeature { System.Buffers.MemoryPool MemoryPool { get; } } - public partial interface IQuicStreamFeature + public partial interface IProtocolErrorCodeFeature + { + long Error { get; set; } + } + public partial interface IStreamDirectionFeature { bool CanRead { get; } bool CanWrite { get; } - long StreamId { get; } } - public partial interface IQuicStreamListenerFeature + public partial interface IStreamIdFeature { - System.Threading.Tasks.ValueTask AcceptAsync(); + long StreamId { get; } } public partial interface ITlsHandshakeFeature { diff --git a/src/Servers/Connections.Abstractions/src/BaseConnectionContext.cs b/src/Servers/Connections.Abstractions/src/BaseConnectionContext.cs new file mode 100644 index 000000000000..662b8c902e9b --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/BaseConnectionContext.cs @@ -0,0 +1,65 @@ +// 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.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Connections +{ + public abstract class BaseConnectionContext : IAsyncDisposable + { + /// + /// Gets or sets a unique identifier to represent this connection in trace logs. + /// + public abstract string ConnectionId { get; set; } + + /// + /// Gets the collection of features provided by the server and middleware available on this connection. + /// + public abstract IFeatureCollection Features { get; } + + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this connection. + /// + public abstract IDictionary Items { get; set; } + + /// + /// Triggered when the client connection is closed. + /// + public virtual CancellationToken ConnectionClosed { get; set; } + + /// + /// Gets or sets the local endpoint for this connection. + /// + public virtual EndPoint LocalEndPoint { get; set; } + + /// + /// Gets or sets the remote endpoint for this connection. + /// + public virtual EndPoint RemoteEndPoint { get; set; } + + /// + /// Aborts the underlying connection. + /// + public abstract void Abort(); + + /// + /// Aborts the underlying connection. + /// + /// An optional describing the reason the connection is being terminated. + public abstract void Abort(ConnectionAbortedException abortReason); + + /// + /// Releases resources for the underlying connection. + /// + /// A that completes when resources have been released. + public virtual ValueTask DisposeAsync() + { + return default; + } + } +} diff --git a/src/Servers/Connections.Abstractions/src/ConnectionBuilderExtensions.cs b/src/Servers/Connections.Abstractions/src/ConnectionBuilderExtensions.cs index 100917b0094a..55b0311eb977 100644 --- a/src/Servers/Connections.Abstractions/src/ConnectionBuilderExtensions.cs +++ b/src/Servers/Connections.Abstractions/src/ConnectionBuilderExtensions.cs @@ -40,4 +40,4 @@ public static IConnectionBuilder Run(this IConnectionBuilder connectionBuilder, }); } } -} \ No newline at end of file +} diff --git a/src/Servers/Connections.Abstractions/src/ConnectionContext.cs b/src/Servers/Connections.Abstractions/src/ConnectionContext.cs index 947066ca796f..02b291c2c816 100644 --- a/src/Servers/Connections.Abstractions/src/ConnectionContext.cs +++ b/src/Servers/Connections.Abstractions/src/ConnectionContext.cs @@ -2,61 +2,26 @@ // 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.IO.Pipelines; -using System.Net; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Connections.Features; -using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Connections { /// /// Encapsulates all information about an individual connection. /// - public abstract class ConnectionContext : IAsyncDisposable + public abstract class ConnectionContext : BaseConnectionContext, IAsyncDisposable { - /// - /// Gets or sets a unique identifier to represent this connection in trace logs. - /// - public abstract string ConnectionId { get; set; } - - /// - /// Gets the collection of features provided by the server and middleware available on this connection. - /// - public abstract IFeatureCollection Features { get; } - - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this connection. - /// - public abstract IDictionary Items { get; set; } - /// /// Gets or sets the that can be used to read or write data on this connection. /// public abstract IDuplexPipe Transport { get; set; } - /// - /// Triggered when the client connection is closed. - /// - public virtual CancellationToken ConnectionClosed { get; set; } - - /// - /// Gets or sets the local endpoint for this connection. - /// - public virtual EndPoint LocalEndPoint { get; set; } - - /// - /// Gets or sets the remote endpoint for this connection. - /// - public virtual EndPoint RemoteEndPoint { get; set; } - /// /// Aborts the underlying connection. /// /// An optional describing the reason the connection is being terminated. - public virtual void Abort(ConnectionAbortedException abortReason) + public override void Abort(ConnectionAbortedException abortReason) { // We expect this to be overridden, but this helps maintain back compat // with implementations of ConnectionContext that predate the addition of @@ -67,15 +32,6 @@ public virtual void Abort(ConnectionAbortedException abortReason) /// /// Aborts the underlying connection. /// - public virtual void Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application via ConnectionContext.Abort().")); - - /// - /// Releases resources for the underlying connection. - /// - /// A that completes when resources have been released. - public virtual ValueTask DisposeAsync() - { - return default; - } + public override void Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application via ConnectionContext.Abort().")); } } diff --git a/src/Servers/Connections.Abstractions/src/Features/IQuicStreamListenerFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IProtocolErrorCodeFeature.cs similarity index 64% rename from src/Servers/Connections.Abstractions/src/Features/IQuicStreamListenerFeature.cs rename to src/Servers/Connections.Abstractions/src/Features/IProtocolErrorCodeFeature.cs index e9f63aeb3667..c47a2485b86d 100644 --- a/src/Servers/Connections.Abstractions/src/Features/IQuicStreamListenerFeature.cs +++ b/src/Servers/Connections.Abstractions/src/Features/IProtocolErrorCodeFeature.cs @@ -1,12 +1,10 @@ // 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.Threading.Tasks; - namespace Microsoft.AspNetCore.Connections.Features { - public interface IQuicStreamListenerFeature + public interface IProtocolErrorCodeFeature { - ValueTask AcceptAsync(); + long Error { get; set; } } } diff --git a/src/Servers/Connections.Abstractions/src/Features/IQuicCreateStreamFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IQuicCreateStreamFeature.cs deleted file mode 100644 index 1de25e43130d..000000000000 --- a/src/Servers/Connections.Abstractions/src/Features/IQuicCreateStreamFeature.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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.Threading.Tasks; - -namespace Microsoft.AspNetCore.Connections.Abstractions.Features -{ - public interface IQuicCreateStreamFeature - { - ValueTask StartUnidirectionalStreamAsync(); - ValueTask StartBidirectionalStreamAsync(); - } -} diff --git a/src/Servers/Connections.Abstractions/src/Features/IQuicStreamFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IStreamDirectionFeature.cs similarity index 80% rename from src/Servers/Connections.Abstractions/src/Features/IQuicStreamFeature.cs rename to src/Servers/Connections.Abstractions/src/Features/IStreamDirectionFeature.cs index 802209c1ea78..66f706dbf912 100644 --- a/src/Servers/Connections.Abstractions/src/Features/IQuicStreamFeature.cs +++ b/src/Servers/Connections.Abstractions/src/Features/IStreamDirectionFeature.cs @@ -3,10 +3,9 @@ namespace Microsoft.AspNetCore.Connections.Features { - public interface IQuicStreamFeature + public interface IStreamDirectionFeature { bool CanRead { get; } bool CanWrite { get; } - long StreamId { get; } } } diff --git a/src/Servers/Connections.Abstractions/src/Features/IStreamIdFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IStreamIdFeature.cs new file mode 100644 index 000000000000..f86f2ee61e6e --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/Features/IStreamIdFeature.cs @@ -0,0 +1,10 @@ +// 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. + +namespace Microsoft.AspNetCore.Connections.Features +{ + public interface IStreamIdFeature + { + long StreamId { get; } + } +} diff --git a/src/Servers/Connections.Abstractions/src/IMulitplexedConnectionListener.cs b/src/Servers/Connections.Abstractions/src/IMulitplexedConnectionListener.cs new file mode 100644 index 000000000000..d867fe0938de --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/IMulitplexedConnectionListener.cs @@ -0,0 +1,37 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Connections +{ + /// + /// Defines an interface that represents a listener bound to a specific . + /// + public interface IMultiplexedConnectionListener : IAsyncDisposable + { + /// + /// The endpoint that was bound. This may differ from the requested endpoint, such as when the caller requested that any free port be selected. + /// + EndPoint EndPoint { get; } + + /// + /// Stops listening for incoming connections. + /// + /// The token to monitor for cancellation requests. + /// A that represents the un-bind operation. + ValueTask UnbindAsync(CancellationToken cancellationToken = default); + + /// + /// Begins an asynchronous operation to accept an incoming connection. + /// + /// A feature collection to pass options when accepting a connection. + /// The token to monitor for cancellation requests. + /// A that completes when a connection is accepted, yielding the representing the connection. + ValueTask AcceptAsync(IFeatureCollection features = null, CancellationToken cancellationToken = default); + } +} diff --git a/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionBuilder.cs b/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionBuilder.cs new file mode 100644 index 000000000000..8f3caf34db54 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionBuilder.cs @@ -0,0 +1,31 @@ +// 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.Connections +{ + /// + /// Defines an interface that provides the mechanisms to configure a connection pipeline. + /// + public interface IMultiplexedConnectionBuilder + { + /// + /// Gets the that provides access to the application's service container. + /// + IServiceProvider ApplicationServices { get; } + + /// + /// Adds a middleware delegate to the application's connection pipeline. + /// + /// The middleware delegate. + /// The . + IMultiplexedConnectionBuilder Use(Func middleware); + + /// + /// Builds the delegate used by this application to process connections. + /// + /// The connection handling delegate. + MultiplexedConnectionDelegate Build(); + } +} diff --git a/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionFactory.cs b/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionFactory.cs new file mode 100644 index 000000000000..a3f69f7a6854 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionFactory.cs @@ -0,0 +1,27 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Connections +{ + /// + /// A factory abstraction for creating connections to an endpoint. + /// + public interface IMultiplexedConnectionFactory + { + /// + /// Creates a new connection to an endpoint. + /// + /// The to connect to. + /// A feature collection to pass options when connecting. + /// The token to monitor for cancellation requests. The default value is . + /// + /// A that represents the asynchronous connect, yielding the for the new connection when completed. + /// + ValueTask ConnectAsync(EndPoint endpoint, IFeatureCollection features = null, CancellationToken cancellationToken = default); + } +} diff --git a/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs b/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs index 65727b7ad831..3b5010beda33 100644 --- a/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs +++ b/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs @@ -1,9 +1,25 @@ // 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + namespace Microsoft.AspNetCore.Connections { - public interface IMultiplexedConnectionListenerFactory : IConnectionListenerFactory + /// + /// Defines an interface that provides the mechanisms for binding to various types of s. + /// + public interface IMultiplexedConnectionListenerFactory { + /// + /// Creates an bound to the specified . + /// + /// The to bind to. + /// A feature collection to pass options when binding. + /// The token to monitor for cancellation requests. + /// A that completes when the listener has been bound, yielding a representing the new listener. + ValueTask BindAsync(EndPoint endpoint, IFeatureCollection features = null, CancellationToken cancellationToken = default); } } diff --git a/src/Servers/Connections.Abstractions/src/MultiplexedConnectionBuilder.cs b/src/Servers/Connections.Abstractions/src/MultiplexedConnectionBuilder.cs new file mode 100644 index 000000000000..202f29df5eb5 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/MultiplexedConnectionBuilder.cs @@ -0,0 +1,43 @@ +// 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.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Connections +{ + public class MultiplexedConnectionBuilder : IMultiplexedConnectionBuilder + { + private readonly IList> _components = new List>(); + + public IServiceProvider ApplicationServices { get; } + + public MultiplexedConnectionBuilder(IServiceProvider applicationServices) + { + ApplicationServices = applicationServices; + } + + public IMultiplexedConnectionBuilder Use(Func middleware) + { + _components.Add(middleware); + return this; + } + + public MultiplexedConnectionDelegate Build() + { + MultiplexedConnectionDelegate app = features => + { + return Task.CompletedTask; + }; + + foreach (var component in _components.Reverse()) + { + app = component(app); + } + + return app; + } + } +} diff --git a/src/Servers/Connections.Abstractions/src/MultiplexedConnectionContext.cs b/src/Servers/Connections.Abstractions/src/MultiplexedConnectionContext.cs new file mode 100644 index 000000000000..ce0850d2816f --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/MultiplexedConnectionContext.cs @@ -0,0 +1,31 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Connections +{ + /// + /// Encapsulates all information about a multiplexed connection. + /// + public abstract class MultiplexedConnectionContext : BaseConnectionContext, IAsyncDisposable + { + /// + /// Asynchronously accept an incoming stream on the connection. + /// + /// + /// + public abstract ValueTask AcceptAsync(CancellationToken cancellationToken = default); + + /// + /// Creates an outbound connection + /// + /// + /// + /// + public abstract ValueTask ConnectAsync(IFeatureCollection features = null, CancellationToken cancellationToken = default); + } +} diff --git a/src/Servers/Connections.Abstractions/src/MultiplexedConnectionDelegate.cs b/src/Servers/Connections.Abstractions/src/MultiplexedConnectionDelegate.cs new file mode 100644 index 000000000000..c85298ea2d9b --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/MultiplexedConnectionDelegate.cs @@ -0,0 +1,14 @@ +// 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.Threading.Tasks; + +namespace Microsoft.AspNetCore.Connections +{ + /// + /// A function that can process a connection. + /// + /// A representing the connection. + /// A that represents the connection lifetime. When the task completes, the connection will be closed. + public delegate Task MultiplexedConnectionDelegate(MultiplexedConnectionContext connection); +} diff --git a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs index 4bc68180469b..6673afaa1cc1 100644 --- a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs +++ b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs @@ -96,6 +96,7 @@ public enum HttpProtocols public partial class KestrelServer : Microsoft.AspNetCore.Hosting.Server.IServer, System.IDisposable { public KestrelServer(Microsoft.Extensions.Options.IOptions options, System.Collections.Generic.IEnumerable transportFactories, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } + public KestrelServer(Microsoft.Extensions.Options.IOptions options, System.Collections.Generic.IEnumerable transportFactories, System.Collections.Generic.IEnumerable multiplexedFactories, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } public Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions Options { get { throw null; } } public void Dispose() { } @@ -149,7 +150,7 @@ public void ListenLocalhost(int port, System.Action configure) { } } - public partial class ListenOptions : Microsoft.AspNetCore.Connections.IConnectionBuilder + public partial class ListenOptions : Microsoft.AspNetCore.Connections.IConnectionBuilder, Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder { internal ListenOptions() { } public System.IServiceProvider ApplicationServices { get { throw null; } } @@ -159,6 +160,8 @@ internal ListenOptions() { } public Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols Protocols { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public string SocketPath { get { throw null; } } public Microsoft.AspNetCore.Connections.ConnectionDelegate Build() { throw null; } + Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder.Build() { throw null; } + Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder.Use(System.Func middleware) { throw null; } public override string ToString() { throw null; } public Microsoft.AspNetCore.Connections.IConnectionBuilder Use(System.Func middleware) { throw null; } } diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index 3a25467bcc2f..f84ed1d2cef1 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -527,6 +527,24 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l A new stream was refused because this connection has reached its stream limit. + + CONNECT requests must not send :scheme or :path headers. + + + The request :scheme header '{requestScheme}' does not match the transport scheme '{transportScheme}'. + + + The Method '{method}' is invalid. + + + The request :path is invalid: '{path}' + + + Less data received than specified in the Content-Length header. + + + More data received than specified in the Content-Length header. + A value greater than zero is required. diff --git a/src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs b/src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs index df08a8d320d9..a373c240bf8d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs +++ b/src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs @@ -53,7 +53,7 @@ async Task AcceptConnectionsAsync() // Add the connection to the connection manager before we queue it for execution var id = Interlocked.Increment(ref _lastConnectionId); - var kestrelConnection = new KestrelConnection(id, _serviceContext, _connectionDelegate, connection, Log); + var kestrelConnection = new KestrelConnection(id, _serviceContext, c => _connectionDelegate(c), connection, Log); _serviceContext.ConnectionManager.AddConnection(id, kestrelConnection); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 47d6e3fa3275..8ca14493dc69 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -511,7 +511,7 @@ private void PreventRequestAbortedCancellation() } } - public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) + public virtual void OnHeader(ReadOnlySpan name, ReadOnlySpan value) { _requestHeadersParsed++; if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount) @@ -1196,7 +1196,7 @@ private HttpResponseHeaders CreateResponseHeaders(bool appCompleted) { foreach (var option in ServerOptions.ListenOptions) { - if (option.Protocols == HttpProtocols.Http3) + if ((option.Protocols & HttpProtocols.Http3) == HttpProtocols.Http3) { responseHeaders.HeaderAltSvc = $"h3-25=\":{option.IPEndPoint.Port}\"; ma=84600"; break; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/DefaultStreamDirectionFeature.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/DefaultStreamDirectionFeature.cs new file mode 100644 index 000000000000..b96554c171c8 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/DefaultStreamDirectionFeature.cs @@ -0,0 +1,20 @@ +// 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.Connections.Features; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal class DefaultStreamDirectionFeature : IStreamDirectionFeature + { + public DefaultStreamDirectionFeature(bool canRead, bool canWrite) + { + CanRead = canRead; + CanWrite = canWrite; + } + + public bool CanRead { get; } + + public bool CanWrite { get; } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs index 9c1c4e888f07..3aa7d990a310 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs @@ -3,39 +3,50 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; using System.Net; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Connections.Abstractions.Features; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { - internal class Http3Connection : IRequestProcessor + internal class Http3Connection : IRequestProcessor, ITimeoutHandler { - public HttpConnectionContext Context { get; private set; } - public DynamicTable DynamicTable { get; set; } public Http3ControlStream ControlStream { get; set; } public Http3ControlStream EncoderStream { get; set; } public Http3ControlStream DecoderStream { get; set; } - private readonly ConcurrentDictionary _streams = new ConcurrentDictionary(); + internal readonly Dictionary _streams = new Dictionary(); private long _highestOpenedStreamId; // TODO lock to access private volatile bool _haveSentGoAway; private object _sync = new object(); - - public Http3Connection(HttpConnectionContext context) + private MultiplexedConnectionContext _multiplexedContext; + private readonly Http3ConnectionContext _context; + private readonly ISystemClock _systemClock; + private readonly TimeoutControl _timeoutControl; + private bool _aborted; + private object _protocolSelectionLock = new object(); + + public Http3Connection(Http3ConnectionContext context) { - Context = context; + _multiplexedContext = context.ConnectionContext; + _context = context; DynamicTable = new DynamicTable(0); + _systemClock = context.ServiceContext.SystemClock; + _timeoutControl = new TimeoutControl(this); + _context.TimeoutControl ??= _timeoutControl; } internal long HighestStreamId @@ -53,10 +64,133 @@ internal long HighestStreamId } } - public async Task ProcessRequestsAsync(IHttpApplication application) + private IKestrelTrace Log => _context.ServiceContext.Log; + + public async Task ProcessRequestsAsync(IHttpApplication httpApplication) + { + try + { + // Ensure TimeoutControl._lastTimestamp is initialized before anything that could set timeouts runs. + _timeoutControl.Initialize(_systemClock.UtcNowTicks); + + var connectionHeartbeatFeature = _context.ConnectionFeatures.Get(); + var connectionLifetimeNotificationFeature = _context.ConnectionFeatures.Get(); + + // These features should never be null in Kestrel itself, if this middleware is ever refactored to run outside of kestrel, + // we'll need to handle these missing. + Debug.Assert(connectionHeartbeatFeature != null, nameof(IConnectionHeartbeatFeature) + " is missing!"); + Debug.Assert(connectionLifetimeNotificationFeature != null, nameof(IConnectionLifetimeNotificationFeature) + " is missing!"); + + // Register the various callbacks once we're going to start processing requests + + // The heart beat for various timeouts + connectionHeartbeatFeature?.OnHeartbeat(state => ((Http3Connection)state).Tick(), this); + + // Register for graceful shutdown of the server + using var shutdownRegistration = connectionLifetimeNotificationFeature?.ConnectionClosedRequested.Register(state => ((Http3Connection)state).StopProcessingNextRequest(), this); + + // Register for connection close + using var closedRegistration = _context.ConnectionContext.ConnectionClosed.Register(state => ((Http3Connection)state).OnConnectionClosed(), this); + + await InnerProcessRequestsAsync(httpApplication); + } + catch (Exception ex) + { + Log.LogCritical(0, ex, $"Unexpected exception in {nameof(Http3Connection)}.{nameof(ProcessRequestsAsync)}."); + } + finally + { + } + } + + // For testing only + internal void Initialize() + { + } + + public void StopProcessingNextRequest() + { + bool previousState; + lock (_protocolSelectionLock) + { + previousState = _aborted; + } + + // TODO figure out how to gracefully close next requests + } + + public void OnConnectionClosed() + { + bool previousState; + lock (_protocolSelectionLock) + { + previousState = _aborted; + } + + // TODO figure out how to gracefully close next requests + } + + public void Abort(ConnectionAbortedException ex) + { + bool previousState; + + lock (_protocolSelectionLock) + { + previousState = _aborted; + _aborted = true; + } + + if (!previousState) + { + InnerAbort(ex); + } + } + + public void Tick() + { + if (_aborted) + { + // It's safe to check for timeouts on a dead connection, + // but try not to in order to avoid extraneous logs. + return; + } + + // It's safe to use UtcNowUnsynchronized since Tick is called by the Heartbeat. + var now = _systemClock.UtcNowUnsynchronized; + _timeoutControl.Tick(now); + } + + public void OnTimeout(TimeoutReason reason) { - var streamListenerFeature = Context.ConnectionFeatures.Get(); + // In the cases that don't log directly here, we expect the setter of the timeout to also be the input + // reader, so when the read is canceled or aborted, the reader should write the appropriate log. + switch (reason) + { + case TimeoutReason.KeepAlive: + StopProcessingNextRequest(); + break; + case TimeoutReason.RequestHeaders: + HandleRequestHeadersTimeout(); + break; + case TimeoutReason.ReadDataRate: + HandleReadDataRateTimeout(); + break; + case TimeoutReason.WriteDataRate: + Log.ResponseMinimumDataRateNotSatisfied(_context.ConnectionId, "" /*TraceIdentifier*/); // TODO trace identifier. + Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)); + break; + case TimeoutReason.RequestBodyDrain: + case TimeoutReason.TimeoutFeature: + Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedOutByServer)); + break; + default: + Debug.Assert(false, "Invalid TimeoutReason"); + break; + } + } + internal async Task InnerProcessRequestsAsync(IHttpApplication application) + { // Start other three unidirectional streams here. var controlTask = CreateControlStream(application); var encoderTask = CreateEncoderStream(application); @@ -66,29 +200,32 @@ public async Task ProcessRequestsAsync(IHttpApplication appl { while (true) { - var connectionContext = await streamListenerFeature.AcceptAsync(); - if (connectionContext == null || _haveSentGoAway) + var streamContext = await _multiplexedContext.AcceptAsync(); + if (streamContext == null || _haveSentGoAway) { break; } - var httpConnectionContext = new HttpConnectionContext + var quicStreamFeature = streamContext.Features.Get(); + var streamIdFeature = streamContext.Features.Get(); + + Debug.Assert(quicStreamFeature != null); + + var httpConnectionContext = new Http3StreamContext { - ConnectionId = connectionContext.ConnectionId, - ConnectionContext = connectionContext, - Protocols = Context.Protocols, - ServiceContext = Context.ServiceContext, - ConnectionFeatures = connectionContext.Features, - MemoryPool = Context.MemoryPool, - Transport = connectionContext.Transport, - TimeoutControl = Context.TimeoutControl, - LocalEndPoint = connectionContext.LocalEndPoint as IPEndPoint, - RemoteEndPoint = connectionContext.RemoteEndPoint as IPEndPoint + ConnectionId = streamContext.ConnectionId, + StreamContext = streamContext, + // TODO connection context is null here. Should we set it to anything? + ServiceContext = _context.ServiceContext, + ConnectionFeatures = streamContext.Features, + MemoryPool = _context.MemoryPool, + Transport = streamContext.Transport, + TimeoutControl = _context.TimeoutControl, + LocalEndPoint = streamContext.LocalEndPoint as IPEndPoint, + RemoteEndPoint = streamContext.RemoteEndPoint as IPEndPoint }; - var streamFeature = httpConnectionContext.ConnectionFeatures.Get(); - - if (!streamFeature.CanWrite) + if (!quicStreamFeature.CanWrite) { // Unidirectional stream var stream = new Http3ControlStream(application, this, httpConnectionContext); @@ -97,13 +234,15 @@ public async Task ProcessRequestsAsync(IHttpApplication appl else { // Keep track of highest stream id seen for GOAWAY - var streamId = streamFeature.StreamId; - + var streamId = streamIdFeature.StreamId; HighestStreamId = streamId; var http3Stream = new Http3Stream(application, this, httpConnectionContext); var stream = http3Stream; - _streams[streamId] = http3Stream; + lock (_streams) + { + _streams[streamId] = http3Stream; + } ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false); } } @@ -111,14 +250,17 @@ public async Task ProcessRequestsAsync(IHttpApplication appl finally { // Abort all streams as connection has shutdown. - foreach (var stream in _streams.Values) + lock (_streams) { - stream.Abort(new ConnectionAbortedException("Connection is shutting down.")); + foreach (var stream in _streams.Values) + { + stream.Abort(new ConnectionAbortedException("Connection is shutting down.")); + } } - ControlStream.Abort(new ConnectionAbortedException("Connection is shutting down.")); - EncoderStream.Abort(new ConnectionAbortedException("Connection is shutting down.")); - DecoderStream.Abort(new ConnectionAbortedException("Connection is shutting down.")); + ControlStream?.Abort(new ConnectionAbortedException("Connection is shutting down.")); + EncoderStream?.Abort(new ConnectionAbortedException("Connection is shutting down.")); + DecoderStream?.Abort(new ConnectionAbortedException("Connection is shutting down.")); await controlTask; await encoderTask; @@ -150,28 +292,26 @@ private async ValueTask CreateDecoderStream(IHttpApplication private async ValueTask CreateNewUnidirectionalStreamAsync(IHttpApplication application) { - var connectionContext = await Context.ConnectionFeatures.Get().StartUnidirectionalStreamAsync(); - var httpConnectionContext = new HttpConnectionContext + var features = new FeatureCollection(); + features.Set(new DefaultStreamDirectionFeature(canRead: false, canWrite: true)); + var streamContext = await _multiplexedContext.ConnectAsync(features); + var httpConnectionContext = new Http3StreamContext { //ConnectionId = "", TODO getting stream ID from stream that isn't started throws an exception. - ConnectionContext = connectionContext, - Protocols = Context.Protocols, - ServiceContext = Context.ServiceContext, - ConnectionFeatures = connectionContext.Features, - MemoryPool = Context.MemoryPool, - Transport = connectionContext.Transport, - TimeoutControl = Context.TimeoutControl, - LocalEndPoint = connectionContext.LocalEndPoint as IPEndPoint, - RemoteEndPoint = connectionContext.RemoteEndPoint as IPEndPoint + StreamContext = streamContext, + Protocols = HttpProtocols.Http3, + ServiceContext = _context.ServiceContext, + ConnectionFeatures = streamContext.Features, + MemoryPool = _context.MemoryPool, + Transport = streamContext.Transport, + TimeoutControl = _context.TimeoutControl, + LocalEndPoint = streamContext.LocalEndPoint as IPEndPoint, + RemoteEndPoint = streamContext.RemoteEndPoint as IPEndPoint }; return new Http3ControlStream(application, this, httpConnectionContext); } - public void StopProcessingNextRequest() - { - } - public void HandleRequestHeadersTimeout() { } @@ -188,7 +328,7 @@ public void Tick(DateTimeOffset now) { } - public void Abort(ConnectionAbortedException ex) + private void InnerAbort(ConnectionAbortedException ex) { lock (_sync) { @@ -202,10 +342,14 @@ public void Abort(ConnectionAbortedException ex) _haveSentGoAway = true; // Abort currently active streams - foreach (var stream in _streams.Values) + lock (_streams) { - stream.Abort(new ConnectionAbortedException("The Http3Connection has been aborted"), Http3ErrorCode.UnexpectedFrame); + foreach (var stream in _streams.Values) + { + stream.Abort(new ConnectionAbortedException("The Http3Connection has been aborted"), Http3ErrorCode.UnexpectedFrame); + } } + // TODO need to figure out if there is server initiated connection close rather than stream close? } @@ -223,5 +367,13 @@ internal void ApplyMaxTableCapacity(long value) // TODO make sure this works //_maxDynamicTableSize = value; } + + internal void RemoveStream(long streamId) + { + lock(_streams) + { + _streams.Remove(streamId); + } + } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index 087013a6ccf7..01c0234fe6fb 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; @@ -21,29 +22,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { internal abstract class Http3Stream : HttpProtocol, IHttpHeadersHandler, IThreadPoolWorkItem { + private static ReadOnlySpan AuthorityBytes => new byte[10] { (byte)':', (byte)'a', (byte)'u', (byte)'t', (byte)'h', (byte)'o', (byte)'r', (byte)'i', (byte)'t', (byte)'y' }; + private static ReadOnlySpan MethodBytes => new byte[7] { (byte)':', (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d' }; + private static ReadOnlySpan PathBytes => new byte[5] { (byte)':', (byte)'p', (byte)'a', (byte)'t', (byte)'h' }; + private static ReadOnlySpan SchemeBytes => new byte[7] { (byte)':', (byte)'s', (byte)'c', (byte)'h', (byte)'e', (byte)'m', (byte)'e' }; + private static ReadOnlySpan StatusBytes => new byte[7] { (byte)':', (byte)'s', (byte)'t', (byte)'a', (byte)'t', (byte)'u', (byte)'s' }; + private static ReadOnlySpan ConnectionBytes => new byte[10] { (byte)'c', (byte)'o', (byte)'n', (byte)'n', (byte)'e', (byte)'c', (byte)'t', (byte)'i', (byte)'o', (byte)'n' }; + private static ReadOnlySpan TeBytes => new byte[2] { (byte)'t', (byte)'e' }; + private static ReadOnlySpan TrailersBytes => new byte[8] { (byte)'t', (byte)'r', (byte)'a', (byte)'i', (byte)'l', (byte)'e', (byte)'r', (byte)'s' }; + private static ReadOnlySpan ConnectBytes => new byte[7] { (byte)'C', (byte)'O', (byte)'N', (byte)'N', (byte)'E', (byte)'C', (byte)'T' }; + private Http3FrameWriter _frameWriter; private Http3OutputProducer _http3Output; private int _isClosed; private int _gracefulCloseInitiator; - private readonly HttpConnectionContext _context; + private readonly Http3StreamContext _context; + private readonly IProtocolErrorCodeFeature _errorCodeFeature; + private readonly IStreamIdFeature _streamIdFeature; private readonly Http3RawFrame _incomingFrame = new Http3RawFrame(); + protected RequestHeaderParsingState _requestHeaderParsingState; + private PseudoHeaderFields _parsedPseudoHeaderFields; + private bool _isMethodConnect; private readonly Http3Connection _http3Connection; private bool _receivedHeaders; + private TaskCompletionSource _appCompleted; + public Pipe RequestBodyPipe { get; } - public Http3Stream(Http3Connection http3Connection, HttpConnectionContext context) + public Http3Stream(Http3Connection http3Connection, Http3StreamContext context) { Initialize(context); + + InputRemaining = null; + // First, determine how we know if an Http3stream is unidirectional or bidirectional var httpLimits = context.ServiceContext.ServerOptions.Limits; var http3Limits = httpLimits.Http3; _http3Connection = http3Connection; _context = context; + _errorCodeFeature = _context.ConnectionFeatures.Get(); + _streamIdFeature = _context.ConnectionFeatures.Get(); + _frameWriter = new Http3FrameWriter( context.Transport.Output, - context.ConnectionContext, + context.StreamContext, context.TimeoutControl, httpLimits.MinResponseDataRate, context.ConnectionId, @@ -63,6 +87,8 @@ public Http3Stream(Http3Connection http3Connection, HttpConnectionContext contex QPackDecoder = new QPackDecoder(_context.ServiceContext.ServerOptions.Limits.Http3.MaxRequestHeaderFieldSize); } + public long? InputRemaining { get; internal set; } + public QPackDecoder QPackDecoder { get; } public PipeReader Input => _context.Transport.Input; @@ -77,7 +103,10 @@ public void Abort(ConnectionAbortedException ex) public void Abort(ConnectionAbortedException ex, Http3ErrorCode errorCode) { - // TODO something with request aborted? + _errorCodeFeature.Error = (long)errorCode; + // TODO replace with IKestrelTrace log. + Log.LogWarning(ex, ex.Message); + _frameWriter.Abort(ex); } public void OnHeadersComplete(bool endStream) @@ -97,6 +126,165 @@ public void OnStaticIndexedHeader(int index, ReadOnlySpan value) OnHeader(knownHeader.Name, value); } + public override void OnHeader(ReadOnlySpan name, ReadOnlySpan value) + { + // TODO MaxRequestHeadersTotalSize? + ValidateHeader(name, value); + try + { + if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) + { + OnTrailer(name, value); + } + else + { + // Throws BadRequest for header count limit breaches. + // Throws InvalidOperation for bad encoding. + base.OnHeader(name, value); + } + } + catch (BadHttpRequestException bre) + { + throw new Http3StreamErrorException(bre.Message, Http3ErrorCode.ProtocolError); + } + catch (InvalidOperationException) + { + throw new Http3StreamErrorException(CoreStrings.BadRequest_MalformedRequestInvalidHeaders, Http3ErrorCode.ProtocolError); + } + } + + private void ValidateHeader(ReadOnlySpan name, ReadOnlySpan value) + { + // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.1 + /* + Intermediaries that process HTTP requests or responses (i.e., any + intermediary not acting as a tunnel) MUST NOT forward a malformed + request or response. Malformed requests or responses that are + detected MUST be treated as a stream error (Section 5.4.2) of type + PROTOCOL_ERROR. + + For malformed requests, a server MAY send an HTTP response prior to + closing or resetting the stream. Clients MUST NOT accept a malformed + response. Note that these requirements are intended to protect + against several types of common attacks against HTTP; they are + deliberately strict because being permissive can expose + implementations to these vulnerabilities.*/ + if (IsPseudoHeaderField(name, out var headerField)) + { + if (_requestHeaderParsingState == RequestHeaderParsingState.Headers) + { + // All pseudo-header fields MUST appear in the header block before regular header fields. + // Any request or response that contains a pseudo-header field that appears in a header + // block after a regular header field MUST be treated as malformed (Section 8.1.2.6). + throw new Http3StreamErrorException(CoreStrings.Http2ErrorPseudoHeaderFieldAfterRegularHeaders, Http3ErrorCode.ProtocolError); + } + + if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) + { + // Pseudo-header fields MUST NOT appear in trailers. + throw new Http3StreamErrorException(CoreStrings.Http2ErrorTrailersContainPseudoHeaderField, Http3ErrorCode.ProtocolError); + } + + _requestHeaderParsingState = RequestHeaderParsingState.PseudoHeaderFields; + + if (headerField == PseudoHeaderFields.Unknown) + { + // Endpoints MUST treat a request or response that contains undefined or invalid pseudo-header + // fields as malformed (Section 8.1.2.6). + throw new Http3StreamErrorException(CoreStrings.Http2ErrorUnknownPseudoHeaderField, Http3ErrorCode.ProtocolError); + } + + if (headerField == PseudoHeaderFields.Status) + { + // Pseudo-header fields defined for requests MUST NOT appear in responses; pseudo-header fields + // defined for responses MUST NOT appear in requests. + throw new Http3StreamErrorException(CoreStrings.Http2ErrorResponsePseudoHeaderField, Http3ErrorCode.ProtocolError); + } + + if ((_parsedPseudoHeaderFields & headerField) == headerField) + { + // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.3 + // All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields + throw new Http3StreamErrorException(CoreStrings.Http2ErrorDuplicatePseudoHeaderField, Http3ErrorCode.ProtocolError); + } + + if (headerField == PseudoHeaderFields.Method) + { + _isMethodConnect = value.SequenceEqual(ConnectBytes); + } + + _parsedPseudoHeaderFields |= headerField; + } + else if (_requestHeaderParsingState != RequestHeaderParsingState.Trailers) + { + _requestHeaderParsingState = RequestHeaderParsingState.Headers; + } + + if (IsConnectionSpecificHeaderField(name, value)) + { + throw new Http3StreamErrorException(CoreStrings.Http2ErrorConnectionSpecificHeaderField, Http3ErrorCode.ProtocolError); + } + + // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2 + // A request or response containing uppercase header field names MUST be treated as malformed (Section 8.1.2.6). + for (var i = 0; i < name.Length; i++) + { + if (name[i] >= 65 && name[i] <= 90) + { + if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) + { + throw new Http3StreamErrorException(CoreStrings.Http2ErrorTrailerNameUppercase, Http3ErrorCode.ProtocolError); + } + else + { + throw new Http3StreamErrorException(CoreStrings.Http2ErrorHeaderNameUppercase, Http3ErrorCode.ProtocolError); + } + } + } + } + + private bool IsPseudoHeaderField(ReadOnlySpan name, out PseudoHeaderFields headerField) + { + headerField = PseudoHeaderFields.None; + + if (name.IsEmpty || name[0] != (byte)':') + { + return false; + } + + if (name.SequenceEqual(PathBytes)) + { + headerField = PseudoHeaderFields.Path; + } + else if (name.SequenceEqual(MethodBytes)) + { + headerField = PseudoHeaderFields.Method; + } + else if (name.SequenceEqual(SchemeBytes)) + { + headerField = PseudoHeaderFields.Scheme; + } + else if (name.SequenceEqual(StatusBytes)) + { + headerField = PseudoHeaderFields.Status; + } + else if (name.SequenceEqual(AuthorityBytes)) + { + headerField = PseudoHeaderFields.Authority; + } + else + { + headerField = PseudoHeaderFields.Unknown; + } + + return true; + } + + private static bool IsConnectionSpecificHeaderField(ReadOnlySpan name, ReadOnlySpan value) + { + return name.SequenceEqual(ConnectionBytes) || (name.SequenceEqual(TeBytes) && !value.SequenceEqual(TrailersBytes)); + } + public void HandleReadDataRateTimeout() { Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, null, Limits.MinRequestBodyDataRate.BytesPerSecond); @@ -112,7 +300,14 @@ public void HandleRequestHeadersTimeout() public void OnInputOrOutputCompleted() { TryClose(); - _frameWriter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient)); + Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient), Http3ErrorCode.NoError); + } + + protected override void OnRequestProcessingEnded() + { + Debug.Assert(_appCompleted != null); + + _appCompleted.SetResult(new object()); } private bool TryClose() @@ -152,6 +347,7 @@ public async Task ProcessRequestAsync(IHttpApplication appli if (result.IsCompleted) { + OnEndStreamReceived(); return; } } @@ -162,33 +358,61 @@ public async Task ProcessRequestAsync(IHttpApplication appli } } } + catch (Http3StreamErrorException ex) + { + error = ex; + Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode); + } catch (Exception ex) { error = ex; - Log.LogWarning(0, ex, "Stream threw an exception."); + Log.LogWarning(0, ex, "Stream threw an unexpected exception."); } finally { var streamError = error as ConnectionAbortedException ?? new ConnectionAbortedException("The stream has completed.", error); + + Input.Complete(); + + await RequestBodyPipe.Writer.CompleteAsync(); + + // Make sure application func is completed before completing writer. + if (_appCompleted != null) + { + await _appCompleted.Task; + } + try { _frameWriter.Complete(); } catch { - _frameWriter.Abort(streamError); + Abort(streamError, Http3ErrorCode.ProtocolError); throw; } finally { - Input.Complete(); - _context.Transport.Input.CancelPendingRead(); - await RequestBodyPipe.Writer.CompleteAsync(); + _http3Connection.RemoveStream(_streamIdFeature.StreamId); } } } + private void OnEndStreamReceived() + { + if (InputRemaining.HasValue) + { + // https://tools.ietf.org/html/rfc7540#section-8.1.2.6 + if (InputRemaining.Value != 0) + { + throw new Http3StreamErrorException(CoreStrings.Http3StreamErrorLessDataThanLength, Http3ErrorCode.ProtocolError); + } + } + + OnTrailersComplete(); + RequestBodyPipe.Writer.Complete(); + } private Task ProcessHttp3Stream(IHttpApplication application, in ReadOnlySequence payload) { @@ -231,13 +455,28 @@ private Task ProcessHeadersFrameAsync(IHttpApplication appli } _receivedHeaders = true; + InputRemaining = HttpRequestHeaders.ContentLength; + + _appCompleted = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false); - Task.Run(() => base.ProcessRequestsAsync(application)); return Task.CompletedTask; } private Task ProcessDataFrameAsync(in ReadOnlySequence payload) { + if (InputRemaining.HasValue) + { + // https://tools.ietf.org/html/rfc7540#section-8.1.2.6 + if (payload.Length > InputRemaining.Value) + { + throw new Http3StreamErrorException(CoreStrings.Http3StreamErrorMoreDataThanLength, Http3ErrorCode.ProtocolError); + } + + InputRemaining -= payload.Length; + } + foreach (var segment in payload) { RequestBodyPipe.Writer.Write(segment.Span); @@ -270,6 +509,8 @@ protected override void OnReset() protected override void ApplicationAbort() { + var abortReason = new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication); + Abort(abortReason, Http3ErrorCode.InternalError); } protected override string CreateRequestId() @@ -307,7 +548,7 @@ private bool TryValidatePseudoHeaders() { if (!string.IsNullOrEmpty(RequestHeaders[HeaderNames.Scheme]) || !string.IsNullOrEmpty(RequestHeaders[HeaderNames.Path])) { - Abort(new ConnectionAbortedException(CoreStrings.Http2ErrorConnectMustNotSendSchemeOrPath), Http3ErrorCode.ProtocolError); + Abort(new ConnectionAbortedException(CoreStrings.Http3ErrorConnectMustNotSendSchemeOrPath), Http3ErrorCode.ProtocolError); return false; } @@ -326,8 +567,8 @@ private bool TryValidatePseudoHeaders() // - We'll need to find some concrete scenarios to warrant unblocking this. if (!string.Equals(RequestHeaders[HeaderNames.Scheme], Scheme, StringComparison.OrdinalIgnoreCase)) { - Abort(new ConnectionAbortedException( - CoreStrings.FormatHttp2StreamErrorSchemeMismatch(RequestHeaders[HeaderNames.Scheme], Scheme)), Http3ErrorCode.ProtocolError); + var str = CoreStrings.FormatHttp3StreamErrorSchemeMismatch(RequestHeaders[HeaderNames.Scheme], Scheme); + Abort(new ConnectionAbortedException(str), Http3ErrorCode.ProtocolError); return false; } @@ -376,7 +617,7 @@ private bool TryValidateMethod() if (Method == Http.HttpMethod.None) { - Abort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http3ErrorCode.ProtocolError); + Abort(new ConnectionAbortedException(CoreStrings.FormatHttp3ErrorMethodInvalid(_methodText)), Http3ErrorCode.ProtocolError); return false; } @@ -384,7 +625,7 @@ private bool TryValidateMethod() { if (HttpCharacters.IndexOfInvalidTokenChar(_methodText) >= 0) { - Abort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http3ErrorCode.ProtocolError); + Abort(new ConnectionAbortedException(CoreStrings.FormatHttp3ErrorMethodInvalid(_methodText)), Http3ErrorCode.ProtocolError); return false; } } @@ -436,7 +677,7 @@ private bool TryValidatePath(ReadOnlySpan pathSegment) // Must start with a leading slash if (pathSegment.Length == 0 || pathSegment[0] != '/') { - Abort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http3ErrorCode.ProtocolError); + Abort(new ConnectionAbortedException(CoreStrings.FormatHttp3StreamErrorPathInvalid(RawTarget)), Http3ErrorCode.ProtocolError); return false; } @@ -466,8 +707,7 @@ private bool TryValidatePath(ReadOnlySpan pathSegment) } catch (InvalidOperationException) { - // TODO change HTTP/2 specific messages to include HTTP/3 - Abort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http3ErrorCode.ProtocolError); + Abort(new ConnectionAbortedException(CoreStrings.FormatHttp3StreamErrorPathInvalid(RawTarget)), Http3ErrorCode.ProtocolError); return false; } } @@ -491,6 +731,26 @@ private Pipe CreateRequestBodyPipe(uint windowSize) /// public abstract void Execute(); + protected enum RequestHeaderParsingState + { + Ready, + PseudoHeaderFields, + Headers, + Trailers + } + + [Flags] + private enum PseudoHeaderFields + { + None = 0x0, + Authority = 0x1, + Method = 0x2, + Path = 0x4, + Scheme = 0x8, + Status = 0x10, + Unknown = 0x40000000 + } + private static class GracefulCloseInitiator { public const int None = 0; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs index 8d8d6a663ffa..b0dc9e472926 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs @@ -10,14 +10,21 @@ class Http3Stream : Http3Stream, IHostContextContainer { private readonly IHttpApplication _application; - public Http3Stream(IHttpApplication application, Http3Connection connection, HttpConnectionContext context) : base(connection, context) + public Http3Stream(IHttpApplication application, Http3Connection connection, Http3StreamContext context) : base(connection, context) { _application = application; } public override void Execute() { - _ = ProcessRequestAsync(_application); + if (_requestHeaderParsingState == Http3Stream.RequestHeaderParsingState.Ready) + { + _ = ProcessRequestAsync(_application); + } + else + { + _ = base.ProcessRequestsAsync(_application); + } } // Pooled Host context diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3ConnectionContext.cs b/src/Servers/Kestrel/Core/src/Internal/Http3ConnectionContext.cs new file mode 100644 index 000000000000..e20f8c33a6f5 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3ConnectionContext.cs @@ -0,0 +1,23 @@ +// 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.Buffers; +using System.Net; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal +{ + internal class Http3ConnectionContext + { + public string ConnectionId { get; set; } + public MultiplexedConnectionContext ConnectionContext { get; set; } + public ServiceContext ServiceContext { get; set; } + public IFeatureCollection ConnectionFeatures { get; set; } + public MemoryPool MemoryPool { get; set; } + public IPEndPoint LocalEndPoint { get; set; } + public IPEndPoint RemoteEndPoint { get; set; } + public ITimeoutControl TimeoutControl { get; set; } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs b/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs new file mode 100644 index 000000000000..9107b6dc8406 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs @@ -0,0 +1,12 @@ +// 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.Connections; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal +{ + internal class Http3StreamContext : HttpConnectionContext + { + public ConnectionContext StreamContext { get; set; } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs index 5c92ae816dd0..a00f6002a485 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs @@ -68,10 +68,6 @@ public async Task ProcessRequestsAsync(IHttpApplication http requestProcessor = new Http2Connection(_context); _protocolSelectionState = ProtocolSelectionState.Selected; break; - case HttpProtocols.Http3: - requestProcessor = new Http3Connection(_context); - _protocolSelectionState = ProtocolSelectionState.Selected; - break; case HttpProtocols.None: // An error was already logged in SelectProtocol(), but we should close the connection. break; @@ -204,11 +200,6 @@ private void Abort(ConnectionAbortedException ex) private HttpProtocols SelectProtocol() { - if (_context.Protocols == HttpProtocols.Http3) - { - return HttpProtocols.Http3; - } - var hasTls = _context.ConnectionFeatures.Get() != null; var applicationProtocol = _context.ConnectionFeatures.Get()?.ApplicationProtocol ?? new ReadOnlyMemory(); diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs b/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs index 562b7bd1a965..69c46ec79c87 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Buffers; -using System.Collections.Generic; using System.IO.Pipelines; using System.Net; using Microsoft.AspNetCore.Connections; diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManager.cs index 05bb0f0726a5..eb20cbac192b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManager.cs @@ -93,7 +93,7 @@ public async Task AbortAllConnectionsAsync() Walk(connection => { - connection.TransportConnection.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedDuringServerShutdown)); + connection.GetTransport().Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedDuringServerShutdown)); abortTasks.Add(connection.ExecutionTask); }); diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionReference.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionReference.cs index b5dc202e01bd..5e1702b7a668 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionReference.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionReference.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -12,7 +12,7 @@ internal class ConnectionReference public ConnectionReference(KestrelConnection connection) { _weakReference = new WeakReference(connection); - ConnectionId = connection.TransportConnection.ConnectionId; + ConnectionId = connection.GetTransport().ConnectionId; } public string ConnectionId { get; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnection.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnection.cs index 67a6b52d305c..ecb09f1784cd 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnection.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure { - internal class KestrelConnection : IConnectionHeartbeatFeature, IConnectionCompleteFeature, IConnectionLifetimeNotificationFeature, IThreadPoolWorkItem + internal abstract class KestrelConnection : IConnectionHeartbeatFeature, IConnectionCompleteFeature, IConnectionLifetimeNotificationFeature { private List<(Action handler, object state)> _heartbeatHandlers; private readonly object _heartbeatLock = new object(); @@ -21,31 +21,22 @@ internal class KestrelConnection : IConnectionHeartbeatFeature, IConnectionCompl private readonly CancellationTokenSource _connectionClosingCts = new CancellationTokenSource(); private readonly TaskCompletionSource _completionTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly long _id; - private readonly ServiceContext _serviceContext; - private readonly ConnectionDelegate _connectionDelegate; + protected readonly long _id; + protected readonly ServiceContext _serviceContext; public KestrelConnection(long id, ServiceContext serviceContext, - ConnectionDelegate connectionDelegate, - ConnectionContext connectionContext, IKestrelTrace logger) { _id = id; _serviceContext = serviceContext; - _connectionDelegate = connectionDelegate; Logger = logger; - TransportConnection = connectionContext; - connectionContext.Features.Set(this); - connectionContext.Features.Set(this); - connectionContext.Features.Set(this); ConnectionClosedRequested = _connectionClosingCts.Token; } - private IKestrelTrace Logger { get; } + protected IKestrelTrace Logger { get; } - public ConnectionContext TransportConnection { get; set; } public CancellationToken ConnectionClosedRequested { get; set; } public Task ExecutionTask => _completionTcs.Task; @@ -65,6 +56,8 @@ public void TickHeartbeat() } } + public abstract BaseConnectionContext GetTransport(); + public void OnHeartbeat(Action action, object state) { lock (_heartbeatLock) @@ -175,49 +168,7 @@ public void Complete() _connectionClosingCts.Dispose(); } - void IThreadPoolWorkItem.Execute() - { - _ = ExecuteAsync(); - } - - internal async Task ExecuteAsync() - { - var connectionContext = TransportConnection; - - try - { - Logger.ConnectionStart(connectionContext.ConnectionId); - KestrelEventSource.Log.ConnectionStart(connectionContext); - - using (BeginConnectionScope(connectionContext)) - { - try - { - await _connectionDelegate(connectionContext); - } - catch (Exception ex) - { - Logger.LogError(0, ex, "Unhandled exception while processing {ConnectionId}.", connectionContext.ConnectionId); - } - } - } - finally - { - await FireOnCompletedAsync(); - - Logger.ConnectionStop(connectionContext.ConnectionId); - KestrelEventSource.Log.ConnectionStop(connectionContext); - - // Dispose the transport connection, this needs to happen before removing it from the - // connection manager so that we only signal completion of this connection after the transport - // is properly torn down. - await TransportConnection.DisposeAsync(); - - _serviceContext.ConnectionManager.RemoveConnection(_id); - } - } - - private IDisposable BeginConnectionScope(ConnectionContext connectionContext) + protected IDisposable BeginConnectionScope(BaseConnectionContext connectionContext) { if (Logger.IsEnabled(LogLevel.Critical)) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnectionOfT.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnectionOfT.cs new file mode 100644 index 000000000000..5da49971779a --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnectionOfT.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure +{ + internal class KestrelConnection : KestrelConnection, IThreadPoolWorkItem where T : BaseConnectionContext + { + private readonly Func _connectionDelegate; + + public T TransportConnection { get; set; } + + public KestrelConnection(long id, + ServiceContext serviceContext, + Func connectionDelegate, + T connectionContext, + IKestrelTrace logger) + : base(id, serviceContext, logger) + { + _connectionDelegate = connectionDelegate; + TransportConnection = connectionContext; + connectionContext.Features.Set(this); + connectionContext.Features.Set(this); + connectionContext.Features.Set(this); + } + + void IThreadPoolWorkItem.Execute() + { + _ = ExecuteAsync(); + } + + internal async Task ExecuteAsync() + { + var connectionContext = TransportConnection; + + try + { + Logger.ConnectionStart(connectionContext.ConnectionId); + KestrelEventSource.Log.ConnectionStart(connectionContext); + + using (BeginConnectionScope(connectionContext)) + { + try + { + await _connectionDelegate(connectionContext); + } + catch (Exception ex) + { + Logger.LogError(0, ex, "Unhandled exception while processing {ConnectionId}.", connectionContext.ConnectionId); + } + } + } + finally + { + await FireOnCompletedAsync(); + + Logger.ConnectionStop(connectionContext.ConnectionId); + KestrelEventSource.Log.ConnectionStop(connectionContext); + + // Dispose the transport connection, this needs to happen before removing it from the + // connection manager so that we only signal completion of this connection after the transport + // is properly torn down. + await TransportConnection.DisposeAsync(); + + _serviceContext.ConnectionManager.RemoveConnection(_id); + } + } + + public override BaseConnectionContext GetTransport() + { + return TransportConnection; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs index fdabf48247f6..19afb9af715c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs @@ -26,7 +26,7 @@ private KestrelEventSource() // - Avoid renaming methods or parameters marked with EventAttribute. EventSource uses these to form the event object. [NonEvent] - public void ConnectionStart(ConnectionContext connection) + public void ConnectionStart(BaseConnectionContext connection) { // avoid allocating strings unless this event source is enabled if (IsEnabled()) @@ -53,7 +53,7 @@ private void ConnectionStart(string connectionId, } [NonEvent] - public void ConnectionStop(ConnectionContext connection) + public void ConnectionStop(BaseConnectionContext connection) { if (IsEnabled()) { diff --git a/src/Servers/Kestrel/Core/src/Internal/MultiplexedConnectionDispatcher.cs b/src/Servers/Kestrel/Core/src/Internal/MultiplexedConnectionDispatcher.cs new file mode 100644 index 000000000000..e0fe1edbdc72 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/MultiplexedConnectionDispatcher.cs @@ -0,0 +1,77 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal +{ + internal class MultiplexedConnectionDispatcher + { + private static long _lastConnectionId = long.MinValue; + + private readonly ServiceContext _serviceContext; + private readonly MultiplexedConnectionDelegate _connectionDelegate; + private readonly TaskCompletionSource _acceptLoopTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + public MultiplexedConnectionDispatcher(ServiceContext serviceContext, MultiplexedConnectionDelegate connectionDelegate) + { + _serviceContext = serviceContext; + _connectionDelegate = connectionDelegate; + } + + private IKestrelTrace Log => _serviceContext.Log; + + public Task StartAcceptingConnections(IMultiplexedConnectionListener listener) + { + ThreadPool.UnsafeQueueUserWorkItem(StartAcceptingConnectionsCore, listener, preferLocal: false); + return _acceptLoopTcs.Task; + } + + private void StartAcceptingConnectionsCore(IMultiplexedConnectionListener listener) + { + // REVIEW: Multiple accept loops in parallel? + _ = AcceptConnectionsAsync(); + + async Task AcceptConnectionsAsync() + { + try + { + while (true) + { + var connection = await listener.AcceptAsync(); + + if (connection == null) + { + // We're done listening + break; + } + + // Add the connection to the connection manager before we queue it for execution + var id = Interlocked.Increment(ref _lastConnectionId); + var kestrelConnection = new KestrelConnection(id, _serviceContext, c => _connectionDelegate(c), connection, Log); + + _serviceContext.ConnectionManager.AddConnection(id, kestrelConnection); + + Log.ConnectionAccepted(connection.ConnectionId); + + ThreadPool.UnsafeQueueUserWorkItem(kestrelConnection, preferLocal: false); + } + } + catch (Exception ex) + { + // REVIEW: If the accept loop ends should this trigger a server shutdown? It will manifest as a hang + Log.LogCritical(0, ex, "The connection listener failed to accept any new connections."); + } + finally + { + _acceptLoopTcs.TrySetResult(null); + } + } + } + } +} diff --git a/src/Servers/Kestrel/Core/src/KestrelServer.cs b/src/Servers/Kestrel/Core/src/KestrelServer.cs index ca1af168f01c..fdd2b47319cf 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServer.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServer.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO.Pipelines; using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; @@ -22,20 +23,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core public class KestrelServer : IServer { private readonly List<(IConnectionListener, Task)> _transports = new List<(IConnectionListener, Task)>(); + private readonly List<(IMultiplexedConnectionListener, Task)> _multiplexedTransports = new List<(IMultiplexedConnectionListener, Task)>(); private readonly IServerAddressesFeature _serverAddresses; private readonly List _transportFactories; + private readonly List _multiplexedTransportFactories; private bool _hasStarted; private int _stopping; private readonly TaskCompletionSource _stoppedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); public KestrelServer(IOptions options, IEnumerable transportFactories, ILoggerFactory loggerFactory) - : this(transportFactories, CreateServiceContext(options, loggerFactory)) + : this(transportFactories, null, CreateServiceContext(options, loggerFactory)) + { + } + public KestrelServer(IOptions options, IEnumerable transportFactories, IEnumerable multiplexedFactories, ILoggerFactory loggerFactory) + : this(transportFactories, multiplexedFactories, CreateServiceContext(options, loggerFactory)) { } // For testing internal KestrelServer(IEnumerable transportFactories, ServiceContext serviceContext) + : this(transportFactories, null, serviceContext) + { + } + + // For testing + internal KestrelServer(IEnumerable transportFactories, IEnumerable multiplexedFactories, ServiceContext serviceContext) { if (transportFactories == null) { @@ -43,8 +56,9 @@ internal KestrelServer(IEnumerable transportFactorie } _transportFactories = transportFactories.ToList(); + _multiplexedTransportFactories = multiplexedFactories?.ToList(); - if (_transportFactories.Count == 0) + if (_transportFactories.Count == 0 && (_multiplexedTransportFactories == null || _multiplexedTransportFactories.Count == 0)) { throw new InvalidOperationException(CoreStrings.TransportNotFound); } @@ -78,6 +92,7 @@ private static ServiceContext CreateServiceContext(IOptions(IHttpApplication application, C async Task OnBind(ListenOptions options) { - // Add the HTTP middleware as the terminal connection middleware - options.UseHttpServer(ServiceContext, application, options.Protocols); - - var connectionDelegate = options.Build(); - - // Add the connection limit middleware - if (Options.Limits.MaxConcurrentConnections.HasValue) - { - connectionDelegate = new ConnectionLimitMiddleware(connectionDelegate, Options.Limits.MaxConcurrentConnections.Value, Trace).OnConnectionAsync; - } - - var connectionDispatcher = new ConnectionDispatcher(ServiceContext, connectionDelegate); - - IConnectionListenerFactory factory = null; - if (options.Protocols >= HttpProtocols.Http3) + // INVESTIGATE: For some reason, MsQuic needs to bind before + // sockets for it to successfully listen. It also seems racy. + if ((options.Protocols & HttpProtocols.Http3) == HttpProtocols.Http3) { - foreach (var transportFactory in _transportFactories) + if (_multiplexedTransportFactories == null || _multiplexedTransportFactories.Count == 0) { - if (transportFactory is IMultiplexedConnectionListenerFactory) - { - // Don't break early. Always use the last registered factory. - factory = transportFactory; - } + throw new InvalidOperationException("Cannot start HTTP/3 server if no MultiplexedTransportFactories are registered."); } - if (factory == null) - { - throw new InvalidOperationException(CoreStrings.QuicTransportNotFound); - } + options.UseHttp3Server(ServiceContext, application, options.Protocols); + var multiplxedConnectionDelegate = ((IMultiplexedConnectionBuilder)options).Build(); + + var multiplexedConnectionDispatcher = new MultiplexedConnectionDispatcher(ServiceContext, multiplxedConnectionDelegate); + var multiplexedFactory = _multiplexedTransportFactories.Last(); + var multiplexedTransport = await multiplexedFactory.BindAsync(options.EndPoint).ConfigureAwait(false); + + var acceptLoopTask = multiplexedConnectionDispatcher.StartAcceptingConnections(multiplexedTransport); + _multiplexedTransports.Add((multiplexedTransport, acceptLoopTask)); + + options.EndPoint = multiplexedTransport.EndPoint; } - else + + // Add the HTTP middleware as the terminal connection middleware + if ((options.Protocols & HttpProtocols.Http1) == HttpProtocols.Http1 + || (options.Protocols & HttpProtocols.Http2) == HttpProtocols.Http2 + || options.Protocols == HttpProtocols.None) // TODO a test fails because it doesn't throw an exception in the right place + // when there is no HttpProtocols in KestrelServer, can we remove/change the test? { - foreach (var transportFactory in _transportFactories) + options.UseHttpServer(ServiceContext, application, options.Protocols); + var connectionDelegate = options.Build(); + + // Add the connection limit middleware + if (Options.Limits.MaxConcurrentConnections.HasValue) { - if (!(transportFactory is IMultiplexedConnectionListenerFactory)) - { - factory = transportFactory; - } + connectionDelegate = new ConnectionLimitMiddleware(connectionDelegate, Options.Limits.MaxConcurrentConnections.Value, Trace).OnConnectionAsync; } - } - var transport = await factory.BindAsync(options.EndPoint).ConfigureAwait(false); + var connectionDispatcher = new ConnectionDispatcher(ServiceContext, connectionDelegate); + var factory = _transportFactories.Last(); + var transport = await factory.BindAsync(options.EndPoint).ConfigureAwait(false); - // Update the endpoint - options.EndPoint = transport.EndPoint; - var acceptLoopTask = connectionDispatcher.StartAcceptingConnections(transport); + var acceptLoopTask = connectionDispatcher.StartAcceptingConnections(transport); - _transports.Add((transport, acceptLoopTask)); + _transports.Add((transport, acceptLoopTask)); + options.EndPoint = transport.EndPoint; + } } await AddressBinder.BindAsync(_serverAddresses, Options, Trace, OnBind).ConfigureAwait(false); @@ -200,13 +213,22 @@ public async Task StopAsync(CancellationToken cancellationToken) try { - var tasks = new Task[_transports.Count]; - for (int i = 0; i < _transports.Count; i++) + var connectionTransportCount = _transports.Count; + var totalTransportCount = _transports.Count + _multiplexedTransports.Count; + var tasks = new Task[totalTransportCount]; + + for (int i = 0; i < connectionTransportCount; i++) { (IConnectionListener listener, Task acceptLoop) = _transports[i]; tasks[i] = Task.WhenAll(listener.UnbindAsync(cancellationToken).AsTask(), acceptLoop); } + for (int i = connectionTransportCount; i < totalTransportCount; i++) + { + (IMultiplexedConnectionListener listener, Task acceptLoop) = _multiplexedTransports[i - connectionTransportCount]; + tasks[i] = Task.WhenAll(listener.UnbindAsync(cancellationToken).AsTask(), acceptLoop); + } + await Task.WhenAll(tasks).ConfigureAwait(false); if (!await ConnectionManager.CloseAllConnectionsAsync(cancellationToken).ConfigureAwait(false)) @@ -219,12 +241,18 @@ public async Task StopAsync(CancellationToken cancellationToken) } } - for (int i = 0; i < _transports.Count; i++) + for (int i = 0; i < connectionTransportCount; i++) { (IConnectionListener listener, Task acceptLoop) = _transports[i]; tasks[i] = listener.DisposeAsync().AsTask(); } + for (int i = connectionTransportCount; i < totalTransportCount; i++) + { + (IMultiplexedConnectionListener listener, Task acceptLoop) = _multiplexedTransports[i - connectionTransportCount]; + tasks[i] = listener.DisposeAsync().AsTask(); + } + await Task.WhenAll(tasks).ConfigureAwait(false); ServiceContext.Heartbeat?.Dispose(); diff --git a/src/Servers/Kestrel/Core/src/ListenOptions.cs b/src/Servers/Kestrel/Core/src/ListenOptions.cs index 14e483c403dd..16a8e0547775 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptions.cs @@ -15,9 +15,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// Describes either an , Unix domain socket path, or a file descriptor for an already open /// socket that Kestrel should bind to or open. /// - public class ListenOptions : IConnectionBuilder + public class ListenOptions : IConnectionBuilder, IMultiplexedConnectionBuilder { internal readonly List> _middleware = new List>(); + internal readonly List> _multiplexedMiddleware = new List>(); internal ListenOptions(IPEndPoint endPoint) { @@ -123,6 +124,12 @@ public IConnectionBuilder Use(Func middl return this; } + IMultiplexedConnectionBuilder IMultiplexedConnectionBuilder.Use(Func middleware) + { + _multiplexedMiddleware.Add(middleware); + return this; + } + public ConnectionDelegate Build() { ConnectionDelegate app = context => @@ -139,6 +146,22 @@ public ConnectionDelegate Build() return app; } + MultiplexedConnectionDelegate IMultiplexedConnectionBuilder.Build() + { + MultiplexedConnectionDelegate app = context => + { + return Task.CompletedTask; + }; + + for (int i = _multiplexedMiddleware.Count - 1; i >= 0; i--) + { + var component = _multiplexedMiddleware[i]; + app = component(app); + } + + return app; + } + internal virtual async Task BindAsync(AddressBindContext context) { await AddressBinder.BindEndpointAsync(this, context).ConfigureAwait(false); diff --git a/src/Servers/Kestrel/Core/src/Middleware/Http3ConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/Http3ConnectionMiddleware.cs new file mode 100644 index 000000000000..c330a69ca660 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Middleware/Http3ConnectionMiddleware.cs @@ -0,0 +1,44 @@ +// 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 System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal +{ + internal class Http3ConnectionMiddleware + { + private readonly ServiceContext _serviceContext; + private readonly IHttpApplication _application; + + public Http3ConnectionMiddleware(ServiceContext serviceContext, IHttpApplication application) + { + _serviceContext = serviceContext; + _application = application; + } + + public Task OnConnectionAsync(MultiplexedConnectionContext connectionContext) + { + var memoryPoolFeature = connectionContext.Features.Get(); + + var http3ConnectionContext = new Http3ConnectionContext + { + ConnectionId = connectionContext.ConnectionId, + ConnectionContext = connectionContext, + ServiceContext = _serviceContext, + ConnectionFeatures = connectionContext.Features, + MemoryPool = memoryPoolFeature.MemoryPool, + LocalEndPoint = connectionContext.LocalEndPoint as IPEndPoint, + RemoteEndPoint = connectionContext.RemoteEndPoint as IPEndPoint + }; + + var connection = new Http3Connection(http3ConnectionContext); + + return connection.ProcessRequestsAsync(_application); + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpConnectionBuilderExtensions.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpConnectionBuilderExtensions.cs index e46a2c2a831e..09c1ce476cfa 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpConnectionBuilderExtensions.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpConnectionBuilderExtensions.cs @@ -1,8 +1,6 @@ // 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 Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Connections; @@ -18,5 +16,14 @@ public static IConnectionBuilder UseHttpServer(this IConnectionBuilder return middleware.OnConnectionAsync; }); } + + public static IMultiplexedConnectionBuilder UseHttp3Server(this IMultiplexedConnectionBuilder builder, ServiceContext serviceContext, IHttpApplication application, HttpProtocols protocols) + { + var middleware = new Http3ConnectionMiddleware(serviceContext, application); + return builder.Use(next => + { + return middleware.OnConnectionAsync; + }); + } } } diff --git a/src/Servers/Kestrel/Core/test/ConnectionDispatcherTests.cs b/src/Servers/Kestrel/Core/test/ConnectionDispatcherTests.cs index 1f6b71ae0359..b9b32bb52164 100644 --- a/src/Servers/Kestrel/Core/test/ConnectionDispatcherTests.cs +++ b/src/Servers/Kestrel/Core/test/ConnectionDispatcherTests.cs @@ -29,7 +29,7 @@ public async Task OnConnectionCreatesLogScopeWithConnectionId() var connection = new Mock { CallBase = true }.Object; connection.ConnectionClosed = new CancellationToken(canceled: true); - var kestrelConnection = new KestrelConnection(0, serviceContext, _ => tcs.Task, connection, serviceContext.Log); + var kestrelConnection = new KestrelConnection(0, serviceContext, _ => tcs.Task, connection, serviceContext.Log); serviceContext.ConnectionManager.AddConnection(0, kestrelConnection); var task = kestrelConnection.ExecuteAsync(); @@ -79,9 +79,9 @@ public async Task OnConnectionFiresOnCompleted() var connection = new Mock { CallBase = true }.Object; connection.ConnectionClosed = new CancellationToken(canceled: true); - var kestrelConnection = new KestrelConnection(0, serviceContext, _ => Task.CompletedTask, connection, serviceContext.Log); + var kestrelConnection = new KestrelConnection(0, serviceContext, _ => Task.CompletedTask, connection, serviceContext.Log); serviceContext.ConnectionManager.AddConnection(0, kestrelConnection); - var completeFeature = kestrelConnection.TransportConnection.Features.Get(); + var completeFeature = kestrelConnection.GetTransport().Features.Get(); Assert.NotNull(completeFeature); object stateObject = new object(); @@ -100,9 +100,9 @@ public async Task OnConnectionOnCompletedExceptionCaught() var logger = ((TestKestrelTrace)serviceContext.Log).Logger; var connection = new Mock { CallBase = true }.Object; connection.ConnectionClosed = new CancellationToken(canceled: true); - var kestrelConnection = new KestrelConnection(0, serviceContext, _ => Task.CompletedTask, connection, serviceContext.Log); + var kestrelConnection = new KestrelConnection(0, serviceContext, _ => Task.CompletedTask, connection, serviceContext.Log); serviceContext.ConnectionManager.AddConnection(0, kestrelConnection); - var completeFeature = kestrelConnection.TransportConnection.Features.Get(); + var completeFeature = kestrelConnection.GetTransport().Features.Get(); Assert.NotNull(completeFeature); object stateObject = new object(); diff --git a/src/Servers/Kestrel/Core/test/HttpConnectionManagerTests.cs b/src/Servers/Kestrel/Core/test/HttpConnectionManagerTests.cs index 815fe5019a4e..42867902ff0a 100644 --- a/src/Servers/Kestrel/Core/test/HttpConnectionManagerTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpConnectionManagerTests.cs @@ -44,7 +44,7 @@ private void UnrootedConnectionsGetRemovedFromHeartbeatInnerScope( var serviceContext = new TestServiceContext(); var mock = new Mock() { CallBase = true }; mock.Setup(m => m.ConnectionId).Returns(connectionId); - var httpConnection = new KestrelConnection(0, serviceContext, _ => Task.CompletedTask, mock.Object, Mock.Of()); + var httpConnection = new KestrelConnection(0, serviceContext, _ => Task.CompletedTask, mock.Object, Mock.Of()); httpConnectionManager.AddConnection(0, httpConnection); diff --git a/src/Servers/Kestrel/Kestrel/test/GeneratedCodeTests.cs b/src/Servers/Kestrel/Kestrel/test/GeneratedCodeTests.cs index f3b0d3acda3e..f2b8985906bf 100644 --- a/src/Servers/Kestrel/Kestrel/test/GeneratedCodeTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/GeneratedCodeTests.cs @@ -20,12 +20,14 @@ public void GeneratedCodeIsUpToDate() var httpProtocolGeneratedPath = Path.Combine(AppContext.BaseDirectory, "shared", "GeneratedContent", "HttpProtocol.Generated.cs"); var httpUtilitiesGeneratedPath = Path.Combine(AppContext.BaseDirectory, "shared", "GeneratedContent", "HttpUtilities.Generated.cs"); var http2ConnectionGeneratedPath = Path.Combine(AppContext.BaseDirectory, "shared", "GeneratedContent", "Http2Connection.Generated.cs"); - var transportConnectionGeneratedPath = Path.Combine(AppContext.BaseDirectory, "shared", "GeneratedContent", "TransportConnection.Generated.cs"); + var transportMultiplexedConnectionGeneratedPath = Path.Combine(AppContext.BaseDirectory,"shared", "GeneratedContent", "TransportMultiplexedConnection.Generated.cs"); + var transportConnectionGeneratedPath = Path.Combine(AppContext.BaseDirectory,"shared", "GeneratedContent", "TransportConnection.Generated.cs"); var testHttpHeadersGeneratedPath = Path.GetTempFileName(); var testHttpProtocolGeneratedPath = Path.GetTempFileName(); var testHttpUtilitiesGeneratedPath = Path.GetTempFileName(); var testHttp2ConnectionGeneratedPath = Path.GetTempFileName(); + var testTransportMultiplexedConnectionGeneratedPath = Path.GetTempFileName(); var testTransportConnectionGeneratedPath = Path.GetTempFileName(); try @@ -34,20 +36,28 @@ public void GeneratedCodeIsUpToDate() var currentHttpProtocolGenerated = File.ReadAllText(httpProtocolGeneratedPath); var currentHttpUtilitiesGenerated = File.ReadAllText(httpUtilitiesGeneratedPath); var currentHttp2ConnectionGenerated = File.ReadAllText(http2ConnectionGeneratedPath); + var currentTransportConnectionBaseGenerated = File.ReadAllText(transportMultiplexedConnectionGeneratedPath); var currentTransportConnectionGenerated = File.ReadAllText(transportConnectionGeneratedPath); - CodeGenerator.Program.Run(testHttpHeadersGeneratedPath, testHttpProtocolGeneratedPath, testHttpUtilitiesGeneratedPath, testTransportConnectionGeneratedPath, testHttp2ConnectionGeneratedPath); + CodeGenerator.Program.Run(testHttpHeadersGeneratedPath, + testHttpProtocolGeneratedPath, + testHttpUtilitiesGeneratedPath, + testHttp2ConnectionGeneratedPath, + testTransportMultiplexedConnectionGeneratedPath, + testTransportConnectionGeneratedPath); var testHttpHeadersGenerated = File.ReadAllText(testHttpHeadersGeneratedPath); var testHttpProtocolGenerated = File.ReadAllText(testHttpProtocolGeneratedPath); var testHttpUtilitiesGenerated = File.ReadAllText(testHttpUtilitiesGeneratedPath); var testHttp2ConnectionGenerated = File.ReadAllText(testHttp2ConnectionGeneratedPath); + var testTransportMultiplxedConnectionGenerated = File.ReadAllText(testTransportMultiplexedConnectionGeneratedPath); var testTransportConnectionGenerated = File.ReadAllText(testTransportConnectionGeneratedPath); Assert.Equal(currentHttpHeadersGenerated, testHttpHeadersGenerated, ignoreLineEndingDifferences: true); Assert.Equal(currentHttpProtocolGenerated, testHttpProtocolGenerated, ignoreLineEndingDifferences: true); Assert.Equal(currentHttpUtilitiesGenerated, testHttpUtilitiesGenerated, ignoreLineEndingDifferences: true); Assert.Equal(currentHttp2ConnectionGenerated, testHttp2ConnectionGenerated, ignoreLineEndingDifferences: true); + Assert.Equal(currentTransportConnectionBaseGenerated, testTransportMultiplxedConnectionGenerated, ignoreLineEndingDifferences: true); Assert.Equal(currentTransportConnectionGenerated, testTransportConnectionGenerated, ignoreLineEndingDifferences: true); } finally @@ -56,6 +66,7 @@ public void GeneratedCodeIsUpToDate() File.Delete(testHttpProtocolGeneratedPath); File.Delete(testHttpUtilitiesGeneratedPath); File.Delete(testHttp2ConnectionGeneratedPath); + File.Delete(testTransportMultiplexedConnectionGeneratedPath); File.Delete(testTransportConnectionGeneratedPath); } } diff --git a/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj b/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj index 33eda7eb1b93..abf2fb6b3a1a 100644 --- a/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj +++ b/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/IQuicTrace.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/IQuicTrace.cs index c70d461430be..4bd88a52d5b0 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/IQuicTrace.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/IQuicTrace.cs @@ -15,5 +15,6 @@ internal interface IQuicTrace : ILogger void StreamPause(string streamId); void StreamResume(string streamId); void StreamShutdownWrite(string streamId, Exception ex); + void StreamAbort(string streamId, Exception ex); } } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs index 34a7e299bcb4..513d45731c9b 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs @@ -3,16 +3,16 @@ using System; using System.Net.Quic; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Connections.Abstractions.Features; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal { - internal class QuicConnectionContext : TransportConnection, IQuicStreamListenerFeature, IQuicCreateStreamFeature + internal class QuicConnectionContext : TransportMultiplexedConnection, IProtocolErrorCodeFeature { private QuicConnection _connection; private readonly QuicTransportContext _context; @@ -20,14 +20,15 @@ internal class QuicConnectionContext : TransportConnection, IQuicStreamListenerF private ValueTask _closeTask; + public long Error { get; set; } + public QuicConnectionContext(QuicConnection connection, QuicTransportContext context) { _log = context.Log; _context = context; _connection = connection; Features.Set(new FakeTlsConnectionFeature()); - Features.Set(this); - Features.Set(this); + Features.Set(this); _log.NewConnection(ConnectionId); } @@ -66,27 +67,51 @@ public override async ValueTask DisposeAsync() _connection.Dispose(); } + public override void Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application via MultiplexedConnectionContext.Abort().")); + public override void Abort(ConnectionAbortedException abortReason) { - _closeTask = _connection.CloseAsync(errorCode: _context.Options.AbortErrorCode); + _closeTask = _connection.CloseAsync(errorCode: Error); } - public async ValueTask AcceptAsync() + public override async ValueTask AcceptAsync(CancellationToken cancellationToken = default) { - var stream = await _connection.AcceptStreamAsync(); try { - // Because the stream is wrapped with a quic connection provider, - // we need to check a property to check if this is null - // Will be removed once the provider abstraction is removed. - _ = stream.CanRead; + var stream = await _connection.AcceptStreamAsync(cancellationToken); + return new QuicStreamContext(stream, this, _context); + } + catch (QuicException ex) + { + // Accept on graceful close throws an aborted exception rather than returning null. + _log.LogDebug($"Accept loop ended with exception: {ex.Message}"); + } + + return null; + } + + public override ValueTask ConnectAsync(IFeatureCollection features = null, CancellationToken cancellationToken = default) + { + QuicStream quicStream; + + if (features != null) + { + var streamDirectionFeature = features.Get(); + if (streamDirectionFeature.CanRead) + { + quicStream = _connection.OpenBidirectionalStream(); + } + else + { + quicStream = _connection.OpenUnidirectionalStream(); + } } - catch (Exception) + else { - return null; + quicStream = _connection.OpenBidirectionalStream(); } - return new QuicStreamContext(stream, this, _context); + return new ValueTask(new QuicStreamContext(quicStream, this, _context)); } } } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs index efbca9b1cb06..1b36d90ab0a7 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs @@ -9,13 +9,15 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal { /// /// Listens for new Quic Connections. /// - internal class QuicConnectionListener : IConnectionListener, IAsyncDisposable + internal class QuicConnectionListener : IMultiplexedConnectionListener, IAsyncDisposable { private readonly IQuicTrace _log; private bool _disposed; @@ -36,22 +38,18 @@ public QuicConnectionListener(QuicTransportOptions options, IQuicTrace log, EndP public EndPoint EndPoint { get; set; } - public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) + public async ValueTask AcceptAsync(IFeatureCollection features = null, CancellationToken cancellationToken = default) { - var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken); try { - // Because the stream is wrapped with a quic connection provider, - // we need to check a property to check if this is null - // Will be removed once the provider abstraction is removed. - _ = quicConnection.LocalEndPoint; + var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken); + return new QuicConnectionContext(quicConnection, _context); } - catch (Exception) + catch (QuicOperationAbortedException ex) { - return null; + _log.LogDebug($"Listener has aborted with exception: {ex.Message}"); } - - return new QuicConnectionContext(quicConnection, _context); + return null; } public async ValueTask UnbindAsync(CancellationToken cancellationToken = default) @@ -68,6 +66,7 @@ public ValueTask DisposeAsync() _disposed = true; + _listener.Close(); _listener.Dispose(); return new ValueTask(); diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs index 6097e78e834f..5b841aa097e9 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal { - internal class QuicStreamContext : TransportConnection, IQuicStreamFeature + internal class QuicStreamContext : TransportConnection, IStreamDirectionFeature, IProtocolErrorCodeFeature, IStreamIdFeature { private readonly Task _processingTask; private readonly QuicStream _stream; @@ -46,7 +46,9 @@ public QuicStreamContext(QuicStream stream, QuicConnectionContext connection, Qu var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions); - Features.Set(this); + Features.Set(this); + Features.Set(this); + Features.Set(this); // TODO populate the ITlsConnectionFeature (requires client certs). Features.Set(new FakeTlsConnectionFeature()); @@ -90,6 +92,8 @@ public override string ConnectionId } } + public long Error { get; set; } + private async Task StartAsync() { try @@ -281,10 +285,12 @@ private async Task ProcessSends() public override void Abort(ConnectionAbortedException abortReason) { // Don't call _stream.Shutdown and _stream.Abort at the same time. + _log.StreamAbort(ConnectionId, abortReason); + lock (_shutdownLock) { - _stream.AbortRead(_context.Options.AbortErrorCode); - _stream.AbortWrite(_context.Options.AbortErrorCode); + _stream.AbortRead(Error); + _stream.AbortWrite(Error); } // Cancel ProcessSends loop after calling shutdown to ensure the correct _shutdownReason gets set. diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicTrace.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicTrace.cs index 18e5d8dc6d3b..6adf0667c02d 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicTrace.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicTrace.cs @@ -21,7 +21,9 @@ internal class QuicTrace : IQuicTrace private static readonly Action _streamResume = LoggerMessage.Define(LogLevel.Debug, new EventId(7, nameof(StreamResume)), @"Stream id ""{ConnectionId}"" resumed."); private static readonly Action _streamShutdownWrite = - LoggerMessage.Define(LogLevel.Debug, new EventId(7, nameof(StreamShutdownWrite)), @"Connection id ""{ConnectionId}"" shutting down writes, exception: ""{Reason}""."); + LoggerMessage.Define(LogLevel.Debug, new EventId(7, nameof(StreamShutdownWrite)), @"Stream id ""{ConnectionId}"" shutting down writes, exception: ""{Reason}""."); + private static readonly Action _streamAborted = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, nameof(StreamShutdownWrite)), @"Stream id ""{ConnectionId}"" aborted by application, exception: ""{Reason}""."); private ILogger _logger; @@ -70,5 +72,10 @@ public void StreamShutdownWrite(string streamId, Exception ex) { _streamShutdownWrite(_logger, streamId, ex.Message, ex); } + + public void StreamAbort(string streamId, Exception ex) + { + _streamAborted(_logger, streamId, ex.Message, ex); + } } } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.csproj b/src/Servers/Kestrel/Transport.Quic/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.csproj index 19b9c387617f..22e92069b1db 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.csproj +++ b/src/Servers/Kestrel/Transport.Quic/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.csproj @@ -1,4 +1,4 @@ - + Quic transport for the ASP.NET Core Kestrel cross-platform web server. @@ -20,6 +20,9 @@ + + + diff --git a/src/Servers/Kestrel/Transport.Quic/src/QuicConnectionFactory.cs b/src/Servers/Kestrel/Transport.Quic/src/QuicConnectionFactory.cs index d2e7c1bcb337..bb67ac4e8049 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/QuicConnectionFactory.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/QuicConnectionFactory.cs @@ -9,13 +9,14 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic { - public class QuicConnectionFactory : IConnectionFactory + public class QuicConnectionFactory : IMultiplexedConnectionFactory { private QuicTransportContext _transportContext; @@ -32,7 +33,7 @@ public QuicConnectionFactory(IOptions options, ILoggerFact _transportContext = new QuicTransportContext(trace, options.Value); } - public async ValueTask ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken = default) + public async ValueTask ConnectAsync(EndPoint endPoint, IFeatureCollection features = null, CancellationToken cancellationToken = default) { if (!(endPoint is IPEndPoint ipEndPoint)) { diff --git a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs index 5962df595e65..64f59b4e16a4 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportFactory.cs @@ -6,8 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -35,10 +35,10 @@ public QuicTransportFactory(ILoggerFactory loggerFactory, IOptions BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) + public ValueTask BindAsync(EndPoint endpoint, IFeatureCollection features = null, CancellationToken cancellationToken = default) { var transport = new QuicConnectionListener(_options, _log, endpoint); - return new ValueTask(transport); + return new ValueTask(transport); } } } diff --git a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportOptions.cs b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportOptions.cs index 09cfbb73b7a3..985222d3d1b4 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportOptions.cs @@ -24,11 +24,6 @@ public class QuicTransportOptions /// public string Alpn { get; set; } - /// - /// The registration name to use in Quic. - /// - public string RegistrationName { get; set; } - /// /// The certificate that MsQuic will use. /// @@ -49,11 +44,6 @@ public class QuicTransportOptions /// public long? MaxWriteBufferSize { get; set; } = 64 * 1024; - /// - /// The error code to abort with - /// - public long AbortErrorCode { get; set; } = 0; - internal Func> MemoryPoolFactory { get; set; } = System.Buffers.SlabMemoryPoolFactory.Create; } diff --git a/src/Servers/Kestrel/Transport.Quic/src/WebHostBuilderMsQuicExtensions.cs b/src/Servers/Kestrel/Transport.Quic/src/WebHostBuilderMsQuicExtensions.cs index 25fb0b581a0e..57ac9024e3a5 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/WebHostBuilderMsQuicExtensions.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/WebHostBuilderMsQuicExtensions.cs @@ -14,7 +14,7 @@ public static IWebHostBuilder UseQuic(this IWebHostBuilder hostBuilder) { return hostBuilder.ConfigureServices(services => { - services.AddSingleton(); + services.AddSingleton(); }); } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs index 1a3f0ed601be..dc860eb8f548 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs b/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs index f989d6260752..7e37138aaf82 100644 --- a/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs +++ b/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Net; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Connections; @@ -15,47 +14,39 @@ public class Program { public static void Main(string[] args) { - var cert = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, true); + var cert = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, false); var hostBuilder = new HostBuilder() - .ConfigureLogging((_, factory) => - { - factory.SetMinimumLevel(LogLevel.Trace); - factory.AddConsole(); - }) - .ConfigureWebHost(webHost => - { - webHost.UseKestrel() - // Things like APLN and cert should be able to be passed from corefx into bedrock - .UseQuic(options => - { - options.Certificate = cert; - options.RegistrationName = "Quic"; - options.Alpn = "h3-25"; - options.IdleTimeout = TimeSpan.FromHours(1); - }) - .ConfigureKestrel((context, options) => - { - var basePort = 443; - options.EnableAltSvc = true; - options.Listen(IPAddress.Any, basePort, listenOptions => - { - listenOptions.UseHttps(httpsOptions => - { - httpsOptions.ServerCertificate = cert; - }); - }); - options.Listen(IPAddress.Any, basePort, listenOptions => - { + .ConfigureLogging((_, factory) => + { + factory.SetMinimumLevel(LogLevel.Trace); + factory.AddConsole(); + }) + .ConfigureWebHost(webHost => + { + webHost.UseKestrel() + .UseQuic(options => + { + options.Certificate = cert; // Shouldn't need this either here. + options.Alpn = "h3-25"; // Shouldn't need to populate this as well. + options.IdleTimeout = TimeSpan.FromHours(1); + }) + .ConfigureKestrel((context, options) => + { + var basePort = 5557; + options.EnableAltSvc = true; + + options.Listen(IPAddress.Any, basePort, listenOptions => + { listenOptions.UseHttps(httpsOptions => - { - httpsOptions.ServerCertificate = cert; - }); - listenOptions.Protocols = HttpProtocols.Http3; - }); - }) - .UseStartup(); - }); + { + httpsOptions.ServerCertificate = cert; + }); + listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3; + }); + }) + .UseStartup(); + }); hostBuilder.Build().Run(); } diff --git a/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs b/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs index e2b6b7b5df36..54a11c63a054 100644 --- a/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs +++ b/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs @@ -1,15 +1,12 @@ using System; using System.Buffers; using System.Net; -using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Server.Kestrel.Https; -using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Logging; namespace QuicSampleApp { @@ -35,7 +32,6 @@ public static void Main(string[] args) .UseQuic(options => { options.Certificate = null; - options.RegistrationName = "AspNetCore-MsQuic"; options.Alpn = "QuicTest"; options.IdleTimeout = TimeSpan.FromHours(1); }) @@ -46,54 +42,37 @@ public static void Main(string[] args) options.Listen(IPAddress.Any, basePort, listenOptions => { listenOptions.Protocols = HttpProtocols.Http3; - listenOptions.Use((next) => - { - return async connection => - { - var streamFeature = connection.Features.Get(); - if (streamFeature != null) - { - while (true) - { - var connectionContext = await streamFeature.AcceptAsync(); - if (connectionContext == null) - { - return; - } - _ = next(connectionContext); - } - } - else - { - await next(connection); - } - }; - }); - async Task EchoServer(ConnectionContext connection) + async Task EchoServer(MultiplexedConnectionContext connection) { // For graceful shutdown - try + + while (true) { + var stream = await connection.AcceptAsync(); while (true) { - var result = await connection.Transport.Input.ReadAsync(); + var result = await stream.Transport.Input.ReadAsync(); if (result.IsCompleted) { break; } - await connection.Transport.Output.WriteAsync(result.Buffer.ToArray()); + await stream.Transport.Output.WriteAsync(result.Buffer.ToArray()); - connection.Transport.Input.AdvanceTo(result.Buffer.End); + stream.Transport.Input.AdvanceTo(result.Buffer.End); } } - catch (OperationCanceledException) - { - } } - listenOptions.Run(EchoServer); + + ((IMultiplexedConnectionBuilder)listenOptions).Use(next => + { + return context => + { + return EchoServer(context); + }; + }); }); }) .UseStartup(); diff --git a/src/Servers/Kestrel/samples/QuicSampleClient/Program.cs b/src/Servers/Kestrel/samples/QuicSampleClient/Program.cs index 3ad16e0dd652..082d95cbfa56 100644 --- a/src/Servers/Kestrel/samples/QuicSampleClient/Program.cs +++ b/src/Servers/Kestrel/samples/QuicSampleClient/Program.cs @@ -1,12 +1,9 @@ using System; using System.Buffers; using System.Net; -using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic; -using Microsoft.AspNetCore.Connections.Abstractions.Features; using Microsoft.Extensions.Hosting; using Microsoft.AspNetCore.Connections; using Microsoft.Extensions.Logging; @@ -26,13 +23,12 @@ static async Task Main(string[] args) }) .ConfigureServices(services => { - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddOptions(); services.Configure((options) => { options.Alpn = "QuicTest"; - options.RegistrationName = "Quic-AspNetCore-client"; options.Certificate = null; options.IdleTimeout = TimeSpan.FromHours(1); }); @@ -43,9 +39,9 @@ static async Task Main(string[] args) private class QuicClientService { - private readonly IConnectionFactory _connectionFactory; + private readonly IMultiplexedConnectionFactory _connectionFactory; private readonly ILogger _logger; - public QuicClientService(IConnectionFactory connectionFactory, ILogger logger) + public QuicClientService(IMultiplexedConnectionFactory connectionFactory, ILogger logger) { _connectionFactory = connectionFactory; _logger = logger; @@ -55,8 +51,7 @@ public async Task RunAsync() { Console.WriteLine("Starting"); var connectionContext = await _connectionFactory.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 5555)); - var createStreamFeature = connectionContext.Features.Get(); - var streamContext = await createStreamFeature.StartBidirectionalStreamAsync(); + var streamContext = await connectionContext.ConnectAsync(); Console.CancelKeyPress += new ConsoleCancelEventHandler((sender, args) => { diff --git a/src/Servers/Kestrel/shared/TransportMultiplexedConnection.FeatureCollection.cs b/src/Servers/Kestrel/shared/TransportMultiplexedConnection.FeatureCollection.cs new file mode 100644 index 000000000000..fae4129d25c5 --- /dev/null +++ b/src/Servers/Kestrel/shared/TransportMultiplexedConnection.FeatureCollection.cs @@ -0,0 +1,37 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.IO.Pipelines; +using System.Threading; +using Microsoft.AspNetCore.Connections.Features; + +namespace Microsoft.AspNetCore.Connections +{ + internal partial class TransportMultiplexedConnection : IConnectionIdFeature, + IConnectionItemsFeature, + IMemoryPoolFeature, + IConnectionLifetimeFeature + { + // NOTE: When feature interfaces are added to or removed from this TransportConnection class implementation, + // then the list of `features` in the generated code project MUST also be updated. + // See also: tools/CodeGenerator/TransportConnectionFeatureCollection.cs + + MemoryPool IMemoryPoolFeature.MemoryPool => MemoryPool; + + IDictionary IConnectionItemsFeature.Items + { + get => Items; + set => Items = value; + } + + CancellationToken IConnectionLifetimeFeature.ConnectionClosed + { + get => ConnectionClosed; + set => ConnectionClosed = value; + } + + void IConnectionLifetimeFeature.Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application via IConnectionLifetimeFeature.Abort().")); + } +} diff --git a/src/Servers/Kestrel/shared/TransportMultiplexedConnection.Generated.cs b/src/Servers/Kestrel/shared/TransportMultiplexedConnection.Generated.cs new file mode 100644 index 000000000000..e7df1de198e7 --- /dev/null +++ b/src/Servers/Kestrel/shared/TransportMultiplexedConnection.Generated.cs @@ -0,0 +1,242 @@ +// 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; +using System.Collections.Generic; + +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Connections +{ + internal partial class TransportMultiplexedConnection : IFeatureCollection + { + private object _currentIConnectionIdFeature; + private object _currentIConnectionTransportFeature; + private object _currentIConnectionItemsFeature; + private object _currentIMemoryPoolFeature; + private object _currentIConnectionLifetimeFeature; + + private int _featureRevision; + + private List> MaybeExtra; + + private void FastReset() + { + _currentIConnectionIdFeature = this; + _currentIConnectionTransportFeature = this; + _currentIConnectionItemsFeature = this; + _currentIMemoryPoolFeature = this; + _currentIConnectionLifetimeFeature = this; + + } + + // Internal for testing + internal void ResetFeatureCollection() + { + FastReset(); + MaybeExtra?.Clear(); + _featureRevision++; + } + + private object ExtraFeatureGet(Type key) + { + if (MaybeExtra == null) + { + return null; + } + for (var i = 0; i < MaybeExtra.Count; i++) + { + var kv = MaybeExtra[i]; + if (kv.Key == key) + { + return kv.Value; + } + } + return null; + } + + private void ExtraFeatureSet(Type key, object value) + { + if (MaybeExtra == null) + { + MaybeExtra = new List>(2); + } + + for (var i = 0; i < MaybeExtra.Count; i++) + { + if (MaybeExtra[i].Key == key) + { + MaybeExtra[i] = new KeyValuePair(key, value); + return; + } + } + MaybeExtra.Add(new KeyValuePair(key, value)); + } + + bool IFeatureCollection.IsReadOnly => false; + + int IFeatureCollection.Revision => _featureRevision; + + object IFeatureCollection.this[Type key] + { + get + { + object feature = null; + if (key == typeof(IConnectionIdFeature)) + { + feature = _currentIConnectionIdFeature; + } + else if (key == typeof(IConnectionTransportFeature)) + { + feature = _currentIConnectionTransportFeature; + } + else if (key == typeof(IConnectionItemsFeature)) + { + feature = _currentIConnectionItemsFeature; + } + else if (key == typeof(IMemoryPoolFeature)) + { + feature = _currentIMemoryPoolFeature; + } + else if (key == typeof(IConnectionLifetimeFeature)) + { + feature = _currentIConnectionLifetimeFeature; + } + else if (MaybeExtra != null) + { + feature = ExtraFeatureGet(key); + } + + return feature; + } + + set + { + _featureRevision++; + + if (key == typeof(IConnectionIdFeature)) + { + _currentIConnectionIdFeature = value; + } + else if (key == typeof(IConnectionTransportFeature)) + { + _currentIConnectionTransportFeature = value; + } + else if (key == typeof(IConnectionItemsFeature)) + { + _currentIConnectionItemsFeature = value; + } + else if (key == typeof(IMemoryPoolFeature)) + { + _currentIMemoryPoolFeature = value; + } + else if (key == typeof(IConnectionLifetimeFeature)) + { + _currentIConnectionLifetimeFeature = value; + } + else + { + ExtraFeatureSet(key, value); + } + } + } + + TFeature IFeatureCollection.Get() + { + TFeature feature = default; + if (typeof(TFeature) == typeof(IConnectionIdFeature)) + { + feature = (TFeature)_currentIConnectionIdFeature; + } + else if (typeof(TFeature) == typeof(IConnectionTransportFeature)) + { + feature = (TFeature)_currentIConnectionTransportFeature; + } + else if (typeof(TFeature) == typeof(IConnectionItemsFeature)) + { + feature = (TFeature)_currentIConnectionItemsFeature; + } + else if (typeof(TFeature) == typeof(IMemoryPoolFeature)) + { + feature = (TFeature)_currentIMemoryPoolFeature; + } + else if (typeof(TFeature) == typeof(IConnectionLifetimeFeature)) + { + feature = (TFeature)_currentIConnectionLifetimeFeature; + } + else if (MaybeExtra != null) + { + feature = (TFeature)(ExtraFeatureGet(typeof(TFeature))); + } + + return feature; + } + + void IFeatureCollection.Set(TFeature feature) + { + _featureRevision++; + if (typeof(TFeature) == typeof(IConnectionIdFeature)) + { + _currentIConnectionIdFeature = feature; + } + else if (typeof(TFeature) == typeof(IConnectionTransportFeature)) + { + _currentIConnectionTransportFeature = feature; + } + else if (typeof(TFeature) == typeof(IConnectionItemsFeature)) + { + _currentIConnectionItemsFeature = feature; + } + else if (typeof(TFeature) == typeof(IMemoryPoolFeature)) + { + _currentIMemoryPoolFeature = feature; + } + else if (typeof(TFeature) == typeof(IConnectionLifetimeFeature)) + { + _currentIConnectionLifetimeFeature = feature; + } + else + { + ExtraFeatureSet(typeof(TFeature), feature); + } + } + + private IEnumerable> FastEnumerable() + { + if (_currentIConnectionIdFeature != null) + { + yield return new KeyValuePair(typeof(IConnectionIdFeature), _currentIConnectionIdFeature); + } + if (_currentIConnectionTransportFeature != null) + { + yield return new KeyValuePair(typeof(IConnectionTransportFeature), _currentIConnectionTransportFeature); + } + if (_currentIConnectionItemsFeature != null) + { + yield return new KeyValuePair(typeof(IConnectionItemsFeature), _currentIConnectionItemsFeature); + } + if (_currentIMemoryPoolFeature != null) + { + yield return new KeyValuePair(typeof(IMemoryPoolFeature), _currentIMemoryPoolFeature); + } + if (_currentIConnectionLifetimeFeature != null) + { + yield return new KeyValuePair(typeof(IConnectionLifetimeFeature), _currentIConnectionLifetimeFeature); + } + + if (MaybeExtra != null) + { + foreach (var item in MaybeExtra) + { + yield return item; + } + } + } + + IEnumerator> IEnumerable>.GetEnumerator() => FastEnumerable().GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => FastEnumerable().GetEnumerator(); + } +} diff --git a/src/Servers/Kestrel/shared/TransportMultiplexedConnection.cs b/src/Servers/Kestrel/shared/TransportMultiplexedConnection.cs new file mode 100644 index 000000000000..7dea0569b9f7 --- /dev/null +++ b/src/Servers/Kestrel/shared/TransportMultiplexedConnection.cs @@ -0,0 +1,75 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.IO.Pipelines; +using System.Net; +using System.Threading; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Connections +{ + internal abstract partial class TransportMultiplexedConnection : MultiplexedConnectionContext + { + private IDictionary _items; + private string _connectionId; + + public TransportMultiplexedConnection() + { + FastReset(); + } + + public override EndPoint LocalEndPoint { get; set; } + public override EndPoint RemoteEndPoint { get; set; } + + public override string ConnectionId + { + get + { + if (_connectionId == null) + { + _connectionId = CorrelationIdGenerator.GetNextId(); + } + + return _connectionId; + } + set + { + _connectionId = value; + } + } + + public override IFeatureCollection Features => this; + + public virtual MemoryPool MemoryPool { get; } + + public IDuplexPipe Application { get; set; } + + public override IDictionary Items + { + get + { + // Lazily allocate connection metadata + return _items ?? (_items = new ConnectionItems()); + } + set + { + _items = value; + } + } + + public override CancellationToken ConnectionClosed { get; set; } + + // DO NOT remove this override to ConnectionContext.Abort. Doing so would cause + // any TransportConnection that does not override Abort or calls base.Abort + // to stack overflow when IConnectionLifetimeFeature.Abort() is called. + // That said, all derived types should override this method should override + // this implementation of Abort because canceling pending output reads is not + // sufficient to abort the connection if there is backpressure. + public override void Abort(ConnectionAbortedException abortReason) + { + Application.Input.CancelPendingRead(); + } + } +} diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs index 330f9a08b7e1..860caa46c08b 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; using Microsoft.Net.Http.Headers; using Xunit; @@ -24,13 +26,68 @@ public async Task HelloWorldTest() var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); var doneWithHeaders = await requestStream.SendHeadersAsync(headers); - await requestStream.SendDataAsync(Encoding.ASCII.GetBytes("Hello world")); + await requestStream.SendDataAsync(Encoding.ASCII.GetBytes("Hello world"), endStream: true); var responseHeaders = await requestStream.ExpectHeadersAsync(); var responseData = await requestStream.ExpectDataAsync(); Assert.Equal("Hello world", Encoding.ASCII.GetString(responseData.ToArray())); } + [Fact] + public async Task EmptyMethod_Reset() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, ""), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); + var doneWithHeaders = await requestStream.SendHeadersAsync(headers); + await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.FormatHttp3ErrorMethodInvalid("")); + } + + [Fact] + public async Task InvalidCustomMethod_Reset() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Hello,World"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); + var doneWithHeaders = await requestStream.SendHeadersAsync(headers); + await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.FormatHttp3ErrorMethodInvalid("Hello,World")); + } + + [Fact] + public async Task CustomMethod_Accepted() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_echoMethod); + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(4, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("Custom", responseHeaders["Method"]); + Assert.Equal("0", responseHeaders["content-length"]); + } + [Fact] public async Task RequestHeadersMaxRequestHeaderFieldSize_EndsStream() { @@ -51,5 +108,498 @@ public async Task RequestHeadersMaxRequestHeaderFieldSize_EndsStream() // TODO figure out how to test errors for request streams that would be set on the Quic Stream. await requestStream.ExpectReceiveEndOfStream(); } + + [Fact] + public async Task ConnectMethod_Accepted() + { + var requestStream = await InitializeConnectionAndStreamsAsync(_echoMethod); + + // :path and :scheme are not allowed, :authority is optional + var headers = new[] { new KeyValuePair(HeaderNames.Method, "CONNECT") }; + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(4, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("CONNECT", responseHeaders["Method"]); + Assert.Equal("0", responseHeaders["content-length"]); + } + + [Fact] + public async Task OptionsStar_LeftOutOfPath() + { + var requestStream = await InitializeConnectionAndStreamsAsync(_echoPath); + var headers = new[] { new KeyValuePair(HeaderNames.Method, "OPTIONS"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Path, "*")}; + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(5, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("", responseHeaders["path"]); + Assert.Equal("*", responseHeaders["rawtarget"]); + Assert.Equal("0", responseHeaders["content-length"]); + } + + [Fact] + public async Task OptionsSlash_Accepted() + { + var requestStream = await InitializeConnectionAndStreamsAsync(_echoPath); + + var headers = new[] { new KeyValuePair(HeaderNames.Method, "OPTIONS"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Path, "/")}; + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(5, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("/", responseHeaders["path"]); + Assert.Equal("/", responseHeaders["rawtarget"]); + Assert.Equal("0", responseHeaders["content-length"]); + } + + [Fact] + public async Task PathAndQuery_Separated() + { + var requestStream = await InitializeConnectionAndStreamsAsync(context => + { + context.Response.Headers["path"] = context.Request.Path.Value; + context.Response.Headers["query"] = context.Request.QueryString.Value; + context.Response.Headers["rawtarget"] = context.Features.Get().RawTarget; + return Task.CompletedTask; + }); + + // :path and :scheme are not allowed, :authority is optional + var headers = new[] { new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Path, "/a/path?a&que%35ry")}; + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(6, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("/a/path", responseHeaders["path"]); + Assert.Equal("?a&que%35ry", responseHeaders["query"]); + Assert.Equal("/a/path?a&que%35ry", responseHeaders["rawtarget"]); + Assert.Equal("0", responseHeaders["content-length"]); + } + + [Theory] + [InlineData("/", "/")] + [InlineData("/a%5E", "/a^")] + [InlineData("/a%E2%82%AC", "/a€")] + [InlineData("/a%2Fb", "/a%2Fb")] // Forward slash, not decoded + [InlineData("/a%b", "/a%b")] // Incomplete encoding, not decoded + [InlineData("/a/b/c/../d", "/a/b/d")] // Navigation processed + [InlineData("/a/b/c/../../../../d", "/d")] // Navigation escape prevented + [InlineData("/a/b/c/.%2E/d", "/a/b/d")] // Decode before navigation processing + public async Task Path_DecodedAndNormalized(string input, string expected) + { + var requestStream = await InitializeConnectionAndStreamsAsync(context => + { + Assert.Equal(expected, context.Request.Path.Value); + Assert.Equal(input, context.Features.Get().RawTarget); + return Task.CompletedTask; + }); + + // :path and :scheme are not allowed, :authority is optional + var headers = new[] { new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Path, input)}; + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(3, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("0", responseHeaders["content-length"]); + } + + [Theory] + [InlineData(":path", "/")] + [InlineData(":scheme", "http")] + public async Task ConnectMethod_WithSchemeOrPath_Reset(string headerName, string value) + { + var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + + // :path and :scheme are not allowed, :authority is optional + var headers = new[] { new KeyValuePair(HeaderNames.Method, "CONNECT"), + new KeyValuePair(headerName, value) }; + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.Http3ErrorConnectMustNotSendSchemeOrPath); + } + + [Fact] + public async Task SchemeMismatch_Reset() + { + var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + + // :path and :scheme are not allowed, :authority is optional + var headers = new[] { new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "https") }; // Not the expected "http" + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.FormatHttp3StreamErrorSchemeMismatch("https", "http")); + } + + [Fact] + public async Task MissingAuthority_200Status() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + }; + await InitializeConnectionAsync(_noopApplication); + + var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(3, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("0", responseHeaders["content-length"]); + } + + [Fact] + public async Task EmptyAuthority_200Status() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, ""), + }; + var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(3, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("0", responseHeaders["content-length"]); + } + + [Fact] + public async Task MissingAuthorityFallsBackToHost_200Status() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair("Host", "abc"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_echoHost); + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(4, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("0", responseHeaders[HeaderNames.ContentLength]); + Assert.Equal("abc", responseHeaders[HeaderNames.Host]); + } + + [Fact] + public async Task EmptyAuthorityIgnoredOverHost_200Status() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, ""), + new KeyValuePair("Host", "abc"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_echoHost); + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(4, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("0", responseHeaders[HeaderNames.ContentLength]); + Assert.Equal("abc", responseHeaders[HeaderNames.Host]); + } + + [Fact] + public async Task AuthorityOverridesHost_200Status() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "def"), + new KeyValuePair("Host", "abc"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_echoHost); + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(4, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("0", responseHeaders[HeaderNames.ContentLength]); + Assert.Equal("def", responseHeaders[HeaderNames.Host]); + } + + [Fact] + public async Task AuthorityOverridesInvalidHost_200Status() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "def"), + new KeyValuePair("Host", "a=bc"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_echoHost); + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(4, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("0", responseHeaders[HeaderNames.ContentLength]); + Assert.Equal("def", responseHeaders[HeaderNames.Host]); + } + + [Fact] + public async Task InvalidAuthority_Reset() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "local=host:80"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, + CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("local=host:80")); + } + + [Fact] + public async Task InvalidAuthorityWithValidHost_Reset() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "d=ef"), + new KeyValuePair("Host", "abc"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, + CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("d=ef")); + } + + [Fact] + public async Task TwoHosts_StreamReset() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair("Host", "host1"), + new KeyValuePair("Host", "host2"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, + CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("host1,host2")); + } + + [Fact] + public async Task MaxRequestLineSize_Reset() + { + // Default 8kb limit + // This test has to work around the HPack parser limit for incoming field sizes over 4kb. That's going to be a problem for people with long urls. + // https://github.com/aspnet/KestrelHttpServer/issues/2872 + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "GET" + new string('a', 1024 * 3)), + new KeyValuePair(HeaderNames.Path, "/Hello/How/Are/You/" + new string('a', 1024 * 3)), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost" + new string('a', 1024 * 3) + ":80"), + }; + var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: true); + + await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, + CoreStrings.BadRequest_RequestLineTooLong); + } + + [Fact] + public async Task ContentLength_Received_SingleDataFrame_Verified() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "POST"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.ContentLength, "12"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(async context => + { + var buffer = new byte[100]; + var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length); + Assert.Equal(12, read); + read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length); + Assert.Equal(0, read); + }); + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: false); + await requestStream.SendDataAsync(new byte[12], endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(3, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("0", responseHeaders[HeaderNames.ContentLength]); + } + + [Fact] + public async Task ContentLength_Received_MultipleDataFrame_Verified() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "POST"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.ContentLength, "12"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(async context => + { + var buffer = new byte[100]; + var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length); + var total = read; + while (read > 0) + { + read = await context.Request.Body.ReadAsync(buffer, total, buffer.Length - total); + total += read; + } + Assert.Equal(12, total); + }); + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: false); + + await requestStream.SendDataAsync(new byte[1], endStream: false); + await requestStream.SendDataAsync(new byte[3], endStream: false); + await requestStream.SendDataAsync(new byte[8], endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(3, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("0", responseHeaders[HeaderNames.ContentLength]); + } + + [Fact] + public async Task ContentLength_Received_MultipleDataFrame_ReadViaPipe_Verified() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "POST"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.ContentLength, "12"), + }; + var requestStream = await InitializeConnectionAndStreamsAsync(async context => + { + var readResult = await context.Request.BodyReader.ReadAsync(); + while (!readResult.IsCompleted) + { + context.Request.BodyReader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); + readResult = await context.Request.BodyReader.ReadAsync(); + } + + Assert.Equal(12, readResult.Buffer.Length); + context.Request.BodyReader.AdvanceTo(readResult.Buffer.End); + }); + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers, endStream: false); + + await requestStream.SendDataAsync(new byte[1], endStream: false); + await requestStream.SendDataAsync(new byte[3], endStream: false); + await requestStream.SendDataAsync(new byte[8], endStream: true); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + + Assert.Equal(3, responseHeaders.Count); + Assert.Contains("date", responseHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", responseHeaders[HeaderNames.Status]); + Assert.Equal("0", responseHeaders[HeaderNames.ContentLength]); + } + + [Fact(Skip = "Http3OutputProducer.Complete is called before input recognizes there is an error. Why is this different than HTTP/2?")] + public async Task ContentLength_Received_NoDataFrames_Reset() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "POST"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.ContentLength, "12"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + + await requestStream.SendHeadersAsync(headers, endStream: true); + + await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.Http3StreamErrorLessDataThanLength); + } } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs index dd7133e0d43e..f767fc08718c 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs @@ -6,50 +6,55 @@ using System.Net.Http; using System.Net.Http.QPack; using System.Reflection; +using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Connections.Abstractions.Features; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; using Moq; using Xunit; using Xunit.Abstractions; +using static System.IO.Pipelines.DuplexPipe; using static Microsoft.AspNetCore.Server.Kestrel.Core.Tests.Http2TestBase; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { - public class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable, IQuicCreateStreamFeature, IQuicStreamListenerFeature + public class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable { internal TestServiceContext _serviceContext; internal Http3Connection _connection; internal readonly TimeoutControl _timeoutControl; internal readonly Mock _mockKestrelTrace = new Mock(); - protected readonly Mock _mockConnectionContext = new Mock(); internal readonly Mock _mockTimeoutHandler = new Mock(); internal readonly Mock _mockTimeoutControl; internal readonly MemoryPool _memoryPool = SlabMemoryPoolFactory.Create(); protected Task _connectionTask; - protected readonly RequestDelegate _echoApplication; + private TestMultiplexedConnectionContext _multiplexedContext; + private readonly CancellationTokenSource _connectionClosingCts = new CancellationTokenSource(); - private readonly Channel _acceptConnectionQueue = Channel.CreateUnbounded(new UnboundedChannelOptions - { - SingleReader = true, - SingleWriter = true - }); + protected readonly RequestDelegate _noopApplication; + protected readonly RequestDelegate _echoApplication; + protected readonly RequestDelegate _echoMethod; + protected readonly RequestDelegate _echoPath; + protected readonly RequestDelegate _echoHost; public Http3TestBase() { _timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object); _mockTimeoutControl = new Mock(_timeoutControl) { CallBase = true }; _timeoutControl.Debugger = Mock.Of(); + + _noopApplication = context => Task.CompletedTask; + _echoApplication = async context => { var buffer = new byte[Http3PeerSettings.MinAllowedMaxFrameSize]; @@ -60,6 +65,28 @@ public Http3TestBase() await context.Response.Body.WriteAsync(buffer, 0, received); } }; + + _echoMethod = context => + { + context.Response.Headers["Method"] = context.Request.Method; + + return Task.CompletedTask; + }; + + _echoPath = context => + { + context.Response.Headers["path"] = context.Request.Path.ToString(); + context.Response.Headers["rawtarget"] = context.Features.Get().RawTarget; + + return Task.CompletedTask; + }; + + _echoHost = context => + { + context.Response.Headers[HeaderNames.Host] = context.Request.Headers[HeaderNames.Host]; + + return Task.CompletedTask; + }; } public override void Initialize(TestContext context, MethodInfo methodInfo, object[] testMethodArguments, ITestOutputHelper testOutputHelper) @@ -79,7 +106,8 @@ protected async Task InitializeConnectionAsync(RequestDelegate application) CreateConnection(); } - _connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(application)); + // Skip all heartbeat and lifetime notification feature registrations. + _connectionTask = _connection.InnerProcessRequestsAsync(new DummyApplication(application)); await Task.CompletedTask; } @@ -100,24 +128,21 @@ protected void CreateConnection() var limits = _serviceContext.ServerOptions.Limits; var features = new FeatureCollection(); - features.Set(this); - features.Set(this); - var httpConnectionContext = new HttpConnectionContext + _multiplexedContext = new TestMultiplexedConnectionContext(this); + + var httpConnectionContext = new Http3ConnectionContext { - ConnectionContext = _mockConnectionContext.Object, + ConnectionContext = _multiplexedContext, ConnectionFeatures = features, ServiceContext = _serviceContext, MemoryPool = _memoryPool, - Transport = null, // Make sure it's null TimeoutControl = _mockTimeoutControl.Object }; _connection = new Http3Connection(httpConnectionContext); - var httpConnection = new HttpConnection(httpConnectionContext); - httpConnection.Initialize(_connection); _mockTimeoutHandler.Setup(h => h.OnTimeout(It.IsAny())) - .Callback(r => httpConnection.OnTimeout(r)); + .Callback(r => _connection.OnTimeout(r)); } private static PipeOptions GetInputPipeOptions(ServiceContext serviceContext, MemoryPool memoryPool, PipeScheduler writerScheduler) => new PipeOptions @@ -155,30 +180,10 @@ private static long GetOutputResponseBufferSize(ServiceContext serviceContext) return bufferSize ?? 0; } - public ValueTask StartUnidirectionalStreamAsync() - { - var stream = new Http3ControlStream(this, _connection); - // TODO put these somewhere to be read. - return new ValueTask(stream.ConnectionContext); - } - - public async ValueTask AcceptAsync() - { - while (await _acceptConnectionQueue.Reader.WaitToReadAsync()) - { - while (_acceptConnectionQueue.Reader.TryRead(out var connection)) - { - return connection; - } - } - - return null; - } - internal async ValueTask CreateControlStream(int id) { - var stream = new Http3ControlStream(this, _connection); - _acceptConnectionQueue.Writer.TryWrite(stream.ConnectionContext); + var stream = new Http3ControlStream(this); + _multiplexedContext.AcceptQueue.Writer.TryWrite(stream.StreamContext); await stream.WriteStreamIdAsync(id); return stream; } @@ -186,7 +191,7 @@ internal async ValueTask CreateControlStream(int id) internal ValueTask CreateRequestStream() { var stream = new Http3RequestStream(this, _connection); - _acceptConnectionQueue.Writer.TryWrite(stream.ConnectionContext); + _multiplexedContext.AcceptQueue.Writer.TryWrite(stream.StreamContext); return new ValueTask(stream); } @@ -194,7 +199,7 @@ public ValueTask StartBidirectionalStreamAsync() { var stream = new Http3RequestStream(this, _connection); // TODO put these somewhere to be read. - return new ValueTask(stream.ConnectionContext); + return new ValueTask(stream.StreamContext); } internal class Http3StreamBase @@ -216,15 +221,17 @@ protected static async Task FlushAsync(PipeWriter writableBuffer) } } - internal class Http3RequestStream : Http3StreamBase, IHttpHeadersHandler, IQuicStreamFeature + internal class Http3RequestStream : Http3StreamBase, IHttpHeadersHandler, IProtocolErrorCodeFeature { - internal ConnectionContext ConnectionContext { get; } + internal ConnectionContext StreamContext { get; } public bool CanRead => true; public bool CanWrite => true; public long StreamId => 0; + public long Error { get; set; } + private readonly byte[] _headerEncodingBuffer = new byte[Http3PeerSettings.MinAllowedMaxFrameSize]; private QPackEncoder _qpackEncoder = new QPackEncoder(); private QPackDecoder _qpackDecoder = new QPackDecoder(8192); @@ -239,13 +246,11 @@ public Http3RequestStream(Http3TestBase testBase, Http3Connection connection) var outputPipeOptions = GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); - - ConnectionContext = new DefaultConnectionContext(); - ConnectionContext.Transport = _pair.Transport; - ConnectionContext.Features.Set(this); + + StreamContext = new TestStreamContext(canRead: true, canWrite: true, _pair, this); } - public async Task SendHeadersAsync(IEnumerable> headers) + public async Task SendHeadersAsync(IEnumerable> headers, bool endStream = false) { var outputWriter = _pair.Application.Output; var frame = new Http3RawFrame(); @@ -256,10 +261,16 @@ public async Task SendHeadersAsync(IEnumerable data) + internal async Task SendDataAsync(Memory data, bool endStream = false) { var outputWriter = _pair.Application.Output; var frame = new Http3RawFrame(); @@ -267,9 +278,14 @@ internal async Task SendDataAsync(Memory data) frame.Length = data.Length; Http3FrameWriter.WriteHeader(frame, outputWriter); await SendAsync(data.Span); + + if (endStream) + { + await _pair.Application.Output.CompleteAsync(); + } } - internal async Task>> ExpectHeadersAsync() + internal async Task> ExpectHeadersAsync() { var http3WithPayload = await ReceiveFrameAsync(); _qpackDecoder.Decode(http3WithPayload.PayloadSequence, this); @@ -347,6 +363,20 @@ public void OnStaticIndexedHeader(int index, ReadOnlySpan value) { _decodedHeaders[((Span)H3StaticTable.Instance[index].Name).GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters(); } + + internal async Task WaitForStreamErrorAsync(Http3ErrorCode protocolError, string expectedErrorMessage) + { + var readResult = await _pair.Application.Input.ReadAsync(); + _testBase.Logger.LogTrace("Input is completed"); + + Assert.True(readResult.IsCompleted); + Assert.Equal((long)protocolError, Error); + + if (expectedErrorMessage != null) + { + Assert.Contains(_testBase.TestApplicationErrorLogger.Messages, m => m.Exception?.Message.Contains(expectedErrorMessage) ?? false); + } + } } internal class Http3FrameWithPayload : Http3RawFrame @@ -362,28 +392,24 @@ public Http3FrameWithPayload() : base() } - internal class Http3ControlStream : Http3StreamBase, IQuicStreamFeature + internal class Http3ControlStream : Http3StreamBase, IProtocolErrorCodeFeature { - internal ConnectionContext ConnectionContext { get; } + internal ConnectionContext StreamContext { get; } public bool CanRead => true; public bool CanWrite => false; - // TODO public long StreamId => 0; - public Http3ControlStream(Http3TestBase testBase, Http3Connection connection) + public long Error { get; set; } + + public Http3ControlStream(Http3TestBase testBase) { _testBase = testBase; - _connection = connection; var inputPipeOptions = GetInputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); var outputPipeOptions = GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); - _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); - - ConnectionContext = new DefaultConnectionContext(); - ConnectionContext.Transport = _pair.Transport; - ConnectionContext.Features.Set(this); + StreamContext = new TestStreamContext(canRead: false, canWrite: true, _pair, this); } public async Task WriteStreamIdAsync(int id) @@ -402,5 +428,100 @@ void WriteSpan(PipeWriter pw) await FlushAsync(writableBuffer); } } + + private class TestMultiplexedConnectionContext : MultiplexedConnectionContext + { + public readonly Channel AcceptQueue = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = true + }); + + private readonly Http3TestBase _testBase; + + public TestMultiplexedConnectionContext(Http3TestBase testBase) + { + _testBase = testBase; + } + + public override string ConnectionId { get; set; } + + public override IFeatureCollection Features { get; } + + public override IDictionary Items { get; set; } + + public override void Abort() + { + } + + public override void Abort(ConnectionAbortedException abortReason) + { + } + + public override async ValueTask AcceptAsync(CancellationToken cancellationToken = default) + { + while (await AcceptQueue.Reader.WaitToReadAsync()) + { + while (AcceptQueue.Reader.TryRead(out var connection)) + { + return connection; + } + } + + return null; + } + + public override ValueTask ConnectAsync(IFeatureCollection features = null, CancellationToken cancellationToken = default) + { + var stream = new Http3ControlStream(_testBase); + // TODO put these somewhere to be read. + return new ValueTask(stream.StreamContext); + } + } + + private class TestStreamContext : ConnectionContext, IStreamDirectionFeature, IStreamIdFeature + { + private DuplexPipePair _pair; + public TestStreamContext(bool canRead, bool canWrite, DuplexPipePair pair, IProtocolErrorCodeFeature feature) + { + _pair = pair; + Features = new FeatureCollection(); + Features.Set(this); + Features.Set(this); + Features.Set(feature); + + CanRead = canRead; + CanWrite = canWrite; + } + + public override string ConnectionId { get; set; } + + public long StreamId { get; } + + public override IFeatureCollection Features { get; } + + public override IDictionary Items { get; set; } + + public override IDuplexPipe Transport + { + get + { + return _pair.Transport; + } + set + { + throw new NotImplementedException(); + } + } + + public bool CanRead { get; } + + public bool CanWrite { get; } + + public override void Abort(ConnectionAbortedException abortReason) + { + _pair.Application.Output.Complete(abortReason); + } + } } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj index b4442f73ba1a..cad32b08bd5f 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -27,4 +27,4 @@ - + \ No newline at end of file diff --git a/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj b/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj index 7337ccfc0cb5..b79bf13aa714 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj +++ b/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj @@ -18,7 +18,7 @@ $(MSBuildThisFileDirectory)..\..\ - Core/src/Internal/Http/HttpHeaders.Generated.cs Core/src/Internal/Http/HttpProtocol.Generated.cs Core/src/Internal/Infrastructure/HttpUtilities.Generated.cs shared/TransportConnection.Generated.cs Core/src/Internal/Http2/Http2Connection.Generated.cs + Core/src/Internal/Http/HttpHeaders.Generated.cs Core/src/Internal/Http/HttpProtocol.Generated.cs Core/src/Internal/Infrastructure/HttpUtilities.Generated.cs Core/src/Internal/Http2/Http2Connection.Generated.cs shared/TransportMultiplexedConnection.Generated.cs shared/TransportConnection.Generated.cs diff --git a/src/Servers/Kestrel/tools/CodeGenerator/Program.cs b/src/Servers/Kestrel/tools/CodeGenerator/Program.cs index c319d9bfb346..48b1fa605f0e 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/Program.cs +++ b/src/Servers/Kestrel/tools/CodeGenerator/Program.cs @@ -27,16 +27,21 @@ public static int Main(string[] args) } else if (args.Length < 4) { - Console.Error.WriteLine("Missing path to TransportConnection.Generated.cs"); + Console.Error.WriteLine("Missing path to Http2Connection.Generated.cs"); return 1; } else if (args.Length < 5) { - Console.Error.WriteLine("Missing path to Http2Connection.Generated.cs"); + Console.Error.WriteLine("Missing path to TransportMultiplexedConnection.Generated.cs"); + return 1; + } + else if (args.Length < 6) + { + Console.Error.WriteLine("Missing path to TransportConnection.Generated.cs"); return 1; } - Run(args[0], args[1], args[2], args[3], args[4]); + Run(args[0], args[1], args[2], args[3], args[4], args[5]); return 0; } @@ -45,43 +50,37 @@ public static void Run( string knownHeadersPath, string httpProtocolFeatureCollectionPath, string httpUtilitiesPath, - string transportConnectionFeatureCollectionPath, - string http2ConnectionPath) + string http2ConnectionPath, + string transportMultiplexedConnectionFeatureCollectionPath, + string transportConnectionFeatureCollectionPath) { var knownHeadersContent = KnownHeaders.GeneratedFile(); var httpProtocolFeatureCollectionContent = HttpProtocolFeatureCollection.GenerateFile(); var httpUtilitiesContent = HttpUtilities.HttpUtilities.GeneratedFile(); + var transportMultiplexedConnectionFeatureCollectionContent = TransportMultiplexedConnectionFeatureCollection.GenerateFile(); var transportConnectionFeatureCollectionContent = TransportConnectionFeatureCollection.GenerateFile(); var http2ConnectionContent = Http2Connection.GenerateFile(); - var existingKnownHeaders = File.Exists(knownHeadersPath) ? File.ReadAllText(knownHeadersPath) : ""; - if (!string.Equals(knownHeadersContent, existingKnownHeaders)) - { - File.WriteAllText(knownHeadersPath, knownHeadersContent); - } - - var existingHttpProtocolFeatureCollection = File.Exists(httpProtocolFeatureCollectionPath) ? File.ReadAllText(httpProtocolFeatureCollectionPath) : ""; - if (!string.Equals(httpProtocolFeatureCollectionContent, existingHttpProtocolFeatureCollection)) - { - File.WriteAllText(httpProtocolFeatureCollectionPath, httpProtocolFeatureCollectionContent); - } - - var existingHttpUtilities = File.Exists(httpUtilitiesPath) ? File.ReadAllText(httpUtilitiesPath) : ""; - if (!string.Equals(httpUtilitiesContent, existingHttpUtilities)) - { - File.WriteAllText(httpUtilitiesPath, httpUtilitiesContent); - } + UpdateFile(knownHeadersPath, knownHeadersContent); + UpdateFile(httpProtocolFeatureCollectionPath, httpProtocolFeatureCollectionContent); + UpdateFile(httpUtilitiesPath, httpUtilitiesContent); + UpdateFile(http2ConnectionPath, http2ConnectionContent); + UpdateFile(transportMultiplexedConnectionFeatureCollectionPath, transportMultiplexedConnectionFeatureCollectionContent); + UpdateFile(transportConnectionFeatureCollectionPath, transportConnectionFeatureCollectionContent); + } - var existingTransportConnectionFeatureCollection = File.Exists(transportConnectionFeatureCollectionPath) ? File.ReadAllText(transportConnectionFeatureCollectionPath) : ""; - if (!string.Equals(transportConnectionFeatureCollectionContent, existingTransportConnectionFeatureCollection)) + public static void UpdateFile(string path, string content) + { + var existingContent = File.Exists(path) ? File.ReadAllText(path) : ""; + if (!string.Equals(content, existingContent)) { - File.WriteAllText(transportConnectionFeatureCollectionPath, transportConnectionFeatureCollectionContent); + File.WriteAllText(path, content); } - var existingHttp2Connection = File.Exists(http2ConnectionPath) ? File.ReadAllText(http2ConnectionPath) : ""; - if (!string.Equals(http2ConnectionContent, existingHttp2Connection)) + var existingHttp2Connection = File.Exists(path) ? File.ReadAllText(path) : ""; + if (!string.Equals(content, existingHttp2Connection)) { - File.WriteAllText(http2ConnectionPath, http2ConnectionContent); + File.WriteAllText(path, content); } } } diff --git a/src/Servers/Kestrel/tools/CodeGenerator/TransportMultiplexedConnectionFeatureCollection.cs b/src/Servers/Kestrel/tools/CodeGenerator/TransportMultiplexedConnectionFeatureCollection.cs new file mode 100644 index 000000000000..8c0ad9d3ff99 --- /dev/null +++ b/src/Servers/Kestrel/tools/CodeGenerator/TransportMultiplexedConnectionFeatureCollection.cs @@ -0,0 +1,34 @@ +// 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. + +namespace CodeGenerator +{ + public class TransportMultiplexedConnectionFeatureCollection + { + public static string GenerateFile() + { + // NOTE: This list MUST always match the set of feature interfaces implemented by TransportConnectionBase. + // See also: shared/TransportConnectionBase.FeatureCollection.cs + var features = new[] + { + "IConnectionIdFeature", + "IConnectionTransportFeature", + "IConnectionItemsFeature", + "IMemoryPoolFeature", + "IConnectionLifetimeFeature" + }; + + var usings = $@" +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http.Features;"; + + return FeatureCollectionGenerator.GenerateFile( + namespaceName: "Microsoft.AspNetCore.Connections", + className: "TransportMultiplexedConnection", + allFeatures: features, + implementedFeatures: features, + extraUsings: usings, + fallbackFeatures: null); + } + } +} diff --git a/src/Shared/runtime/Http3/QPack/QPackEncoder.cs b/src/Shared/runtime/Http3/QPack/QPackEncoder.cs index 348106aa15b2..ecf0f1e183ab 100644 --- a/src/Shared/runtime/Http3/QPack/QPackEncoder.cs +++ b/src/Shared/runtime/Http3/QPack/QPackEncoder.cs @@ -340,7 +340,6 @@ private static bool EncodeHeaderBlockPrefix(Span destination, out int byte return true; } - // TODO these are fairly hard coded for the first two bytes to be zero. public bool BeginEncode(IEnumerable> headers, Span buffer, out int length) { _enumerator = headers.GetEnumerator(); @@ -351,7 +350,11 @@ public bool BeginEncode(IEnumerable> headers, Span< buffer[0] = 0; buffer[1] = 0; - return Encode(buffer.Slice(2), out length); + bool doneEncode = Encode(buffer.Slice(2), out length); + + // Add two for the first two bytes. + length += 2; + return doneEncode; } public bool BeginEncode(int statusCode, IEnumerable> headers, Span buffer, out int length) diff --git a/src/Shared/runtime/Quic/Implementations/MsQuic/Internal/MsQuicApi.cs b/src/Shared/runtime/Quic/Implementations/MsQuic/Internal/MsQuicApi.cs index 30e5cba6cbed..fb6330c024b1 100644 --- a/src/Shared/runtime/Quic/Implementations/MsQuic/Internal/MsQuicApi.cs +++ b/src/Shared/runtime/Quic/Implementations/MsQuic/Internal/MsQuicApi.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -143,12 +143,6 @@ static MsQuicApi() OperatingSystem ver = Environment.OSVersion; - if (ver.Platform == PlatformID.Win32NT && ver.Version < new Version(10, 0, 19041, 0)) - { - IsQuicSupported = false; - return; - } - // TODO: try to initialize TLS 1.3 in SslStream. try