Initial HTTP/3 Implementation in Kestrel (#16914)

This commit is contained in:
Justin Kotalik 2019-11-15 14:50:54 -08:00 committed by GitHub
parent 2ff8f45193
commit 0f580f1082
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 5595 additions and 443 deletions

View File

@ -133,6 +133,7 @@ namespace Microsoft.Net.Http.Headers
public static readonly string AccessControlRequestMethod; public static readonly string AccessControlRequestMethod;
public static readonly string Age; public static readonly string Age;
public static readonly string Allow; public static readonly string Allow;
public static readonly string AltSvc;
public static readonly string Authority; public static readonly string Authority;
public static readonly string Authorization; public static readonly string Authorization;
public static readonly string CacheControl; public static readonly string CacheControl;

View File

@ -21,6 +21,7 @@ namespace Microsoft.Net.Http.Headers
public static readonly string AccessControlRequestMethod = "Access-Control-Request-Method"; public static readonly string AccessControlRequestMethod = "Access-Control-Request-Method";
public static readonly string Age = "Age"; public static readonly string Age = "Age";
public static readonly string Allow = "Allow"; public static readonly string Allow = "Allow";
public static readonly string AltSvc = "Alt-Svc";
public static readonly string Authority = ":authority"; public static readonly string Authority = ":authority";
public static readonly string Authorization = "Authorization"; public static readonly string Authorization = "Authorization";
public static readonly string CacheControl = "Cache-Control"; public static readonly string CacheControl = "Cache-Control";

View File

@ -123,6 +123,9 @@ 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)); 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
{
}
[System.FlagsAttribute] [System.FlagsAttribute]
public enum TransferFormat public enum TransferFormat
{ {
@ -193,6 +196,11 @@ namespace Microsoft.AspNetCore.Connections.Features
{ {
System.Buffers.MemoryPool<byte> MemoryPool { get; } System.Buffers.MemoryPool<byte> MemoryPool { get; }
} }
public partial interface IQuicStreamFeature
{
bool IsUnidirectional { get; }
long StreamId { get; }
}
public partial interface IQuicStreamListenerFeature public partial interface IQuicStreamListenerFeature
{ {
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync(); System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync();
@ -212,7 +220,4 @@ namespace Microsoft.AspNetCore.Connections.Features
Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; } Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; }
Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; } Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; }
} }
public partial interface IUnidirectionalStreamFeature
{
}
} }

View File

@ -123,6 +123,9 @@ 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)); 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
{
}
[System.FlagsAttribute] [System.FlagsAttribute]
public enum TransferFormat public enum TransferFormat
{ {
@ -193,6 +196,11 @@ namespace Microsoft.AspNetCore.Connections.Features
{ {
System.Buffers.MemoryPool<byte> MemoryPool { get; } System.Buffers.MemoryPool<byte> MemoryPool { get; }
} }
public partial interface IQuicStreamFeature
{
bool IsUnidirectional { get; }
long StreamId { get; }
}
public partial interface IQuicStreamListenerFeature public partial interface IQuicStreamListenerFeature
{ {
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync(); System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync();
@ -212,7 +220,4 @@ namespace Microsoft.AspNetCore.Connections.Features
Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; } Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; }
Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; } Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; }
} }
public partial interface IUnidirectionalStreamFeature
{
}
} }

View File

@ -123,6 +123,9 @@ 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)); 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
{
}
[System.FlagsAttribute] [System.FlagsAttribute]
public enum TransferFormat public enum TransferFormat
{ {
@ -193,6 +196,11 @@ namespace Microsoft.AspNetCore.Connections.Features
{ {
System.Buffers.MemoryPool<byte> MemoryPool { get; } System.Buffers.MemoryPool<byte> MemoryPool { get; }
} }
public partial interface IQuicStreamFeature
{
bool IsUnidirectional { get; }
long StreamId { get; }
}
public partial interface IQuicStreamListenerFeature public partial interface IQuicStreamListenerFeature
{ {
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync(); System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync();
@ -212,7 +220,4 @@ namespace Microsoft.AspNetCore.Connections.Features
Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; } Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; }
Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; } Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; }
} }
public partial interface IUnidirectionalStreamFeature
{
}
} }

View File

@ -3,7 +3,9 @@
namespace Microsoft.AspNetCore.Connections.Features namespace Microsoft.AspNetCore.Connections.Features
{ {
public interface IUnidirectionalStreamFeature public interface IQuicStreamFeature
{ {
bool IsUnidirectional { get; }
long StreamId { get; }
} }
} }

View File

@ -0,0 +1,9 @@
// 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
{
public interface IMultiplexedConnectionListenerFactory : IConnectionListenerFactory
{
}
}

View File

@ -77,6 +77,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
public int MaxRequestHeaderFieldSize { get { throw null; } set { } } public int MaxRequestHeaderFieldSize { get { throw null; } set { } }
public int MaxStreamsPerConnection { get { throw null; } set { } } public int MaxStreamsPerConnection { get { throw null; } set { } }
} }
public partial class Http3Limits
{
public Http3Limits() { }
public int HeaderTableSize { get { throw null; } set { } }
public int MaxRequestHeaderFieldSize { get { throw null; } set { } }
}
[System.FlagsAttribute] [System.FlagsAttribute]
public enum HttpProtocols public enum HttpProtocols
{ {
@ -84,10 +90,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
Http1 = 1, Http1 = 1,
Http2 = 2, Http2 = 2,
Http1AndHttp2 = 3, Http1AndHttp2 = 3,
Http3 = 4,
Http1AndHttp2AndHttp3 = 7,
} }
public partial class KestrelServer : Microsoft.AspNetCore.Hosting.Server.IServer, System.IDisposable public partial class KestrelServer : Microsoft.AspNetCore.Hosting.Server.IServer, System.IDisposable
{ {
public KestrelServer(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions> options, Microsoft.AspNetCore.Connections.IConnectionListenerFactory transportFactory, 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, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
public Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } 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 Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions Options { get { throw null; } }
public void Dispose() { } public void Dispose() { }
@ -100,6 +108,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
{ {
public KestrelServerLimits() { } public KestrelServerLimits() { }
public Microsoft.AspNetCore.Server.Kestrel.Core.Http2Limits Http2 { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public Microsoft.AspNetCore.Server.Kestrel.Core.Http2Limits Http2 { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public Microsoft.AspNetCore.Server.Kestrel.Core.Http3Limits Http3 { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public System.TimeSpan KeepAliveTimeout { get { throw null; } set { } } public System.TimeSpan KeepAliveTimeout { get { throw null; } set { } }
public long? MaxConcurrentConnections { get { throw null; } set { } } public long? MaxConcurrentConnections { get { throw null; } set { } }
public long? MaxConcurrentUpgradedConnections { get { throw null; } set { } } public long? MaxConcurrentUpgradedConnections { get { throw null; } set { } }
@ -121,6 +130,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public bool DisableStringReuse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public bool DisableStringReuse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public bool EnableAltSvc { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure() { throw null; } public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure() { throw null; }
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config) { throw null; } public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config) { throw null; }
@ -216,6 +226,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
Http10 = 0, Http10 = 0,
Http11 = 1, Http11 = 1,
Http2 = 2, Http2 = 2,
Http3 = 3,
} }
public partial interface IHttpRequestLineHandler public partial interface IHttpRequestLineHandler
{ {

View File

@ -562,4 +562,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
</data> <data name="BadDeveloperCertificateState" xml:space="preserve"> </data> <data name="BadDeveloperCertificateState" xml:space="preserve">
<value>The ASP.NET Core developer certificate is in an invalid state. To fix this issue, run the following commands 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' to remove all existing ASP.NET Core development certificates and create a new untrusted developer certificate. On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate.</value> <value>The ASP.NET Core developer certificate is in an invalid state. To fix this issue, run the following commands 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' to remove all existing ASP.NET Core development certificates and create a new untrusted developer certificate. On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate.</value>
</data> </data>
<data name="QPackErrorIndexOutOfRange" xml:space="preserve">
<value>Index {index} is outside the bounds of the header field table.</value>
</data>
<data name="QPackErrorIntegerTooBig" xml:space="preserve">
<value>The decoded integer exceeds the maximum value of Int32.MaxValue.</value>
</data>
<data name="QPackHuffmanError" xml:space="preserve">
<value>Huffman decoding error.</value>
</data>
<data name="QPackStringLengthTooLarge" xml:space="preserve">
<value>Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets.</value>
</data>
</root> </root>

View File

@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Server.Kestrel.Core
{
public class Http3Limits
{
private int _headerTableSize = 4096;
private int _maxRequestHeaderFieldSize = 8192;
/// <summary>
/// Limits the size of the header compression table, in octets, the HPACK decoder on the server can use.
/// <para>
/// Value must be greater than 0, defaults to 4096
/// </para>
/// </summary>
public int HeaderTableSize
{
get => _headerTableSize;
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanZeroRequired);
}
_headerTableSize = value;
}
}
/// <summary>
/// Indicates the size of the maximum allowed size of a request header field sequence. This limit applies to both name and value sequences in their compressed and uncompressed representations.
/// <para>
/// Value must be greater than 0, defaults to 8192
/// </para>
/// </summary>
public int MaxRequestHeaderFieldSize
{
get => _maxRequestHeaderFieldSize;
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanZeroRequired);
}
_maxRequestHeaderFieldSize = value;
}
}
}
}

View File

@ -12,5 +12,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
Http1 = 0x1, Http1 = 0x1,
Http2 = 0x2, Http2 = 0x2,
Http1AndHttp2 = Http1 | Http2, Http1AndHttp2 = Http1 | Http2,
Http3 = 0x4,
Http1AndHttp2AndHttp3 = Http1 | Http2 | Http3
} }
} }

View File

@ -155,6 +155,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
return HttpUtilities.Http2Version; return HttpUtilities.Http2Version;
} }
if (_httpVersion == Http.HttpVersion.Http3)
{
return HttpUtilities.Http3Version;
}
return string.Empty; return string.Empty;
} }
@ -176,6 +180,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
_httpVersion = Http.HttpVersion.Http2; _httpVersion = Http.HttpVersion.Http2;
} }
else if (ReferenceEquals(value, HttpUtilities.Http3Version))
{
_httpVersion = Http.HttpVersion.Http3;
}
else else
{ {
HttpVersionSetSlow(value); HttpVersionSetSlow(value);
@ -198,6 +206,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
_httpVersion = Http.HttpVersion.Http2; _httpVersion = Http.HttpVersion.Http2;
} }
else if (value == HttpUtilities.Http3Version)
{
_httpVersion = Http.HttpVersion.Http3;
}
else else
{ {
_httpVersion = Http.HttpVersion.Unknown; _httpVersion = Http.HttpVersion.Unknown;
@ -1044,7 +1056,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private Task WriteSuffix() private Task WriteSuffix()
{ {
if (_autoChunk || _httpVersion == Http.HttpVersion.Http2) if (_autoChunk || _httpVersion >= Http.HttpVersion.Http2)
{ {
// For the same reason we call CheckLastWrite() in Content-Length responses. // For the same reason we call CheckLastWrite() in Content-Length responses.
PreventRequestAbortedCancellation(); PreventRequestAbortedCancellation();
@ -1160,7 +1172,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
responseHeaders.SetReadOnly(); responseHeaders.SetReadOnly();
if (!hasConnection && _httpVersion != Http.HttpVersion.Http2) if (!hasConnection && _httpVersion < Http.HttpVersion.Http2)
{ {
if (!_keepAlive) if (!_keepAlive)
{ {

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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
@ -8,6 +8,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
Unknown = -1, Unknown = -1,
Http10 = 0, Http10 = 0,
Http11 = 1, Http11 = 1,
Http2 Http2 = 2,
Http3 = 3
} }
} }

View File

@ -0,0 +1,101 @@
// 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.Server.Kestrel.Core.Internal.Http3
{
internal enum Http3ErrorCode : uint
{
/// <summary>
/// HTTP_NO_ERROR (0x100):
/// No error. This is used when the connection or stream needs to be closed, but there is no error to signal.
/// </summary>
NoError = 0x100,
/// <summary>
/// HTTP_GENERAL_PROTOCOL_ERROR (0x101):
/// Peer violated protocol requirements in a way which doesnt match a more specific error code,
/// or endpoint declines to use the more specific error code.
/// </summary>
ProtocolError = 0x101,
/// <summary>
/// HTTP_INTERNAL_ERROR (0x102):
/// An internal error has occurred in the HTTP stack.
/// </summary>
InternalError = 0x102,
/// <summary>
/// HTTP_STREAM_CREATION_ERROR (0x103):
/// The endpoint detected that its peer created a stream that it will not accept.
/// </summary>
StreamCreationError = 0x103,
/// <summary>
/// HTTP_CLOSED_CRITICAL_STREAM (0x104):
/// A stream required by the connection was closed or reset.
/// </summary>
ClosedCriticalStream = 0x104,
/// <summary>
/// HTTP_UNEXPECTED_FRAME (0x105):
/// A frame was received which was not permitted in the current state.
/// </summary>
UnexpectedFrame = 0x105,
/// <summary>
/// HTTP_FRAME_ERROR (0x106):
/// A frame that fails to satisfy layout requirements or with an invalid size was received.
/// </summary>
FrameError = 0x106,
/// <summary>
/// HTTP_EXCESSIVE_LOAD (0x107):
/// The endpoint detected that its peer is exhibiting a behavior that might be generating excessive load.
/// </summary>
ExcessiveLoad = 0x107,
/// <summary>
/// HTTP_WRONG_STREAM (0x108):
/// A frame was received on a stream where it is not permitted.
/// </summary>
WrongStream = 0x108,
/// <summary>
/// HTTP_ID_ERROR (0x109):
/// A Stream ID, Push ID, or Placeholder ID was used incorrectly, such as exceeding a limit, reducing a limit, or being reused.
/// </summary>
IdError = 0x109,
/// <summary>
/// HTTP_SETTINGS_ERROR (0x10A):
/// An endpoint detected an error in the payload of a SETTINGS frame: a duplicate setting was detected,
/// a client-only setting was sent by a server, or a server-only setting by a client.
/// </summary>
SettingsError = 0x10a,
/// <summary>
/// HTTP_MISSING_SETTINGS (0x10B):
/// No SETTINGS frame was received at the beginning of the control stream.
/// </summary>
MissingSettings = 0x10b,
/// <summary>
/// HTTP_REQUEST_REJECTED (0x10C):
/// A server rejected a request without performing any application processing.
/// </summary>
RequestRejected = 0x10c,
/// <summary>
/// HTTP_REQUEST_CANCELLED (0x10D):
/// The request or its response (including pushed response) is cancelled.
/// </summary>
RequestCancelled = 0x10d,
/// <summary>
/// HTTP_REQUEST_INCOMPLETE (0x10E):
/// The clients stream terminated without containing a fully-formed request.
/// </summary>
RequestIncomplete = 0x10e,
/// <summary>
/// HTTP_EARLY_RESPONSE (0x10F):
/// The remainder of the clients request is not needed to produce a response. For use in STOP_SENDING only.
/// </summary>
EarlyResponse = 0x10f,
/// <summary>
/// HTTP_CONNECT_ERROR (0x110):
/// The connection established in response to a CONNECT request was reset or abnormally closed.
/// </summary>
ConnectError = 0x110,
/// <summary>
/// HTTP_VERSION_FALLBACK (0x111):
/// The requested operation cannot be served over HTTP/3. The peer should retry over HTTP/1.1.
/// </summary>
VersionFallback = 0x111,
}
}

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.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
internal partial class Http3Frame
{
public void PrepareData()
{
Length = 0;
Type = Http3FrameType.Data;
}
}
}

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.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
internal partial class Http3Frame
{
public void PrepareGoAway()
{
Length = 0;
Type = Http3FrameType.GoAway;
}
}
}

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.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
internal partial class Http3Frame
{
public void PrepareHeaders()
{
Length = 0;
Type = Http3FrameType.Headers;
}
}
}

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.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
internal partial class Http3Frame
{
public void PrepareSettings()
{
Length = 0;
Type = Http3FrameType.Settings;
}
}
}

View File

@ -0,0 +1,17 @@
// 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.Server.Kestrel.Core.Internal.Http3
{
internal partial class Http3Frame
{
public long Length { get; set; }
public Http3FrameType Type { get; internal set; }
public override string ToString()
{
return $"{Type} Length: {Length}";
}
}
}

View File

@ -0,0 +1,17 @@
// 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.Server.Kestrel.Core.Internal.Http3
{
internal enum Http3FrameType
{
Data = 0x0,
Headers = 0x1,
CancelPush = 0x3,
Settings = 0x4,
PushPromise = 0x5,
GoAway = 0x7,
MaxPushId = 0xD,
DuplicatePush = 0xE
}
}

View File

@ -0,0 +1,115 @@
// 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.Buffers;
using System.Buffers.Binary;
using System.Diagnostics;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
/// <summary>
/// Variable length integer encoding and decoding methods. Based on https://tools.ietf.org/html/draft-ietf-quic-transport-24#section-16.
/// Either will take up 1, 2, 4, or 8 bytes.
/// </summary>
internal static class VariableLengthIntegerHelper
{
private const int TwoByteSubtract = 0x4000;
private const uint FourByteSubtract = 0x80000000;
private const ulong EightByteSubtract = 0xC000000000000000;
private const int OneByteLimit = 64;
private const int TwoByteLimit = 16383;
private const int FourByteLimit = 1073741823;
public static long GetInteger(in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
{
consumed = buffer.Start;
examined = buffer.End;
if (buffer.Length == 0)
{
return -1;
}
// The first two bits of the first byte represent the length of the
// variable length integer
// 00 = length 1
// 01 = length 2
// 10 = length 4
// 11 = length 8
var span = buffer.Slice(0, Math.Min(buffer.Length, 8)).ToSpan();
var firstByte = span[0];
if ((firstByte & 0xC0) == 0)
{
consumed = examined = buffer.GetPosition(1);
return firstByte & 0x3F;
}
else if ((firstByte & 0xC0) == 0x40)
{
if (span.Length < 2)
{
return -1;
}
consumed = examined = buffer.GetPosition(2);
return BinaryPrimitives.ReadUInt16BigEndian(span) - TwoByteSubtract;
}
else if ((firstByte & 0xC0) == 0x80)
{
if (span.Length < 4)
{
return -1;
}
consumed = examined = buffer.GetPosition(4);
return BinaryPrimitives.ReadUInt32BigEndian(span) - FourByteSubtract;
}
else
{
if (span.Length < 8)
{
return -1;
}
consumed = examined = buffer.GetPosition(8);
return (long)(BinaryPrimitives.ReadUInt64BigEndian(span) - EightByteSubtract);
}
}
public static int WriteInteger(Span<byte> buffer, long longToEncode)
{
Debug.Assert(buffer.Length >= 8);
Debug.Assert(longToEncode < long.MaxValue / 2);
if (longToEncode < OneByteLimit)
{
buffer[0] = (byte)longToEncode;
return 1;
}
else if (longToEncode < TwoByteLimit)
{
BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)longToEncode);
buffer[0] += 0x40;
return 2;
}
else if (longToEncode < FourByteLimit)
{
BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)longToEncode);
buffer[0] += 0x80;
return 4;
}
else
{
BinaryPrimitives.WriteUInt64BigEndian(buffer, (ulong)longToEncode);
buffer[0] += 0xC0;
return 8;
}
}
}
}

