Initial HTTP/3 Implementation in Kestrel (#16914)
This commit is contained in:
parent
2ff8f45193
commit
0f580f1082
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
namespace Microsoft.AspNetCore.Connections.Features
|
||||
{
|
||||
public interface IUnidirectionalStreamFeature
|
||||
public interface IQuicStreamFeature
|
||||
{
|
||||
bool IsUnidirectional { get; }
|
||||
long StreamId { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,5 +12,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
Http1 = 0x1,
|
||||
Http2 = 0x2,
|
||||
Http1AndHttp2 = Http1 | Http2,
|
||||
Http3 = 0x4,
|
||||
Http1AndHttp2AndHttp3 = Http1 | Http2 | Http3
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 doesn’t 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 client’s stream terminated without containing a fully-formed request.
|
||||
/// </summary>
|
||||
RequestIncomplete = 0x10e,
|
||||
/// <summary>
|
||||
/// HTTP_EARLY_RESPONSE (0x10F):
|
||||
/// The remainder of the client’s 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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://";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ namespace CodeGenerator
|
|||
{
|
||||
"Accept-Ranges",
|
||||
"Age",
|
||||
"Alt-Svc",
|
||||
"ETag",
|
||||
"Location",
|
||||
"Proxy-Authenticate",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue