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 Age;
public static readonly string Allow;
public static readonly string AltSvc;
public static readonly string Authority;
public static readonly string Authorization;
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 Age = "Age";
public static readonly string Allow = "Allow";
public static readonly string AltSvc = "Alt-Svc";
public static readonly string Authority = ":authority";
public static readonly string Authorization = "Authorization";
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));
}
public partial interface IMultiplexedConnectionListenerFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory
{
}
[System.FlagsAttribute]
public enum TransferFormat
{
@ -193,6 +196,11 @@ namespace Microsoft.AspNetCore.Connections.Features
{
System.Buffers.MemoryPool<byte> MemoryPool { get; }
}
public partial interface IQuicStreamFeature
{
bool IsUnidirectional { get; }
long StreamId { get; }
}
public partial interface IQuicStreamListenerFeature
{
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 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));
}
public partial interface IMultiplexedConnectionListenerFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory
{
}
[System.FlagsAttribute]
public enum TransferFormat
{
@ -193,6 +196,11 @@ namespace Microsoft.AspNetCore.Connections.Features
{
System.Buffers.MemoryPool<byte> MemoryPool { get; }
}
public partial interface IQuicStreamFeature
{
bool IsUnidirectional { get; }
long StreamId { get; }
}
public partial interface IQuicStreamListenerFeature
{
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 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));
}
public partial interface IMultiplexedConnectionListenerFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory
{
}
[System.FlagsAttribute]
public enum TransferFormat
{
@ -193,6 +196,11 @@ namespace Microsoft.AspNetCore.Connections.Features
{
System.Buffers.MemoryPool<byte> MemoryPool { get; }
}
public partial interface IQuicStreamFeature
{
bool IsUnidirectional { get; }
long StreamId { get; }
}
public partial interface IQuicStreamListenerFeature
{
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 SupportedFormats { get; }
}
public partial interface IUnidirectionalStreamFeature
{
}
}

View File

