Introduce multiplexed bedrock types (#17213)

This commit is contained in:
Justin Kotalik 2020-02-24 17:56:31 -08:00 committed by GitHub
parent eb552a6fbd
commit d8b6823111
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 2646 additions and 579 deletions

View File

@ -8,6 +8,19 @@ namespace Microsoft.AspNetCore.Connections
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<object, object> 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 @@ namespace Microsoft.AspNetCore.Connections
public static Microsoft.AspNetCore.Connections.IConnectionBuilder Use(this Microsoft.AspNetCore.Connections.IConnectionBuilder connectionBuilder, System.Func<Microsoft.AspNetCore.Connections.ConnectionContext, System.Func<System.Threading.Tasks.Task>, System.Threading.Tasks.Task> middleware) { throw null; }
public static Microsoft.AspNetCore.Connections.IConnectionBuilder UseConnectionHandler<TConnectionHandler>(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<object, object> 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 @@ namespace Microsoft.AspNetCore.Connections
{
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.IConnectionListener> 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<Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate, Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate> middleware);
}
public partial interface IMultiplexedConnectionFactory
{
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.MultiplexedConnectionContext> 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<Microsoft.AspNetCore.Connections.MultiplexedConnectionContext> 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<Microsoft.AspNetCore.Connections.IMultiplexedConnectionListener> 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<Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate, Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate> middleware) { throw null; }
}
public abstract partial class MultiplexedConnectionContext : Microsoft.AspNetCore.Connections.BaseConnectionContext, System.IAsyncDisposable
{
protected MultiplexedConnectionContext() { }
public abstract System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
public abstract System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> 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 @@ namespace Microsoft.AspNetCore.Connections
public override string ToString() { throw null; }
}
}
namespace Microsoft.AspNetCore.Connections.Abstractions.Features
{
public partial interface IQuicCreateStreamFeature
{
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> StartBidirectionalStreamAsync();
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> StartUnidirectionalStreamAsync();
}
}
namespace Microsoft.AspNetCore.Connections.Features
{
public partial interface IConnectionCompleteFeature
@ -196,15 +225,18 @@ namespace Microsoft.AspNetCore.Connections.Features
{
System.Buffers.MemoryPool<byte> 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<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync();
long StreamId { get; }
}
public partial interface ITlsHandshakeFeature
{

View File

@ -8,6 +8,19 @@ namespace Microsoft.AspNetCore.Connections
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<object, object> 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 @@ namespace Microsoft.AspNetCore.Connections
public static Microsoft.AspNetCore.Connections.IConnectionBuilder Use(this Microsoft.AspNetCore.Connections.IConnectionBuilder connectionBuilder, System.Func<Microsoft.AspNetCore.Connections.ConnectionContext, System.Func<System.Threading.Tasks.Task>, System.Threading.Tasks.Task> middleware) { throw null; }
public static Microsoft.AspNetCore.Connections.IConnectionBuilder UseConnectionHandler<TConnectionHandler>(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<object, object> 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 @@ namespace Microsoft.AspNetCore.Connections
{
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.IConnectionListener> 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<Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate, Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate> middleware);
}
public partial interface IMultiplexedConnectionFactory
{
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.MultiplexedConnectionContext> 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<Microsoft.AspNetCore.Connections.MultiplexedConnectionContext> 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<Microsoft.AspNetCore.Connections.IMultiplexedConnectionListener> 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<Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate, Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate> middleware) { throw null; }
}
public abstract partial class MultiplexedConnectionContext : Microsoft.AspNetCore.Connections.BaseConnectionContext, System.IAsyncDisposable
{
protected MultiplexedConnectionContext() { }
public abstract System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
public abstract System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> 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 @@ namespace Microsoft.AspNetCore.Connections
public override string ToString() { throw null; }
}
}
namespace Microsoft.AspNetCore.Connections.Abstractions.Features
{
public partial interface IQuicCreateStreamFeature
{
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> StartBidirectionalStreamAsync();
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> StartUnidirectionalStreamAsync();
}
}
namespace Microsoft.AspNetCore.Connections.Features
{
public partial interface IConnectionCompleteFeature
@ -196,15 +225,18 @@ namespace Microsoft.AspNetCore.Connections.Features
{
System.Buffers.MemoryPool<byte> 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<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync();
long StreamId { get; }
}
public partial interface ITlsHandshakeFeature
{

View File

@ -8,6 +8,19 @@ namespace Microsoft.AspNetCore.Connections
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<object, object> 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 @@ namespace Microsoft.AspNetCore.Connections
public static Microsoft.AspNetCore.Connections.IConnectionBuilder Use(this Microsoft.AspNetCore.Connections.IConnectionBuilder connectionBuilder, System.Func<Microsoft.AspNetCore.Connections.ConnectionContext, System.Func<System.Threading.Tasks.Task>, System.Threading.Tasks.Task> middleware) { throw null; }
public static Microsoft.AspNetCore.Connections.IConnectionBuilder UseConnectionHandler<TConnectionHandler>(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<object, object> 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 @@ namespace Microsoft.AspNetCore.Connections
{
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.IConnectionListener> 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<Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate, Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate> middleware);
}
public partial interface IMultiplexedConnectionFactory
{
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.MultiplexedConnectionContext> 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<Microsoft.AspNetCore.Connections.MultiplexedConnectionContext> 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<Microsoft.AspNetCore.Connections.IMultiplexedConnectionListener> 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<Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate, Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate> middleware) { throw null; }
}
public abstract partial class MultiplexedConnectionContext : Microsoft.AspNetCore.Connections.BaseConnectionContext, System.IAsyncDisposable
{
protected MultiplexedConnectionContext() { }
public abstract System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
public abstract System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> 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 @@ namespace Microsoft.AspNetCore.Connections
public override string ToString() { throw null; }
}
}
namespace Microsoft.AspNetCore.Connections.Abstractions.Features
{
public partial interface IQuicCreateStreamFeature
{
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> StartBidirectionalStreamAsync();
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> StartUnidirectionalStreamAsync();
}
}
namespace Microsoft.AspNetCore.Connections.Features
{
public partial interface IConnectionCompleteFeature
@ -196,15 +225,18 @@ namespace Microsoft.AspNetCore.Connections.Features
{
System.Buffers.MemoryPool<byte> 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<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync();
long StreamId { get; }
}
public partial interface ITlsHandshakeFeature
{

View File

@ -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
{
/// <summary>
/// Gets or sets a unique identifier to represent this connection in trace logs.
/// </summary>
public abstract string ConnectionId { get; set; }
/// <summary>
/// Gets the collection of features provided by the server and middleware available on this connection.
/// </summary>
public abstract IFeatureCollection Features { get; }
/// <summary>
/// Gets or sets a key/value collection that can be used to share data within the scope of this connection.
/// </summary>
public abstract IDictionary<object, object> Items { get; set; }
/// <summary>
/// Triggered when the client connection is closed.
/// </summary>
public virtual CancellationToken ConnectionClosed { get; set; }
/// <summary>
/// Gets or sets the local endpoint for this connection.
/// </summary>
public virtual EndPoint LocalEndPoint { get; set; }
/// <summary>
/// Gets or sets the remote endpoint for this connection.
/// </summary>
public virtual EndPoint RemoteEndPoint { get; set; }
/// <summary>
/// Aborts the underlying connection.
/// </summary>
public abstract void Abort();
/// <summary>
/// Aborts the underlying connection.
/// </summary>
/// <param name="abortReason">An optional <see cref="ConnectionAbortedException"/> describing the reason the connection is being terminated.</param>
public abstract void Abort(ConnectionAbortedException abortReason);
/// <summary>
/// Releases resources for the underlying connection.
/// </summary>
/// <returns>A <see cref="ValueTask"/> that completes when resources have been released.</returns>
public virtual ValueTask DisposeAsync()
{
return default;
}
}
}

View File

@ -40,4 +40,4 @@ namespace Microsoft.AspNetCore.Connections
});
}
}
}
}

View File

@ -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
{
/// <summary>
/// Encapsulates all information about an individual connection.
/// </summary>
public abstract class ConnectionContext : IAsyncDisposable
public abstract class ConnectionContext : BaseConnectionContext, IAsyncDisposable
{
/// <summary>
/// Gets or sets a unique identifier to represent this connection in trace logs.
/// </summary>
public abstract string ConnectionId { get; set; }
/// <summary>
/// Gets the collection of features provided by the server and middleware available on this connection.
/// </summary>
public abstract IFeatureCollection Features { get; }
/// <summary>
/// Gets or sets a key/value collection that can be used to share data within the scope of this connection.
/// </summary>
public abstract IDictionary<object, object> Items { get; set; }
/// <summary>
/// Gets or sets the <see cref="IDuplexPipe"/> that can be used to read or write data on this connection.
/// </summary>
public abstract IDuplexPipe Transport { get; set; }
/// <summary>
/// Triggered when the client connection is closed.
/// </summary>
public virtual CancellationToken ConnectionClosed { get; set; }
/// <summary>
/// Gets or sets the local endpoint for this connection.
/// </summary>
public virtual EndPoint LocalEndPoint { get; set; }
/// <summary>
/// Gets or sets the remote endpoint for this connection.
/// </summary>
public virtual EndPoint RemoteEndPoint { get; set; }
/// <summary>
/// Aborts the underlying connection.
/// </summary>
/// <param name="abortReason">An optional <see cref="ConnectionAbortedException"/> describing the reason the connection is being terminated.</param>
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 @@ namespace Microsoft.AspNetCore.Connections
/// <summary>
/// Aborts the underlying connection.
/// </summary>
public virtual void Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application via ConnectionContext.Abort()."));
/// <summary>
/// Releases resources for the underlying connection.
/// </summary>
/// <returns>A <see cref="ValueTask"/> that completes when resources have been released.</returns>
public virtual ValueTask DisposeAsync()
{
return default;
}
public override void Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application via ConnectionContext.Abort()."));
}
}

View File

@ -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<ConnectionContext> AcceptAsync();
long Error { get; set; }
}
}

View File

@ -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<ConnectionContext> StartUnidirectionalStreamAsync();
ValueTask<ConnectionContext> StartBidirectionalStreamAsync();
}
}

View File

@ -3,10 +3,9 @@
namespace Microsoft.AspNetCore.Connections.Features
{
public interface IQuicStreamFeature
public interface IStreamDirectionFeature
{
bool CanRead { get; }
bool CanWrite { get; }
long StreamId { get; }
}
}

View File

@ -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; }
}
}

View File

@ -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
{
/// <summary>
/// Defines an interface that represents a listener bound to a specific <see cref="EndPoint"/>.
/// </summary>
public interface IMultiplexedConnectionListener : IAsyncDisposable
{
/// <summary>
/// The endpoint that was bound. This may differ from the requested endpoint, such as when the caller requested that any free port be selected.
/// </summary>
EndPoint EndPoint { get; }
/// <summary>
/// Stops listening for incoming connections.
/// </summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A <see cref="ValueTask"/> that represents the un-bind operation.</returns>
ValueTask UnbindAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Begins an asynchronous operation to accept an incoming connection.
/// </summary>
/// <param name="features">A feature collection to pass options when accepting a connection.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A <see cref="ValueTask{ConnectionContext}"/> that completes when a connection is accepted, yielding the <see cref="MultiplexedConnectionContext" /> representing the connection.</returns>
ValueTask<MultiplexedConnectionContext> AcceptAsync(IFeatureCollection features = null, CancellationToken cancellationToken = default);
}
}

View File

@ -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
{
/// <summary>
/// Defines an interface that provides the mechanisms to configure a connection pipeline.
/// </summary>
public interface IMultiplexedConnectionBuilder
{
/// <summary>
/// Gets the <see cref="IServiceProvider"/> that provides access to the application's service container.
/// </summary>
IServiceProvider ApplicationServices { get; }
/// <summary>
/// Adds a middleware delegate to the application's connection pipeline.
/// </summary>
/// <param name="middleware">The middleware delegate.</param>
/// <returns>The <see cref="IMultiplexedConnectionBuilder"/>.</returns>
IMultiplexedConnectionBuilder Use(Func<MultiplexedConnectionDelegate, MultiplexedConnectionDelegate> middleware);
/// <summary>
/// Builds the delegate used by this application to process connections.
/// </summary>
/// <returns>The connection handling delegate.</returns>
MultiplexedConnectionDelegate Build();
}
}

View File

@ -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
{
/// <summary>
/// A factory abstraction for creating connections to an endpoint.
/// </summary>
public interface IMultiplexedConnectionFactory
{
/// <summary>
/// Creates a new connection to an endpoint.
/// </summary>
/// <param name="endpoint">The <see cref="EndPoint"/> to connect to.</param>
/// <param name="features">A feature collection to pass options when connecting.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None" />.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}" /> that represents the asynchronous connect, yielding the <see cref="MultiplexedConnectionContext" /> for the new connection when completed.
/// </returns>
ValueTask<MultiplexedConnectionContext> ConnectAsync(EndPoint endpoint, IFeatureCollection features = null, CancellationToken cancellationToken = default);
}
}

View File

@ -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
/// <summary>
/// Defines an interface that provides the mechanisms for binding to various types of <see cref="EndPoint"/>s.
/// </summary>
public interface IMultiplexedConnectionListenerFactory
{
/// <summary>
/// Creates an <see cref="IMultiplexedConnectionListener"/> bound to the specified <see cref="EndPoint"/>.
/// </summary>
/// <param name="endpoint">The <see cref="EndPoint" /> to bind to.</param>
/// <param name="features">A feature collection to pass options when binding.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A <see cref="ValueTask{IMultiplexedConnectionListener}"/> that completes when the listener has been bound, yielding a <see cref="IMultiplexedConnectionListener" /> representing the new listener.</returns>
ValueTask<IMultiplexedConnectionListener> BindAsync(EndPoint endpoint, IFeatureCollection features = null, CancellationToken cancellationToken = default);
}
}

View File

@ -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<Func<MultiplexedConnectionDelegate, MultiplexedConnectionDelegate>> _components = new List<Func<MultiplexedConnectionDelegate, MultiplexedConnectionDelegate>>();
public IServiceProvider ApplicationServices { get; }
public MultiplexedConnectionBuilder(IServiceProvider applicationServices)
{
ApplicationServices = applicationServices;
}
public IMultiplexedConnectionBuilder Use(Func<MultiplexedConnectionDelegate, MultiplexedConnectionDelegate> 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;
}
}
}

View File

@ -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
{
/// <summary>
/// Encapsulates all information about a multiplexed connection.
/// </summary>
public abstract class MultiplexedConnectionContext : BaseConnectionContext, IAsyncDisposable
{
/// <summary>
/// Asynchronously accept an incoming stream on the connection.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public abstract ValueTask<ConnectionContext> AcceptAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Creates an outbound connection
/// </summary>
/// <param name="features"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public abstract ValueTask<ConnectionContext> ConnectAsync(IFeatureCollection features = null, CancellationToken cancellationToken = default);
}
}