View File

@ -0,0 +1,206 @@
// 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.Concurrent;
using System.Net;
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.Server.Kestrel.Core.Internal.Http3.QPack;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
internal class Http3Connection : IRequestProcessor
{
public HttpConnectionContext Context { get; private set; }
public DynamicTable DynamicTable { get; set; }
public Http3ControlStream SettingsStream { get; set; }
public Http3ControlStream EncoderStream { get; set; }
public Http3ControlStream DecoderStream { get; set; }
private readonly ConcurrentDictionary<long, Http3Stream> _streams = new ConcurrentDictionary<long, Http3Stream>();
// To be used by GO_AWAY
private long _highestOpenedStreamId; // TODO lock to access
//private volatile bool _haveSentGoAway;
public Http3Connection(HttpConnectionContext context)
{
Context = context;
DynamicTable = new DynamicTable(0);
}
internal long HighestStreamId
{
get
{
return _highestOpenedStreamId;
}
set
{
if (_highestOpenedStreamId < value)
{
_highestOpenedStreamId = value;
}
}
}
public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> application)
{
var streamListenerFeature = Context.ConnectionFeatures.Get<IQuicStreamListenerFeature>();
// Start other three unidirectional streams here.
var settingsStream = CreateSettingsStream(application);
var encoderStream = CreateEncoderStream(application);
var decoderStream = CreateDecoderStream(application);
try
{
while (true)
{
var connectionContext = await streamListenerFeature.AcceptAsync();
if (connectionContext == null)
{
break;
}
//if (_haveSentGoAway)
//{
// // error here.
//}
var httpConnectionContext = new HttpConnectionContext
{
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
};
var streamFeature = httpConnectionContext.ConnectionFeatures.Get<IQuicStreamFeature>();
var streamId = streamFeature.StreamId;
HighestStreamId = streamId;
if (streamFeature.IsUnidirectional)
{
var stream = new Http3ControlStream<TContext>(application, this, httpConnectionContext);
ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
}
else
{
var http3Stream = new Http3Stream<TContext>(application, this, httpConnectionContext);
var stream = http3Stream;
_streams[streamId] = http3Stream;
ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
}
}
}
finally
{
await settingsStream;
await encoderStream;
await decoderStream;
foreach (var stream in _streams.Values)
{
stream.Abort(new ConnectionAbortedException(""));
}
}
}
private async ValueTask CreateSettingsStream<TContext>(IHttpApplication<TContext> application)
{
var stream = await CreateNewUnidirectionalStreamAsync(application);
SettingsStream = stream;
await stream.SendStreamIdAsync(id: 0);
await stream.SendSettingsFrameAsync();
}
private async ValueTask CreateEncoderStream<TContext>(IHttpApplication<TContext> application)
{
var stream = await CreateNewUnidirectionalStreamAsync(application);
EncoderStream = stream;
await stream.SendStreamIdAsync(id: 2);
}
private async ValueTask CreateDecoderStream<TContext>(IHttpApplication<TContext> application)
{
var stream = await CreateNewUnidirectionalStreamAsync(application);
DecoderStream = stream;
await stream.SendStreamIdAsync(id: 3);
}
private async ValueTask<Http3ControlStream> CreateNewUnidirectionalStreamAsync<TContext>(IHttpApplication<TContext> application)
{
var connectionContext = await Context.ConnectionFeatures.Get<IQuicCreateStreamFeature>().StartUnidirectionalStreamAsync();
var httpConnectionContext = new HttpConnectionContext
{
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
};
return new Http3ControlStream<TContext>(application, this, httpConnectionContext);
}
public void StopProcessingNextRequest()
{
}
public void HandleRequestHeadersTimeout()
{
}
public void HandleReadDataRateTimeout()
{
}
public void OnInputOrOutputCompleted()
{
}
public void Tick(DateTimeOffset now)
{
}
public void Abort(ConnectionAbortedException ex)
{
// Send goaway
// Abort currently active streams
// TODO need to figure out if there is server initiated connection close rather than stream close?
}
public void ApplyMaxHeaderListSize(long value)
{
// TODO something here to call OnHeader?
}
internal void ApplyBlockedStream(long value)
{
}
internal void ApplyMaxTableCapacity(long value)
{
// TODO make sure this works
//_maxDynamicTableSize = value;
}
}
}

View File

@ -0,0 +1,28 @@
// 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.Runtime.Serialization;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
[Serializable]
internal class Http3ConnectionException : Exception
{
public Http3ConnectionException()
{
}
public Http3ConnectionException(string message) : base(message)
{
}
public Http3ConnectionException(string message, Exception innerException) : base(message, innerException)
{
}
protected Http3ConnectionException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

View File

@ -0,0 +1,388 @@
// 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.Buffers;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
internal abstract class Http3ControlStream : IThreadPoolWorkItem
{
private const int ControlStream = 0;
private const int EncoderStream = 2;
private const int DecoderStream = 3;
private Http3FrameWriter _frameWriter;
private readonly Http3Connection _http3Connection;
private HttpConnectionContext _context;
private readonly Http3Frame _incomingFrame = new Http3Frame();
private volatile int _isClosed;
private int _gracefulCloseInitiator;
private bool _haveReceivedSettingsFrame;
public Http3ControlStream(Http3Connection http3Connection, HttpConnectionContext context)
{
var httpLimits = context.ServiceContext.ServerOptions.Limits;
_http3Connection = http3Connection;
_context = context;
_frameWriter = new Http3FrameWriter(
context.Transport.Output,
context.ConnectionContext,
context.TimeoutControl,
httpLimits.MinResponseDataRate,
context.ConnectionId,
context.MemoryPool,
context.ServiceContext.Log);
}
private void OnStreamClosed()
{
Abort(new ConnectionAbortedException("HTTP_CLOSED_CRITICAL_STREAM"));
}
public PipeReader Input => _context.Transport.Input;
public void Abort(ConnectionAbortedException ex)
{
}
public void HandleReadDataRateTimeout()
{
//Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, null, Limits.MinRequestBodyDataRate.BytesPerSecond);
Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestBodyTimeout));
}
public void HandleRequestHeadersTimeout()
{
//Log.ConnectionBadRequest(ConnectionId, BadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout));
Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout));
}
public void OnInputOrOutputCompleted()
{
TryClose();
_frameWriter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient));
}
private bool TryClose()
{
if (Interlocked.Exchange(ref _isClosed, 1) == 0)
{
return true;
}
// TODO make this actually close the Http3Stream by telling msquic to close the stream.
return false;
}
private async ValueTask HandleEncodingTask()
{
var encoder = new EncoderStreamReader(10000); // TODO get value from limits
while (_isClosed == 0)
{
var result = await Input.ReadAsync();
var readableBuffer = result.Buffer;
if (!readableBuffer.IsEmpty)
{
// This should always read all bytes in the input no matter what.
encoder.Read(readableBuffer);
}
Input.AdvanceTo(readableBuffer.End);
}
}
private async ValueTask HandleDecodingTask()
{
var decoder = new DecoderStreamReader();
while (_isClosed == 0)
{
var result = await Input.ReadAsync();
var readableBuffer = result.Buffer;
var consumed = readableBuffer.Start;
var examined = readableBuffer.Start;
if (!readableBuffer.IsEmpty)
{
decoder.Read(readableBuffer);
}
Input.AdvanceTo(readableBuffer.End);
}
}
internal async ValueTask SendStreamIdAsync(int id)
{
await _frameWriter.WriteStreamIdAsync(id);
}
internal async ValueTask SendSettingsFrameAsync()
{
await _frameWriter.WriteSettingsAsync(null);
}
private async ValueTask<long> TryReadStreamIdAsync()
{
while (_isClosed == 0)
{
var result = await Input.ReadAsync();
var readableBuffer = result.Buffer;
var consumed = readableBuffer.Start;
var examined = readableBuffer.End;
try
{
if (!readableBuffer.IsEmpty)
{
var id = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out examined);
if (id != -1)
{
return id;
}
}
if (result.IsCompleted)
{
return -1;
}
}
finally
{
Input.AdvanceTo(consumed, examined);
}
}
return -1;
}
public async Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> application)
{
var streamType = await TryReadStreamIdAsync();
if (streamType == -1)
{
return;
}
if (streamType == ControlStream)
{
if (_http3Connection.SettingsStream != null)
{
throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR");
}
await HandleControlStream();
}
else if (streamType == EncoderStream)
{
if (_http3Connection.EncoderStream != null)
{
throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR");
}
await HandleEncodingTask();
return;
}
else if (streamType == DecoderStream)
{
if (_http3Connection.DecoderStream != null)
{
throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR");
}
await HandleDecodingTask();
}
else
{
// TODO Close the control stream as it's unexpected.
}
return;
}
private async Task HandleControlStream()
{
while (_isClosed == 0)
{
var result = await Input.ReadAsync();
var readableBuffer = result.Buffer;
var consumed = readableBuffer.Start;
var examined = readableBuffer.End;
try
{
if (!readableBuffer.IsEmpty)
{
// need to kick off httpprotocol process request async here.
while (Http3FrameReader.TryReadFrame(ref readableBuffer, _incomingFrame, 16 * 1024, out var framePayload))
{
//Log.Http2FrameReceived(ConnectionId, _incomingFrame);
consumed = examined = framePayload.End;
await ProcessHttp3ControlStream(framePayload);
}
}
if (result.IsCompleted)
{
return;
}
}
catch (Http3StreamErrorException)
{
}
finally
{
Input.AdvanceTo(consumed, examined);
}
}
}
private ValueTask ProcessHttp3ControlStream(in ReadOnlySequence<byte> payload)
{
// Two things:
// settings must be sent as the first frame of each control stream by each peer
// Can't send more than two settings frames.
switch (_incomingFrame.Type)
{
case Http3FrameType.Data:
case Http3FrameType.Headers:
case Http3FrameType.DuplicatePush:
case Http3FrameType.PushPromise:
throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
case Http3FrameType.Settings:
return ProcessSettingsFrameAsync(payload);
case Http3FrameType.GoAway:
return ProcessGoAwayFrameAsync();
case Http3FrameType.CancelPush:
return ProcessCancelPushFrameAsync();
case Http3FrameType.MaxPushId:
return ProcessMaxPushIdFrameAsync();
default:
return ProcessUnknownFrameAsync();
}
}
private ValueTask ProcessSettingsFrameAsync(ReadOnlySequence<byte> payload)
{
if (_haveReceivedSettingsFrame)
{
throw new Http3ConnectionException("H3_SETTINGS_ERROR");
}
_haveReceivedSettingsFrame = true;
using var closedRegistration = _context.ConnectionContext.ConnectionClosed.Register(state => ((Http3ControlStream)state).OnStreamClosed(), this);
while (true)
{
var id = VariableLengthIntegerHelper.GetInteger(payload, out var consumed, out var examinded);
if (id == -1)
{
break;
}
payload = payload.Slice(consumed);
var value = VariableLengthIntegerHelper.GetInteger(payload, out consumed, out examinded);
if (id == -1)
{
break;
}
payload = payload.Slice(consumed);
ProcessSetting(id, value);
}
return default;
}
private void ProcessSetting(long id, long value)
{
// These are client settings, for outbound traffic.
switch (id)
{
case (long)Http3SettingType.QPackMaxTableCapacity:
_http3Connection.ApplyMaxTableCapacity(value);
break;
case (long)Http3SettingType.MaxHeaderListSize:
_http3Connection.ApplyMaxHeaderListSize(value);
break;
case (long)Http3SettingType.QPackBlockedStreams:
_http3Connection.ApplyBlockedStream(value);
break;
default:
// Ignore all unknown settings.
break;
}
}
private ValueTask ProcessGoAwayFrameAsync()
{
if (!_haveReceivedSettingsFrame)
{
throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
}
// Get highest stream id and write that to the response.
return default;
}
private ValueTask ProcessCancelPushFrameAsync()
{
if (!_haveReceivedSettingsFrame)
{
throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
}
// This should just noop.
return default;
}
private ValueTask ProcessMaxPushIdFrameAsync()
{
if (!_haveReceivedSettingsFrame)
{
throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
}
return default;
}
private ValueTask ProcessUnknownFrameAsync()
{
if (!_haveReceivedSettingsFrame)
{
throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
}
return default;
}
public void StopProcessingNextRequest()
=> StopProcessingNextRequest(serverInitiated: true);
public void StopProcessingNextRequest(bool serverInitiated)
{
var initiator = serverInitiated ? GracefulCloseInitiator.Server : GracefulCloseInitiator.Client;
if (Interlocked.CompareExchange(ref _gracefulCloseInitiator, initiator, GracefulCloseInitiator.None) == GracefulCloseInitiator.None)
{
Input.CancelPendingRead();
}
}
public void Tick(DateTimeOffset now)
{
}
/// <summary>
/// Used to kick off the request processing loop by derived classes.
/// </summary>
public abstract void Execute();
private static class GracefulCloseInitiator
{
public const int None = 0;
public const int Server = 1;
public const int Client = 2;
}
}
}

View File

@ -0,0 +1,26 @@
// 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.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Abstractions;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
internal sealed class Http3ControlStream<TContext> : Http3ControlStream, IHostContextContainer<TContext>
{
private readonly IHttpApplication<TContext> _application;
public Http3ControlStream(IHttpApplication<TContext> application, Http3Connection connection, HttpConnectionContext context) : base(connection, context)
{
_application = application;
}
public override void Execute()
{
_ = ProcessRequestAsync(_application);
}
// Pooled Host context
TContext IHostContextContainer<TContext>.HostContext { get; set; }
}
}

View File

@ -0,0 +1,59 @@
// 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;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
internal class Http3FrameReader
{
/* https://quicwg.org/base-drafts/draft-ietf-quic-http.html#frame-layout
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length (i) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Frame Payload (*) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
internal static bool TryReadFrame(ref ReadOnlySequence<byte> readableBuffer, Http3Frame frame, uint maxFrameSize, out ReadOnlySequence<byte> framePayload)
{
framePayload = ReadOnlySequence<byte>.Empty;
var consumed = readableBuffer.Start;
var examined = readableBuffer.Start;
var type = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out examined);
if (type == -1)
{
return false;
}
var firstLengthBuffer = readableBuffer.Slice(consumed);
var length = VariableLengthIntegerHelper.GetInteger(firstLengthBuffer, out consumed, out examined);
// Make sure the whole frame is buffered
if (length == -1)
{
return false;
}
var startOfFramePayload = readableBuffer.Slice(consumed);
if (startOfFramePayload.Length < length)
{
return false;
}
frame.Length = length;
frame.Type = (Http3FrameType)type;
// The remaining payload minus the extra fields
framePayload = startOfFramePayload.Slice(0, length);
readableBuffer = readableBuffer.Slice(framePayload.End);
return true;
}
}
}

View File

@ -0,0 +1,326 @@
// 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.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeWriterHelpers;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
internal class Http3FrameWriter
{
private readonly object _writeLock = new object();
private readonly QPackEncoder _qpackEncoder = new QPackEncoder();
private readonly PipeWriter _outputWriter;
private readonly ConnectionContext _connectionContext;
private readonly ITimeoutControl _timeoutControl;
private readonly MinDataRate _minResponseDataRate;
private readonly MemoryPool<byte> _memoryPool;
private readonly IKestrelTrace _log;
private readonly Http3Frame _outgoingFrame;
private readonly TimingPipeFlusher _flusher;
// TODO update max frame size
private uint _maxFrameSize = 10000; //Http3PeerSettings.MinAllowedMaxFrameSize;
private byte[] _headerEncodingBuffer;
private long _unflushedBytes;
private bool _completed;
private bool _aborted;
//private int _unflushedBytes;
public Http3FrameWriter(PipeWriter output, ConnectionContext connectionContext, ITimeoutControl timeoutControl, MinDataRate minResponseDataRate, string connectionId, MemoryPool<byte> memoryPool, IKestrelTrace log)
{
_outputWriter = output;
_connectionContext = connectionContext;
_timeoutControl = timeoutControl;
_minResponseDataRate = minResponseDataRate;
_memoryPool = memoryPool;
_log = log;
_outgoingFrame = new Http3Frame();
_flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, log);
_headerEncodingBuffer = new byte[_maxFrameSize];
}
public void UpdateMaxFrameSize(uint maxFrameSize)
{
lock (_writeLock)
{
if (_maxFrameSize != maxFrameSize)
{
_maxFrameSize = maxFrameSize;
_headerEncodingBuffer = new byte[_maxFrameSize];
}
}
}
// TODO actually write settings here.
internal Task WriteSettingsAsync(IList<Http3PeerSettings> settings)
{
_outgoingFrame.PrepareSettings();
var buffer = _outputWriter.GetSpan(2);
buffer[0] = (byte)_outgoingFrame.Type;
buffer[1] = 0;
_outputWriter.Advance(2);
return _outputWriter.FlushAsync().AsTask();
}
internal Task WriteStreamIdAsync(long id)
{
var buffer = _outputWriter.GetSpan(8);
_outputWriter.Advance(VariableLengthIntegerHelper.WriteInteger(buffer, id));
return _outputWriter.FlushAsync().AsTask();
}
public ValueTask<FlushResult> WriteDataAsync(in ReadOnlySequence<byte> data)
{
// The Length property of a ReadOnlySequence can be expensive, so we cache the value.
var dataLength = data.Length;
lock (_writeLock)
{
if (_completed)
{
return default;
}
WriteDataUnsynchronized(data, dataLength);
return TimeFlushUnsynchronizedAsync();
}
}
private void WriteDataUnsynchronized(in ReadOnlySequence<byte> data, long dataLength)
{
Debug.Assert(dataLength == data.Length);
_outgoingFrame.PrepareData();
if (dataLength > _maxFrameSize)
{
SplitAndWriteDataUnsynchronized(in data, dataLength);
return;
}
_outgoingFrame.Length = (int)dataLength;
WriteHeaderUnsynchronized();
foreach (var buffer in data)
{
_outputWriter.Write(buffer.Span);
}
return;
void SplitAndWriteDataUnsynchronized(in ReadOnlySequence<byte> data, long dataLength)
{
Debug.Assert(dataLength == data.Length);
var dataPayloadLength = (int)_maxFrameSize;
Debug.Assert(dataLength > dataPayloadLength);
var remainingData = data;
do
{
var currentData = remainingData.Slice(0, dataPayloadLength);
_outgoingFrame.Length = dataPayloadLength;
WriteHeaderUnsynchronized();
foreach (var buffer in currentData)
{
_outputWriter.Write(buffer.Span);
}
dataLength -= dataPayloadLength;
remainingData = remainingData.Slice(dataPayloadLength);
} while (dataLength > dataPayloadLength);
_outgoingFrame.Length = (int)dataLength;
WriteHeaderUnsynchronized();
foreach (var buffer in remainingData)
{
_outputWriter.Write(buffer.Span);
}
}
}
private void WriteHeaderUnsynchronized()
{
var headerLength = WriteHeader(_outgoingFrame, _outputWriter);
// We assume the payload will be written prior to the next flush.
_unflushedBytes += headerLength + _outgoingFrame.Length;
}
internal static int WriteHeader(Http3Frame frame, PipeWriter output)
{
// max size of the header is 16, most likely it will be smaller.
var buffer = output.GetSpan(16);
var typeLength = VariableLengthIntegerHelper.WriteInteger(buffer, (int)frame.Type);
buffer = buffer.Slice(typeLength);
var lengthLength = VariableLengthIntegerHelper.WriteInteger(buffer, (int)frame.Length);
var totalLength = typeLength + lengthLength;
output.Advance(typeLength + lengthLength);
return totalLength;
}
public ValueTask<FlushResult> WriteResponseTrailers(int streamId, HttpResponseTrailers headers)
{
lock (_writeLock)
{
if (_completed)
{
return default;
}
try
{
_outgoingFrame.PrepareHeaders();
var buffer = _headerEncodingBuffer.AsSpan();
var done = _qpackEncoder.BeginEncode(EnumerateHeaders(headers), buffer, out var payloadLength);
FinishWritingHeaders(streamId, payloadLength, done);
}
catch (QPackEncodingException)
{
//_log.HPackEncodingError(_connectionId, streamId, hex);
//_http3Stream.Abort(new ConnectionAbortedException(hex.Message, hex));
}
return TimeFlushUnsynchronizedAsync();
}
}
private ValueTask<FlushResult> TimeFlushUnsynchronizedAsync()
{
var bytesWritten = _unflushedBytes;
_unflushedBytes = 0;
return _flusher.FlushAsync(_minResponseDataRate, bytesWritten);
}
public ValueTask<FlushResult> FlushAsync(IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
{
lock (_writeLock)
{
if (_completed)
{
return default;
}
var bytesWritten = _unflushedBytes;
_unflushedBytes = 0;
return _flusher.FlushAsync(_minResponseDataRate, bytesWritten, outputAborter, cancellationToken);
}
}
internal void WriteResponseHeaders(int streamId, int statusCode, IHeaderDictionary headers)
{
lock (_writeLock)
{
if (_completed)
{
return;
}
try
{
_outgoingFrame.PrepareHeaders();
var buffer = _headerEncodingBuffer.AsSpan();
var done = _qpackEncoder.BeginEncode(statusCode, EnumerateHeaders(headers), buffer, out var payloadLength);
FinishWritingHeaders(streamId, payloadLength, done);
}
catch (QPackEncodingException hex)
{
// TODO figure out how to abort the stream here.
//_http3Stream.Abort(new ConnectionAbortedException(hex.Message, hex));
throw new InvalidOperationException(hex.Message, hex); // Report the error to the user if this was the first write.
}
}
}
private void FinishWritingHeaders(int streamId, int payloadLength, bool done)
{
var buffer = _headerEncodingBuffer.AsSpan();
_outgoingFrame.Length = payloadLength;
WriteHeaderUnsynchronized();
_outputWriter.Write(buffer.Slice(0, payloadLength));
while (!done)
{
done = _qpackEncoder.Encode(buffer, out payloadLength);
_outgoingFrame.Length = payloadLength;
WriteHeaderUnsynchronized();
_outputWriter.Write(buffer.Slice(0, payloadLength));
}
}
public void Complete()
{
lock (_writeLock)
{
if (_completed)
{
return;
}
_completed = true;
_outputWriter.Complete();
}
}
public void Abort(ConnectionAbortedException error)
{
lock (_writeLock)
{
if (_aborted)
{
return;
}
_aborted = true;
_connectionContext.Abort(error);
Complete();
}
}
private static IEnumerable<KeyValuePair<string, string>> EnumerateHeaders(IHeaderDictionary headers)
{
foreach (var header in headers)
{
foreach (var value in header.Value)
{
yield return new KeyValuePair<string, string>(header.Key, value);
}
}
}
}
}

View File

@ -0,0 +1,126 @@
// 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.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
internal sealed class Http3MessageBody : MessageBody
{
private readonly Http3Stream _context;
private ReadResult _readResult;
private Http3MessageBody(Http3Stream context)
: base(context)
{
_context = context;
}
protected override void OnReadStarting()
{
// Note ContentLength or MaxRequestBodySize may be null
if (_context.RequestHeaders.ContentLength > _context.MaxRequestBodySize)
{
BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge);
}
}
protected override void OnReadStarted()
{
}
protected override void OnDataRead(long bytesRead)
{
AddAndCheckConsumedBytes(bytesRead);
}
public static MessageBody For(Http3Stream context)
{
return new Http3MessageBody(context);
}
public override void AdvanceTo(SequencePosition consumed)
{
AdvanceTo(consumed, consumed);
}
public override void AdvanceTo(SequencePosition consumed, SequencePosition examined)
{
OnAdvance(_readResult, consumed, examined);
_context.RequestBodyPipe.Reader.AdvanceTo(consumed, examined);
}
public override bool TryRead(out ReadResult readResult)
{
TryStart();
var hasResult = _context.RequestBodyPipe.Reader.TryRead(out readResult);
if (hasResult)
{
_readResult = readResult;
CountBytesRead(readResult.Buffer.Length);
if (readResult.IsCompleted)
{
TryStop();
}
}
return hasResult;
}
public override async ValueTask<ReadResult> ReadAsync(CancellationToken cancellationToken = default)
{
TryStart();
try
{
var readAwaitable = _context.RequestBodyPipe.Reader.ReadAsync(cancellationToken);
_readResult = await StartTimingReadAsync(readAwaitable, cancellationToken);
}
catch (ConnectionAbortedException ex)
{
throw new TaskCanceledException("The request was aborted", ex);
}
StopTimingRead(_readResult.Buffer.Length);
if (_readResult.IsCompleted)
{
TryStop();
}
return _readResult;
}
public override void Complete(Exception exception)
{
_context.RequestBodyPipe.Reader.Complete();
_context.ReportApplicationError(exception);
}
public override void CancelPendingRead()
{
_context.RequestBodyPipe.Reader.CancelPendingRead();
}
protected override Task OnStopAsync()
{
if (!_context.HasStartedConsumingRequestBody)
{
return Task.CompletedTask;
}
_context.RequestBodyPipe.Reader.Complete();
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,406 @@
// 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.Buffers;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeWriterHelpers;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using Microsoft.AspNetCore.Internal;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
internal class Http3OutputProducer : IHttpOutputProducer, IHttpOutputAborter
{
private readonly int _streamId;
private readonly Http3FrameWriter _frameWriter;
private readonly TimingPipeFlusher _flusher;
private readonly IKestrelTrace _log;
private readonly MemoryPool<byte> _memoryPool;
private readonly Http3Stream _stream;
private readonly PipeWriter _pipeWriter;
private readonly PipeReader _pipeReader;
private readonly object _dataWriterLock = new object();
private readonly ValueTask<FlushResult> _dataWriteProcessingTask;
private bool _startedWritingDataFrames;
private bool _completed;
private bool _disposed;
private bool _suffixSent;
private IMemoryOwner<byte> _fakeMemoryOwner;
public Http3OutputProducer(
int streamId,
Http3FrameWriter frameWriter,
MemoryPool<byte> pool,
Http3Stream stream,
IKestrelTrace log)
{
_streamId = streamId;
_frameWriter = frameWriter;
_memoryPool = pool;
_stream = stream;
_log = log;
var pipe = CreateDataPipe(pool);
_pipeWriter = pipe.Writer;
_pipeReader = pipe.Reader;
_flusher = new TimingPipeFlusher(_pipeWriter, timeoutControl: null, log);
_dataWriteProcessingTask = ProcessDataWrites();
}
public void Dispose()
{
lock (_dataWriterLock)
{
if (_disposed)
{
return;
}
_disposed = true;
Stop();
if (_fakeMemoryOwner != null)
{
_fakeMemoryOwner.Dispose();
_fakeMemoryOwner = null;
}
}
}
void IHttpOutputAborter.Abort(ConnectionAbortedException abortReason)
{
_stream.Abort(abortReason, Http3ErrorCode.InternalError);
}
public void Advance(int bytes)
{
lock (_dataWriterLock)
{
ThrowIfSuffixSent();
if (_completed)
{
return;
}
_startedWritingDataFrames = true;
_pipeWriter.Advance(bytes);
}
}
public void CancelPendingFlush()
{
lock (_dataWriterLock)
{
if (_completed)
{
return;
}
_pipeWriter.CancelPendingFlush();
}
}
public ValueTask<FlushResult> FirstWriteAsync(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan<byte> data, CancellationToken cancellationToken)
{
lock (_dataWriterLock)
{
WriteResponseHeaders(statusCode, reasonPhrase, responseHeaders, autoChunk, appCompleted: false);
return WriteDataToPipeAsync(data, cancellationToken);
}
}
public ValueTask<FlushResult> FirstWriteChunkedAsync(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan<byte> data, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public ValueTask<FlushResult> FlushAsync(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask<FlushResult>(Task.FromCanceled<FlushResult>(cancellationToken));
}
lock (_dataWriterLock)
{
ThrowIfSuffixSent();
if (_completed)
{
return default;
}
if (_startedWritingDataFrames)
{
// If there's already been response data written to the stream, just wait for that. Any header
// should be in front of the data frames in the connection pipe. Trailers could change things.
return _flusher.FlushAsync(this, cancellationToken);
}
else
{
// Flushing the connection pipe ensures headers already in the pipe are flushed even if no data
// frames have been written.
return _frameWriter.FlushAsync(this, cancellationToken);
}
}
}
public Memory<byte> GetMemory(int sizeHint = 0)
{
lock (_dataWriterLock)
{
ThrowIfSuffixSent();
if (_completed)
{
return GetFakeMemory(sizeHint);
}
return _pipeWriter.GetMemory(sizeHint);
}
}
public Span<byte> GetSpan(int sizeHint = 0)
{
lock (_dataWriterLock)
{
ThrowIfSuffixSent();
if (_completed)
{
return GetFakeMemory(sizeHint).Span;
}
return _pipeWriter.GetSpan(sizeHint);
}
}
private Memory<byte> GetFakeMemory(int sizeHint)
{
if (_fakeMemoryOwner == null)
{
_fakeMemoryOwner = _memoryPool.Rent(sizeHint);
}
return _fakeMemoryOwner.Memory;
}
[StackTraceHidden]
private void ThrowIfSuffixSent()
{
if (_suffixSent)
{
ThrowSuffixSent();
}
}
[StackTraceHidden]
private static void ThrowSuffixSent()
{
throw new InvalidOperationException("Writing is not allowed after writer was completed.");
}
public void Reset()
{
}
public void Stop()
{
lock (_dataWriterLock)
{
if (_completed)
{
return;
}
_completed = true;
_pipeWriter.Complete(new OperationCanceledException());
}
}
public ValueTask<FlushResult> Write100ContinueAsync()
{
throw new NotImplementedException();
}
public ValueTask<FlushResult> WriteChunkAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public Task WriteDataAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
lock (_dataWriterLock)
{
ThrowIfSuffixSent();
// This length check is important because we don't want to set _startedWritingDataFrames unless a data
// frame will actually be written causing the headers to be flushed.
if (_completed || data.Length == 0)
{
return Task.CompletedTask;
}
_startedWritingDataFrames = true;
_pipeWriter.Write(data);
return _flusher.FlushAsync(this, cancellationToken).GetAsTask();
}
}
public ValueTask<FlushResult> WriteDataToPipeAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return new ValueTask<FlushResult>(Task.FromCanceled<FlushResult>(cancellationToken));
}
lock (_dataWriterLock)
{
ThrowIfSuffixSent();
// This length check is important because we don't want to set _startedWritingDataFrames unless a data
// frame will actually be written causing the headers to be flushed.
if (_completed || data.Length == 0)
{
return default;
}
_startedWritingDataFrames = true;
_pipeWriter.Write(data);
return _flusher.FlushAsync(this, cancellationToken);
}
}
public void WriteResponseHeaders(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, bool appCompleted)
{
lock (_dataWriterLock)
{
if (_completed)
{
return;
}
if (appCompleted && !_startedWritingDataFrames && (_stream.ResponseTrailers == null || _stream.ResponseTrailers.Count == 0))
{
// TODO figure out something to do here.
}
_frameWriter.WriteResponseHeaders(_streamId, statusCode, responseHeaders);
}
}
public ValueTask<FlushResult> WriteStreamSuffixAsync()
{
lock (_dataWriterLock)
{
if (_completed)
{
return _dataWriteProcessingTask;
}
_completed = true;
_suffixSent = true;
_pipeWriter.Complete();
return _dataWriteProcessingTask;
}
}
private async ValueTask<FlushResult> ProcessDataWrites()
{
FlushResult flushResult = default;
try
{
ReadResult readResult;
do
{
readResult = await _pipeReader.ReadAsync();
if (readResult.IsCompleted && _stream.ResponseTrailers?.Count > 0)
{
// Output is ending and there are trailers to write
// Write any remaining content then write trailers
if (readResult.Buffer.Length > 0)
{
flushResult = await _frameWriter.WriteDataAsync(readResult.Buffer);
}
_stream.ResponseTrailers.SetReadOnly();
flushResult = await _frameWriter.WriteResponseTrailers(_streamId, _stream.ResponseTrailers);
}
else if (readResult.IsCompleted)
{
if (readResult.Buffer.Length != 0)
{
ThrowUnexpectedState();
}
// Headers have already been written and there is no other content to write
// TODO complete something here.
flushResult = await _frameWriter.FlushAsync(outputAborter: null, cancellationToken: default);
_frameWriter.Complete();
}
else
{
flushResult = await _frameWriter.WriteDataAsync(readResult.Buffer);
}
_pipeReader.AdvanceTo(readResult.Buffer.End);
} while (!readResult.IsCompleted);
}
catch (OperationCanceledException)
{
// Writes should not throw for aborted streams/connections.
}
catch (Exception ex)
{
_log.LogCritical(ex, nameof(Http3OutputProducer) + "." + nameof(ProcessDataWrites) + " observed an unexpected exception.");
}
_pipeReader.Complete();
return flushResult;
static void ThrowUnexpectedState()
{
throw new InvalidOperationException(nameof(Http3OutputProducer) + "." + nameof(ProcessDataWrites) + " observed an unexpected state where the streams output ended with data still remaining in the pipe.");
}
}
private static Pipe CreateDataPipe(MemoryPool<byte> pool)
=> new Pipe(new PipeOptions
(
pool: pool,
readerScheduler: PipeScheduler.Inline,
writerScheduler: PipeScheduler.ThreadPool,
pauseWriterThreshold: 1,
resumeWriterThreshold: 1,
useSynchronizationContext: false,
minimumSegmentSize: pool.GetMinimumSegmentSize()
));
}
}

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.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
internal class Http3PeerSettings
{
internal const uint DefaultMaxFrameSize = 16 * 1024;
public static int MinAllowedMaxFrameSize { get; internal set; } = 16 * 1024;
}
}

View File

@ -0,0 +1,15 @@
// 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.Server.Kestrel.Core.Internal.Http3
{
enum Http3SettingType : long
{
QPackMaxTableCapacity = 0x1,
/// <summary>
/// SETTINGS_MAX_HEADER_LIST_SIZE, default is unlimited.
/// </summary>
MaxHeaderListSize = 0x6,
QPackBlockedStreams = 0x7
}
}

View File

@ -0,0 +1,471 @@
// 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.Buffers;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
internal abstract class Http3Stream : HttpProtocol, IHttpHeadersHandler, IThreadPoolWorkItem
{
private Http3FrameWriter _frameWriter;
private Http3OutputProducer _http3Output;
private int _isClosed;
private int _gracefulCloseInitiator;
private readonly HttpConnectionContext _context;
private readonly Http3Frame _incomingFrame = new Http3Frame();
private readonly Http3Connection _http3Connection;
private bool _receivedHeaders;
public Pipe RequestBodyPipe { get; }
public Http3Stream(Http3Connection http3Connection, HttpConnectionContext context) : base(context)
{
// 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;
_frameWriter = new Http3FrameWriter(
context.Transport.Output,
context.ConnectionContext,
context.TimeoutControl,
httpLimits.MinResponseDataRate,
context.ConnectionId,
context.MemoryPool,
context.ServiceContext.Log);
// ResponseHeaders aren't set, kind of ugly that we need to reset.
Reset();
_http3Output = new Http3OutputProducer(
0, // TODO streamid
_frameWriter,
context.MemoryPool,
this,
context.ServiceContext.Log);
RequestBodyPipe = CreateRequestBodyPipe(64 * 1024); // windowSize?
Output = _http3Output;
}
public QPackDecoder QPackDecoder { get; set; } = new QPackDecoder(10000, 10000);
public PipeReader Input => _context.Transport.Input;
public ISystemClock SystemClock => _context.ServiceContext.SystemClock;
public KestrelServerLimits Limits => _context.ServiceContext.ServerOptions.Limits;
public void Abort(ConnectionAbortedException ex)
{
Abort(ex, Http3ErrorCode.InternalError);
}
public void Abort(ConnectionAbortedException ex, Http3ErrorCode errorCode)
{
}
public void OnHeadersComplete(bool endStream)
{
OnHeadersComplete();
}
public void HandleReadDataRateTimeout()
{
Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, null, Limits.MinRequestBodyDataRate.BytesPerSecond);
Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestBodyTimeout), Http3ErrorCode.RequestRejected);
}
public void HandleRequestHeadersTimeout()
{
Log.ConnectionBadRequest(ConnectionId, BadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout));
Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout), Http3ErrorCode.RequestRejected);
}
public void OnInputOrOutputCompleted()
{
TryClose();
_frameWriter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient));
}
private bool TryClose()
{
if (Interlocked.Exchange(ref _isClosed, 1) == 0)
{
return true;
}
// TODO make this actually close the Http3Stream by telling msquic to close the stream.
return false;
}
public async Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> application)
{
try
{
while (_isClosed == 0)
{
var result = await Input.ReadAsync();
var readableBuffer = result.Buffer;
var consumed = readableBuffer.Start;
var examined = readableBuffer.End;
try
{
if (!readableBuffer.IsEmpty)
{
while (Http3FrameReader.TryReadFrame(ref readableBuffer, _incomingFrame, 16 * 1024, out var framePayload))
{
consumed = examined = framePayload.End;
await ProcessHttp3Stream(application, framePayload);
}
}
if (result.IsCompleted)
{
return;
}
}
catch (Http3StreamErrorException)
{
// TODO
}
finally
{
Input.AdvanceTo(consumed, examined);
}
}
}
catch (Exception)
{
// TODO
}
finally
{
await RequestBodyPipe.Writer.CompleteAsync();
}
}
private Task ProcessHttp3Stream<TContext>(IHttpApplication<TContext> application, in ReadOnlySequence<byte> payload)
{
switch (_incomingFrame.Type)
{
case Http3FrameType.Data:
return ProcessDataFrameAsync(payload);
case Http3FrameType.Headers:
return ProcessHeadersFrameAsync(application, payload);
// need to be on control stream
case Http3FrameType.DuplicatePush:
case Http3FrameType.PushPromise:
case Http3FrameType.Settings:
case Http3FrameType.GoAway:
case Http3FrameType.CancelPush:
case Http3FrameType.MaxPushId:
throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED");
default:
return ProcessUnknownFrameAsync();
}
}
private Task ProcessUnknownFrameAsync()
{
// Unknown frames must be explicitly ignored.
return Task.CompletedTask;
}
private Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext> application, ReadOnlySequence<byte> payload)
{
QPackDecoder.Decode(payload, handler: this);
// start off a request once qpack has decoded
// Make sure to await this task.
if (_receivedHeaders)
{
// trailers
// TODO figure out if there is anything else to do here.
return Task.CompletedTask;
}
_receivedHeaders = true;
Task.Run(() => base.ProcessRequestsAsync(application));
return Task.CompletedTask;
}
private Task ProcessDataFrameAsync(in ReadOnlySequence<byte> payload)
{
foreach (var segment in payload)
{
RequestBodyPipe.Writer.Write(segment.Span);
}
// TODO this can be better.
return RequestBodyPipe.Writer.FlushAsync().AsTask();
}
public void StopProcessingNextRequest()
=> StopProcessingNextRequest(serverInitiated: true);
public void StopProcessingNextRequest(bool serverInitiated)
{
var initiator = serverInitiated ? GracefulCloseInitiator.Server : GracefulCloseInitiator.Client;
if (Interlocked.CompareExchange(ref _gracefulCloseInitiator, initiator, GracefulCloseInitiator.None) == GracefulCloseInitiator.None)
{
Input.CancelPendingRead();
}
}
public void Tick(DateTimeOffset now)
{
}
protected override void OnReset()
{
}
protected override void ApplicationAbort()
{
}
protected override string CreateRequestId()
{
// TODO include stream id.
return ConnectionId;
}
protected override MessageBody CreateMessageBody()
=> Http3MessageBody.For(this);
protected override bool TryParseRequest(ReadResult result, out bool endConnection)
{
endConnection = !TryValidatePseudoHeaders();
return true;
}
private bool TryValidatePseudoHeaders()
{
_httpVersion = Http.HttpVersion.Http3;
if (!TryValidateMethod())
{
return false;
}
if (!TryValidateAuthorityAndHost(out var hostText))
{
return false;
}
// CONNECT - :scheme and :path must be excluded
if (Method == Http.HttpMethod.Connect)
{
if (!string.IsNullOrEmpty(RequestHeaders[HeaderNames.Scheme]) || !string.IsNullOrEmpty(RequestHeaders[HeaderNames.Path]))
{
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.Http2ErrorConnectMustNotSendSchemeOrPath), Http2ErrorCode.PROTOCOL_ERROR);
return false;
}
RawTarget = hostText;
return true;
}
// :scheme https://tools.ietf.org/html/rfc7540#section-8.1.2.3
// ":scheme" is not restricted to "http" and "https" schemed URIs. A
// proxy or gateway can translate requests for non - HTTP schemes,
// enabling the use of HTTP to interact with non - HTTP services.
// - That said, we shouldn't allow arbitrary values or use them to populate Request.Scheme, right?
// - For now we'll restrict it to http/s and require it match the transport.
// - We'll need to find some concrete scenarios to warrant unblocking this.
if (!string.Equals(RequestHeaders[HeaderNames.Scheme], Scheme, StringComparison.OrdinalIgnoreCase))
{
//ResetAndAbort(new ConnectionAbortedException(
// CoreStrings.FormatHttp2StreamErrorSchemeMismatch(RequestHeaders[HeaderNames.Scheme], Scheme)), Http2ErrorCode.PROTOCOL_ERROR);
return false;
}
// :path (and query) - Required
// Must start with / except may be * for OPTIONS
var path = RequestHeaders[HeaderNames.Path].ToString();
RawTarget = path;
// OPTIONS - https://tools.ietf.org/html/rfc7540#section-8.1.2.3
// This pseudo-header field MUST NOT be empty for "http" or "https"
// URIs; "http" or "https" URIs that do not contain a path component
// MUST include a value of '/'. The exception to this rule is an
// OPTIONS request for an "http" or "https" URI that does not include
// a path component; these MUST include a ":path" pseudo-header field
// with a value of '*'.
if (Method == Http.HttpMethod.Options && path.Length == 1 && path[0] == '*')
{
// * is stored in RawTarget only since HttpRequest expects Path to be empty or start with a /.
Path = string.Empty;
QueryString = string.Empty;
return true;
}
// Approximate MaxRequestLineSize by totaling the required pseudo header field lengths.
var requestLineLength = _methodText.Length + Scheme.Length + hostText.Length + path.Length;
if (requestLineLength > ServerOptions.Limits.MaxRequestLineSize)
{
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestLineTooLong), Http2ErrorCode.PROTOCOL_ERROR);
return false;
}
var queryIndex = path.IndexOf('?');
QueryString = queryIndex == -1 ? string.Empty : path.Substring(queryIndex);
var pathSegment = queryIndex == -1 ? path.AsSpan() : path.AsSpan(0, queryIndex);
return TryValidatePath(pathSegment);
}
private bool TryValidateMethod()
{
// :method
_methodText = RequestHeaders[HeaderNames.Method].ToString();
Method = HttpUtilities.GetKnownMethod(_methodText);
if (Method == Http.HttpMethod.None)
{
// TODO
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http2ErrorCode.PROTOCOL_ERROR);
return false;
}
if (Method == Http.HttpMethod.Custom)
{
if (HttpCharacters.IndexOfInvalidTokenChar(_methodText) >= 0)
{
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http2ErrorCode.PROTOCOL_ERROR);
return false;
}
}
return true;
}
private bool TryValidateAuthorityAndHost(out string hostText)
{
// :authority (optional)
// Prefer this over Host
var authority = RequestHeaders[HeaderNames.Authority];
var host = HttpRequestHeaders.HeaderHost;
if (!StringValues.IsNullOrEmpty(authority))
{
// https://tools.ietf.org/html/rfc7540#section-8.1.2.3
// Clients that generate HTTP/2 requests directly SHOULD use the ":authority"
// pseudo - header field instead of the Host header field.
// An intermediary that converts an HTTP/2 request to HTTP/1.1 MUST
// create a Host header field if one is not present in a request by
// copying the value of the ":authority" pseudo - header field.
// We take this one step further, we don't want mismatched :authority
// and Host headers, replace Host if :authority is defined. The application
// will operate on the Host header.
HttpRequestHeaders.HeaderHost = authority;
host = authority;
}
// https://tools.ietf.org/html/rfc7230#section-5.4
// A server MUST respond with a 400 (Bad Request) status code to any
// HTTP/1.1 request message that lacks a Host header field and to any
// request message that contains more than one Host header field or a
// Host header field with an invalid field-value.
hostText = host.ToString();
if (host.Count > 1 || !HttpUtilities.IsHostHeaderValid(hostText))
{
// RST replaces 400
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(hostText)), Http2ErrorCode.PROTOCOL_ERROR);
return false;
}
return true;
}
private bool TryValidatePath(ReadOnlySpan<char> pathSegment)
{
// Must start with a leading slash
if (pathSegment.Length == 0 || pathSegment[0] != '/')
{
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http2ErrorCode.PROTOCOL_ERROR);
return false;
}
var pathEncoded = pathSegment.Contains('%');
// Compare with Http1Connection.OnOriginFormTarget
// URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11
// Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8;
// then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
try
{
// The decoder operates only on raw bytes
var pathBuffer = new byte[pathSegment.Length].AsSpan();
for (int i = 0; i < pathSegment.Length; i++)
{
var ch = pathSegment[i];
// The header parser should already be checking this
Debug.Assert(32 < ch && ch < 127);
pathBuffer[i] = (byte)ch;
}
Path = PathNormalizer.DecodePath(pathBuffer, pathEncoded, RawTarget, QueryString.Length);
return true;
}
catch (InvalidOperationException)
{
//ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http2ErrorCode.PROTOCOL_ERROR);
return false;
}
}
private Pipe CreateRequestBodyPipe(uint windowSize)
=> new Pipe(new PipeOptions
(
pool: _context.MemoryPool,
readerScheduler: ServiceContext.Scheduler,
writerScheduler: PipeScheduler.Inline,
// Never pause within the window range. Flow control will prevent more data from being added.
// See the assert in OnDataAsync.
pauseWriterThreshold: windowSize + 1,
resumeWriterThreshold: windowSize + 1,
useSynchronizationContext: false,
minimumSegmentSize: _context.MemoryPool.GetMinimumSegmentSize()
));
/// <summary>
/// Used to kick off the request processing loop by derived classes.
/// </summary>
public abstract void Execute();
private static class GracefulCloseInitiator
{
public const int None = 0;
public const int Server = 1;
public const int Client = 2;
}
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
class Http3StreamErrorException : Exception
{
public Http3StreamErrorException(string message, Http3ErrorCode errorCode)
: base($"HTTP/3 stream error ({errorCode}): {message}")
{
ErrorCode = errorCode;
}
public Http3ErrorCode ErrorCode { get; }
}
}

View File

@ -0,0 +1,26 @@
// 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.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Abstractions;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
class Http3Stream<TContext> : Http3Stream, IHostContextContainer<TContext>
{
private readonly IHttpApplication<TContext> _application;
public Http3Stream(IHttpApplication<TContext> application, Http3Connection connection, HttpConnectionContext context) : base(connection, context)
{
_application = application;
}
public override void Execute()
{
_ = ProcessRequestAsync(_application);
}
// Pooled Host context
TContext IHostContextContainer<TContext>.HostContext { get; set; }
}
}

View File

@ -0,0 +1,130 @@
// 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.Http.HPack;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
{
internal class DecoderStreamReader
{
private enum State
{
Ready,
HeaderAckowledgement,
StreamCancellation,
InsertCountIncrement
}
//0 1 2 3 4 5 6 7
//+---+---+---+---+---+---+---+---+
//| 1 | Stream ID(7+) |
//+---+---------------------------+
private const byte HeaderAcknowledgementMask = 0x80;
private const byte HeaderAcknowledgementRepresentation = 0x80;
private const byte HeaderAcknowledgementPrefixMask = 0x7F;
private const int HeaderAcknowledgementPrefix = 7;
//0 1 2 3 4 5 6 7
//+---+---+---+---+---+---+---+---+
//| 0 | 1 | Stream ID(6+) |
//+---+---+-----------------------+
private const byte StreamCancellationMask = 0xC0;
private const byte StreamCancellationRepresentation = 0x40;
private const byte StreamCancellationPrefixMask = 0x3F;
private const int StreamCancellationPrefix = 6;
//0 1 2 3 4 5 6 7
//+---+---+---+---+---+---+---+---+
//| 0 | 0 | Increment(6+) |
//+---+---+-----------------------+
private const byte InsertCountIncrementMask = 0xC0;
private const byte InsertCountIncrementRepresentation = 0x00;
private const byte InsertCountIncrementPrefixMask = 0x3F;
private const int InsertCountIncrementPrefix = 6;
private IntegerDecoder _integerDecoder = new IntegerDecoder();
private State _state;
public DecoderStreamReader()
{
}
public void Read(ReadOnlySequence<byte> data)
{
foreach (var segment in data)
{
var span = segment.Span;
for (var i = 0; i < span.Length; i++)
{
OnByte(span[i]);
}
}
}
private void OnByte(byte b)
{
int intResult;
int prefixInt;
switch (_state)
{
case State.Ready:
if ((b & HeaderAcknowledgementMask) == HeaderAcknowledgementRepresentation)
{
prefixInt = HeaderAcknowledgementPrefixMask & b;
if (_integerDecoder.BeginTryDecode((byte)prefixInt, HeaderAcknowledgementPrefix, out intResult))
{
OnHeaderAcknowledgement(intResult);
}
else
{
_state = State.HeaderAckowledgement;
}
}
else if ((b & StreamCancellationMask) == StreamCancellationRepresentation)
{
prefixInt = StreamCancellationPrefixMask & b;
if (_integerDecoder.BeginTryDecode((byte)prefixInt, StreamCancellationPrefix, out intResult))
{
OnStreamCancellation(intResult);
}
else
{
_state = State.StreamCancellation;
}
}
else if ((b & InsertCountIncrementMask) == InsertCountIncrementRepresentation)
{
prefixInt = InsertCountIncrementPrefixMask & b;
if (_integerDecoder.BeginTryDecode((byte)prefixInt, InsertCountIncrementPrefix, out intResult))
{
OnInsertCountIncrement(intResult);
}
else
{
_state = State.InsertCountIncrement;
}
}
break;
}
}
private void OnInsertCountIncrement(int intResult)
{
// increment some count.
_state = State.Ready;
}
private void OnStreamCancellation(int streamId)
{
// Remove stream?
_state = State.Ready;
}
private void OnHeaderAcknowledgement(int intResult)
{
// Acknowledge header somehow
_state = State.Ready;
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
{
// The size of the dynamic table is the sum of the size of its entries.
// The size of an entry is the sum of its name's length in bytes (as
// defined in Section 4.1.2), its value's length in bytes, and 32.
internal class DynamicTable
{
// The encoder sends a Set Dynamic Table Capacity
// instruction(Section 4.3.1) with a non-zero capacity to begin using
// the dynamic table.
public DynamicTable(int maxSize)
{
}
public HeaderField this[int index]
{
get
{
return new HeaderField();
}
}
// TODO
public void Insert(Span<byte> name, Span<byte> value)
{
}
// TODO
public void Resize(int maxSize)
{
}
// TODO
internal void Duplicate(int index)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,332 @@
// 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.Buffers;
using System.Net.Http.HPack;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
{
internal class EncoderStreamReader
{
private enum State
{
Ready,
DynamicTableCapcity,
NameIndex,
NameLength,
Name,
ValueLength,
ValueLengthContinue,
Value,
Duplicate
}
//0 1 2 3 4 5 6 7
//+---+---+---+---+---+---+---+---+
//| 0 | 0 | 1 | Capacity(5+) |
//+---+---+---+-------------------+
private const byte DynamicTableCapacityMask = 0xE0;
private const byte DynamicTableCapacityRepresentation = 0x20;
private const byte DynamicTableCapacityPrefixMask = 0x1F;
private const int DynamicTableCapacityPrefix = 5;
//0 1 2 3 4 5 6 7
//+---+---+---+---+---+---+---+---+
//| 1 | S | Name Index(6+) |
//+---+---+-----------------------+
//| H | Value Length(7+) |
//+---+---------------------------+
//| Value String(Length bytes) |
//+-------------------------------+
private const byte InsertWithNameReferenceMask = 0x80;
private const byte InsertWithNameReferenceRepresentation = 0x80;
private const byte InsertWithNameReferencePrefixMask = 0x3F;
private const byte InsertWithNameReferenceStaticMask = 0x40;
private const int InsertWithNameReferencePrefix = 6;
//0 1 2 3 4 5 6 7
//+---+---+---+---+---+---+---+---+
//| 0 | 1 | H | Name Length(5+) |
//+---+---+---+-------------------+
//| Name String(Length bytes) |
//+---+---------------------------+
//| H | Value Length(7+) |
//+---+---------------------------+
//| Value String(Length bytes) |
//+-------------------------------+
private const byte InsertWithoutNameReferenceMask = 0xC0;
private const byte InsertWithoutNameReferenceRepresentation = 0x40;
private const byte InsertWithoutNameReferencePrefixMask = 0x1F;
private const byte InsertWithoutNameReferenceHuffmanMask = 0x20;
private const int InsertWithoutNameReferencePrefix = 5;
//0 1 2 3 4 5 6 7
//+---+---+---+---+---+---+---+---+
//| 0 | 0 | 0 | Index(5+) |
//+---+---+---+-------------------+
private const byte DuplicateMask = 0xE0;
private const byte DuplicateRepresentation = 0x00;
private const byte DuplicatePrefixMask = 0x1F;
private const int DuplicatePrefix = 5;
private const int StringLengthPrefix = 7;
private const byte HuffmanMask = 0x80;
private bool _s;
private byte[] _stringOctets;
private byte[] _headerNameOctets;
private byte[] _headerValueOctets;
private byte[] _headerName;
private int _headerNameLength;
private int _headerValueLength;
private int _stringLength;
private int _stringIndex;
private DynamicTable _dynamicTable = new DynamicTable(1000); // TODO figure out architecture.
private readonly IntegerDecoder _integerDecoder = new IntegerDecoder();
private State _state = State.Ready;
private bool _huffman;
public EncoderStreamReader(int maxRequestHeaderFieldSize)
{
// TODO how to propagate dynamic table around.
_stringOctets = new byte[maxRequestHeaderFieldSize];
_headerNameOctets = new byte[maxRequestHeaderFieldSize];
_headerValueOctets = new byte[maxRequestHeaderFieldSize];
}
public void Read(ReadOnlySequence<byte> data)
{
foreach (var segment in data)
{
var span = segment.Span;
for (var i = 0; i < span.Length; i++)
{
OnByte(span[i]);
}
}
}
private void OnByte(byte b)
{
int intResult;
int prefixInt;
switch (_state)
{
case State.Ready:
if ((b & DynamicTableCapacityMask) == DynamicTableCapacityRepresentation)
{
prefixInt = DynamicTableCapacityPrefixMask & b;
if (_integerDecoder.BeginTryDecode((byte)prefixInt, DynamicTableCapacityPrefix, out intResult))
{
OnDynamicTableCapacity(intResult);
}
else
{
_state = State.DynamicTableCapcity;
}
}
else if ((b & InsertWithNameReferenceMask) == InsertWithNameReferenceRepresentation)
{
prefixInt = InsertWithNameReferencePrefixMask & b;
_s = (InsertWithNameReferenceStaticMask & b) == InsertWithNameReferenceStaticMask;
if (_integerDecoder.BeginTryDecode((byte)prefixInt, InsertWithNameReferencePrefix, out intResult))
{
OnNameIndex(intResult);
}
else
{
_state = State.NameIndex;
}
}
else if ((b & InsertWithoutNameReferenceMask) == InsertWithoutNameReferenceRepresentation)
{
prefixInt = InsertWithoutNameReferencePrefixMask & b;
_huffman = (InsertWithoutNameReferenceHuffmanMask & b) == InsertWithoutNameReferenceHuffmanMask;
if (_integerDecoder.BeginTryDecode((byte)prefixInt, InsertWithoutNameReferencePrefix, out intResult))
{
OnStringLength(intResult, State.Name);
}
else
{
_state = State.NameIndex;
}
}
else if ((b & DuplicateMask) == DuplicateRepresentation)
{
prefixInt = DuplicatePrefixMask & b;
if (_integerDecoder.BeginTryDecode((byte)prefixInt, DuplicatePrefix, out intResult))
{
OnDuplicate(intResult);
}
else
{
_state = State.Duplicate;
}
}
break;
case State.DynamicTableCapcity:
if (_integerDecoder.TryDecode(b, out intResult))
{
OnDynamicTableCapacity(intResult);
}
break;
case State.NameIndex:
if (_integerDecoder.TryDecode(b, out intResult))
{
OnNameIndex(intResult);
}
break;
case State.NameLength:
if (_integerDecoder.TryDecode(b, out intResult))
{
OnStringLength(intResult, nextState: State.Name);
}
break;
case State.Name:
_stringOctets[_stringIndex++] = b;
if (_stringIndex == _stringLength)
{
OnString(nextState: State.ValueLength);
}
break;
case State.ValueLength:
_huffman = (b & HuffmanMask) != 0;
// TODO confirm this.
if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult))
{
OnStringLength(intResult, nextState: State.Value);
if (intResult == 0)
{
ProcessValue();
}
}
else
{
_state = State.ValueLengthContinue;
}
break;
case State.ValueLengthContinue:
if (_integerDecoder.TryDecode(b, out intResult))
{
OnStringLength(intResult, nextState: State.Value);
if (intResult == 0)
{
ProcessValue();
}
}
break;
case State.Value:
_stringOctets[_stringIndex++] = b;
if (_stringIndex == _stringLength)
{
ProcessValue();
}
break;
case State.Duplicate:
if (_integerDecoder.TryDecode(b, out intResult))
{
OnDuplicate(intResult);
}
break;
}
}
private void OnStringLength(int length, State nextState)
{
if (length > _stringOctets.Length)
{
throw new QPackDecodingException(/*CoreStrings.FormatQPackStringLengthTooLarge(length, _stringOctets.Length)*/);
}
_stringLength = length;
_stringIndex = 0;
_state = nextState;
}
private void ProcessValue()
{
OnString(nextState: State.Ready);
var headerNameSpan = new Span<byte>(_headerName, 0, _headerNameLength);
var headerValueSpan = new Span<byte>(_headerValueOctets, 0, _headerValueLength);
_dynamicTable.Insert(headerNameSpan, headerValueSpan);
}
private void OnString(State nextState)
{
int Decode(byte[] dst)
{
if (_huffman)
{
return Huffman.Decode(new ReadOnlySpan<byte>(_stringOctets, 0, _stringLength), ref dst);
}
else
{
Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength);
return _stringLength;
}
}
try
{
if (_state == State.Name)
{
_headerName = _headerNameOctets;
_headerNameLength = Decode(_headerNameOctets);
}
else
{
_headerValueLength = Decode(_headerValueOctets);
}
}
catch (HuffmanDecodingException ex)
{
throw new QPackDecodingException(""/*CoreStrings.QPackHuffmanError*/, ex);
}
_state = nextState;
}
private void OnNameIndex(int index)
{
var header = GetHeader(index);
_headerName = header.Name;
_headerNameLength = header.Name.Length;
_state = State.ValueLength;
}
private void OnDynamicTableCapacity(int dynamicTableSize)
{
// Call Decoder to update the table size.
_dynamicTable.Resize(dynamicTableSize);
_state = State.Ready;
}
private void OnDuplicate(int index)
{
_dynamicTable.Duplicate(index);
_state = State.Ready;
}
private HeaderField GetHeader(int index)
{
try
{
return _s ? StaticTable.Instance[index] : _dynamicTable[index];
}
catch (IndexOutOfRangeException ex)
{
throw new QPackDecodingException( "" /*CoreStrings.FormatQPackErrorIndexOutOfRange(index)*/, ex);
}
}
}
}

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;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
{
internal readonly struct HeaderField
{
public HeaderField(Span<byte> name, Span<byte> value)
{
Name = new byte[name.Length];
name.CopyTo(Name);
Value = new byte[value.Length];
value.CopyTo(Value);
}
public byte[] Name { get; }
public byte[] Value { get; }
public int Length => GetLength(Name.Length, Value.Length);
public static int GetLength(int nameLength, int valueLength) => nameLength + valueLength;
}
}

View File

@ -0,0 +1,513 @@
// 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.Buffers;
using System.Net.Http;
using System.Net.Http.HPack;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
{
internal class QPackDecoder
{
private enum State
{
Ready,
RequiredInsertCount,
RequiredInsertCountDone,
Base,
CompressedHeaders,
HeaderFieldIndex,
HeaderNameIndex,
HeaderNameLength,
HeaderNameLengthContinue,
HeaderName,
HeaderValueLength,
HeaderValueLengthContinue,
HeaderValue,
DynamicTableSizeUpdate,
PostBaseIndex,
LiteralHeaderFieldWithNameReference,
HeaderNameIndexPostBase
}
//0 1 2 3 4 5 6 7
//+---+---+---+---+---+---+---+---+
//| Required Insert Count(8+) |
//+---+---------------------------+
//| S | Delta Base(7+) |
//+---+---------------------------+
//| Compressed Headers ...
private const int RequiredInsertCountPrefix = 8;
private const int BaseMask = 0x80;
private const int BasePrefix = 7;
//+-------------------------------+
//https://tools.ietf.org/html/draft-ietf-quic-qpack-09#section-4.5.2
//0 1 2 3 4 5 6 7
//+---+---+---+---+---+---+---+---+
//| 1 | S | Index(6+) |
//+---+---+-----------------------+
private const byte IndexedHeaderFieldMask = 0x80;
private const byte IndexedHeaderFieldRepresentation = 0x80;
private const byte IndexedHeaderStaticMask = 0x40;
private const byte IndexedHeaderStaticRepresentation = 0x40;
private const byte IndexedHeaderFieldPrefixMask = 0x3F;
private const int IndexedHeaderFieldPrefix = 6;
//0 1 2 3 4 5 6 7
//+---+---+---+---+---+---+---+---+
//| 0 | 0 | 0 | 1 | Index(4+) |
//+---+---+---+---+---------------+
private const byte PostBaseIndexMask = 0xF0;
private const byte PostBaseIndexRepresentation = 0x10;
private const int PostBaseIndexPrefix = 4;
//0 1 2 3 4 5 6 7
//+---+---+---+---+---+---+---+---+
//| 0 | 1 | N | S |Name Index(4+)|
//+---+---+---+---+---------------+
//| H | Value Length(7+) |
//+---+---------------------------+
//| Value String(Length bytes) |
//+-------------------------------+
private const byte LiteralHeaderFieldMask = 0xC0;
private const byte LiteralHeaderFieldRepresentation = 0x40;
private const byte LiteralHeaderFieldNMask = 0x20;
private const byte LiteralHeaderFieldStaticMask = 0x10;
private const byte LiteralHeaderFieldPrefixMask = 0x0F;
private const int LiteralHeaderFieldPrefix = 4;
//0 1 2 3 4 5 6 7
//+---+---+---+---+---+---+---+---+
//| 0 | 0 | 0 | 0 | N |NameIdx(3+)|
//+---+---+---+---+---+-----------+
//| H | Value Length(7+) |
//+---+---------------------------+
//| Value String(Length bytes) |
//+-------------------------------+
private const byte LiteralHeaderFieldPostBaseMask = 0xF0;
private const byte LiteralHeaderFieldPostBaseRepresentation = 0x00;
private const byte LiteralHeaderFieldPostBaseNMask = 0x08;
private const byte LiteralHeaderFieldPostBasePrefixMask = 0x07;
private const int LiteralHeaderFieldPostBasePrefix = 3;
//0 1 2 3 4 5 6 7
//+---+---+---+---+---+---+---+---+
//| 0 | 0 | 1 | N | H |NameLen(3+)|
//+---+---+---+---+---+-----------+
//| Name String(Length bytes) |
//+---+---------------------------+
//| H | Value Length(7+) |
//+---+---------------------------+
//| Value String(Length bytes) |
//+-------------------------------+
private const byte LiteralHeaderFieldWithoutNameReferenceMask = 0xE0;
private const byte LiteralHeaderFieldWithoutNameReferenceRepresentation = 0x20;
private const byte LiteralHeaderFieldWithoutNameReferenceNMask = 0x10;
private const byte LiteralHeaderFieldWithoutNameReferenceHuffmanMask = 0x08;
private const byte LiteralHeaderFieldWithoutNameReferencePrefixMask = 0x07;
private const int LiteralHeaderFieldWithoutNameReferencePrefix = 3;
private const int StringLengthPrefix = 7;
private const byte HuffmanMask = 0x80;
private State _state = State.Ready;
// TODO break out dynamic table entirely.
private long _maxDynamicTableSize;
private DynamicTable _dynamicTable;
// TODO idk what these are for.
private byte[] _stringOctets;
private byte[] _headerNameOctets;
private byte[] _headerValueOctets;
private int _requiredInsertCount;
//private int _insertCount;
private int _base;
// s is used for whatever s is in each field. This has multiple definition
private bool _s;
private bool _n;
private bool _huffman;
private bool _index;
private byte[] _headerName;
private int _headerNameLength;
private int _headerValueLength;
private int _stringLength;
private int _stringIndex;
private readonly IntegerDecoder _integerDecoder = new IntegerDecoder();
// Decoders are on the http3stream now, each time we see a header block
public QPackDecoder(int maxDynamicTableSize, int maxRequestHeaderFieldSize)
: this(maxDynamicTableSize, maxRequestHeaderFieldSize, new DynamicTable(maxDynamicTableSize)) { }
// For testing.
internal QPackDecoder(int maxDynamicTableSize, int maxRequestHeaderFieldSize, DynamicTable dynamicTable)
{
_maxDynamicTableSize = maxDynamicTableSize;
_dynamicTable = dynamicTable;
_stringOctets = new byte[maxRequestHeaderFieldSize];
_headerNameOctets = new byte[maxRequestHeaderFieldSize];
_headerValueOctets = new byte[maxRequestHeaderFieldSize];
}
// sequence will probably be a header block instead.
public void Decode(in ReadOnlySequence<byte> headerBlock, IHttpHeadersHandler handler)
{
// TODO I need to get the RequiredInsertCount and DeltaBase
// These are always present in the header block
// TODO need to figure out if I have read an entire header block.
// (I think this can be done based on length outside of this)
foreach (var segment in headerBlock)
{
var span = segment.Span;
for (var i = 0; i < span.Length; i++)
{
OnByte(span[i], handler);
}
}
}
private void OnByte(byte b, IHttpHeadersHandler handler)
{
int intResult;
int prefixInt;
switch (_state)
{
case State.Ready:
if (_integerDecoder.BeginTryDecode(b, RequiredInsertCountPrefix, out intResult))
{
OnRequiredInsertCount(intResult);
}
else
{
_state = State.RequiredInsertCount;
}
break;
case State.RequiredInsertCount:
if (_integerDecoder.TryDecode(b, out intResult))
{
OnRequiredInsertCount(intResult);
}
break;
case State.RequiredInsertCountDone:
prefixInt = ~BaseMask & b;
_s = (b & BaseMask) == BaseMask;
if (_integerDecoder.BeginTryDecode(b, BasePrefix, out intResult))
{
OnBase(intResult);
}
else
{
_state = State.Base;
}
break;
case State.Base:
if (_integerDecoder.TryDecode(b, out intResult))
{
OnBase(intResult);
}
break;
case State.CompressedHeaders:
if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation)
{
prefixInt = IndexedHeaderFieldPrefixMask & b;
_s = (b & IndexedHeaderStaticMask) == IndexedHeaderStaticRepresentation;
if (_integerDecoder.BeginTryDecode((byte)prefixInt, IndexedHeaderFieldPrefix, out intResult))
{
OnIndexedHeaderField(intResult, handler);
}
else
{
_state = State.HeaderFieldIndex;
}
}
else if ((b & PostBaseIndexMask) == PostBaseIndexRepresentation)
{
prefixInt = ~PostBaseIndexMask & b;
if (_integerDecoder.BeginTryDecode((byte)prefixInt, PostBaseIndexPrefix, out intResult))
{
OnPostBaseIndex(intResult, handler);
}
else
{
_state = State.PostBaseIndex;
}
}
else if ((b & LiteralHeaderFieldMask) == LiteralHeaderFieldRepresentation)
{
_index = true;
// Represents whether an intermediary is permitted to add this header to the dynamic header table on
// subsequent hops.
// if n is set, the encoded header must always be encoded with a literal representation
_n = (LiteralHeaderFieldNMask & b) == LiteralHeaderFieldNMask;
_s = (LiteralHeaderFieldStaticMask & b) == LiteralHeaderFieldStaticMask;
prefixInt = b & LiteralHeaderFieldPrefixMask;
if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldPrefix, out intResult))
{
OnIndexedHeaderName(intResult);
}
else
{
_state = State.HeaderNameIndex;
}
}
else if ((b & LiteralHeaderFieldPostBaseMask) == LiteralHeaderFieldPostBaseRepresentation)
{
_index = true;
_n = (LiteralHeaderFieldPostBaseNMask & b) == LiteralHeaderFieldPostBaseNMask;
prefixInt = b & LiteralHeaderFieldPostBasePrefixMask;
if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldPostBasePrefix, out intResult))
{
OnIndexedHeaderNamePostBase(intResult);
}
else
{
_state = State.HeaderNameIndexPostBase;
}
}
else if ((b & LiteralHeaderFieldWithoutNameReferenceMask) == LiteralHeaderFieldWithoutNameReferenceRepresentation)
{
_index = false;
_n = (LiteralHeaderFieldWithoutNameReferenceNMask & b) == LiteralHeaderFieldWithoutNameReferenceNMask;
_huffman = (b & LiteralHeaderFieldWithoutNameReferenceHuffmanMask) != 0;
prefixInt = b & LiteralHeaderFieldWithoutNameReferencePrefixMask;
if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldWithoutNameReferencePrefix, out intResult))
{
OnStringLength(intResult, State.HeaderName);
}
else
{
_state = State.HeaderNameLength;
}
}
break;
case State.HeaderNameLength:
// huffman has already been processed.
if (_integerDecoder.TryDecode(b, out intResult))
{
OnStringLength(intResult, nextState: State.HeaderName);
}
break;
case State.HeaderName:
_stringOctets[_stringIndex++] = b;
if (_stringIndex == _stringLength)
{
OnString(nextState: State.HeaderValueLength);
}
break;
case State.HeaderNameIndex:
if (_integerDecoder.TryDecode(b, out intResult))
{
OnIndexedHeaderName(intResult);
}
break;
case State.HeaderNameIndexPostBase:
if (_integerDecoder.TryDecode(b, out intResult))
{
OnIndexedHeaderNamePostBase(intResult);
}
break;
case State.HeaderValueLength:
_huffman = (b & HuffmanMask) != 0;
// TODO confirm this.
if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult))
{
OnStringLength(intResult, nextState: State.HeaderValue);
if (intResult == 0)
{
ProcessHeaderValue(handler);
}
}
else
{
_state = State.HeaderValueLengthContinue;
}
break;
case State.HeaderValueLengthContinue:
if (_integerDecoder.TryDecode(b, out intResult))
{
OnStringLength(intResult, nextState: State.HeaderValue);
if (intResult == 0)
{
ProcessHeaderValue(handler);
}
}
break;
case State.HeaderValue:
_stringOctets[_stringIndex++] = b;
if (_stringIndex == _stringLength)
{
ProcessHeaderValue(handler);
}
break;
case State.HeaderFieldIndex:
if (_integerDecoder.TryDecode(b, out intResult))
{
OnIndexedHeaderField(intResult, handler);
}
break;
case State.PostBaseIndex:
if (_integerDecoder.TryDecode(b, out intResult))
{
OnPostBaseIndex(intResult, handler);
}
break;
case State.LiteralHeaderFieldWithNameReference:
break;
}
}
private void OnStringLength(int length, State nextState)
{
if (length > _stringOctets.Length)
{
throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.FormatQPackStringLengthTooLarge(length, _stringOctets.Length)*/);
}
_stringLength = length;
_stringIndex = 0;
_state = nextState;
}
private void ProcessHeaderValue(IHttpHeadersHandler handler)
{
OnString(nextState: State.CompressedHeaders);
var headerNameSpan = new Span<byte>(_headerName, 0, _headerNameLength);
var headerValueSpan = new Span<byte>(_headerValueOctets, 0, _headerValueLength);
handler.OnHeader(headerNameSpan, headerValueSpan);
if (_index)
{
_dynamicTable.Insert(headerNameSpan, headerValueSpan);
}
}
private void OnString(State nextState)
{
int Decode(byte[] dst)
{
if (_huffman)
{
return Huffman.Decode(new ReadOnlySpan<byte>(_stringOctets, 0, _stringLength), ref dst);
}
else
{
Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength);
return _stringLength;
}
}
try
{
if (_state == State.HeaderName)
{
_headerName = _headerNameOctets;
_headerNameLength = Decode(_headerNameOctets);
}
else
{
_headerValueLength = Decode(_headerValueOctets);
}
}
catch (HuffmanDecodingException ex)
{
throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.QPackHuffmanError, */, ex);
}
_state = nextState;
}
private void OnIndexedHeaderName(int index)
{
var header = GetHeader(index);
_headerName = header.Name;
_headerNameLength = header.Name.Length;
_state = State.HeaderValueLength;
}
private void OnIndexedHeaderNamePostBase(int index)
{
// TODO update with postbase index
var header = GetHeader(index);
_headerName = header.Name;
_headerNameLength = header.Name.Length;
_state = State.HeaderValueLength;
}
private void OnPostBaseIndex(int intResult, IHttpHeadersHandler handler)
{
// TODO
_state = State.CompressedHeaders;
}
private void OnBase(int deltaBase)
{
_state = State.CompressedHeaders;
if (_s)
{
_base = _requiredInsertCount - deltaBase - 1;
}
else
{
_base = _requiredInsertCount + deltaBase;
}
}
// TODO
private void OnRequiredInsertCount(int requiredInsertCount)
{
_requiredInsertCount = requiredInsertCount;
_state = State.RequiredInsertCountDone;
// This is just going to noop for now. I don't get this algorithm at all.
// var encoderInsertCount = 0;
// var maxEntries = _maxDynamicTableSize / HeaderField.RfcOverhead;
// if (requiredInsertCount != 0)
// {
// encoderInsertCount = (requiredInsertCount % ( 2 * maxEntries)) + 1;
// }
// // Dude I don't get this algorithm...
// var fullRange = 2 * maxEntries;
// if (encoderInsertCount == 0)
// {
// }
}
private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler)
{
// Indexes start at 0 in QPack
var header = GetHeader(index);
handler.OnHeader(new Span<byte>(header.Name), new Span<byte>(header.Value));
_state = State.CompressedHeaders;
}
private HeaderField GetHeader(int index)
{
try
{
return _s ? StaticTable.Instance[index] : _dynamicTable[index];
}
catch (IndexOutOfRangeException ex)
{
throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.FormatQPackErrorIndexOutOfRange(index), */, ex);
}
}
}
}

View File

@ -0,0 +1,28 @@
// 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.Runtime.Serialization;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
{
[Serializable]
internal class QPackDecodingException : Exception
{
public QPackDecodingException()
{
}
public QPackDecodingException(string message) : base(message)
{
}
public QPackDecodingException(string message, Exception innerException) : base(message, innerException)
{
}
protected QPackDecodingException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

View File

@ -0,0 +1,504 @@
// 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.Diagnostics;
using System.Net.Http;
using System.Net.Http.HPack;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
{
internal class QPackEncoder
{
private IEnumerator<KeyValuePair<string, string>> _enumerator;
// TODO these all need to be updated!
/*
* 0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 1 | S | Index (6+) |
+---+---+-----------------------+
*/
public static bool EncodeIndexedHeaderField(int index, Span<byte> destination, out int bytesWritten)
{
if (destination.IsEmpty)
{
bytesWritten = 0;
return false;
}
EncodeHeaderBlockPrefix(destination, out bytesWritten);
destination = destination.Slice(bytesWritten);
return IntegerEncoder.Encode(index, 6, destination, out bytesWritten);
}
public static bool EncodeIndexHeaderFieldWithPostBaseIndex(int index, Span<byte> destination, out int bytesWritten)
{
bytesWritten = 0;
return false;
}
/// <summary>Encodes a "Literal Header Field without Indexing".</summary>
public static bool EncodeLiteralHeaderFieldWithNameReference(int index, string value, Span<byte> destination, out int bytesWritten)
{
if (destination.IsEmpty)
{
bytesWritten = 0;
return false;
}
EncodeHeaderBlockPrefix(destination, out bytesWritten);
destination = destination.Slice(bytesWritten);
return IntegerEncoder.Encode(index, 6, destination, out bytesWritten);
}
/*
* 0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 1 | N | S |Name Index (4+)|
+---+---+---+---+---------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length bytes) |
+-------------------------------+
*/
public static bool EncodeLiteralHeaderFieldWithPostBaseNameReference(int index, Span<byte> destination, out int bytesWritten)
{
bytesWritten = 0;
return false;
}
public static bool EncodeLiteralHeaderFieldWithoutNameReference(int index, Span<byte> destination, out int bytesWritten)
{
bytesWritten = 0;
return false;
}
/*
* 0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| Required Insert Count (8+) |
+---+---------------------------+
| S | Delta Base (7+) |
+---+---------------------------+
| Compressed Headers ...
+-------------------------------+
*
*/
private static bool EncodeHeaderBlockPrefix(Span<byte> destination, out int bytesWritten)
{
int length;
bytesWritten = 0;
// Required insert count as first int
if (!IntegerEncoder.Encode(0, 8, destination, out length))
{
return false;
}
bytesWritten += length;
destination = destination.Slice(length);
// Delta base
if (destination.IsEmpty)
{
return false;
}
destination[0] = 0x00;
if (!IntegerEncoder.Encode(0, 7, destination, out length))
{
return false;
}
bytesWritten += length;
return true;
}
private static bool EncodeLiteralHeaderName(string value, Span<byte> destination, out int bytesWritten)
{
// From https://tools.ietf.org/html/rfc7541#section-5.2
// ------------------------------------------------------
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | H | String Length (7+) |
// +---+---------------------------+
// | String Data (Length octets) |
// +-------------------------------+
if (!destination.IsEmpty)
{
destination[0] = 0; // TODO: Use Huffman encoding
if (IntegerEncoder.Encode(value.Length, 7, destination, out int integerLength))
{
Debug.Assert(integerLength >= 1);
destination = destination.Slice(integerLength);
if (value.Length <= destination.Length)
{
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
destination[i] = (byte)((uint)(c - 'A') <= ('Z' - 'A') ? c | 0x20 : c);
}
bytesWritten = integerLength + value.Length;
return true;
}
}
}
bytesWritten = 0;
return false;
}
private static bool EncodeStringLiteralValue(string value, Span<byte> destination, out int bytesWritten)
{
if (value.Length <= destination.Length)
{
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
if ((c & 0xFF80) != 0)
{
throw new HttpRequestException("");
}
destination[i] = (byte)c;
}
bytesWritten = value.Length;
return true;
}
bytesWritten = 0;
return false;
}
public static bool EncodeStringLiteral(string value, Span<byte> destination, out int bytesWritten)
{
// From https://tools.ietf.org/html/rfc7541#section-5.2
// ------------------------------------------------------
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | H | String Length (7+) |
// +---+---------------------------+
// | String Data (Length octets) |
// +-------------------------------+
if (!destination.IsEmpty)
{
destination[0] = 0; // TODO: Use Huffman encoding
if (IntegerEncoder.Encode(value.Length, 7, destination, out int integerLength))
{
Debug.Assert(integerLength >= 1);
if (EncodeStringLiteralValue(value, destination.Slice(integerLength), out int valueLength))
{
bytesWritten = integerLength + valueLength;
return true;
}
}
}
bytesWritten = 0;
return false;
}
public static bool EncodeStringLiterals(string[] values, string separator, Span<byte> destination, out int bytesWritten)
{
bytesWritten = 0;
if (values.Length == 0)
{
return EncodeStringLiteral("", destination, out bytesWritten);
}
else if (values.Length == 1)
{
return EncodeStringLiteral(values[0], destination, out bytesWritten);
}
if (!destination.IsEmpty)
{
int valueLength = 0;
// Calculate length of all parts and separators.
foreach (string part in values)
{
valueLength = checked((int)(valueLength + part.Length));
}
valueLength = checked((int)(valueLength + (values.Length - 1) * separator.Length));
if (IntegerEncoder.Encode(valueLength, 7, destination, out int integerLength))
{
Debug.Assert(integerLength >= 1);
int encodedLength = 0;
for (int j = 0; j < values.Length; j++)
{
if (j != 0 && !EncodeStringLiteralValue(separator, destination.Slice(integerLength), out encodedLength))
{
return false;
}
integerLength += encodedLength;
if (!EncodeStringLiteralValue(values[j], destination.Slice(integerLength), out encodedLength))
{
return false;
}
integerLength += encodedLength;
}
bytesWritten = integerLength;
return true;
}
}
return false;
}
/// <summary>
/// Encodes a "Literal Header Field without Indexing" to a new array, but only the index portion;
/// a subsequent call to <see cref="EncodeStringLiteral"/> must be used to encode the associated value.
/// </summary>
public static byte[] EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(int index)
{
Span<byte> span = stackalloc byte[256];
bool success = EncodeLiteralHeaderFieldWithPostBaseNameReference(index, span, out int length);
Debug.Assert(success, $"Stack-allocated space was too small for index '{index}'.");
return span.Slice(0, length).ToArray();
}
/// <summary>
/// Encodes a "Literal Header Field without Indexing - New Name" to a new array, but only the name portion;
/// a subsequent call to <see cref="EncodeStringLiteral"/> must be used to encode the associated value.
/// </summary>
public static byte[] EncodeLiteralHeaderFieldWithoutIndexingNewNameToAllocatedArray(string name)
{
Span<byte> span = stackalloc byte[256];
bool success = EncodeLiteralHeaderFieldWithoutIndexingNewName(name, span, out int length);
Debug.Assert(success, $"Stack-allocated space was too small for \"{name}\".");
return span.Slice(0, length).ToArray();
}
private static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, Span<byte> span, out int length)
{
throw new NotImplementedException();
}
/// <summary>Encodes a "Literal Header Field without Indexing" to a new array.</summary>
public static byte[] EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(int index, string value)
{
Span<byte> span =
#if DEBUG
stackalloc byte[4]; // to validate growth algorithm
#else
stackalloc byte[512];
#endif
while (true)
{
if (EncodeLiteralHeaderFieldWithNameReference(index, value, span, out int length))
{
return span.Slice(0, length).ToArray();
}
// This is a rare path, only used once per HTTP/2 connection and only
// for very long host names. Just allocate rather than complicate
// the code with ArrayPool usage. In practice we should never hit this,
// as hostnames should be <= 255 characters.
span = new byte[span.Length * 2];
}
}
// 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();
_enumerator.MoveNext();
buffer[0] = 0;
buffer[1] = 0;
return Encode(buffer.Slice(2), out length);
}
public bool BeginEncode(int statusCode, IEnumerable<KeyValuePair<string, string>> headers, Span<byte> buffer, out int length)
{
_enumerator = headers.GetEnumerator();
_enumerator.MoveNext();
// https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#header-prefix
buffer[0] = 0;
buffer[1] = 0;
var statusCodeLength = EncodeStatusCode(statusCode, buffer.Slice(2));
var done = Encode(buffer.Slice(statusCodeLength + 2), throwIfNoneEncoded: false, out var headersLength);
length = statusCodeLength + headersLength + 2;
return done;
}
public bool Encode(Span<byte> buffer, out int length)
{
return Encode(buffer, throwIfNoneEncoded: true, out length);
}
private bool Encode(Span<byte> buffer, bool throwIfNoneEncoded, out int length)
{
length = 0;
do
{
if (!EncodeHeader(_enumerator.Current.Key, _enumerator.Current.Value, buffer.Slice(length), out var headerLength))
{
if (length == 0 && throwIfNoneEncoded)
{
throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */);
}
return false;
}
length += headerLength;
} while (_enumerator.MoveNext());
return true;
}
private bool EncodeHeader(string name, string value, Span<byte> buffer, out int length)
{
var i = 0;
length = 0;
if (buffer.IsEmpty)
{
return false;
}
if (!EncodeNameString(name, buffer.Slice(i), out var nameLength, lowercase: true))
{
return false;
}
i += nameLength;
if (i >= buffer.Length)
{
return false;
}
if (!EncodeValueString(value, buffer.Slice(i), out var valueLength, lowercase: false))
{
return false;
}
i += valueLength;
length = i;
return true;
}
private bool EncodeValueString(string s, Span<byte> buffer, out int length, bool lowercase)
{
const int toLowerMask = 0x20;
var i = 0;
length = 0;
if (buffer.IsEmpty)
{
return false;
}
buffer[0] = 0;
if (!IntegerEncoder.Encode(s.Length, 7, buffer, out var nameLength))
{
return false;
}
i += nameLength;
// TODO: use huffman encoding
for (var j = 0; j < s.Length; j++)
{
if (i >= buffer.Length)
{
return false;
}
buffer[i++] = (byte)(s[j] | (lowercase && s[j] >= (byte)'A' && s[j] <= (byte)'Z' ? toLowerMask : 0));
}
length = i;
return true;
}
private bool EncodeNameString(string s, Span<byte> buffer, out int length, bool lowercase)
{
const int toLowerMask = 0x20;
var i = 0;
length = 0;
if (buffer.IsEmpty)
{
return false;
}
buffer[0] = 0x30;
if (!IntegerEncoder.Encode(s.Length, 3, buffer, out var nameLength))
{
return false;
}
i += nameLength;
// TODO: use huffman encoding
for (var j = 0; j < s.Length; j++)
{
if (i >= buffer.Length)
{
return false;
}
buffer[i++] = (byte)(s[j] | (lowercase && s[j] >= (byte)'A' && s[j] <= (byte)'Z' ? toLowerMask : 0));
}
length = i;
return true;
}
private int EncodeStatusCode(int statusCode, Span<byte> buffer)
{
switch (statusCode)
{
case 200:
case 204:
case 206:
case 304:
case 400:
case 404:
case 500:
// TODO this isn't safe, some index can be larger than 64. Encoded here!
buffer[0] = (byte)(0xC0 | StaticTable.Instance.StatusIndex[statusCode]);
return 1;
default:
// Send as Literal Header Field Without Indexing - Indexed Name
buffer[0] = 0x08;
var statusBytes = StatusCodes.ToStatusBytes(statusCode);
buffer[1] = (byte)statusBytes.Length;
((ReadOnlySpan<byte>)statusBytes).CopyTo(buffer.Slice(2));
return 2 + statusBytes.Length;
}
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
{
internal sealed class QPackEncodingException : Exception
{
public QPackEncodingException(string message)
: base(message)
{
}
public QPackEncodingException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@ -0,0 +1,162 @@
// 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.Collections.Generic;
using System.Net.Http;
using System.Text;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack
{
internal class StaticTable
{
private static readonly StaticTable _instance = new StaticTable();
private readonly Dictionary<int, int> _statusIndex = new Dictionary<int, int>
{
[103] = 24,
[200] = 25,
[304] = 26,
[404] = 27,
[503] = 28,
[100] = 63,
[204] = 64,
[206] = 65,
[302] = 66,
[400] = 67,
[403] = 68,
[421] = 69,
[425] = 70,
[500] = 71,
};
private readonly Dictionary<HttpMethod, int> _methodIndex = new Dictionary<HttpMethod, int>
{
// TODO connect is intenral to system.net.http
[HttpMethod.Delete] = 16,
[HttpMethod.Get] = 17,
[HttpMethod.Head] = 18,
[HttpMethod.Options] = 19,
[HttpMethod.Post] = 20,
[HttpMethod.Put] = 21,
};
private StaticTable()
{
}
public static StaticTable Instance => _instance;
public int Count => _staticTable.Length;
public HeaderField this[int index] => _staticTable[index];
public IReadOnlyDictionary<int, int> StatusIndex => _statusIndex;
public IReadOnlyDictionary<HttpMethod, int> MethodIndex => _methodIndex;
private readonly HeaderField[] _staticTable = new HeaderField[]
{
CreateHeaderField(":authority", ""), // 0
CreateHeaderField(":path", "/"), // 1
CreateHeaderField("age", "0"), // 2
CreateHeaderField("content-disposition", ""),
CreateHeaderField("content-length", "0"),
CreateHeaderField("cookie", ""),
CreateHeaderField("date", ""),
CreateHeaderField("etag", ""),
CreateHeaderField("if-modified-since", ""),
CreateHeaderField("if-none-match", ""),
CreateHeaderField("last-modified", ""), // 10
CreateHeaderField("link", ""),
CreateHeaderField("location", ""),
CreateHeaderField("referer", ""),
CreateHeaderField("set-cookie", ""),
CreateHeaderField(":method", "CONNECT"),
CreateHeaderField(":method", "DELETE"),
CreateHeaderField(":method", "GET"),
CreateHeaderField(":method", "HEAD"),
CreateHeaderField(":method", "OPTIONS"),
CreateHeaderField(":method", "POST"), // 20
CreateHeaderField(":method", "PUT"),
CreateHeaderField(":scheme", "http"),
CreateHeaderField(":scheme", "https"),
CreateHeaderField(":status", "103"),
CreateHeaderField(":status", "200"),
CreateHeaderField(":status", "304"),
CreateHeaderField(":status", "404"),
CreateHeaderField(":status", "503"),
CreateHeaderField("accept", "*/*"),
CreateHeaderField("accept", "application/dns-message"), // 30
CreateHeaderField("accept-encoding", "gzip, deflate, br"),
CreateHeaderField("accept-ranges", "bytes"),
CreateHeaderField("access-control-allow-headers", "cache-control"),
CreateHeaderField("access-control-allow-origin", "content-type"),
CreateHeaderField("access-control-allow-origin", "*"),
CreateHeaderField("cache-control", "max-age=0"),
CreateHeaderField("cache-control", "max-age=2592000"),
CreateHeaderField("cache-control", "max-age=604800"),
CreateHeaderField("cache-control", "no-cache"),
CreateHeaderField("cache-control", "no-store"), // 40
CreateHeaderField("cache-control", "public, max-age=31536000"),
CreateHeaderField("content-encoding", "br"),
CreateHeaderField("content-encoding", "gzip"),
CreateHeaderField("content-type", "application/dns-message"),
CreateHeaderField("content-type", "application/javascript"),
CreateHeaderField("content-type", "application/json"),
CreateHeaderField("content-type", "application/x-www-form-urlencoded"),
CreateHeaderField("content-type", "image/gif"),
CreateHeaderField("content-type", "image/jpeg"),
CreateHeaderField("content-type", "image/png"), // 50
CreateHeaderField("content-type", "text/css"),
CreateHeaderField("content-type", "text/html; charset=utf-8"),
CreateHeaderField("content-type", "text/plain"),
CreateHeaderField("content-type", "text/plain;charset=utf-8"),
CreateHeaderField("range", "bytes=0-"),
CreateHeaderField("strict-transport-security", "max-age=31536000"),
CreateHeaderField("strict-transport-security", "max-age=31536000;includesubdomains"), // TODO confirm spaces here don't matter?
CreateHeaderField("strict-transport-security", "max-age=31536000;includesubdomains; preload"),
CreateHeaderField("vary", "accept-encoding"),
CreateHeaderField("vary", "origin"), // 60
CreateHeaderField("x-content-type-options", "nosniff"),
CreateHeaderField("x-xss-protection", "1; mode=block"),
CreateHeaderField(":status", "100"),
CreateHeaderField(":status", "204"),
CreateHeaderField(":status", "206"),
CreateHeaderField(":status", "302"),
CreateHeaderField(":status", "400"),
CreateHeaderField(":status", "403"),
CreateHeaderField(":status", "421"),
CreateHeaderField(":status", "425"), // 70
CreateHeaderField(":status", "500"),
CreateHeaderField("accept-language", ""),
CreateHeaderField("access-control-allow-credentials", "FALSE"),
CreateHeaderField("access-control-allow-credentials", "TRUE"),
CreateHeaderField("access-control-allow-headers", "*"),
CreateHeaderField("access-control-allow-methods", "get"),
CreateHeaderField("access-control-allow-methods", "get, post, options"),
CreateHeaderField("access-control-allow-methods", "options"),
CreateHeaderField("access-control-expose-headers", "content-length"),
CreateHeaderField("access-control-request-headers", "content-type"), // 80
CreateHeaderField("access-control-request-method", "get"),
CreateHeaderField("access-control-request-method", "post"),
CreateHeaderField("alt-svc", "clear"),
CreateHeaderField("authorization", ""),
CreateHeaderField("content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"),
CreateHeaderField("early-data", "1"),
CreateHeaderField("expect-ct", ""),
CreateHeaderField("forwarded", ""),
CreateHeaderField("if-range", ""),
CreateHeaderField("origin", ""), // 90
CreateHeaderField("purpose", "prefetch"),
CreateHeaderField("server", ""),
CreateHeaderField("timing-allow-origin", "*"),
CreateHeaderField("upgrading-insecure-requests", "1"),
CreateHeaderField("user-agent", ""),
CreateHeaderField("x-forwarded-for", ""),
CreateHeaderField("x-frame-options", "deny"),
CreateHeaderField("x-frame-options", "sameorigin"),
};
private static HeaderField CreateHeaderField(string name, string value)
=> new HeaderField(Encoding.ASCII.GetBytes(name), Encoding.ASCII.GetBytes(value));
}
}

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Connections.Features;
@ -11,6 +12,7 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -66,12 +68,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
requestProcessor = new Http2Connection(_context); requestProcessor = new Http2Connection(_context);
_protocolSelectionState = ProtocolSelectionState.Selected; _protocolSelectionState = ProtocolSelectionState.Selected;
break; break;
case HttpProtocols.Http3:
requestProcessor = new Http3Connection(_context);
_protocolSelectionState = ProtocolSelectionState.Selected;
break;
case HttpProtocols.None: case HttpProtocols.None:
// An error was already logged in SelectProtocol(), but we should close the connection. // An error was already logged in SelectProtocol(), but we should close the connection.
break; break;
default: default:
// SelectProtocol() only returns Http1, Http2 or None. // SelectProtocol() only returns Http1, Http2 or None.
throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2 or None."); throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2, Http3 or None.");
} }
_requestProcessor = requestProcessor; _requestProcessor = requestProcessor;
@ -197,6 +204,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private HttpProtocols SelectProtocol() private HttpProtocols SelectProtocol()
{ {
if (_context.Protocols == HttpProtocols.Http3)
{
return HttpProtocols.Http3;
}
var hasTls = _context.ConnectionFeatures.Get<ITlsConnectionFeature>() != null; var hasTls = _context.ConnectionFeatures.Get<ITlsConnectionFeature>() != null;
var applicationProtocol = _context.ConnectionFeatures.Get<ITlsApplicationProtocolFeature>()?.ApplicationProtocol var applicationProtocol = _context.ConnectionFeatures.Get<ITlsApplicationProtocolFeature>()?.ApplicationProtocol
?? new ReadOnlyMemory<byte>(); ?? new ReadOnlyMemory<byte>();

View File

@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
public const string Http10Version = "HTTP/1.0"; public const string Http10Version = "HTTP/1.0";
public const string Http11Version = "HTTP/1.1"; public const string Http11Version = "HTTP/1.1";
public const string Http2Version = "HTTP/2"; public const string Http2Version = "HTTP/2";
public const string Http3Version = "HTTP/3";
public const string HttpUriScheme = "http://"; public const string HttpUriScheme = "http://";
public const string HttpsUriScheme = "https://"; public const string HttpsUriScheme = "https://";

View File

@ -23,26 +23,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
{ {
private readonly List<(IConnectionListener, Task)> _transports = new List<(IConnectionListener, Task)>(); private readonly List<(IConnectionListener, Task)> _transports = new List<(IConnectionListener, Task)>();
private readonly IServerAddressesFeature _serverAddresses; private readonly IServerAddressesFeature _serverAddresses;
private readonly IConnectionListenerFactory _transportFactory; private readonly IEnumerable<IConnectionListenerFactory> _transportFactories;
private bool _hasStarted; private bool _hasStarted;
private int _stopping; private int _stopping;
private readonly TaskCompletionSource<object> _stoppedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); private readonly TaskCompletionSource<object> _stoppedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
public KestrelServer(IOptions<KestrelServerOptions> options, IConnectionListenerFactory transportFactory, ILoggerFactory loggerFactory) public KestrelServer(IOptions<KestrelServerOptions> options, IEnumerable<IConnectionListenerFactory> transportFactories, ILoggerFactory loggerFactory)
: this(transportFactory, CreateServiceContext(options, loggerFactory)) : this(transportFactories, CreateServiceContext(options, loggerFactory))
{ {
} }
// For testing // For testing
internal KestrelServer(IConnectionListenerFactory transportFactory, ServiceContext serviceContext) internal KestrelServer(IEnumerable<IConnectionListenerFactory> transportFactories, ServiceContext serviceContext)
{ {
if (transportFactory == null) if (transportFactories == null)
{ {
throw new ArgumentNullException(nameof(transportFactory)); throw new ArgumentNullException(nameof(transportFactories));
} }
_transportFactory = transportFactory; _transportFactories = transportFactories;
ServiceContext = serviceContext; ServiceContext = serviceContext;
Features = new FeatureCollection(); Features = new FeatureCollection();
@ -135,7 +135,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
} }
var connectionDispatcher = new ConnectionDispatcher(ServiceContext, connectionDelegate); var connectionDispatcher = new ConnectionDispatcher(ServiceContext, connectionDelegate);
var transport = await _transportFactory.BindAsync(options.EndPoint).ConfigureAwait(false);
IConnectionListenerFactory factory = null;
if (options.Protocols >= HttpProtocols.Http3)
{
foreach (var transportFactory in _transportFactories)
{
if (transportFactory is IMultiplexedConnectionListenerFactory)
{
factory = transportFactory;
break;
}
}
if (factory == null)
{
throw new Exception("Quic transport not found when using HTTP/3");
}
}
else
{
factory = _transportFactories.Single();
}
var transport = await factory.BindAsync(options.EndPoint).ConfigureAwait(false);
// Update the endpoint // Update the endpoint
options.EndPoint = transport.EndPoint; options.EndPoint = transport.EndPoint;

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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -258,6 +258,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </summary> /// </summary>
public Http2Limits Http2 { get; } = new Http2Limits(); public Http2Limits Http2 { get; } = new Http2Limits();
/// <summary>
/// Limits only applicable to HTTP/3 connections.
/// </summary>
public Http3Limits Http3 { get; } = new Http3Limits();
/// <summary> /// <summary>
/// Gets or sets the request body minimum data rate in bytes/second. /// Gets or sets the request body minimum data rate in bytes/second.
/// Setting this property to null indicates no minimum data rate should be enforced. /// Setting this property to null indicates no minimum data rate should be enforced.

View File

@ -72,6 +72,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </summary> /// </summary>
public KestrelConfigurationLoader ConfigurationLoader { get; set; } public KestrelConfigurationLoader ConfigurationLoader { get; set; }
/// <summary>
/// Controls whether to return the AltSvcHeader from on an HTTP/2 or lower response for HTTP/3
/// </summary>
public bool EnableAltSvc { get; set; } = false;
/// <summary> /// <summary>
/// A default configuration action for all endpoints. Use for Listen, configuration, the default url, and URLs. /// A default configuration action for all endpoints. Use for Listen, configuration, the default url, and URLs.
/// </summary> /// </summary>

View File

@ -79,11 +79,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
_options = options; _options = options;
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>(); _logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
} }
public async Task OnConnectionAsync(ConnectionContext context) public async Task OnConnectionAsync(ConnectionContext context)
{ {
await Task.Yield(); await Task.Yield();
bool certificateRequired; bool certificateRequired;
if (context.Features.Get<ITlsConnectionFeature>() != null)
{
await _next(context);
return;
}
var feature = new Core.Internal.TlsConnectionFeature(); var feature = new Core.Internal.TlsConnectionFeature();
context.Features.Set<ITlsConnectionFeature>(feature); context.Features.Set<ITlsConnectionFeature>(feature);
context.Features.Set<ITlsHandshakeFeature>(feature); context.Features.Set<ITlsHandshakeFeature>(feature);

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Threading; using System.Threading;
@ -203,7 +204,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockLoggerFactory = new Mock<ILoggerFactory>(); var mockLoggerFactory = new Mock<ILoggerFactory>();
var mockLogger = new Mock<ILogger>(); var mockLogger = new Mock<ILogger>();
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object); mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
new KestrelServer(Options.Create<KestrelServerOptions>(null), Mock.Of<IConnectionListenerFactory>(), mockLoggerFactory.Object); new KestrelServer(Options.Create<KestrelServerOptions>(null), Mock.Of<IEnumerable<IConnectionListenerFactory>>(), mockLoggerFactory.Object);
mockLoggerFactory.Verify(factory => factory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel")); mockLoggerFactory.Verify(factory => factory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel"));
} }
@ -216,7 +217,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var exception = Assert.Throws<ArgumentNullException>(() => var exception = Assert.Throws<ArgumentNullException>(() =>
new KestrelServer(Options.Create<KestrelServerOptions>(null), null, mockLoggerFactory.Object)); new KestrelServer(Options.Create<KestrelServerOptions>(null), null, mockLoggerFactory.Object));
Assert.Equal("transportFactory", exception.ParamName); Assert.Equal("transportFactories", exception.ParamName);
} }
[Fact] [Fact]
@ -257,7 +258,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockLoggerFactory = new Mock<ILoggerFactory>(); var mockLoggerFactory = new Mock<ILoggerFactory>();
var mockLogger = new Mock<ILogger>(); var mockLogger = new Mock<ILogger>();
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object); mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object); var server = new KestrelServer(Options.Create(options), new List<IConnectionListenerFactory>() { mockTransportFactory.Object }, mockLoggerFactory.Object);
await server.StartAsync(new DummyApplication(), CancellationToken.None); await server.StartAsync(new DummyApplication(), CancellationToken.None);
var stopTask1 = server.StopAsync(default); var stopTask1 = server.StopAsync(default);
@ -315,7 +316,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockLoggerFactory = new Mock<ILoggerFactory>(); var mockLoggerFactory = new Mock<ILoggerFactory>();
var mockLogger = new Mock<ILogger>(); var mockLogger = new Mock<ILogger>();
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object); mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object); var server = new KestrelServer(Options.Create(options), new List<IConnectionListenerFactory>() { mockTransportFactory.Object }, mockLoggerFactory.Object);
await server.StartAsync(new DummyApplication(), CancellationToken.None); await server.StartAsync(new DummyApplication(), CancellationToken.None);
var stopTask1 = server.StopAsync(default); var stopTask1 = server.StopAsync(default);
@ -370,7 +371,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockLoggerFactory = new Mock<ILoggerFactory>(); var mockLoggerFactory = new Mock<ILoggerFactory>();
var mockLogger = new Mock<ILogger>(); var mockLogger = new Mock<ILogger>();
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object); mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object); var server = new KestrelServer(Options.Create(options), new List<IConnectionListenerFactory>() { mockTransportFactory.Object }, mockLoggerFactory.Object);
await server.StartAsync(new DummyApplication(), default); await server.StartAsync(new DummyApplication(), default);
var stopTask1 = server.StopAsync(default); var stopTask1 = server.StopAsync(default);
@ -416,7 +417,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
DebuggerWrapper.Singleton, DebuggerWrapper.Singleton,
testContext.Log); testContext.Log);
using (var server = new KestrelServer(new MockTransportFactory(), testContext)) using (var server = new KestrelServer(new List<IConnectionListenerFactory>() { new MockTransportFactory() }, testContext))
{ {
Assert.Null(testContext.DateHeaderValueManager.GetDateHeaderValues()); Assert.Null(testContext.DateHeaderValueManager.GetDateHeaderValues());
@ -433,12 +434,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
private static KestrelServer CreateServer(KestrelServerOptions options, ILogger testLogger) private static KestrelServer CreateServer(KestrelServerOptions options, ILogger testLogger)
{ {
return new KestrelServer(Options.Create(options), new MockTransportFactory(), new LoggerFactory(new[] { new KestrelTestLoggerProvider(testLogger) })); return new KestrelServer(Options.Create(options), new List<IConnectionListenerFactory>() { new MockTransportFactory() }, new LoggerFactory(new[] { new KestrelTestLoggerProvider(testLogger) }));
} }
private static KestrelServer CreateServer(KestrelServerOptions options, bool throwOnCriticalErrors = true) private static KestrelServer CreateServer(KestrelServerOptions options, bool throwOnCriticalErrors = true)
{ {
return new KestrelServer(Options.Create(options), new MockTransportFactory(), new LoggerFactory(new[] { new KestrelTestLoggerProvider(throwOnCriticalErrors) })); return new KestrelServer(Options.Create(options), new List<IConnectionListenerFactory>() { new MockTransportFactory() }, new LoggerFactory(new[] { new KestrelTestLoggerProvider(throwOnCriticalErrors) }));
} }
private static void StartDummyApplication(IServer server) private static void StartDummyApplication(IServer server)

View File

@ -0,0 +1,46 @@
using System;
using System.Buffers;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class VariableIntHelperTests
{
[Theory]
[MemberData(nameof(IntegerData))]
public void CheckDecoding(long expected, byte[] input)
{
var decoded = VariableLengthIntegerHelper.GetInteger(new ReadOnlySequence<byte>(input), out _, out _);
Assert.Equal(expected, decoded);
}
[Theory]
[MemberData(nameof(IntegerData))]
public void CheckEncoding(long input, byte[] expected)
{
var outputBuffer = new Span<byte>(new byte[8]);
var encodedLength = VariableLengthIntegerHelper.WriteInteger(outputBuffer, input);
Assert.Equal(expected.Length, encodedLength);
for(var i = 0; i < expected.Length; i++)
{
Assert.Equal(expected[i], outputBuffer[i]);
}
}
public static TheoryData<long, byte[]> IntegerData
{
get
{
var data = new TheoryData<long, byte[]>();
data.Add(151288809941952652, new byte[] { 0xc2, 0x19, 0x7c, 0x5e, 0xff, 0x14, 0xe8, 0x8c });
data.Add(494878333, new byte[] { 0x9d, 0x7f, 0x3e, 0x7d });
data.Add(15293, new byte[] { 0x7b, 0xbd });
data.Add(37, new byte[] { 0x25 });
return data;
}
}
}
}

View File

@ -88,7 +88,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuicSampleApp", "samples\Qu
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic", "Transport.MsQuic\src\Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.csproj", "{62CFF861-807E-43F6-9403-22AA7F06C9A6}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic", "Transport.MsQuic\src\Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.csproj", "{62CFF861-807E-43F6-9403-22AA7F06C9A6}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuicSampleClient", "samples\QuicSampleClient\QuicSampleClient.csproj", "{F39A942B-85A8-4C1B-A5BC-435555E79F20}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuicSampleClient", "samples\QuicSampleClient\QuicSampleClient.csproj", "{F39A942B-85A8-4C1B-A5BC-435555E79F20}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Http3SampleApp", "samples\Http3SampleApp\Http3SampleApp.csproj", "{B3CDC83A-A9C5-45DF-9828-6BC419C24308}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -508,6 +510,18 @@ Global
{F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x64.Build.0 = Release|Any CPU {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x64.Build.0 = Release|Any CPU
{F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x86.ActiveCfg = Release|Any CPU {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x86.ActiveCfg = Release|Any CPU
{F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x86.Build.0 = Release|Any CPU {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x86.Build.0 = Release|Any CPU
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|x64.ActiveCfg = Debug|Any CPU
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|x64.Build.0 = Debug|Any CPU
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|x86.ActiveCfg = Debug|Any CPU
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|x86.Build.0 = Debug|Any CPU
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|Any CPU.Build.0 = Release|Any CPU
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|x64.ActiveCfg = Release|Any CPU
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|x64.Build.0 = Release|Any CPU
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|x86.ActiveCfg = Release|Any CPU
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -548,6 +562,7 @@ Global
{53A8634C-DFC5-4A5B-8864-9EF1707E3F18} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0} {53A8634C-DFC5-4A5B-8864-9EF1707E3F18} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0}
{62CFF861-807E-43F6-9403-22AA7F06C9A6} = {2B456D08-F72B-4EB8-B663-B6D78FC04BF8} {62CFF861-807E-43F6-9403-22AA7F06C9A6} = {2B456D08-F72B-4EB8-B663-B6D78FC04BF8}
{F39A942B-85A8-4C1B-A5BC-435555E79F20} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0} {F39A942B-85A8-4C1B-A5BC-435555E79F20} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0}
{B3CDC83A-A9C5-45DF-9828-6BC419C24308} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {48207B50-7D05-4B10-B585-890FE0F4FCE1} SolutionGuid = {48207B50-7D05-4B10-B585-890FE0F4FCE1}

View File

@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic
[System.Diagnostics.DebuggerStepThroughAttribute] [System.Diagnostics.DebuggerStepThroughAttribute]
public System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> ConnectAsync(System.Net.EndPoint endPoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> ConnectAsync(System.Net.EndPoint endPoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
} }
public partial class MsQuicTransportFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory public partial class MsQuicTransportFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory, Microsoft.AspNetCore.Connections.IMultiplexedConnectionListenerFactory
{ {
public MsQuicTransportFactory(Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.MsQuicTransportOptions> options) { } public MsQuicTransportFactory(Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.MsQuicTransportOptions> options) { }
[System.Diagnostics.DebuggerStepThroughAttribute] [System.Diagnostics.DebuggerStepThroughAttribute]

View File

@ -163,6 +163,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
Buffer.Buffer); Buffer.Buffer);
} }
internal unsafe uint UnsafeGetParam(
IntPtr Handle,
uint Level,
uint Param,
ref MsQuicNativeMethods.QuicBuffer Buffer)
{
return GetParamDelegate(
Handle,
Level,
Param,
out Buffer.Length,
out Buffer.Buffer);
}
public async ValueTask<QuicSecConfig> CreateSecurityConfig(X509Certificate2 certificate) public async ValueTask<QuicSecConfig> CreateSecurityConfig(X509Certificate2 certificate)
{ {
QuicSecConfig secConfig = null; QuicSecConfig secConfig = null;

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Abstractions.Features; using Microsoft.AspNetCore.Connections.Abstractions.Features;
using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features;
using static Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal.MsQuicNativeMethods; using static Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal.MsQuicNativeMethods;
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
@ -38,6 +39,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
SetIdleTimeout(_context.Options.IdleTimeout); SetIdleTimeout(_context.Options.IdleTimeout);
Features.Set<ITlsConnectionFeature>(new FakeTlsConnectionFeature());
Features.Set<IQuicStreamListenerFeature>(this); Features.Set<IQuicStreamListenerFeature>(this);
Features.Set<IQuicCreateStreamFeature>(this); Features.Set<IQuicCreateStreamFeature>(this);

View File

@ -99,6 +99,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
_session.SetPeerUnidirectionalStreamCount(_transportContext.Options.MaxBidirectionalStreamCount); _session.SetPeerUnidirectionalStreamCount(_transportContext.Options.MaxBidirectionalStreamCount);
var address = MsQuicNativeMethods.Convert(EndPoint as IPEndPoint); var address = MsQuicNativeMethods.Convert(EndPoint as IPEndPoint);
MsQuicStatusException.ThrowIfFailed(_api.ListenerStartDelegate( MsQuicStatusException.ThrowIfFailed(_api.ListenerStartDelegate(
_nativeObjPtr, _nativeObjPtr,
ref address)); ref address));
@ -111,6 +112,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
{ {
case QUIC_LISTENER_EVENT.NEW_CONNECTION: case QUIC_LISTENER_EVENT.NEW_CONNECTION:
{ {
evt.Data.NewConnection.SecurityConfig = _secConfig.NativeObjPtr; evt.Data.NewConnection.SecurityConfig = _secConfig.NativeObjPtr;
var msQuicConnection = new MsQuicConnection(_api, _transportContext, evt.Data.NewConnection.Connection); var msQuicConnection = new MsQuicConnection(_api, _transportContext, evt.Data.NewConnection.Connection);
_acceptConnectionQueue.Writer.TryWrite(msQuicConnection); _acceptConnectionQueue.Writer.TryWrite(msQuicConnection);

View File

@ -82,8 +82,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
IntPtr Handle, IntPtr Handle,
uint Level, uint Level,
uint Param, uint Param,
IntPtr BufferLength, out uint BufferLength,
IntPtr Buffer); out byte* Buffer);
internal delegate uint RegistrationOpenDelegate(byte[] appName, out IntPtr RegistrationContext); internal delegate uint RegistrationOpenDelegate(byte[] appName, out IntPtr RegistrationContext);
@ -533,36 +533,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
{ {
var socketAddress = new SOCKADDR_INET(); var socketAddress = new SOCKADDR_INET();
var buffer = endpoint.Address.GetAddressBytes(); var buffer = endpoint.Address.GetAddressBytes();
switch (endpoint.Address.AddressFamily) if (endpoint.Address != IPAddress.Any && endpoint.Address != IPAddress.IPv6Any)
{ {
case AddressFamily.InterNetwork: switch (endpoint.Address.AddressFamily)
socketAddress.Ipv4.sin_addr0 = buffer[0]; {
socketAddress.Ipv4.sin_addr1 = buffer[1]; case AddressFamily.InterNetwork:
socketAddress.Ipv4.sin_addr2 = buffer[2]; socketAddress.Ipv4.sin_addr0 = buffer[0];
socketAddress.Ipv4.sin_addr3 = buffer[3]; socketAddress.Ipv4.sin_addr1 = buffer[1];
socketAddress.Ipv4.sin_family = IPv4; socketAddress.Ipv4.sin_addr2 = buffer[2];
break; socketAddress.Ipv4.sin_addr3 = buffer[3];
case AddressFamily.InterNetworkV6: socketAddress.Ipv4.sin_family = IPv4;
socketAddress.Ipv6.sin6_addr0 = buffer[0]; break;
socketAddress.Ipv6.sin6_addr1 = buffer[1]; case AddressFamily.InterNetworkV6:
socketAddress.Ipv6.sin6_addr2 = buffer[2]; socketAddress.Ipv6.sin6_addr0 = buffer[0];
socketAddress.Ipv6.sin6_addr3 = buffer[3]; socketAddress.Ipv6.sin6_addr1 = buffer[1];
socketAddress.Ipv6.sin6_addr4 = buffer[4]; socketAddress.Ipv6.sin6_addr2 = buffer[2];
socketAddress.Ipv6.sin6_addr5 = buffer[5]; socketAddress.Ipv6.sin6_addr3 = buffer[3];
socketAddress.Ipv6.sin6_addr6 = buffer[6]; socketAddress.Ipv6.sin6_addr4 = buffer[4];
socketAddress.Ipv6.sin6_addr7 = buffer[7]; socketAddress.Ipv6.sin6_addr5 = buffer[5];
socketAddress.Ipv6.sin6_addr8 = buffer[8]; socketAddress.Ipv6.sin6_addr6 = buffer[6];
socketAddress.Ipv6.sin6_addr9 = buffer[9]; socketAddress.Ipv6.sin6_addr7 = buffer[7];
socketAddress.Ipv6.sin6_addr10 = buffer[10]; socketAddress.Ipv6.sin6_addr8 = buffer[8];
socketAddress.Ipv6.sin6_addr11 = buffer[11]; socketAddress.Ipv6.sin6_addr9 = buffer[9];
socketAddress.Ipv6.sin6_addr12 = buffer[12]; socketAddress.Ipv6.sin6_addr10 = buffer[10];
socketAddress.Ipv6.sin6_addr13 = buffer[13]; socketAddress.Ipv6.sin6_addr11 = buffer[11];
socketAddress.Ipv6.sin6_addr14 = buffer[14]; socketAddress.Ipv6.sin6_addr12 = buffer[12];
socketAddress.Ipv6.sin6_addr15 = buffer[15]; socketAddress.Ipv6.sin6_addr13 = buffer[13];
socketAddress.Ipv6.sin6_family = IPv6; socketAddress.Ipv6.sin6_addr14 = buffer[14];
break; socketAddress.Ipv6.sin6_addr15 = buffer[15];
default: socketAddress.Ipv6.sin6_family = IPv6;
throw new ArgumentException("Only IPv4 or IPv6 are supported"); break;
default:
throw new ArgumentException("Only IPv4 or IPv6 are supported");
}
} }
SetPort(endpoint.Address.AddressFamily, ref socketAddress, endpoint.Port); SetPort(endpoint.Address.AddressFamily, ref socketAddress, endpoint.Port);

View File

@ -17,7 +17,7 @@ using static Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal.MsQui
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
{ {
internal class MsQuicStream : TransportConnection, IUnidirectionalStreamFeature internal class MsQuicStream : TransportConnection, IQuicStreamFeature
{ {
private Task _processingTask; private Task _processingTask;
private MsQuicConnection _connection; private MsQuicConnection _connection;
@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
private GCHandle _handle; private GCHandle _handle;
private StreamCallbackDelegate _delegate; private StreamCallbackDelegate _delegate;
private string _connectionId; private string _connectionId;
private long _streamId = -1;
internal ResettableCompletionSource _resettableCompletion; internal ResettableCompletionSource _resettableCompletion;
private MemoryHandle[] _bufferArrays; private MemoryHandle[] _bufferArrays;
@ -56,15 +57,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions); var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions);
// TODO when stream is unidirectional, don't create an output pipe. Features.Set<IQuicStreamFeature>(this);
if (flags.HasFlag(QUIC_STREAM_OPEN_FLAG.UNIDIRECTIONAL))
{
Features.Set<IUnidirectionalStreamFeature>(this);
}
// TODO populate the ITlsConnectionFeature (requires client certs). // TODO populate the ITlsConnectionFeature (requires client certs).
var feature = new FakeTlsConnectionFeature(); Features.Set<ITlsConnectionFeature>(new FakeTlsConnectionFeature());
Features.Set<ITlsConnectionFeature>(feature); if (flags.HasFlag(QUIC_STREAM_OPEN_FLAG.UNIDIRECTIONAL))
{
IsUnidirectional = true;
}
Transport = pair.Transport; Transport = pair.Transport;
Application = pair.Application; Application = pair.Application;
@ -72,21 +72,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
SetCallbackHandler(); SetCallbackHandler();
_processingTask = ProcessSends(); _processingTask = ProcessSends();
// Concatenate stream id with ConnectionId.
_log.NewStream(ConnectionId);
} }
public override MemoryPool<byte> MemoryPool { get; } public override MemoryPool<byte> MemoryPool { get; }
public PipeWriter Input => Application.Output; public PipeWriter Input => Application.Output;
public PipeReader Output => Application.Input; public PipeReader Output => Application.Input;
public bool IsUnidirectional { get; }
public long StreamId
{
get
{
if (_streamId == -1)
{
_streamId = GetStreamId();
}
return _streamId;
}
}
public override string ConnectionId { public override string ConnectionId {
get get
{ {
if (_connectionId == null) if (_connectionId == null)
{ {
_connectionId = $"{_connection.ConnectionId}:{base.ConnectionId}"; _connectionId = $"{_connection.ConnectionId}:{StreamId}";
} }
return _connectionId; return _connectionId;
} }
@ -390,20 +402,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
SetParam(QUIC_PARAM_STREAM.RECEIVE_ENABLED, buffer); SetParam(QUIC_PARAM_STREAM.RECEIVE_ENABLED, buffer);
} }
private unsafe long GetStreamId()
{
byte* ptr = stackalloc byte[sizeof(long)];
var buffer = new QuicBuffer
{
Length = sizeof(long),
Buffer = ptr
};
GetParam(QUIC_PARAM_STREAM.ID, buffer);
return *(long*)ptr;
}
private void GetParam(
QUIC_PARAM_STREAM param,
QuicBuffer buf)
{
MsQuicStatusException.ThrowIfFailed(Api.UnsafeGetParam(
_nativeObjPtr,
(uint)QUIC_PARAM_LEVEL.STREAM,
(uint)param,
ref buf));
}
private void SetParam( private void SetParam(
QUIC_PARAM_STREAM param, QUIC_PARAM_STREAM param,
QuicBuffer buf) QuicBuffer buf)
{ {
MsQuicStatusException.ThrowIfFailed(Api.UnsafeSetParam( MsQuicStatusException.ThrowIfFailed(Api.UnsafeSetParam(
_nativeObjPtr, _nativeObjPtr,
(uint)QUIC_PARAM_LEVEL.SESSION, (uint)QUIC_PARAM_LEVEL.STREAM,
(uint)param, (uint)param,
buf)); buf));
} }
~MsQuicStream() ~MsQuicStream()
{ {
_log.LogDebug("Destructor");
Dispose(false); Dispose(false);
} }

View File

@ -13,7 +13,7 @@ using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic
{ {
public class MsQuicTransportFactory : IConnectionListenerFactory public class MsQuicTransportFactory : IMultiplexedConnectionListenerFactory
{ {
private MsQuicTrace _log; private MsQuicTrace _log;
private IHostApplicationLifetime _applicationLifetime; private IHostApplicationLifetime _applicationLifetime;

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
<Reference Include="Microsoft.Extensions.Logging.Console" />
<Reference Include="Microsoft.Extensions.Hosting" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,52 @@
using System;
using System.IO;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Http3SampleApp
{
public class Program
{
public static void Main(string[] args)
{
var cert = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, true);
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
.UseMsQuic(options =>
{
options.Certificate = cert;
options.RegistrationName = "Quic";
options.Alpn = "h3-24";
options.IdleTimeout = TimeSpan.FromHours(1);
})
.ConfigureKestrel((context, options) =>
{
var basePort = 5555;
options.Listen(IPAddress.Any, basePort, listenOptions =>
{
listenOptions.UseHttps();
listenOptions.Protocols = HttpProtocols.Http3;
});
})
.UseStartup<Startup>();
});
hostBuilder.Build().Run();
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace Http3SampleApp
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
var memory = new Memory<byte>(new byte[4096]);
var length = await context.Request.Body.ReadAsync(memory);
context.Response.Headers["test"] = "foo";
// for testing
await context.Response.WriteAsync("Hello World! " + context.Request.Protocol);
});
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Diagnostics; using System.Diagnostics;
using Microsoft.AspNetCore.Server.Kestrel.Core;
namespace QuicSampleApp namespace QuicSampleApp
{ {
@ -46,6 +47,7 @@ namespace QuicSampleApp
options.Listen(IPAddress.Any, basePort, listenOptions => options.Listen(IPAddress.Any, basePort, listenOptions =>
{ {
listenOptions.Protocols = HttpProtocols.Http3;
listenOptions.Use((next) => listenOptions.Use((next) =>
{ {
return async connection => return async connection =>

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

View File

@ -53,6 +53,8 @@ namespace QuicSampleClient
public async Task RunAsync() public async Task RunAsync()
{ {
var start = Console.ReadLine();
Console.WriteLine("Starting");
var connectionContext = await _connectionFactory.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 5555)); var connectionContext = await _connectionFactory.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 5555));
var createStreamFeature = connectionContext.Features.Get<IQuicCreateStreamFeature>(); var createStreamFeature = connectionContext.Features.Get<IQuicCreateStreamFeature>();
var streamContext = await createStreamFeature.StartBidirectionalStreamAsync(); var streamContext = await createStreamFeature.StartBidirectionalStreamAsync();

View File

@ -145,6 +145,7 @@ namespace CodeGenerator
{ {
"Accept-Ranges", "Accept-Ranges",
"Age", "Age",
"Alt-Svc",
"ETag", "ETag",
"Location", "Location",
"Proxy-Authenticate", "Proxy-Authenticate",

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Reflection; using System.Reflection;
@ -89,7 +90,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
c.Configure(context.ServerOptions); c.Configure(context.ServerOptions);
} }
return new KestrelServer(sp.GetRequiredService<IConnectionListenerFactory>(), context); return new KestrelServer(new List<IConnectionListenerFactory>() { sp.GetRequiredService<IConnectionListenerFactory>() }, context);
}); });
configureServices(services); configureServices(services);
}) })

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class Http3StreamTests : Http3TestBase
{
[Fact]
public async Task HelloWorldTest()
{
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(_echoApplication);
var doneWithHeaders = await requestStream.SendHeadersAsync(headers);
await requestStream.SendDataAsync(Encoding.ASCII.GetBytes("Hello world"));
var responseHeaders = await requestStream.ExpectHeadersAsync();
var responseData = await requestStream.ExpectDataAsync();
Assert.Equal("Hello world", Encoding.ASCII.GetString(responseData.ToArray()));
}
}
}

View File

@ -0,0 +1,386 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Net.Http;
using System.Reflection;
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 Moq;
using Xunit;
using Xunit.Abstractions;
using static Microsoft.AspNetCore.Server.Kestrel.Core.Tests.Http2TestBase;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable, IQuicCreateStreamFeature, IQuicStreamListenerFeature
{
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 readonly Channel<ConnectionContext> _acceptConnectionQueue = Channel.CreateUnbounded<ConnectionContext>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = true
});
public Http3TestBase()
{
_timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object);
_mockTimeoutControl = new Mock<MockTimeoutControlBase>(_timeoutControl) { CallBase = true };
_timeoutControl.Debugger = Mock.Of<IDebugger>();
_echoApplication = async context =>
{
var buffer = new byte[Http3PeerSettings.MinAllowedMaxFrameSize];
var received = 0;
while ((received = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await context.Response.Body.WriteAsync(buffer, 0, received);
}
};
}
public override void Initialize(TestContext context, MethodInfo methodInfo, object[] testMethodArguments, ITestOutputHelper testOutputHelper)
{
base.Initialize(context, methodInfo, testMethodArguments, testOutputHelper);
_serviceContext = new TestServiceContext(LoggerFactory, _mockKestrelTrace.Object)
{
Scheduler = PipeScheduler.Inline,
};
}
protected async Task InitializeConnectionAsync(RequestDelegate application)
{
if (_connection == null)
{
CreateConnection();
}
_connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(application));
await Task.CompletedTask;
}
internal async ValueTask<Http3RequestStream> InitializeConnectionAndStreamsAsync(RequestDelegate application)
{
await InitializeConnectionAsync(application);
var controlStream1 = await CreateControlStream(0);
var controlStream2 = await CreateControlStream(2);
var controlStream3 = await CreateControlStream(3);
return await CreateRequestStream();
}
protected void CreateConnection()
{
var limits = _serviceContext.ServerOptions.Limits;
var features = new FeatureCollection();
features.Set<IQuicCreateStreamFeature>(this);
features.Set<IQuicStreamListenerFeature>(this);
var httpConnectionContext = new HttpConnectionContext
{
ConnectionContext = _mockConnectionContext.Object,
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));
}
private static PipeOptions GetInputPipeOptions(ServiceContext serviceContext, MemoryPool<byte> memoryPool, PipeScheduler writerScheduler) => new PipeOptions
(
pool: memoryPool,
readerScheduler: serviceContext.Scheduler,
writerScheduler: writerScheduler,
pauseWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0,
resumeWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0,
useSynchronizationContext: false,
minimumSegmentSize: memoryPool.GetMinimumSegmentSize()
);
private static PipeOptions GetOutputPipeOptions(ServiceContext serviceContext, MemoryPool<byte> memoryPool, PipeScheduler readerScheduler) => new PipeOptions
(
pool: memoryPool,
readerScheduler: readerScheduler,
writerScheduler: serviceContext.Scheduler,
pauseWriterThreshold: GetOutputResponseBufferSize(serviceContext),
resumeWriterThreshold: GetOutputResponseBufferSize(serviceContext),
useSynchronizationContext: false,
minimumSegmentSize: memoryPool.GetMinimumSegmentSize()
);
private static long GetOutputResponseBufferSize(ServiceContext serviceContext)
{
var bufferSize = serviceContext.ServerOptions.Limits.MaxResponseBufferSize;
if (bufferSize == 0)
{
// 0 = no buffering so we need to configure the pipe so the writer waits on the reader directly
return 1;
}
// null means that we have no back pressure
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);
await stream.WriteStreamIdAsync(id);
return stream;
}
internal ValueTask<Http3RequestStream> CreateRequestStream()
{
var stream = new Http3RequestStream(this, _connection);
_acceptConnectionQueue.Writer.TryWrite(stream.ConnectionContext);
return new ValueTask<Http3RequestStream>(stream);
}
public ValueTask<ConnectionContext> StartBidirectionalStreamAsync()
{
var stream = new Http3RequestStream(this, _connection);
// TODO put these somewhere to be read.
return new ValueTask<ConnectionContext>(stream.ConnectionContext);
}
internal class Http3StreamBase
{
protected DuplexPipe.DuplexPipePair _pair;
protected Http3TestBase _testBase;
protected Http3Connection _connection;
protected Task SendAsync(ReadOnlySpan<byte> span)
{
var writableBuffer = _pair.Application.Output;
writableBuffer.Write(span);
return FlushAsync(writableBuffer);
}
protected static async Task FlushAsync(PipeWriter writableBuffer)
{
await writableBuffer.FlushAsync().AsTask().DefaultTimeout();
}
}
internal class Http3RequestStream : Http3StreamBase, IHttpHeadersHandler, IQuicStreamFeature
{
internal ConnectionContext ConnectionContext { get; }
public bool IsUnidirectional => false;
public long StreamId => 0;
private readonly byte[] _headerEncodingBuffer = new byte[Http3PeerSettings.MinAllowedMaxFrameSize];
private QPackEncoder _qpackEncoder = new QPackEncoder();
private QPackDecoder _qpackDecoder = new QPackDecoder(10000, 10000);
private long _bytesReceived;
protected readonly Dictionary<string, string> _decodedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public Http3RequestStream(Http3TestBase testBase, Http3Connection connection)
{
_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);
}
public async Task<bool> SendHeadersAsync(IEnumerable<KeyValuePair<string, string>> headers)
{
var outputWriter = _pair.Application.Output;
var frame = new Http3Frame();
frame.PrepareHeaders();
var buffer = _headerEncodingBuffer.AsMemory();
var done = _qpackEncoder.BeginEncode(headers, buffer.Span, out var length);
frame.Length = length;
// 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));
return done;
}
internal async Task SendDataAsync(Memory<byte> data)
{
var outputWriter = _pair.Application.Output;
var frame = new Http3Frame();
frame.PrepareData();
frame.Length = data.Length;
Http3FrameWriter.WriteHeader(frame, outputWriter);
await SendAsync(data.Span);
}
internal async Task<IEnumerable<KeyValuePair<string, string>>> ExpectHeadersAsync()
{
var http3WithPayload = await ReceiveFrameAsync();
_qpackDecoder.Decode(http3WithPayload.PayloadSequence, this);
return _decodedHeaders;
}
internal async Task<Memory<byte>> ExpectDataAsync()
{
var http3WithPayload = await ReceiveFrameAsync();
return http3WithPayload.Payload;
}
internal async Task<Http3FrameWithPayload> ReceiveFrameAsync(uint maxFrameSize = Http3PeerSettings.DefaultMaxFrameSize)
{
var frame = new Http3FrameWithPayload();
while (true)
{
var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout();
var buffer = result.Buffer;
var consumed = buffer.Start;
var examined = buffer.Start;
var copyBuffer = buffer;
try
{
Assert.True(buffer.Length > 0);
if (Http3FrameReader.TryReadFrame(ref buffer, frame, maxFrameSize, out var framePayload))
{
consumed = examined = framePayload.End;
frame.Payload = framePayload.ToArray();
return frame;
}
else
{
examined = buffer.End;
}
if (result.IsCompleted)
{
throw new IOException("The reader completed without returning a frame.");
}
}
finally
{
_bytesReceived += copyBuffer.Slice(copyBuffer.Start, consumed).Length;
_pair.Application.Input.AdvanceTo(consumed, examined);
}
}
}
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters();
}
public void OnHeadersComplete(bool endHeaders)
{
}
}
internal class Http3FrameWithPayload : Http3Frame
{
public Http3FrameWithPayload() : base()
{
}
// This does not contain extended headers
public Memory<byte> Payload { get; set; }
public ReadOnlySequence<byte> PayloadSequence => new ReadOnlySequence<byte>(Payload);
}
internal class Http3ControlStream : Http3StreamBase, IQuicStreamFeature
{
internal ConnectionContext ConnectionContext { get; }
public bool IsUnidirectional => true;
// TODO
public long StreamId => 0;
public Http3ControlStream(Http3TestBase testBase, Http3Connection connection)
{
_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);
}
public async Task WriteStreamIdAsync(int id)
{
var writableBuffer = _pair.Application.Output;
void WriteSpan(PipeWriter pw)
{
var buffer = pw.GetSpan(sizeHint: 8);
var lengthWritten = VariableLengthIntegerHelper.WriteInteger(buffer, id);
pw.Advance(lengthWritten);
}
WriteSpan(writableBuffer);
await FlushAsync(writableBuffer);
}
}
}
}

View File

@ -21,7 +21,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System.Threading.Channels" /> <Reference Include="System.Threading.Channels" />
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" /> <Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" /> <Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
<Reference Include="Microsoft.Extensions.TypeNameHelper.Sources" /> <Reference Include="Microsoft.Extensions.TypeNameHelper.Sources" />

View File

@ -3,11 +3,13 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Net; using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -80,7 +82,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTrans
{ {
context.ServerOptions.ApplicationServices = sp; context.ServerOptions.ApplicationServices = sp;
configureKestrel(context.ServerOptions); configureKestrel(context.ServerOptions);
return new KestrelServer(_transportFactory, context); return new KestrelServer(new List<IConnectionListenerFactory>() { _transportFactory }, context);
}); });
}); });