@ -3,7 +3,9 @@
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 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]
public enum HttpProtocols
{
@ -84,10 +90,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
Http1 = 1,
Http2 = 2,
Http1AndHttp2 = 3,
Http3 = 4,
Http1AndHttp2AndHttp3 = 7,
}
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.Server.Kestrel.Core.KestrelServerOptions Options { get { throw null; } }
public void Dispose() { }
@ -100,6 +108,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
{
public KestrelServerLimits() { }
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 long? MaxConcurrentConnections { 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 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 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.KestrelConfigurationLoader Configure() { 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,
Http11 = 1,
Http2 = 2,
Http3 = 3,
}
public partial interface IHttpRequestLineHandler
{

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -562,4 +562,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
</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>
</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>

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,
Http2 = 0x2,
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;
}
if (_httpVersion == Http.HttpVersion.Http3)
{
return HttpUtilities.Http3Version;
}
return string.Empty;
}
@ -176,6 +180,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_httpVersion = Http.HttpVersion.Http2;
}
else if (ReferenceEquals(value, HttpUtilities.Http3Version))
{
_httpVersion = Http.HttpVersion.Http3;
}
else
{
HttpVersionSetSlow(value);
@ -198,6 +206,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_httpVersion = Http.HttpVersion.Http2;
}
else if (value == HttpUtilities.Http3Version)
{
_httpVersion = Http.HttpVersion.Http3;
}
else
{
_httpVersion = Http.HttpVersion.Unknown;
@ -1044,7 +1056,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
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.
PreventRequestAbortedCancellation();
@ -1160,7 +1172,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
responseHeaders.SetReadOnly();
if (!hasConnection && _httpVersion != Http.HttpVersion.Http2)
if (!hasConnection && _httpVersion < Http.HttpVersion.Http2)
{
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.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
@ -8,6 +8,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
Unknown = -1,
Http10 = 0,
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.Diagnostics;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
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.Internal.Http;
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.Extensions.Logging;
@ -66,13 +68,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
requestProcessor = new Http2Connection(_context);
_protocolSelectionState = ProtocolSelectionState.Selected;
break;
case HttpProtocols.Http3:
requestProcessor = new Http3Connection(_context);
_protocolSelectionState = ProtocolSelectionState.Selected;
break;
case HttpProtocols.None:
// An error was already logged in SelectProtocol(), but we should close the connection.
break;
default:
// 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;
@ -197,6 +204,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private HttpProtocols SelectProtocol()
{
if (_context.Protocols == HttpProtocols.Http3)
{
return HttpProtocols.Http3;
}
var hasTls = _context.ConnectionFeatures.Get<ITlsConnectionFeature>() != null;
var applicationProtocol = _context.ConnectionFeatures.Get<ITlsApplicationProtocolFeature>()?.ApplicationProtocol
?? new ReadOnlyMemory<byte>();

View File

@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
public const string Http10Version = "HTTP/1.0";
public const string Http11Version = "HTTP/1.1";
public const string Http2Version = "HTTP/2";
public const string Http3Version = "HTTP/3";
public const string HttpUriScheme = "http://";
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 IServerAddressesFeature _serverAddresses;
private readonly IConnectionListenerFactory _transportFactory;
private readonly IEnumerable<IConnectionListenerFactory> _transportFactories;
private bool _hasStarted;
private int _stopping;
private readonly TaskCompletionSource<object> _stoppedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
public KestrelServer(IOptions<KestrelServerOptions> options, IConnectionListenerFactory transportFactory, ILoggerFactory loggerFactory)
: this(transportFactory, CreateServiceContext(options, loggerFactory))
public KestrelServer(IOptions<KestrelServerOptions> options, IEnumerable<IConnectionListenerFactory> transportFactories, ILoggerFactory loggerFactory)
: this(transportFactories, CreateServiceContext(options, loggerFactory))
{
}
// 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;
Features = new FeatureCollection();
@ -135,7 +135,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
}
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
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.
using System;
@ -258,6 +258,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </summary>
public Http2Limits Http2 { get; } = new Http2Limits();
/// <summary>
/// Limits only applicable to HTTP/3 connections.
/// </summary>
public Http3Limits Http3 { get; } = new Http3Limits();
/// <summary>
/// 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.

View File

@ -72,6 +72,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </summary>
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>
/// A default configuration action for all endpoints. Use for Listen, configuration, the default url, and URLs.
/// </summary>

View File

@ -79,11 +79,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
_options = options;
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
}
public async Task OnConnectionAsync(ConnectionContext context)
{
await Task.Yield();
bool certificateRequired;
if (context.Features.Get<ITlsConnectionFeature>() != null)
{
await _next(context);
return;
}
var feature = new Core.Internal.TlsConnectionFeature();
context.Features.Set<ITlsConnectionFeature>(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.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
@ -203,7 +204,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockLoggerFactory = new Mock<ILoggerFactory>();
var mockLogger = new Mock<ILogger>();
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"));
}
@ -216,7 +217,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var exception = Assert.Throws<ArgumentNullException>(() =>
new KestrelServer(Options.Create<KestrelServerOptions>(null), null, mockLoggerFactory.Object));
Assert.Equal("transportFactory", exception.ParamName);
Assert.Equal("transportFactories", exception.ParamName);
}
[Fact]
@ -257,7 +258,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockLoggerFactory = new Mock<ILoggerFactory>();
var mockLogger = new Mock<ILogger>();
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);
var stopTask1 = server.StopAsync(default);
@ -315,7 +316,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockLoggerFactory = new Mock<ILoggerFactory>();
var mockLogger = new Mock<ILogger>();
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);
var stopTask1 = server.StopAsync(default);
@ -370,7 +371,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockLoggerFactory = new Mock<ILoggerFactory>();
var mockLogger = new Mock<ILogger>();
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);
var stopTask1 = server.StopAsync(default);
@ -416,7 +417,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
DebuggerWrapper.Singleton,
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());
@ -433,12 +434,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
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)
{
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)

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
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
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
Global
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|x86.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -548,6 +562,7 @@ Global
{53A8634C-DFC5-4A5B-8864-9EF1707E3F18} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0}
{62CFF861-807E-43F6-9403-22AA7F06C9A6} = {2B456D08-F72B-4EB8-B663-B6D78FC04BF8}
{F39A942B-85A8-4C1B-A5BC-435555E79F20} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0}
{B3CDC83A-A9C5-45DF-9828-6BC419C24308} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {48207B50-7D05-4B10-B585-890FE0F4FCE1}

View File

@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic
[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 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) { }
[System.Diagnostics.DebuggerStepThroughAttribute]

View File

@ -163,6 +163,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
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)
{
QuicSecConfig secConfig = null;

View File

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

View File

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

View File

@ -82,8 +82,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
IntPtr Handle,
uint Level,
uint Param,
IntPtr BufferLength,
IntPtr Buffer);
out uint BufferLength,
out byte* Buffer);
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 buffer = endpoint.Address.GetAddressBytes();
switch (endpoint.Address.AddressFamily)
if (endpoint.Address != IPAddress.Any && endpoint.Address != IPAddress.IPv6Any)
{
case AddressFamily.InterNetwork:
socketAddress.Ipv4.sin_addr0 = buffer[0];
socketAddress.Ipv4.sin_addr1 = buffer[1];
socketAddress.Ipv4.sin_addr2 = buffer[2];
socketAddress.Ipv4.sin_addr3 = buffer[3];
socketAddress.Ipv4.sin_family = IPv4;
break;
case AddressFamily.InterNetworkV6:
socketAddress.Ipv6.sin6_addr0 = buffer[0];
socketAddress.Ipv6.sin6_addr1 = buffer[1];
socketAddress.Ipv6.sin6_addr2 = buffer[2];
socketAddress.Ipv6.sin6_addr3 = buffer[3];
socketAddress.Ipv6.sin6_addr4 = buffer[4];
socketAddress.Ipv6.sin6_addr5 = buffer[5];
socketAddress.Ipv6.sin6_addr6 = buffer[6];
socketAddress.Ipv6.sin6_addr7 = buffer[7];
socketAddress.Ipv6.sin6_addr8 = buffer[8];
socketAddress.Ipv6.sin6_addr9 = buffer[9];
socketAddress.Ipv6.sin6_addr10 = buffer[10];
socketAddress.Ipv6.sin6_addr11 = buffer[11];
socketAddress.Ipv6.sin6_addr12 = buffer[12];
socketAddress.Ipv6.sin6_addr13 = buffer[13];
socketAddress.Ipv6.sin6_addr14 = buffer[14];
socketAddress.Ipv6.sin6_addr15 = buffer[15];
socketAddress.Ipv6.sin6_family = IPv6;
break;
default:
throw new ArgumentException("Only IPv4 or IPv6 are supported");
switch (endpoint.Address.AddressFamily)
{
case AddressFamily.InterNetwork:
socketAddress.Ipv4.sin_addr0 = buffer[0];
socketAddress.Ipv4.sin_addr1 = buffer[1];
socketAddress.Ipv4.sin_addr2 = buffer[2];
socketAddress.Ipv4.sin_addr3 = buffer[3];
socketAddress.Ipv4.sin_family = IPv4;
break;
case AddressFamily.InterNetworkV6:
socketAddress.Ipv6.sin6_addr0 = buffer[0];
socketAddress.Ipv6.sin6_addr1 = buffer[1];
socketAddress.Ipv6.sin6_addr2 = buffer[2];
socketAddress.Ipv6.sin6_addr3 = buffer[3];
socketAddress.Ipv6.sin6_addr4 = buffer[4];
socketAddress.Ipv6.sin6_addr5 = buffer[5];
socketAddress.Ipv6.sin6_addr6 = buffer[6];
socketAddress.Ipv6.sin6_addr7 = buffer[7];
socketAddress.Ipv6.sin6_addr8 = buffer[8];
socketAddress.Ipv6.sin6_addr9 = buffer[9];
socketAddress.Ipv6.sin6_addr10 = buffer[10];
socketAddress.Ipv6.sin6_addr11 = buffer[11];
socketAddress.Ipv6.sin6_addr12 = buffer[12];
socketAddress.Ipv6.sin6_addr13 = buffer[13];
socketAddress.Ipv6.sin6_addr14 = buffer[14];
socketAddress.Ipv6.sin6_addr15 = buffer[15];
socketAddress.Ipv6.sin6_family = IPv6;
break;
default:
throw new ArgumentException("Only IPv4 or IPv6 are supported");
}
}
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
{
internal class MsQuicStream : TransportConnection, IUnidirectionalStreamFeature
internal class MsQuicStream : TransportConnection, IQuicStreamFeature
{
private Task _processingTask;
private MsQuicConnection _connection;
@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
private GCHandle _handle;
private StreamCallbackDelegate _delegate;
private string _connectionId;
private long _streamId = -1;
internal ResettableCompletionSource _resettableCompletion;
private MemoryHandle[] _bufferArrays;
@ -56,15 +57,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions);
// TODO when stream is unidirectional, don't create an output pipe.
if (flags.HasFlag(QUIC_STREAM_OPEN_FLAG.UNIDIRECTIONAL))
{
Features.Set<IUnidirectionalStreamFeature>(this);
}
Features.Set<IQuicStreamFeature>(this);
// TODO populate the ITlsConnectionFeature (requires client certs).
var feature = new FakeTlsConnectionFeature();
Features.Set<ITlsConnectionFeature>(feature);
Features.Set<ITlsConnectionFeature>(new FakeTlsConnectionFeature());
if (flags.HasFlag(QUIC_STREAM_OPEN_FLAG.UNIDIRECTIONAL))
{
IsUnidirectional = true;
}
Transport = pair.Transport;
Application = pair.Application;
@ -72,21 +72,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
SetCallbackHandler();
_processingTask = ProcessSends();
// Concatenate stream id with ConnectionId.
_log.NewStream(ConnectionId);
}
public override MemoryPool<byte> MemoryPool { get; }
public PipeWriter Input => Application.Output;
public PipeReader Output => Application.Input;
public bool IsUnidirectional { get; }
public long StreamId
{
get
{
if (_streamId == -1)
{
_streamId = GetStreamId();
}
return _streamId;
}
}
public override string ConnectionId {
get
{
if (_connectionId == null)
{
_connectionId = $"{_connection.ConnectionId}:{base.ConnectionId}";
_connectionId = $"{_connection.ConnectionId}:{StreamId}";
}
return _connectionId;
}
@ -390,20 +402,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
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(
QUIC_PARAM_STREAM param,
QuicBuffer buf)
{
MsQuicStatusException.ThrowIfFailed(Api.UnsafeSetParam(
_nativeObjPtr,
(uint)QUIC_PARAM_LEVEL.SESSION,
(uint)QUIC_PARAM_LEVEL.STREAM,
(uint)param,
buf));
}
~MsQuicStream()
{
_log.LogDebug("Destructor");
Dispose(false);
}

View File

@ -13,7 +13,7 @@ using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic
{
public class MsQuicTransportFactory : IConnectionListenerFactory
public class MsQuicTransportFactory : IMultiplexedConnectionListenerFactory
{
private MsQuicTrace _log;
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.Extensions.Logging;
using System.Diagnostics;
using Microsoft.AspNetCore.Server.Kestrel.Core;
namespace QuicSampleApp
{
@ -46,6 +47,7 @@ namespace QuicSampleApp
options.Listen(IPAddress.Any, basePort, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http3;
listenOptions.Use((next) =>
{
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()
{
var start = Console.ReadLine();
Console.WriteLine("Starting");
var connectionContext = await _connectionFactory.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 5555));
var createStreamFeature = connectionContext.Features.Get<IQuicCreateStreamFeature>();
var streamContext = await createStreamFeature.StartBidirectionalStreamAsync();

View File

@ -145,6 +145,7 @@ namespace CodeGenerator
{
"Accept-Ranges",
"Age",
"Alt-Svc",
"ETag",
"Location",
"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.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Reflection;
@ -89,7 +90,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
c.Configure(context.ServerOptions);
}
return new KestrelServer(sp.GetRequiredService<IConnectionListenerFactory>(), context);
return new KestrelServer(new List<IConnectionListenerFactory>() { sp.GetRequiredService<IConnectionListenerFactory>() }, context);
});
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>
<Reference Include="System.Threading.Channels" />
<Reference Include="System.Threading.Channels" />
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
<Reference Include="Microsoft.Extensions.TypeNameHelper.Sources" />

View File

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