View File

@ -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
{
/// <summary>
/// A function that can process a connection.
/// </summary>
/// <param name="connection">A <see cref="MultiplexedConnectionContext" /> representing the connection.</param>
/// <returns>A <see cref="Task"/> that represents the connection lifetime. When the task completes, the connection will be closed.</returns>
public delegate Task MultiplexedConnectionDelegate(MultiplexedConnectionContext connection);
}

View File

@ -96,6 +96,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
public partial class KestrelServer : Microsoft.AspNetCore.Hosting.Server.IServer, System.IDisposable
{
public KestrelServer(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions> options, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Connections.IConnectionListenerFactory> transportFactories, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
public KestrelServer(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions> options, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Connections.IConnectionListenerFactory> transportFactories, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Connections.IMultiplexedConnectionListenerFactory> 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
public void ListenUnixSocket(string socketPath) { }
public void ListenUnixSocket(string socketPath, System.Action<Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions> 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
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<Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate, Microsoft.AspNetCore.Connections.MultiplexedConnectionDelegate> middleware) { throw null; }
public override string ToString() { throw null; }
public Microsoft.AspNetCore.Connections.IConnectionBuilder Use(System.Func<Microsoft.AspNetCore.Connections.ConnectionDelegate, Microsoft.AspNetCore.Connections.ConnectionDelegate> middleware) { throw null; }
}

View File

@ -527,6 +527,24 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="Http2ErrorMaxStreams" xml:space="preserve">
<value>A new stream was refused because this connection has reached its stream limit.</value>
</data>
<data name="Http3ErrorConnectMustNotSendSchemeOrPath" xml:space="preserve">
<value>CONNECT requests must not send :scheme or :path headers.</value>
</data>
<data name="Http3StreamErrorSchemeMismatch" xml:space="preserve">
<value>The request :scheme header '{requestScheme}' does not match the transport scheme '{transportScheme}'.</value>
</data>
<data name="Http3ErrorMethodInvalid" xml:space="preserve">
<value>The Method '{method}' is invalid.</value>
</data>
<data name="Http3StreamErrorPathInvalid" xml:space="preserve">
<value>The request :path is invalid: '{path}'</value>
</data>
<data name="Http3StreamErrorLessDataThanLength" xml:space="preserve">
<value>Less data received than specified in the Content-Length header.</value>
</data>
<data name="Http3StreamErrorMoreDataThanLength" xml:space="preserve">
<value>More data received than specified in the Content-Length header.</value>
</data>
<data name="GreaterThanZeroRequired" xml:space="preserve">
<value>A value greater than zero is required.</value>
</data>

View File

@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
// 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<ConnectionContext>(id, _serviceContext, c => _connectionDelegate(c), connection, Log);
_serviceContext.ConnectionManager.AddConnection(id, kestrelConnection);

View File

@ -511,7 +511,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
public virtual void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
_requestHeadersParsed++;
if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
@ -1196,7 +1196,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
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;

View File

@ -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; }
}
}

View File

@ -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<long, Http3Stream> _streams = new ConcurrentDictionary<long, Http3Stream>();
internal readonly Dictionary<long, Http3Stream> _streams = new Dictionary<long, Http3Stream>();
private long _highestOpenedStreamId; // TODO lock to access
private volatile bool _haveSentGoAway;
private object _sync = new object();
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(HttpConnectionContext context)
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
}
}
public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> application)
{
var streamListenerFeature = Context.ConnectionFeatures.Get<IQuicStreamListenerFeature>();
private IKestrelTrace Log => _context.ServiceContext.Log;
public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> httpApplication)
{
try
{
// Ensure TimeoutControl._lastTimestamp is initialized before anything that could set timeouts runs.
_timeoutControl.Initialize(_systemClock.UtcNowTicks);
var connectionHeartbeatFeature = _context.ConnectionFeatures.Get<IConnectionHeartbeatFeature>();
var connectionLifetimeNotificationFeature = _context.ConnectionFeatures.Get<IConnectionLifetimeNotificationFeature>();
// 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)
{
// 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<TContext>(IHttpApplication<TContext> application)
{
// Start other three unidirectional streams here.
var controlTask = CreateControlStream(application);
var encoderTask = CreateEncoderStream(application);
@ -66,29 +200,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
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<IStreamDirectionFeature>();
var streamIdFeature = streamContext.Features.Get<IStreamIdFeature>();
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<IQuicStreamFeature>();
if (!streamFeature.CanWrite)
if (!quicStreamFeature.CanWrite)
{
// Unidirectional stream
var stream = new Http3ControlStream<TContext>(application, this, httpConnectionContext);
@ -97,13 +234,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
else
{
// Keep track of highest stream id seen for GOAWAY
var streamId = streamFeature.StreamId;
var streamId = streamIdFeature.StreamId;
HighestStreamId = streamId;
var http3Stream = new Http3Stream<TContext>(application, this, httpConnectionContext);
var stream = http3Stream;
_streams[streamId] = http3Stream;
lock (_streams)
{
_streams[streamId] = http3Stream;
}
ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
}
}
@ -111,14 +250,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
private async ValueTask<Http3ControlStream> CreateNewUnidirectionalStreamAsync<TContext>(IHttpApplication<TContext> application)
{
var connectionContext = await Context.ConnectionFeatures.Get<IQuicCreateStreamFeature>().StartUnidirectionalStreamAsync();
var httpConnectionContext = new HttpConnectionContext
var features = new FeatureCollection();
features.Set<IStreamDirectionFeature>(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<TContext>(application, this, httpConnectionContext);
}
public void StopProcessingNextRequest()
{
}
public void HandleRequestHeadersTimeout()
{
}
@ -188,7 +328,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
}
public void Abort(ConnectionAbortedException ex)
private void InnerAbort(ConnectionAbortedException ex)
{
lock (_sync)
{
@ -202,10 +342,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
_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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
// TODO make sure this works
//_maxDynamicTableSize = value;
}
internal void RemoveStream(long streamId)
{
lock(_streams)
{
_streams.Remove(streamId);
}
}
}
}

View File

@ -10,6 +10,7 @@ using System.Net.Http.QPack;
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<byte> 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<byte> MethodBytes => new byte[7] { (byte)':', (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d' };
private static ReadOnlySpan<byte> PathBytes => new byte[5] { (byte)':', (byte)'p', (byte)'a', (byte)'t', (byte)'h' };
private static ReadOnlySpan<byte> SchemeBytes => new byte[7] { (byte)':', (byte)'s', (byte)'c', (byte)'h', (byte)'e', (byte)'m', (byte)'e' };
private static ReadOnlySpan<byte> StatusBytes => new byte[7] { (byte)':', (byte)'s', (byte)'t', (byte)'a', (byte)'t', (byte)'u', (byte)'s' };
private static ReadOnlySpan<byte> 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<byte> TeBytes => new byte[2] { (byte)'t', (byte)'e' };
private static ReadOnlySpan<byte> TrailersBytes => new byte[8] { (byte)'t', (byte)'r', (byte)'a', (byte)'i', (byte)'l', (byte)'e', (byte)'r', (byte)'s' };
private static ReadOnlySpan<byte> 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<object> _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<IProtocolErrorCodeFeature>();
_streamIdFeature = _context.ConnectionFeatures.Get<IStreamIdFeature>();
_frameWriter = new Http3FrameWriter(
context.Transport.Output,
context.ConnectionContext,
context.StreamContext,
context.TimeoutControl,
httpLimits.MinResponseDataRate,
context.ConnectionId,
@ -63,6 +87,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
OnHeader(knownHeader.Name, value);
}
public override void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> 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<byte> name, ReadOnlySpan<byte> 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<byte> 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<byte> name, ReadOnlySpan<byte> 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
if (result.IsCompleted)
{
OnEndStreamReceived();
return;
}
}
@ -162,33 +358,61 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
}
}
}
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<TContext>(IHttpApplication<TContext> application, in ReadOnlySequence<byte> payload)
{
@ -231,13 +455,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
}
_receivedHeaders = true;
InputRemaining = HttpRequestHeaders.ContentLength;
_appCompleted = new TaskCompletionSource<object>();
ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false);
Task.Run(() => base.ProcessRequestsAsync(application));
return Task.CompletedTask;
}
private Task ProcessDataFrameAsync(in ReadOnlySequence<byte> 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
protected override void ApplicationAbort()
{
var abortReason = new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication);
Abort(abortReason, Http3ErrorCode.InternalError);
}
protected override string CreateRequestId()
@ -307,7 +548,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
// - 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
// 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
}
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
/// </summary>
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;

View File

@ -10,14 +10,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
private readonly IHttpApplication<TContext> _application;
public Http3Stream(IHttpApplication<TContext> application, Http3Connection connection, HttpConnectionContext context) : base(connection, context)
public Http3Stream(IHttpApplication<TContext> 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

View File

@ -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<byte> MemoryPool { get; set; }
public IPEndPoint LocalEndPoint { get; set; }
public IPEndPoint RemoteEndPoint { get; set; }
public ITimeoutControl TimeoutControl { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -68,10 +68,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private HttpProtocols SelectProtocol()
{
if (_context.Protocols == HttpProtocols.Http3)
{
return HttpProtocols.Http3;
}
var hasTls = _context.ConnectionFeatures.Get<ITlsConnectionFeature>() != null;
var applicationProtocol = _context.ConnectionFeatures.Get<ITlsApplicationProtocolFeature>()?.ApplicationProtocol
?? new ReadOnlyMemory<byte>();

View File

@ -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;

View File

@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
Walk(connection =>
{
connection.TransportConnection.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedDuringServerShutdown));
connection.GetTransport().Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedDuringServerShutdown));
abortTasks.Add(connection.ExecutionTask);
});

View File

@ -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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
public ConnectionReference(KestrelConnection connection)
{
_weakReference = new WeakReference<KestrelConnection>(connection);
ConnectionId = connection.TransportConnection.ConnectionId;
ConnectionId = connection.GetTransport().ConnectionId;
}
public string ConnectionId { get; }

View File

@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging;
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<object> handler, object state)> _heartbeatHandlers;
private readonly object _heartbeatLock = new object();
@ -21,31 +21,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
private readonly CancellationTokenSource _connectionClosingCts = new CancellationTokenSource();
private readonly TaskCompletionSource<object> _completionTcs = new TaskCompletionSource<object>(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<IConnectionHeartbeatFeature>(this);
connectionContext.Features.Set<IConnectionCompleteFeature>(this);
connectionContext.Features.Set<IConnectionLifetimeNotificationFeature>(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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
}
}
public abstract BaseConnectionContext GetTransport();
public void OnHeartbeat(Action<object> action, object state)
{
lock (_heartbeatLock)
@ -175,49 +168,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
_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))
{

View File

@ -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<T> : KestrelConnection, IThreadPoolWorkItem where T : BaseConnectionContext
{
private readonly Func<T, Task> _connectionDelegate;
public T TransportConnection { get; set; }
public KestrelConnection(long id,
ServiceContext serviceContext,
Func<T, Task> connectionDelegate,
T connectionContext,
IKestrelTrace logger)
: base(id, serviceContext, logger)
{
_connectionDelegate = connectionDelegate;
TransportConnection = connectionContext;
connectionContext.Features.Set<IConnectionHeartbeatFeature>(this);
connectionContext.Features.Set<IConnectionCompleteFeature>(this);
connectionContext.Features.Set<IConnectionLifetimeNotificationFeature>(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;
}
}
}

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
// - 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
}
[NonEvent]
public void ConnectionStop(ConnectionContext connection)
public void ConnectionStop(BaseConnectionContext connection)
{
if (IsEnabled())
{

View File

@ -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<object> _acceptLoopTcs = new TaskCompletionSource<object>(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<MultiplexedConnectionContext>(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);
}
}
}
}
}

View File

@ -5,6 +5,7 @@ using System;
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<IConnectionListenerFactory> _transportFactories;
private readonly List<IMultiplexedConnectionListenerFactory> _multiplexedTransportFactories;
private bool _hasStarted;
private int _stopping;
private readonly TaskCompletionSource<object> _stoppedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
public KestrelServer(IOptions<KestrelServerOptions> options, IEnumerable<IConnectionListenerFactory> transportFactories, ILoggerFactory loggerFactory)
: this(transportFactories, CreateServiceContext(options, loggerFactory))
: this(transportFactories, null, CreateServiceContext(options, loggerFactory))
{
}
public KestrelServer(IOptions<KestrelServerOptions> options, IEnumerable<IConnectionListenerFactory> transportFactories, IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories, ILoggerFactory loggerFactory)
: this(transportFactories, multiplexedFactories, CreateServiceContext(options, loggerFactory))
{
}
// For testing
internal KestrelServer(IEnumerable<IConnectionListenerFactory> transportFactories, ServiceContext serviceContext)
: this(transportFactories, null, serviceContext)
{
}
// For testing
internal KestrelServer(IEnumerable<IConnectionListenerFactory> transportFactories, IEnumerable<IMultiplexedConnectionListenerFactory> multiplexedFactories, ServiceContext serviceContext)
{
if (transportFactories == null)
{
@ -43,8 +56,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
}
_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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
var heartbeatManager = new HeartbeatManager(connectionManager);
var dateHeaderValueManager = new DateHeaderValueManager();
var heartbeat = new Heartbeat(
new IHeartbeatHandler[] { dateHeaderValueManager, heartbeatManager },
new SystemClock(),
@ -129,54 +144,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
async Task OnBind(ListenOptions options)
{
// 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)
{
if (_multiplexedTransportFactories == null || _multiplexedTransportFactories.Count == 0)
{
throw new InvalidOperationException("Cannot start HTTP/3 server if no MultiplexedTransportFactories are registered.");
}
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;
}
// 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)
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?
{
connectionDelegate = new ConnectionLimitMiddleware(connectionDelegate, Options.Limits.MaxConcurrentConnections.Value, Trace).OnConnectionAsync;
}
options.UseHttpServer(ServiceContext, application, options.Protocols);
var connectionDelegate = options.Build();
var connectionDispatcher = new ConnectionDispatcher(ServiceContext, connectionDelegate);
IConnectionListenerFactory factory = null;
if (options.Protocols >= HttpProtocols.Http3)
{
foreach (var transportFactory in _transportFactories)
// Add the connection limit middleware
if (Options.Limits.MaxConcurrentConnections.HasValue)
{
if (transportFactory is IMultiplexedConnectionListenerFactory)
{
// Don't break early. Always use the last registered factory.
factory = transportFactory;
}
connectionDelegate = new ConnectionLimitMiddleware(connectionDelegate, Options.Limits.MaxConcurrentConnections.Value, Trace).OnConnectionAsync;
}
if (factory == null)
{
throw new InvalidOperationException(CoreStrings.QuicTransportNotFound);
}
var connectionDispatcher = new ConnectionDispatcher(ServiceContext, connectionDelegate);
var factory = _transportFactories.Last();
var transport = await factory.BindAsync(options.EndPoint).ConfigureAwait(false);
var acceptLoopTask = connectionDispatcher.StartAcceptingConnections(transport);
_transports.Add((transport, acceptLoopTask));
options.EndPoint = transport.EndPoint;
}
else
{
foreach (var transportFactory in _transportFactories)
{
if (!(transportFactory is IMultiplexedConnectionListenerFactory))
{
factory = transportFactory;
}
}
}
var transport = await factory.BindAsync(options.EndPoint).ConfigureAwait(false);
// Update the endpoint
options.EndPoint = transport.EndPoint;
var acceptLoopTask = connectionDispatcher.StartAcceptingConnections(transport);
_transports.Add((transport, acceptLoopTask));
}
await AddressBinder.BindAsync(_serverAddresses, Options, Trace, OnBind).ConfigureAwait(false);
@ -200,13 +213,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
}
}
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();

View File

@ -15,9 +15,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// Describes either an <see cref="IPEndPoint"/>, Unix domain socket path, or a file descriptor for an already open
/// socket that Kestrel should bind to or open.
/// </summary>
public class ListenOptions : IConnectionBuilder
public class ListenOptions : IConnectionBuilder, IMultiplexedConnectionBuilder
{
internal readonly List<Func<ConnectionDelegate, ConnectionDelegate>> _middleware = new List<Func<ConnectionDelegate, ConnectionDelegate>>();
internal readonly List<Func<MultiplexedConnectionDelegate, MultiplexedConnectionDelegate>> _multiplexedMiddleware = new List<Func<MultiplexedConnectionDelegate, MultiplexedConnectionDelegate>>();
internal ListenOptions(IPEndPoint endPoint)
{
@ -123,6 +124,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
return this;
}
IMultiplexedConnectionBuilder IMultiplexedConnectionBuilder.Use(Func<MultiplexedConnectionDelegate, MultiplexedConnectionDelegate> middleware)
{
_multiplexedMiddleware.Add(middleware);
return this;
}
public ConnectionDelegate Build()
{
ConnectionDelegate app = context =>
@ -139,6 +146,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
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);

View File

@ -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<TContext>
{
private readonly ServiceContext _serviceContext;
private readonly IHttpApplication<TContext> _application;
public Http3ConnectionMiddleware(ServiceContext serviceContext, IHttpApplication<TContext> application)
{
_serviceContext = serviceContext;
_application = application;
}
public Task OnConnectionAsync(MultiplexedConnectionContext connectionContext)
{
var memoryPoolFeature = connectionContext.Features.Get<IMemoryPoolFeature>();
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);
}
}
}

View File

@ -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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
return middleware.OnConnectionAsync;
});
}
public static IMultiplexedConnectionBuilder UseHttp3Server<TContext>(this IMultiplexedConnectionBuilder builder, ServiceContext serviceContext, IHttpApplication<TContext> application, HttpProtocols protocols)
{
var middleware = new Http3ConnectionMiddleware<TContext>(serviceContext, application);
return builder.Use(next =>
{
return middleware.OnConnectionAsync;
});
}
}
}

View File

@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var connection = new Mock<DefaultConnectionContext> { CallBase = true }.Object;
connection.ConnectionClosed = new CancellationToken(canceled: true);
var kestrelConnection = new KestrelConnection(0, serviceContext, _ => tcs.Task, connection, serviceContext.Log);
var kestrelConnection = new KestrelConnection<ConnectionContext>(0, serviceContext, _ => tcs.Task, connection, serviceContext.Log);
serviceContext.ConnectionManager.AddConnection(0, kestrelConnection);
var task = kestrelConnection.ExecuteAsync();
@ -79,9 +79,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var connection = new Mock<DefaultConnectionContext> { CallBase = true }.Object;
connection.ConnectionClosed = new CancellationToken(canceled: true);
var kestrelConnection = new KestrelConnection(0, serviceContext, _ => Task.CompletedTask, connection, serviceContext.Log);
var kestrelConnection = new KestrelConnection<ConnectionContext>(0, serviceContext, _ => Task.CompletedTask, connection, serviceContext.Log);
serviceContext.ConnectionManager.AddConnection(0, kestrelConnection);
var completeFeature = kestrelConnection.TransportConnection.Features.Get<IConnectionCompleteFeature>();
var completeFeature = kestrelConnection.GetTransport().Features.Get<IConnectionCompleteFeature>();
Assert.NotNull(completeFeature);
object stateObject = new object();
@ -100,9 +100,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var logger = ((TestKestrelTrace)serviceContext.Log).Logger;
var connection = new Mock<DefaultConnectionContext> { CallBase = true }.Object;
connection.ConnectionClosed = new CancellationToken(canceled: true);
var kestrelConnection = new KestrelConnection(0, serviceContext, _ => Task.CompletedTask, connection, serviceContext.Log);
var kestrelConnection = new KestrelConnection<ConnectionContext>(0, serviceContext, _ => Task.CompletedTask, connection, serviceContext.Log);
serviceContext.ConnectionManager.AddConnection(0, kestrelConnection);
var completeFeature = kestrelConnection.TransportConnection.Features.Get<IConnectionCompleteFeature>();
var completeFeature = kestrelConnection.GetTransport().Features.Get<IConnectionCompleteFeature>();
Assert.NotNull(completeFeature);
object stateObject = new object();

View File

@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var serviceContext = new TestServiceContext();
var mock = new Mock<DefaultConnectionContext>() { CallBase = true };
mock.Setup(m => m.ConnectionId).Returns(connectionId);
var httpConnection = new KestrelConnection(0, serviceContext, _ => Task.CompletedTask, mock.Object, Mock.Of<IKestrelTrace>());
var httpConnection = new KestrelConnection<ConnectionContext>(0, serviceContext, _ => Task.CompletedTask, mock.Object, Mock.Of<IKestrelTrace>());
httpConnectionManager.AddConnection(0, httpConnection);

View File

@ -20,12 +20,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
File.Delete(testHttpProtocolGeneratedPath);
File.Delete(testHttpUtilitiesGeneratedPath);
File.Delete(testHttp2ConnectionGeneratedPath);
File.Delete(testTransportMultiplexedConnectionGeneratedPath);
File.Delete(testTransportConnectionGeneratedPath);
}
}

View File

@ -14,6 +14,7 @@
<Content Include="$(KestrelRoot)Core\src\Internal\Infrastructure\HttpUtilities.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(KestrelRoot)Core\src\Internal\Http2\Http2Connection.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(KestrelSharedSourceRoot)\TransportConnection.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
<Content Include="$(KestrelSharedSourceRoot)\TransportMultiplexedConnection.Generated.cs" LinkBase="shared\GeneratedContent" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETFramework'">

View File

@ -15,5 +15,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
void StreamPause(string streamId);
void StreamResume(string streamId);
void StreamShutdownWrite(string streamId, Exception ex);
void StreamAbort(string streamId, Exception ex);
}
}

View File

@ -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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
private ValueTask _closeTask;
public long Error { get; set; }
public QuicConnectionContext(QuicConnection connection, QuicTransportContext context)
{
_log = context.Log;
_context = context;
_connection = connection;
Features.Set<ITlsConnectionFeature>(new FakeTlsConnectionFeature());
Features.Set<IQuicStreamListenerFeature>(this);
Features.Set<IQuicCreateStreamFeature>(this);
Features.Set<IProtocolErrorCodeFeature>(this);
_log.NewConnection(ConnectionId);
}
@ -66,27 +67,51 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
_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<ConnectionContext> AcceptAsync()
public override async ValueTask<ConnectionContext> 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 (Exception)
catch (QuicException ex)
{
return null;
// Accept on graceful close throws an aborted exception rather than returning null.
_log.LogDebug($"Accept loop ended with exception: {ex.Message}");
}
return new QuicStreamContext(stream, this, _context);
return null;
}
public override ValueTask<ConnectionContext> ConnectAsync(IFeatureCollection features = null, CancellationToken cancellationToken = default)
{
QuicStream quicStream;
if (features != null)
{
var streamDirectionFeature = features.Get<IStreamDirectionFeature>();
if (streamDirectionFeature.CanRead)
{
quicStream = _connection.OpenBidirectionalStream();
}
else
{
quicStream = _connection.OpenUnidirectionalStream();
}
}
else
{
quicStream = _connection.OpenBidirectionalStream();
}
return new ValueTask<ConnectionContext>(new QuicStreamContext(quicStream, this, _context));
}
}
}

View File

@ -9,13 +9,15 @@ using System.Net.Security;
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
{
/// <summary>
/// Listens for new Quic Connections.
/// </summary>
internal class QuicConnectionListener : IConnectionListener, IAsyncDisposable
internal class QuicConnectionListener : IMultiplexedConnectionListener, IAsyncDisposable
{
private readonly IQuicTrace _log;
private bool _disposed;
@ -36,22 +38,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
public EndPoint EndPoint { get; set; }
public async ValueTask<ConnectionContext> AcceptAsync(CancellationToken cancellationToken = default)
public async ValueTask<MultiplexedConnectionContext> 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
_disposed = true;
_listener.Close();
_listener.Dispose();
return new ValueTask();

View File

@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions);
Features.Set<IQuicStreamFeature>(this);
Features.Set<IStreamDirectionFeature>(this);
Features.Set<IProtocolErrorCodeFeature>(this);
Features.Set<IStreamIdFeature>(this);
// TODO populate the ITlsConnectionFeature (requires client certs).
Features.Set<ITlsConnectionFeature>(new FakeTlsConnectionFeature());
@ -90,6 +92,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
}
}
public long Error { get; set; }
private async Task StartAsync()
{
try
@ -281,10 +285,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
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.

View File

@ -21,7 +21,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
private static readonly Action<ILogger, string, Exception> _streamResume =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(7, nameof(StreamResume)), @"Stream id ""{ConnectionId}"" resumed.");
private static readonly Action<ILogger, string, string, Exception> _streamShutdownWrite =
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(7, nameof(StreamShutdownWrite)), @"Connection id ""{ConnectionId}"" shutting down writes, exception: ""{Reason}"".");
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(7, nameof(StreamShutdownWrite)), @"Stream id ""{ConnectionId}"" shutting down writes, exception: ""{Reason}"".");
private static readonly Action<ILogger, string, string, Exception> _streamAborted =
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(7, nameof(StreamShutdownWrite)), @"Stream id ""{ConnectionId}"" aborted by application, exception: ""{Reason}"".");
private ILogger _logger;
@ -70,5 +72,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
{
_streamShutdownWrite(_logger, streamId, ex.Message, ex);
}
public void StreamAbort(string streamId, Exception ex)
{
_streamAborted(_logger, streamId, ex.Message, ex);
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Quic transport for the ASP.NET Core Kestrel cross-platform web server.</Description>
@ -20,6 +20,9 @@
<Compile Include="$(KestrelSharedSourceRoot)\TransportConnection.cs" Link="Internal\TransportConnection.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\TransportConnection.Generated.cs" Link="Internal\TransportConnection.Generated.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\TransportConnection.FeatureCollection.cs" Link="Internal\TransportConnection.FeatureCollection.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\TransportMultiplexedConnection.cs" Link="Internal\TransportMultiplexedConnection.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\TransportMultiplexedConnection.Generated.cs" Link="Internal\TransportMultiplexedConnection.Generated.cs" />
<Compile Include="$(KestrelSharedSourceRoot)\TransportMultiplexedConnection.FeatureCollection.cs" Link="Internal\TransportMultiplexedConnection.FeatureCollection.cs" />
<Compile Include="$(RepoRoot)src\Shared\TaskToApm.cs" Link="Internal\TaskToApm.cs" />
</ItemGroup>

View File

@ -9,13 +9,14 @@ using System.Net.Security;
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic
_transportContext = new QuicTransportContext(trace, options.Value);
}
public async ValueTask<ConnectionContext> ConnectAsync(EndPoint endPoint, CancellationToken cancellationToken = default)
public async ValueTask<MultiplexedConnectionContext> ConnectAsync(EndPoint endPoint, IFeatureCollection features = null, CancellationToken cancellationToken = default)
{
if (!(endPoint is IPEndPoint ipEndPoint))
{

View File

@ -6,8 +6,8 @@ using System.Net;
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic
_options = options.Value;
}
public ValueTask<IConnectionListener> BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default)
public ValueTask<IMultiplexedConnectionListener> BindAsync(EndPoint endpoint, IFeatureCollection features = null, CancellationToken cancellationToken = default)
{
var transport = new QuicConnectionListener(_options, _log, endpoint);
return new ValueTask<IConnectionListener>(transport);
return new ValueTask<IMultiplexedConnectionListener>(transport);
}
}
}

View File

@ -24,11 +24,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic
/// </summary>
public string Alpn { get; set; }
/// <summary>
/// The registration name to use in Quic.
/// </summary>
public string RegistrationName { get; set; }
/// <summary>
/// The certificate that MsQuic will use.
/// </summary>
@ -49,11 +44,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic
/// </summary>
public long? MaxWriteBufferSize { get; set; } = 64 * 1024;
/// <summary>
/// The error code to abort with
/// </summary>
public long AbortErrorCode { get; set; } = 0;
internal Func<MemoryPool<byte>> MemoryPoolFactory { get; set; } = System.Buffers.SlabMemoryPoolFactory.Create;
}

View File

@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Hosting
{
return hostBuilder.ConfigureServices(services =>
{
services.AddSingleton<IConnectionListenerFactory, QuicTransportFactory>();
services.AddSingleton<IMultiplexedConnectionListenerFactory, QuicTransportFactory>();
});
}

View File

@ -9,6 +9,7 @@ using System.Net.Sockets;
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;

View File

@ -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 @@ namespace Http3SampleApp
{
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<Startup>();
});
{
httpsOptions.ServerCertificate = cert;
});
listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
});
})
.UseStartup<Startup>();
});
hostBuilder.Build().Run();
}

View File

@ -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 @@ namespace QuicSampleApp
.UseQuic(options =>
{
options.Certificate = null;
options.RegistrationName = "AspNetCore-MsQuic";
options.Alpn = "QuicTest";
options.IdleTimeout = TimeSpan.FromHours(1);
})
@ -46,54 +42,37 @@ namespace QuicSampleApp
options.Listen(IPAddress.Any, basePort, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http3;
listenOptions.Use((next) =>
{
return async connection =>
{
var streamFeature = connection.Features.Get<IQuicStreamListenerFeature>();
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<Startup>();

View File

@ -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 @@ namespace QuicSampleClient
})
.ConfigureServices(services =>
{
services.AddSingleton<IConnectionFactory, QuicConnectionFactory>();
services.AddSingleton<IMultiplexedConnectionFactory, QuicConnectionFactory>();
services.AddSingleton<QuicClientService>();
services.AddOptions<QuicTransportOptions>();
services.Configure<QuicTransportOptions>((options) =>
{
options.Alpn = "QuicTest";
options.RegistrationName = "Quic-AspNetCore-client";
options.Certificate = null;
options.IdleTimeout = TimeSpan.FromHours(1);
});
@ -43,9 +39,9 @@ namespace QuicSampleClient
private class QuicClientService
{
private readonly IConnectionFactory _connectionFactory;
private readonly IMultiplexedConnectionFactory _connectionFactory;
private readonly ILogger<QuicClientService> _logger;
public QuicClientService(IConnectionFactory connectionFactory, ILogger<QuicClientService> logger)
public QuicClientService(IMultiplexedConnectionFactory connectionFactory, ILogger<QuicClientService> logger)
{
_connectionFactory = connectionFactory;
_logger = logger;
@ -55,8 +51,7 @@ namespace QuicSampleClient
{
Console.WriteLine("Starting");
var connectionContext = await _connectionFactory.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 5555));
var createStreamFeature = connectionContext.Features.Get<IQuicCreateStreamFeature>();
var streamContext = await createStreamFeature.StartBidirectionalStreamAsync();
var streamContext = await connectionContext.ConnectAsync();
Console.CancelKeyPress += new ConsoleCancelEventHandler((sender, args) =>
{

View File

@ -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<byte> IMemoryPoolFeature.MemoryPool => MemoryPool;
IDictionary<object, object> 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()."));
}
}

View File

@ -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<KeyValuePair<Type, object>> 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<KeyValuePair<Type, object>>(2);
}
for (var i = 0; i < MaybeExtra.Count; i++)
{
if (MaybeExtra[i].Key == key)
{
MaybeExtra[i] = new KeyValuePair<Type, object>(key, value);
return;
}
}
MaybeExtra.Add(new KeyValuePair<Type, object>(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>()
{
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>(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<KeyValuePair<Type, object>> FastEnumerable()
{
if (_currentIConnectionIdFeature != null)
{
yield return new KeyValuePair<Type, object>(typeof(IConnectionIdFeature), _currentIConnectionIdFeature);
}
if (_currentIConnectionTransportFeature != null)
{
yield return new KeyValuePair<Type, object>(typeof(IConnectionTransportFeature), _currentIConnectionTransportFeature);
}
if (_currentIConnectionItemsFeature != null)
{
yield return new KeyValuePair<Type, object>(typeof(IConnectionItemsFeature), _currentIConnectionItemsFeature);
}
if (_currentIMemoryPoolFeature != null)
{
yield return new KeyValuePair<Type, object>(typeof(IMemoryPoolFeature), _currentIMemoryPoolFeature);
}
if (_currentIConnectionLifetimeFeature != null)
{
yield return new KeyValuePair<Type, object>(typeof(IConnectionLifetimeFeature), _currentIConnectionLifetimeFeature);
}
if (MaybeExtra != null)
{
foreach (var item in MaybeExtra)
{
yield return item;
}
}
}
IEnumerator<KeyValuePair<Type, object>> IEnumerable<KeyValuePair<Type, object>>.GetEnumerator() => FastEnumerable().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => FastEnumerable().GetEnumerator();
}
}

View File

@ -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<object, object> _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<byte> MemoryPool { get; }
public IDuplexPipe Application { get; set; }
public override IDictionary<object, object> 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();
}
}
}

View File

@ -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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
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<string, string>(HeaderNames.Method, ""),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "Hello,World"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "Custom"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// 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<string, string>(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<string, string>(HeaderNames.Method, "OPTIONS"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "OPTIONS"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<IHttpRequestFeature>().RawTarget;
return Task.CompletedTask;
});
// :path and :scheme are not allowed, :authority is optional
var headers = new[] { new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<IHttpRequestFeature>().RawTarget);
return Task.CompletedTask;
});
// :path and :scheme are not allowed, :authority is optional
var headers = new[] { new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "CONNECT"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>("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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, ""),
new KeyValuePair<string, string>("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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, "def"),
new KeyValuePair<string, string>("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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, "def"),
new KeyValuePair<string, string>("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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, "d=ef"),
new KeyValuePair<string, string>("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<string, string>(HeaderNames.Method, "GET"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>("Host", "host1"),
new KeyValuePair<string, string>("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<string, string>(HeaderNames.Method, "GET" + new string('a', 1024 * 3)),
new KeyValuePair<string, string>(HeaderNames.Path, "/Hello/How/Are/You/" + new string('a', 1024 * 3)),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "POST"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "POST"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "POST"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, "POST"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.ContentLength, "12"),
};
var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication);
await requestStream.SendHeadersAsync(headers, endStream: true);
await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.Http3StreamErrorLessDataThanLength);
}
}
}

View File

@ -6,50 +6,55 @@ using System.IO.Pipelines;
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<IKestrelTrace> _mockKestrelTrace = new Mock<IKestrelTrace>();
protected readonly Mock<ConnectionContext> _mockConnectionContext = new Mock<ConnectionContext>();
internal readonly Mock<ITimeoutHandler> _mockTimeoutHandler = new Mock<ITimeoutHandler>();
internal readonly Mock<MockTimeoutControlBase> _mockTimeoutControl;
internal readonly MemoryPool<byte> _memoryPool = SlabMemoryPoolFactory.Create();
protected Task _connectionTask;
protected readonly RequestDelegate _echoApplication;
private TestMultiplexedConnectionContext _multiplexedContext;
private readonly CancellationTokenSource _connectionClosingCts = new CancellationTokenSource();
private readonly Channel<ConnectionContext> _acceptConnectionQueue = Channel.CreateUnbounded<ConnectionContext>(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<MockTimeoutControlBase>(_timeoutControl) { CallBase = true };
_timeoutControl.Debugger = Mock.Of<IDebugger>();
_noopApplication = context => Task.CompletedTask;
_echoApplication = async context =>
{
var buffer = new byte[Http3PeerSettings.MinAllowedMaxFrameSize];
@ -60,6 +65,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
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<IHttpRequestFeature>().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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var limits = _serviceContext.ServerOptions.Limits;
var features = new FeatureCollection();
features.Set<IQuicCreateStreamFeature>(this);
features.Set<IQuicStreamListenerFeature>(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<TimeoutReason>()))
.Callback<TimeoutReason>(r => httpConnection.OnTimeout(r));
.Callback<TimeoutReason>(r => _connection.OnTimeout(r));
}
private static PipeOptions GetInputPipeOptions(ServiceContext serviceContext, MemoryPool<byte> memoryPool, PipeScheduler writerScheduler) => new PipeOptions
@ -155,30 +180,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
return bufferSize ?? 0;
}
public ValueTask<ConnectionContext> StartUnidirectionalStreamAsync()
{
var stream = new Http3ControlStream(this, _connection);
// TODO put these somewhere to be read.
return new ValueTask<ConnectionContext>(stream.ConnectionContext);
}
public async ValueTask<ConnectionContext> AcceptAsync()
{
while (await _acceptConnectionQueue.Reader.WaitToReadAsync())
{
while (_acceptConnectionQueue.Reader.TryRead(out var connection))
{
return connection;
}
}
return null;
}
internal async ValueTask<Http3ControlStream> 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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
internal ValueTask<Http3RequestStream> CreateRequestStream()
{
var stream = new Http3RequestStream(this, _connection);
_acceptConnectionQueue.Writer.TryWrite(stream.ConnectionContext);
_multiplexedContext.AcceptQueue.Writer.TryWrite(stream.StreamContext);
return new ValueTask<Http3RequestStream>(stream);
}
@ -194,7 +199,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
var stream = new Http3RequestStream(this, _connection);
// TODO put these somewhere to be read.
return new ValueTask<ConnectionContext>(stream.ConnectionContext);
return new ValueTask<ConnectionContext>(stream.StreamContext);
}
internal class Http3StreamBase
@ -216,15 +221,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
}
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var outputPipeOptions = GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool);
_pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
ConnectionContext = new DefaultConnectionContext();
ConnectionContext.Transport = _pair.Transport;
ConnectionContext.Features.Set<IQuicStreamFeature>(this);
StreamContext = new TestStreamContext(canRead: true, canWrite: true, _pair, this);
}
public async Task<bool> SendHeadersAsync(IEnumerable<KeyValuePair<string, string>> headers)
public async Task<bool> SendHeadersAsync(IEnumerable<KeyValuePair<string, string>> headers, bool endStream = false)
{
var outputWriter = _pair.Application.Output;
var frame = new Http3RawFrame();
@ -256,10 +261,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// TODO may want to modify behavior of input frames to mock different client behavior (client can send anything).
Http3FrameWriter.WriteHeader(frame, outputWriter);
await SendAsync(buffer.Span.Slice(0, length));
if (endStream)
{
await _pair.Application.Output.CompleteAsync();
}
return done;
}
internal async Task SendDataAsync(Memory<byte> data)
internal async Task SendDataAsync(Memory<byte> data, bool endStream = false)
{
var outputWriter = _pair.Application.Output;
var frame = new Http3RawFrame();
@ -267,9 +278,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
frame.Length = data.Length;
Http3FrameWriter.WriteHeader(frame, outputWriter);
await SendAsync(data.Span);
if (endStream)
{
await _pair.Application.Output.CompleteAsync();
}
}
internal async Task<IEnumerable<KeyValuePair<string, string>>> ExpectHeadersAsync()
internal async Task<Dictionary<string, string>> ExpectHeadersAsync()
{
var http3WithPayload = await ReceiveFrameAsync();
_qpackDecoder.Decode(http3WithPayload.PayloadSequence, this);
@ -347,6 +363,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
_decodedHeaders[((Span<byte>)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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
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<IQuicStreamFeature>(this);
StreamContext = new TestStreamContext(canRead: false, canWrite: true, _pair, this);
}
public async Task WriteStreamIdAsync(int id)
@ -402,5 +428,100 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await FlushAsync(writableBuffer);
}
}
private class TestMultiplexedConnectionContext : MultiplexedConnectionContext
{
public readonly Channel<ConnectionContext> AcceptQueue = Channel.CreateUnbounded<ConnectionContext>(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<object, object> Items { get; set; }
public override void Abort()
{
}
public override void Abort(ConnectionAbortedException abortReason)
{
}
public override async ValueTask<ConnectionContext> AcceptAsync(CancellationToken cancellationToken = default)
{
while (await AcceptQueue.Reader.WaitToReadAsync())
{
while (AcceptQueue.Reader.TryRead(out var connection))
{
return connection;
}
}
return null;
}
public override ValueTask<ConnectionContext> ConnectAsync(IFeatureCollection features = null, CancellationToken cancellationToken = default)
{
var stream = new Http3ControlStream(_testBase);
// TODO put these somewhere to be read.
return new ValueTask<ConnectionContext>(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<IStreamDirectionFeature>(this);
Features.Set<IStreamIdFeature>(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<object, object> 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);
}
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
@ -27,4 +27,4 @@
<Reference Include="Newtonsoft.Json" />
</ItemGroup>
</Project>
</Project>

View File

@ -18,7 +18,7 @@
<PropertyGroup>
<StartWorkingDirectory>$(MSBuildThisFileDirectory)..\..\</StartWorkingDirectory>
<StartArguments>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</StartArguments>
<StartArguments>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</StartArguments>
</PropertyGroup>
</Project>

View File

@ -27,16 +27,21 @@ namespace CodeGenerator
}
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 @@ namespace CodeGenerator
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))
UpdateFile(knownHeadersPath, knownHeadersContent);
UpdateFile(httpProtocolFeatureCollectionPath, httpProtocolFeatureCollectionContent);
UpdateFile(httpUtilitiesPath, httpUtilitiesContent);
UpdateFile(http2ConnectionPath, http2ConnectionContent);
UpdateFile(transportMultiplexedConnectionFeatureCollectionPath, transportMultiplexedConnectionFeatureCollectionContent);
UpdateFile(transportConnectionFeatureCollectionPath, transportConnectionFeatureCollectionContent);
}
public static void UpdateFile(string path, string content)
{
var existingContent = File.Exists(path) ? File.ReadAllText(path) : "";
if (!string.Equals(content, existingContent))
{
File.WriteAllText(knownHeadersPath, knownHeadersContent);
File.WriteAllText(path, content);
}
var existingHttpProtocolFeatureCollection = File.Exists(httpProtocolFeatureCollectionPath) ? File.ReadAllText(httpProtocolFeatureCollectionPath) : "";
if (!string.Equals(httpProtocolFeatureCollectionContent, existingHttpProtocolFeatureCollection))
var existingHttp2Connection = File.Exists(path) ? File.ReadAllText(path) : "";
if (!string.Equals(content, existingHttp2Connection))
{
File.WriteAllText(httpProtocolFeatureCollectionPath, httpProtocolFeatureCollectionContent);
}
var existingHttpUtilities = File.Exists(httpUtilitiesPath) ? File.ReadAllText(httpUtilitiesPath) : "";
if (!string.Equals(httpUtilitiesContent, existingHttpUtilities))
{
File.WriteAllText(httpUtilitiesPath, httpUtilitiesContent);
}
var existingTransportConnectionFeatureCollection = File.Exists(transportConnectionFeatureCollectionPath) ? File.ReadAllText(transportConnectionFeatureCollectionPath) : "";
if (!string.Equals(transportConnectionFeatureCollectionContent, existingTransportConnectionFeatureCollection))
{
File.WriteAllText(transportConnectionFeatureCollectionPath, transportConnectionFeatureCollectionContent);
}
var existingHttp2Connection = File.Exists(http2ConnectionPath) ? File.ReadAllText(http2ConnectionPath) : "";
if (!string.Equals(http2ConnectionContent, existingHttp2Connection))
{
File.WriteAllText(http2ConnectionPath, http2ConnectionContent);
File.WriteAllText(path, content);
}
}
}

View File

@ -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);
}
}
}

View File

@ -340,7 +340,6 @@ namespace System.Net.Http.QPack
return true;
}
// TODO these are fairly hard coded for the first two bytes to be zero.
public bool BeginEncode(IEnumerable<KeyValuePair<string, string>> headers, Span<byte> buffer, out int length)
{
_enumerator = headers.GetEnumerator();
@ -351,7 +350,11 @@ namespace System.Net.Http.QPack
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<KeyValuePair<string, string>> headers, Span<byte> buffer, out int length)

View File

@ -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 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
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