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 AccessControlRequestMethod;
|
||||||
public static readonly string Age;
|
public static readonly string Age;
|
||||||
public static readonly string Allow;
|
public static readonly string Allow;
|
||||||
|
public static readonly string AltSvc;
|
||||||
public static readonly string Authority;
|
public static readonly string Authority;
|
||||||
public static readonly string Authorization;
|
public static readonly string Authorization;
|
||||||
public static readonly string CacheControl;
|
public static readonly string CacheControl;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ namespace Microsoft.Net.Http.Headers
|
||||||
public static readonly string AccessControlRequestMethod = "Access-Control-Request-Method";
|
public static readonly string AccessControlRequestMethod = "Access-Control-Request-Method";
|
||||||
public static readonly string Age = "Age";
|
public static readonly string Age = "Age";
|
||||||
public static readonly string Allow = "Allow";
|
public static readonly string Allow = "Allow";
|
||||||
|
public static readonly string AltSvc = "Alt-Svc";
|
||||||
public static readonly string Authority = ":authority";
|
public static readonly string Authority = ":authority";
|
||||||
public static readonly string Authorization = "Authorization";
|
public static readonly string Authorization = "Authorization";
|
||||||
public static readonly string CacheControl = "Cache-Control";
|
public static readonly string CacheControl = "Cache-Control";
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,9 @@ namespace Microsoft.AspNetCore.Connections
|
||||||
{
|
{
|
||||||
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.IConnectionListener> BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
|
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.IConnectionListener> BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
|
||||||
}
|
}
|
||||||
|
public partial interface IMultiplexedConnectionListenerFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory
|
||||||
|
{
|
||||||
|
}
|
||||||
[System.FlagsAttribute]
|
[System.FlagsAttribute]
|
||||||
public enum TransferFormat
|
public enum TransferFormat
|
||||||
{
|
{
|
||||||
|
|
@ -193,6 +196,11 @@ namespace Microsoft.AspNetCore.Connections.Features
|
||||||
{
|
{
|
||||||
System.Buffers.MemoryPool<byte> MemoryPool { get; }
|
System.Buffers.MemoryPool<byte> MemoryPool { get; }
|
||||||
}
|
}
|
||||||
|
public partial interface IQuicStreamFeature
|
||||||
|
{
|
||||||
|
bool IsUnidirectional { get; }
|
||||||
|
long StreamId { get; }
|
||||||
|
}
|
||||||
public partial interface IQuicStreamListenerFeature
|
public partial interface IQuicStreamListenerFeature
|
||||||
{
|
{
|
||||||
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync();
|
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync();
|
||||||
|
|
@ -212,7 +220,4 @@ namespace Microsoft.AspNetCore.Connections.Features
|
||||||
Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; }
|
Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; }
|
||||||
Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; }
|
Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; }
|
||||||
}
|
}
|
||||||
public partial interface IUnidirectionalStreamFeature
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,9 @@ namespace Microsoft.AspNetCore.Connections
|
||||||
{
|
{
|
||||||
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.IConnectionListener> BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
|
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.IConnectionListener> BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
|
||||||
}
|
}
|
||||||
|
public partial interface IMultiplexedConnectionListenerFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory
|
||||||
|
{
|
||||||
|
}
|
||||||
[System.FlagsAttribute]
|
[System.FlagsAttribute]
|
||||||
public enum TransferFormat
|
public enum TransferFormat
|
||||||
{
|
{
|
||||||
|
|
@ -193,6 +196,11 @@ namespace Microsoft.AspNetCore.Connections.Features
|
||||||
{
|
{
|
||||||
System.Buffers.MemoryPool<byte> MemoryPool { get; }
|
System.Buffers.MemoryPool<byte> MemoryPool { get; }
|
||||||
}
|
}
|
||||||
|
public partial interface IQuicStreamFeature
|
||||||
|
{
|
||||||
|
bool IsUnidirectional { get; }
|
||||||
|
long StreamId { get; }
|
||||||
|
}
|
||||||
public partial interface IQuicStreamListenerFeature
|
public partial interface IQuicStreamListenerFeature
|
||||||
{
|
{
|
||||||
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync();
|
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync();
|
||||||
|
|
@ -212,7 +220,4 @@ namespace Microsoft.AspNetCore.Connections.Features
|
||||||
Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; }
|
Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; }
|
||||||
Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; }
|
Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; }
|
||||||
}
|
}
|
||||||
public partial interface IUnidirectionalStreamFeature
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,9 @@ namespace Microsoft.AspNetCore.Connections
|
||||||
{
|
{
|
||||||
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.IConnectionListener> BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
|
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.IConnectionListener> BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
|
||||||
}
|
}
|
||||||
|
public partial interface IMultiplexedConnectionListenerFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory
|
||||||
|
{
|
||||||
|
}
|
||||||
[System.FlagsAttribute]
|
[System.FlagsAttribute]
|
||||||
public enum TransferFormat
|
public enum TransferFormat
|
||||||
{
|
{
|
||||||
|
|
@ -193,6 +196,11 @@ namespace Microsoft.AspNetCore.Connections.Features
|
||||||
{
|
{
|
||||||
System.Buffers.MemoryPool<byte> MemoryPool { get; }
|
System.Buffers.MemoryPool<byte> MemoryPool { get; }
|
||||||
}
|
}
|
||||||
|
public partial interface IQuicStreamFeature
|
||||||
|
{
|
||||||
|
bool IsUnidirectional { get; }
|
||||||
|
long StreamId { get; }
|
||||||
|
}
|
||||||
public partial interface IQuicStreamListenerFeature
|
public partial interface IQuicStreamListenerFeature
|
||||||
{
|
{
|
||||||
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync();
|
System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> AcceptAsync();
|
||||||
|
|
@ -212,7 +220,4 @@ namespace Microsoft.AspNetCore.Connections.Features
|
||||||
Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; }
|
Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; }
|
||||||
Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; }
|
Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; }
|
||||||
}
|
}
|
||||||
public partial interface IUnidirectionalStreamFeature
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Connections.Features
|
namespace Microsoft.AspNetCore.Connections.Features
|
||||||
{
|
{
|
||||||
public interface IUnidirectionalStreamFeature
|
public interface IQuicStreamFeature
|
||||||
{
|
{
|
||||||
|
bool IsUnidirectional { get; }
|
||||||
|
long StreamId { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 MaxRequestHeaderFieldSize { get { throw null; } set { } }
|
||||||
public int MaxStreamsPerConnection { get { throw null; } set { } }
|
public int MaxStreamsPerConnection { get { throw null; } set { } }
|
||||||
}
|
}
|
||||||
|
public partial class Http3Limits
|
||||||
|
{
|
||||||
|
public Http3Limits() { }
|
||||||
|
public int HeaderTableSize { get { throw null; } set { } }
|
||||||
|
public int MaxRequestHeaderFieldSize { get { throw null; } set { } }
|
||||||
|
}
|
||||||
[System.FlagsAttribute]
|
[System.FlagsAttribute]
|
||||||
public enum HttpProtocols
|
public enum HttpProtocols
|
||||||
{
|
{
|
||||||
|
|
@ -84,10 +90,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||||
Http1 = 1,
|
Http1 = 1,
|
||||||
Http2 = 2,
|
Http2 = 2,
|
||||||
Http1AndHttp2 = 3,
|
Http1AndHttp2 = 3,
|
||||||
|
Http3 = 4,
|
||||||
|
Http1AndHttp2AndHttp3 = 7,
|
||||||
}
|
}
|
||||||
public partial class KestrelServer : Microsoft.AspNetCore.Hosting.Server.IServer, System.IDisposable
|
public partial class KestrelServer : Microsoft.AspNetCore.Hosting.Server.IServer, System.IDisposable
|
||||||
{
|
{
|
||||||
public KestrelServer(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions> options, Microsoft.AspNetCore.Connections.IConnectionListenerFactory transportFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
|
public KestrelServer(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions> options, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Connections.IConnectionListenerFactory> transportFactories, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
|
||||||
public Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
public Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
||||||
public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions Options { get { throw null; } }
|
public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions Options { get { throw null; } }
|
||||||
public void Dispose() { }
|
public void Dispose() { }
|
||||||
|
|
@ -100,6 +108,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||||
{
|
{
|
||||||
public KestrelServerLimits() { }
|
public KestrelServerLimits() { }
|
||||||
public Microsoft.AspNetCore.Server.Kestrel.Core.Http2Limits Http2 { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
public Microsoft.AspNetCore.Server.Kestrel.Core.Http2Limits Http2 { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
||||||
|
public Microsoft.AspNetCore.Server.Kestrel.Core.Http3Limits Http3 { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
||||||
public System.TimeSpan KeepAliveTimeout { get { throw null; } set { } }
|
public System.TimeSpan KeepAliveTimeout { get { throw null; } set { } }
|
||||||
public long? MaxConcurrentConnections { get { throw null; } set { } }
|
public long? MaxConcurrentConnections { get { throw null; } set { } }
|
||||||
public long? MaxConcurrentUpgradedConnections { get { throw null; } set { } }
|
public long? MaxConcurrentUpgradedConnections { get { throw null; } set { } }
|
||||||
|
|
@ -121,6 +130,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||||
public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
public bool DisableStringReuse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
public bool DisableStringReuse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
|
public bool EnableAltSvc { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
||||||
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure() { throw null; }
|
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure() { throw null; }
|
||||||
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config) { throw null; }
|
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config) { throw null; }
|
||||||
|
|
@ -216,6 +226,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
Http10 = 0,
|
Http10 = 0,
|
||||||
Http11 = 1,
|
Http11 = 1,
|
||||||
Http2 = 2,
|
Http2 = 2,
|
||||||
|
Http3 = 3,
|
||||||
}
|
}
|
||||||
public partial interface IHttpRequestLineHandler
|
public partial interface IHttpRequestLineHandler
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<root>
|
<root>
|
||||||
<!--
|
<!--
|
||||||
Microsoft ResX Schema
|
Microsoft ResX Schema
|
||||||
|
|
||||||
Version 2.0
|
Version 2.0
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
The primary goals of this format is to allow a simple XML format
|
||||||
that is mostly human readable. The generation and parsing of the
|
that is mostly human readable. The generation and parsing of the
|
||||||
various data types are done through the TypeConverter classes
|
various data types are done through the TypeConverter classes
|
||||||
associated with the data types.
|
associated with the data types.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
... ado.net/XML headers & schema ...
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
<resheader name="version">2.0</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>
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
<comment>This is a comment</comment>
|
<comment>This is a comment</comment>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
There are any number of "resheader" rows that contain simple
|
||||||
name/value pairs.
|
name/value pairs.
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
Each data row contains a name, and value. The row also contains a
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
text/value conversion through the TypeConverter architecture.
|
text/value conversion through the TypeConverter architecture.
|
||||||
Classes that don't support this are serialized and stored with the
|
Classes that don't support this are serialized and stored with the
|
||||||
mimetype set.
|
mimetype set.
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
The mimetype is used for serialized objects, and tells the
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
read any of the formats listed below.
|
read any of the formats listed below.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
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
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
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
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
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
|
: using a System.ComponentModel.TypeConverter
|
||||||
: and then encoded with base64 encoding.
|
: 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">
|
</data> <data name="BadDeveloperCertificateState" xml:space="preserve">
|
||||||
<value>The ASP.NET Core developer certificate is in an invalid state. To fix this issue, run the following commands 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' to remove all existing ASP.NET Core development certificates and create a new untrusted developer certificate. On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate.</value>
|
<value>The ASP.NET Core developer certificate is in an invalid state. To fix this issue, run the following commands 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' to remove all existing ASP.NET Core development certificates and create a new untrusted developer certificate. On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="QPackErrorIndexOutOfRange" xml:space="preserve">
|
||||||
|
<value>Index {index} is outside the bounds of the header field table.</value>
|
||||||
|
</data>
|
||||||
|
<data name="QPackErrorIntegerTooBig" xml:space="preserve">
|
||||||
|
<value>The decoded integer exceeds the maximum value of Int32.MaxValue.</value>
|
||||||
|
</data>
|
||||||
|
<data name="QPackHuffmanError" xml:space="preserve">
|
||||||
|
<value>Huffman decoding error.</value>
|
||||||
|
</data>
|
||||||
|
<data name="QPackStringLengthTooLarge" xml:space="preserve">
|
||||||
|
<value>Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
Http1 = 0x1,
|
||||||
Http2 = 0x2,
|
Http2 = 0x2,
|
||||||
Http1AndHttp2 = Http1 | Http2,
|
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;
|
return HttpUtilities.Http2Version;
|
||||||
}
|
}
|
||||||
|
if (_httpVersion == Http.HttpVersion.Http3)
|
||||||
|
{
|
||||||
|
return HttpUtilities.Http3Version;
|
||||||
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
@ -176,6 +180,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
_httpVersion = Http.HttpVersion.Http2;
|
_httpVersion = Http.HttpVersion.Http2;
|
||||||
}
|
}
|
||||||
|
else if (ReferenceEquals(value, HttpUtilities.Http3Version))
|
||||||
|
{
|
||||||
|
_httpVersion = Http.HttpVersion.Http3;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
HttpVersionSetSlow(value);
|
HttpVersionSetSlow(value);
|
||||||
|
|
@ -198,6 +206,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
_httpVersion = Http.HttpVersion.Http2;
|
_httpVersion = Http.HttpVersion.Http2;
|
||||||
}
|
}
|
||||||
|
else if (value == HttpUtilities.Http3Version)
|
||||||
|
{
|
||||||
|
_httpVersion = Http.HttpVersion.Http3;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_httpVersion = Http.HttpVersion.Unknown;
|
_httpVersion = Http.HttpVersion.Unknown;
|
||||||
|
|
@ -1044,7 +1056,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
private Task WriteSuffix()
|
private Task WriteSuffix()
|
||||||
{
|
{
|
||||||
if (_autoChunk || _httpVersion == Http.HttpVersion.Http2)
|
if (_autoChunk || _httpVersion >= Http.HttpVersion.Http2)
|
||||||
{
|
{
|
||||||
// For the same reason we call CheckLastWrite() in Content-Length responses.
|
// For the same reason we call CheckLastWrite() in Content-Length responses.
|
||||||
PreventRequestAbortedCancellation();
|
PreventRequestAbortedCancellation();
|
||||||
|
|
@ -1160,7 +1172,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
responseHeaders.SetReadOnly();
|
responseHeaders.SetReadOnly();
|
||||||
|
|
||||||
if (!hasConnection && _httpVersion != Http.HttpVersion.Http2)
|
if (!hasConnection && _httpVersion < Http.HttpVersion.Http2)
|
||||||
{
|
{
|
||||||
if (!_keepAlive)
|
if (!_keepAlive)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
@ -8,6 +8,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
Unknown = -1,
|
Unknown = -1,
|
||||||
Http10 = 0,
|
Http10 = 0,
|
||||||
Http11 = 1,
|
Http11 = 1,
|
||||||
Http2
|
Http2 = 2,
|
||||||
|
Http3 = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Connections;
|
using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.Connections.Features;
|
using Microsoft.AspNetCore.Connections.Features;
|
||||||
|
|
@ -11,6 +12,7 @@ using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|
@ -66,13 +68,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
||||||
requestProcessor = new Http2Connection(_context);
|
requestProcessor = new Http2Connection(_context);
|
||||||
_protocolSelectionState = ProtocolSelectionState.Selected;
|
_protocolSelectionState = ProtocolSelectionState.Selected;
|
||||||
break;
|
break;
|
||||||
|
case HttpProtocols.Http3:
|
||||||
|
requestProcessor = new Http3Connection(_context);
|
||||||
|
_protocolSelectionState = ProtocolSelectionState.Selected;
|
||||||
|
break;
|
||||||
case HttpProtocols.None:
|
case HttpProtocols.None:
|
||||||
// An error was already logged in SelectProtocol(), but we should close the connection.
|
// An error was already logged in SelectProtocol(), but we should close the connection.
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// SelectProtocol() only returns Http1, Http2 or None.
|
// SelectProtocol() only returns Http1, Http2 or None.
|
||||||
throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2 or None.");
|
throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2, Http3 or None.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_requestProcessor = requestProcessor;
|
_requestProcessor = requestProcessor;
|
||||||
|
|
||||||
|
|
@ -197,6 +204,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
||||||
|
|
||||||
private HttpProtocols SelectProtocol()
|
private HttpProtocols SelectProtocol()
|
||||||
{
|
{
|
||||||
|
if (_context.Protocols == HttpProtocols.Http3)
|
||||||
|
{
|
||||||
|
return HttpProtocols.Http3;
|
||||||
|
}
|
||||||
|
|
||||||
var hasTls = _context.ConnectionFeatures.Get<ITlsConnectionFeature>() != null;
|
var hasTls = _context.ConnectionFeatures.Get<ITlsConnectionFeature>() != null;
|
||||||
var applicationProtocol = _context.ConnectionFeatures.Get<ITlsApplicationProtocolFeature>()?.ApplicationProtocol
|
var applicationProtocol = _context.ConnectionFeatures.Get<ITlsApplicationProtocolFeature>()?.ApplicationProtocol
|
||||||
?? new ReadOnlyMemory<byte>();
|
?? new ReadOnlyMemory<byte>();
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||||
public const string Http10Version = "HTTP/1.0";
|
public const string Http10Version = "HTTP/1.0";
|
||||||
public const string Http11Version = "HTTP/1.1";
|
public const string Http11Version = "HTTP/1.1";
|
||||||
public const string Http2Version = "HTTP/2";
|
public const string Http2Version = "HTTP/2";
|
||||||
|
public const string Http3Version = "HTTP/3";
|
||||||
|
|
||||||
public const string HttpUriScheme = "http://";
|
public const string HttpUriScheme = "http://";
|
||||||
public const string HttpsUriScheme = "https://";
|
public const string HttpsUriScheme = "https://";
|
||||||
|
|
|
||||||
|
|
@ -23,26 +23,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||||
{
|
{
|
||||||
private readonly List<(IConnectionListener, Task)> _transports = new List<(IConnectionListener, Task)>();
|
private readonly List<(IConnectionListener, Task)> _transports = new List<(IConnectionListener, Task)>();
|
||||||
private readonly IServerAddressesFeature _serverAddresses;
|
private readonly IServerAddressesFeature _serverAddresses;
|
||||||
private readonly IConnectionListenerFactory _transportFactory;
|
private readonly IEnumerable<IConnectionListenerFactory> _transportFactories;
|
||||||
|
|
||||||
private bool _hasStarted;
|
private bool _hasStarted;
|
||||||
private int _stopping;
|
private int _stopping;
|
||||||
private readonly TaskCompletionSource<object> _stoppedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
private readonly TaskCompletionSource<object> _stoppedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
public KestrelServer(IOptions<KestrelServerOptions> options, IConnectionListenerFactory transportFactory, ILoggerFactory loggerFactory)
|
public KestrelServer(IOptions<KestrelServerOptions> options, IEnumerable<IConnectionListenerFactory> transportFactories, ILoggerFactory loggerFactory)
|
||||||
: this(transportFactory, CreateServiceContext(options, loggerFactory))
|
: this(transportFactories, CreateServiceContext(options, loggerFactory))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
// For testing
|
// For testing
|
||||||
internal KestrelServer(IConnectionListenerFactory transportFactory, ServiceContext serviceContext)
|
internal KestrelServer(IEnumerable<IConnectionListenerFactory> transportFactories, ServiceContext serviceContext)
|
||||||
{
|
{
|
||||||
if (transportFactory == null)
|
if (transportFactories == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(transportFactory));
|
throw new ArgumentNullException(nameof(transportFactories));
|
||||||
}
|
}
|
||||||
|
|
||||||
_transportFactory = transportFactory;
|
_transportFactories = transportFactories;
|
||||||
ServiceContext = serviceContext;
|
ServiceContext = serviceContext;
|
||||||
|
|
||||||
Features = new FeatureCollection();
|
Features = new FeatureCollection();
|
||||||
|
|
@ -135,7 +135,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectionDispatcher = new ConnectionDispatcher(ServiceContext, connectionDelegate);
|
var connectionDispatcher = new ConnectionDispatcher(ServiceContext, connectionDelegate);
|
||||||
var transport = await _transportFactory.BindAsync(options.EndPoint).ConfigureAwait(false);
|
|
||||||
|
IConnectionListenerFactory factory = null;
|
||||||
|
if (options.Protocols >= HttpProtocols.Http3)
|
||||||
|
{
|
||||||
|
foreach (var transportFactory in _transportFactories)
|
||||||
|
{
|
||||||
|
if (transportFactory is IMultiplexedConnectionListenerFactory)
|
||||||
|
{
|
||||||
|
factory = transportFactory;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (factory == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Quic transport not found when using HTTP/3");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
factory = _transportFactories.Single();
|
||||||
|
}
|
||||||
|
|
||||||
|
var transport = await factory.BindAsync(options.EndPoint).ConfigureAwait(false);
|
||||||
|
|
||||||
// Update the endpoint
|
// Update the endpoint
|
||||||
options.EndPoint = transport.EndPoint;
|
options.EndPoint = transport.EndPoint;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -258,6 +258,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Http2Limits Http2 { get; } = new Http2Limits();
|
public Http2Limits Http2 { get; } = new Http2Limits();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Limits only applicable to HTTP/3 connections.
|
||||||
|
/// </summary>
|
||||||
|
public Http3Limits Http3 { get; } = new Http3Limits();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the request body minimum data rate in bytes/second.
|
/// Gets or sets the request body minimum data rate in bytes/second.
|
||||||
/// Setting this property to null indicates no minimum data rate should be enforced.
|
/// Setting this property to null indicates no minimum data rate should be enforced.
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public KestrelConfigurationLoader ConfigurationLoader { get; set; }
|
public KestrelConfigurationLoader ConfigurationLoader { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls whether to return the AltSvcHeader from on an HTTP/2 or lower response for HTTP/3
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableAltSvc { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A default configuration action for all endpoints. Use for Listen, configuration, the default url, and URLs.
|
/// A default configuration action for all endpoints. Use for Listen, configuration, the default url, and URLs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
||||||
_options = options;
|
_options = options;
|
||||||
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
|
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task OnConnectionAsync(ConnectionContext context)
|
public async Task OnConnectionAsync(ConnectionContext context)
|
||||||
{
|
{
|
||||||
await Task.Yield();
|
await Task.Yield();
|
||||||
|
|
||||||
bool certificateRequired;
|
bool certificateRequired;
|
||||||
|
if (context.Features.Get<ITlsConnectionFeature>() != null)
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var feature = new Core.Internal.TlsConnectionFeature();
|
var feature = new Core.Internal.TlsConnectionFeature();
|
||||||
context.Features.Set<ITlsConnectionFeature>(feature);
|
context.Features.Set<ITlsConnectionFeature>(feature);
|
||||||
context.Features.Set<ITlsHandshakeFeature>(feature);
|
context.Features.Set<ITlsHandshakeFeature>(feature);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
@ -203,7 +204,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
var mockLoggerFactory = new Mock<ILoggerFactory>();
|
var mockLoggerFactory = new Mock<ILoggerFactory>();
|
||||||
var mockLogger = new Mock<ILogger>();
|
var mockLogger = new Mock<ILogger>();
|
||||||
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
|
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
|
||||||
new KestrelServer(Options.Create<KestrelServerOptions>(null), Mock.Of<IConnectionListenerFactory>(), mockLoggerFactory.Object);
|
new KestrelServer(Options.Create<KestrelServerOptions>(null), Mock.Of<IEnumerable<IConnectionListenerFactory>>(), mockLoggerFactory.Object);
|
||||||
mockLoggerFactory.Verify(factory => factory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel"));
|
mockLoggerFactory.Verify(factory => factory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,7 +217,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
var exception = Assert.Throws<ArgumentNullException>(() =>
|
var exception = Assert.Throws<ArgumentNullException>(() =>
|
||||||
new KestrelServer(Options.Create<KestrelServerOptions>(null), null, mockLoggerFactory.Object));
|
new KestrelServer(Options.Create<KestrelServerOptions>(null), null, mockLoggerFactory.Object));
|
||||||
|
|
||||||
Assert.Equal("transportFactory", exception.ParamName);
|
Assert.Equal("transportFactories", exception.ParamName);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -257,7 +258,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
var mockLoggerFactory = new Mock<ILoggerFactory>();
|
var mockLoggerFactory = new Mock<ILoggerFactory>();
|
||||||
var mockLogger = new Mock<ILogger>();
|
var mockLogger = new Mock<ILogger>();
|
||||||
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
|
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
|
||||||
var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object);
|
var server = new KestrelServer(Options.Create(options), new List<IConnectionListenerFactory>() { mockTransportFactory.Object }, mockLoggerFactory.Object);
|
||||||
await server.StartAsync(new DummyApplication(), CancellationToken.None);
|
await server.StartAsync(new DummyApplication(), CancellationToken.None);
|
||||||
|
|
||||||
var stopTask1 = server.StopAsync(default);
|
var stopTask1 = server.StopAsync(default);
|
||||||
|
|
@ -315,7 +316,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
var mockLoggerFactory = new Mock<ILoggerFactory>();
|
var mockLoggerFactory = new Mock<ILoggerFactory>();
|
||||||
var mockLogger = new Mock<ILogger>();
|
var mockLogger = new Mock<ILogger>();
|
||||||
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
|
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
|
||||||
var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object);
|
var server = new KestrelServer(Options.Create(options), new List<IConnectionListenerFactory>() { mockTransportFactory.Object }, mockLoggerFactory.Object);
|
||||||
await server.StartAsync(new DummyApplication(), CancellationToken.None);
|
await server.StartAsync(new DummyApplication(), CancellationToken.None);
|
||||||
|
|
||||||
var stopTask1 = server.StopAsync(default);
|
var stopTask1 = server.StopAsync(default);
|
||||||
|
|
@ -370,7 +371,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
var mockLoggerFactory = new Mock<ILoggerFactory>();
|
var mockLoggerFactory = new Mock<ILoggerFactory>();
|
||||||
var mockLogger = new Mock<ILogger>();
|
var mockLogger = new Mock<ILogger>();
|
||||||
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
|
mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
|
||||||
var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object);
|
var server = new KestrelServer(Options.Create(options), new List<IConnectionListenerFactory>() { mockTransportFactory.Object }, mockLoggerFactory.Object);
|
||||||
await server.StartAsync(new DummyApplication(), default);
|
await server.StartAsync(new DummyApplication(), default);
|
||||||
|
|
||||||
var stopTask1 = server.StopAsync(default);
|
var stopTask1 = server.StopAsync(default);
|
||||||
|
|
@ -416,7 +417,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
DebuggerWrapper.Singleton,
|
DebuggerWrapper.Singleton,
|
||||||
testContext.Log);
|
testContext.Log);
|
||||||
|
|
||||||
using (var server = new KestrelServer(new MockTransportFactory(), testContext))
|
using (var server = new KestrelServer(new List<IConnectionListenerFactory>() { new MockTransportFactory() }, testContext))
|
||||||
{
|
{
|
||||||
Assert.Null(testContext.DateHeaderValueManager.GetDateHeaderValues());
|
Assert.Null(testContext.DateHeaderValueManager.GetDateHeaderValues());
|
||||||
|
|
||||||
|
|
@ -433,12 +434,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
|
|
||||||
private static KestrelServer CreateServer(KestrelServerOptions options, ILogger testLogger)
|
private static KestrelServer CreateServer(KestrelServerOptions options, ILogger testLogger)
|
||||||
{
|
{
|
||||||
return new KestrelServer(Options.Create(options), new MockTransportFactory(), new LoggerFactory(new[] { new KestrelTestLoggerProvider(testLogger) }));
|
return new KestrelServer(Options.Create(options), new List<IConnectionListenerFactory>() { new MockTransportFactory() }, new LoggerFactory(new[] { new KestrelTestLoggerProvider(testLogger) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static KestrelServer CreateServer(KestrelServerOptions options, bool throwOnCriticalErrors = true)
|
private static KestrelServer CreateServer(KestrelServerOptions options, bool throwOnCriticalErrors = true)
|
||||||
{
|
{
|
||||||
return new KestrelServer(Options.Create(options), new MockTransportFactory(), new LoggerFactory(new[] { new KestrelTestLoggerProvider(throwOnCriticalErrors) }));
|
return new KestrelServer(Options.Create(options), new List<IConnectionListenerFactory>() { new MockTransportFactory() }, new LoggerFactory(new[] { new KestrelTestLoggerProvider(throwOnCriticalErrors) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void StartDummyApplication(IServer server)
|
private static void StartDummyApplication(IServer server)
|
||||||
|
|
|
||||||
|
|
@ -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
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic", "Transport.MsQuic\src\Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.csproj", "{62CFF861-807E-43F6-9403-22AA7F06C9A6}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic", "Transport.MsQuic\src\Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.csproj", "{62CFF861-807E-43F6-9403-22AA7F06C9A6}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuicSampleClient", "samples\QuicSampleClient\QuicSampleClient.csproj", "{F39A942B-85A8-4C1B-A5BC-435555E79F20}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuicSampleClient", "samples\QuicSampleClient\QuicSampleClient.csproj", "{F39A942B-85A8-4C1B-A5BC-435555E79F20}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Http3SampleApp", "samples\Http3SampleApp\Http3SampleApp.csproj", "{B3CDC83A-A9C5-45DF-9828-6BC419C24308}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
|
@ -508,6 +510,18 @@ Global
|
||||||
{F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x64.Build.0 = Release|Any CPU
|
{F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x86.ActiveCfg = Release|Any CPU
|
{F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x86.Build.0 = Release|Any CPU
|
{F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -548,6 +562,7 @@ Global
|
||||||
{53A8634C-DFC5-4A5B-8864-9EF1707E3F18} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0}
|
{53A8634C-DFC5-4A5B-8864-9EF1707E3F18} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0}
|
||||||
{62CFF861-807E-43F6-9403-22AA7F06C9A6} = {2B456D08-F72B-4EB8-B663-B6D78FC04BF8}
|
{62CFF861-807E-43F6-9403-22AA7F06C9A6} = {2B456D08-F72B-4EB8-B663-B6D78FC04BF8}
|
||||||
{F39A942B-85A8-4C1B-A5BC-435555E79F20} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0}
|
{F39A942B-85A8-4C1B-A5BC-435555E79F20} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0}
|
||||||
|
{B3CDC83A-A9C5-45DF-9828-6BC419C24308} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {48207B50-7D05-4B10-B585-890FE0F4FCE1}
|
SolutionGuid = {48207B50-7D05-4B10-B585-890FE0F4FCE1}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic
|
||||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||||
public System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> ConnectAsync(System.Net.EndPoint endPoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
|
public System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext> ConnectAsync(System.Net.EndPoint endPoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
|
||||||
}
|
}
|
||||||
public partial class MsQuicTransportFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory
|
public partial class MsQuicTransportFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory, Microsoft.AspNetCore.Connections.IMultiplexedConnectionListenerFactory
|
||||||
{
|
{
|
||||||
public MsQuicTransportFactory(Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.MsQuicTransportOptions> options) { }
|
public MsQuicTransportFactory(Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.MsQuicTransportOptions> options) { }
|
||||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
||||||
Buffer.Buffer);
|
Buffer.Buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal unsafe uint UnsafeGetParam(
|
||||||
|
IntPtr Handle,
|
||||||
|
uint Level,
|
||||||
|
uint Param,
|
||||||
|
ref MsQuicNativeMethods.QuicBuffer Buffer)
|
||||||
|
{
|
||||||
|
return GetParamDelegate(
|
||||||
|
Handle,
|
||||||
|
Level,
|
||||||
|
Param,
|
||||||
|
out Buffer.Length,
|
||||||
|
out Buffer.Buffer);
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask<QuicSecConfig> CreateSecurityConfig(X509Certificate2 certificate)
|
public async ValueTask<QuicSecConfig> CreateSecurityConfig(X509Certificate2 certificate)
|
||||||
{
|
{
|
||||||
QuicSecConfig secConfig = null;
|
QuicSecConfig secConfig = null;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Connections;
|
using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.Connections.Abstractions.Features;
|
using Microsoft.AspNetCore.Connections.Abstractions.Features;
|
||||||
using Microsoft.AspNetCore.Connections.Features;
|
using Microsoft.AspNetCore.Connections.Features;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using static Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal.MsQuicNativeMethods;
|
using static Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal.MsQuicNativeMethods;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
||||||
|
|
@ -38,6 +39,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
||||||
|
|
||||||
SetIdleTimeout(_context.Options.IdleTimeout);
|
SetIdleTimeout(_context.Options.IdleTimeout);
|
||||||
|
|
||||||
|
Features.Set<ITlsConnectionFeature>(new FakeTlsConnectionFeature());
|
||||||
Features.Set<IQuicStreamListenerFeature>(this);
|
Features.Set<IQuicStreamListenerFeature>(this);
|
||||||
Features.Set<IQuicCreateStreamFeature>(this);
|
Features.Set<IQuicCreateStreamFeature>(this);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
||||||
_session.SetPeerUnidirectionalStreamCount(_transportContext.Options.MaxBidirectionalStreamCount);
|
_session.SetPeerUnidirectionalStreamCount(_transportContext.Options.MaxBidirectionalStreamCount);
|
||||||
|
|
||||||
var address = MsQuicNativeMethods.Convert(EndPoint as IPEndPoint);
|
var address = MsQuicNativeMethods.Convert(EndPoint as IPEndPoint);
|
||||||
|
|
||||||
MsQuicStatusException.ThrowIfFailed(_api.ListenerStartDelegate(
|
MsQuicStatusException.ThrowIfFailed(_api.ListenerStartDelegate(
|
||||||
_nativeObjPtr,
|
_nativeObjPtr,
|
||||||
ref address));
|
ref address));
|
||||||
|
|
@ -111,6 +112,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
||||||
{
|
{
|
||||||
case QUIC_LISTENER_EVENT.NEW_CONNECTION:
|
case QUIC_LISTENER_EVENT.NEW_CONNECTION:
|
||||||
{
|
{
|
||||||
|
|
||||||
evt.Data.NewConnection.SecurityConfig = _secConfig.NativeObjPtr;
|
evt.Data.NewConnection.SecurityConfig = _secConfig.NativeObjPtr;
|
||||||
var msQuicConnection = new MsQuicConnection(_api, _transportContext, evt.Data.NewConnection.Connection);
|
var msQuicConnection = new MsQuicConnection(_api, _transportContext, evt.Data.NewConnection.Connection);
|
||||||
_acceptConnectionQueue.Writer.TryWrite(msQuicConnection);
|
_acceptConnectionQueue.Writer.TryWrite(msQuicConnection);
|
||||||
|
|
|
||||||
|
|
@ -82,8 +82,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
||||||
IntPtr Handle,
|
IntPtr Handle,
|
||||||
uint Level,
|
uint Level,
|
||||||
uint Param,
|
uint Param,
|
||||||
IntPtr BufferLength,
|
out uint BufferLength,
|
||||||
IntPtr Buffer);
|
out byte* Buffer);
|
||||||
|
|
||||||
internal delegate uint RegistrationOpenDelegate(byte[] appName, out IntPtr RegistrationContext);
|
internal delegate uint RegistrationOpenDelegate(byte[] appName, out IntPtr RegistrationContext);
|
||||||
|
|
||||||
|
|
@ -533,36 +533,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
||||||
{
|
{
|
||||||
var socketAddress = new SOCKADDR_INET();
|
var socketAddress = new SOCKADDR_INET();
|
||||||
var buffer = endpoint.Address.GetAddressBytes();
|
var buffer = endpoint.Address.GetAddressBytes();
|
||||||
switch (endpoint.Address.AddressFamily)
|
if (endpoint.Address != IPAddress.Any && endpoint.Address != IPAddress.IPv6Any)
|
||||||
{
|
{
|
||||||
case AddressFamily.InterNetwork:
|
switch (endpoint.Address.AddressFamily)
|
||||||
socketAddress.Ipv4.sin_addr0 = buffer[0];
|
{
|
||||||
socketAddress.Ipv4.sin_addr1 = buffer[1];
|
case AddressFamily.InterNetwork:
|
||||||
socketAddress.Ipv4.sin_addr2 = buffer[2];
|
socketAddress.Ipv4.sin_addr0 = buffer[0];
|
||||||
socketAddress.Ipv4.sin_addr3 = buffer[3];
|
socketAddress.Ipv4.sin_addr1 = buffer[1];
|
||||||
socketAddress.Ipv4.sin_family = IPv4;
|
socketAddress.Ipv4.sin_addr2 = buffer[2];
|
||||||
break;
|
socketAddress.Ipv4.sin_addr3 = buffer[3];
|
||||||
case AddressFamily.InterNetworkV6:
|
socketAddress.Ipv4.sin_family = IPv4;
|
||||||
socketAddress.Ipv6.sin6_addr0 = buffer[0];
|
break;
|
||||||
socketAddress.Ipv6.sin6_addr1 = buffer[1];
|
case AddressFamily.InterNetworkV6:
|
||||||
socketAddress.Ipv6.sin6_addr2 = buffer[2];
|
socketAddress.Ipv6.sin6_addr0 = buffer[0];
|
||||||
socketAddress.Ipv6.sin6_addr3 = buffer[3];
|
socketAddress.Ipv6.sin6_addr1 = buffer[1];
|
||||||
socketAddress.Ipv6.sin6_addr4 = buffer[4];
|
socketAddress.Ipv6.sin6_addr2 = buffer[2];
|
||||||
socketAddress.Ipv6.sin6_addr5 = buffer[5];
|
socketAddress.Ipv6.sin6_addr3 = buffer[3];
|
||||||
socketAddress.Ipv6.sin6_addr6 = buffer[6];
|
socketAddress.Ipv6.sin6_addr4 = buffer[4];
|
||||||
socketAddress.Ipv6.sin6_addr7 = buffer[7];
|
socketAddress.Ipv6.sin6_addr5 = buffer[5];
|
||||||
socketAddress.Ipv6.sin6_addr8 = buffer[8];
|
socketAddress.Ipv6.sin6_addr6 = buffer[6];
|
||||||
socketAddress.Ipv6.sin6_addr9 = buffer[9];
|
socketAddress.Ipv6.sin6_addr7 = buffer[7];
|
||||||
socketAddress.Ipv6.sin6_addr10 = buffer[10];
|
socketAddress.Ipv6.sin6_addr8 = buffer[8];
|
||||||
socketAddress.Ipv6.sin6_addr11 = buffer[11];
|
socketAddress.Ipv6.sin6_addr9 = buffer[9];
|
||||||
socketAddress.Ipv6.sin6_addr12 = buffer[12];
|
socketAddress.Ipv6.sin6_addr10 = buffer[10];
|
||||||
socketAddress.Ipv6.sin6_addr13 = buffer[13];
|
socketAddress.Ipv6.sin6_addr11 = buffer[11];
|
||||||
socketAddress.Ipv6.sin6_addr14 = buffer[14];
|
socketAddress.Ipv6.sin6_addr12 = buffer[12];
|
||||||
socketAddress.Ipv6.sin6_addr15 = buffer[15];
|
socketAddress.Ipv6.sin6_addr13 = buffer[13];
|
||||||
socketAddress.Ipv6.sin6_family = IPv6;
|
socketAddress.Ipv6.sin6_addr14 = buffer[14];
|
||||||
break;
|
socketAddress.Ipv6.sin6_addr15 = buffer[15];
|
||||||
default:
|
socketAddress.Ipv6.sin6_family = IPv6;
|
||||||
throw new ArgumentException("Only IPv4 or IPv6 are supported");
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentException("Only IPv4 or IPv6 are supported");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SetPort(endpoint.Address.AddressFamily, ref socketAddress, endpoint.Port);
|
SetPort(endpoint.Address.AddressFamily, ref socketAddress, endpoint.Port);
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ using static Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal.MsQui
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
||||||
{
|
{
|
||||||
internal class MsQuicStream : TransportConnection, IUnidirectionalStreamFeature
|
internal class MsQuicStream : TransportConnection, IQuicStreamFeature
|
||||||
{
|
{
|
||||||
private Task _processingTask;
|
private Task _processingTask;
|
||||||
private MsQuicConnection _connection;
|
private MsQuicConnection _connection;
|
||||||
|
|
@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
||||||
private GCHandle _handle;
|
private GCHandle _handle;
|
||||||
private StreamCallbackDelegate _delegate;
|
private StreamCallbackDelegate _delegate;
|
||||||
private string _connectionId;
|
private string _connectionId;
|
||||||
|
private long _streamId = -1;
|
||||||
|
|
||||||
internal ResettableCompletionSource _resettableCompletion;
|
internal ResettableCompletionSource _resettableCompletion;
|
||||||
private MemoryHandle[] _bufferArrays;
|
private MemoryHandle[] _bufferArrays;
|
||||||
|
|
@ -56,15 +57,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
||||||
|
|
||||||
var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions);
|
var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions);
|
||||||
|
|
||||||
// TODO when stream is unidirectional, don't create an output pipe.
|
Features.Set<IQuicStreamFeature>(this);
|
||||||
if (flags.HasFlag(QUIC_STREAM_OPEN_FLAG.UNIDIRECTIONAL))
|
|
||||||
{
|
|
||||||
Features.Set<IUnidirectionalStreamFeature>(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO populate the ITlsConnectionFeature (requires client certs).
|
// TODO populate the ITlsConnectionFeature (requires client certs).
|
||||||
var feature = new FakeTlsConnectionFeature();
|
Features.Set<ITlsConnectionFeature>(new FakeTlsConnectionFeature());
|
||||||
Features.Set<ITlsConnectionFeature>(feature);
|
if (flags.HasFlag(QUIC_STREAM_OPEN_FLAG.UNIDIRECTIONAL))
|
||||||
|
{
|
||||||
|
IsUnidirectional = true;
|
||||||
|
}
|
||||||
|
|
||||||
Transport = pair.Transport;
|
Transport = pair.Transport;
|
||||||
Application = pair.Application;
|
Application = pair.Application;
|
||||||
|
|
@ -72,21 +72,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
||||||
SetCallbackHandler();
|
SetCallbackHandler();
|
||||||
|
|
||||||
_processingTask = ProcessSends();
|
_processingTask = ProcessSends();
|
||||||
|
|
||||||
// Concatenate stream id with ConnectionId.
|
|
||||||
_log.NewStream(ConnectionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override MemoryPool<byte> MemoryPool { get; }
|
public override MemoryPool<byte> MemoryPool { get; }
|
||||||
public PipeWriter Input => Application.Output;
|
public PipeWriter Input => Application.Output;
|
||||||
public PipeReader Output => Application.Input;
|
public PipeReader Output => Application.Input;
|
||||||
|
|
||||||
|
public bool IsUnidirectional { get; }
|
||||||
|
|
||||||
|
public long StreamId
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_streamId == -1)
|
||||||
|
{
|
||||||
|
_streamId = GetStreamId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _streamId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override string ConnectionId {
|
public override string ConnectionId {
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_connectionId == null)
|
if (_connectionId == null)
|
||||||
{
|
{
|
||||||
_connectionId = $"{_connection.ConnectionId}:{base.ConnectionId}";
|
_connectionId = $"{_connection.ConnectionId}:{StreamId}";
|
||||||
}
|
}
|
||||||
return _connectionId;
|
return _connectionId;
|
||||||
}
|
}
|
||||||
|
|
@ -390,20 +402,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal
|
||||||
SetParam(QUIC_PARAM_STREAM.RECEIVE_ENABLED, buffer);
|
SetParam(QUIC_PARAM_STREAM.RECEIVE_ENABLED, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unsafe long GetStreamId()
|
||||||
|
{
|
||||||
|
byte* ptr = stackalloc byte[sizeof(long)];
|
||||||
|
var buffer = new QuicBuffer
|
||||||
|
{
|
||||||
|
Length = sizeof(long),
|
||||||
|
Buffer = ptr
|
||||||
|
};
|
||||||
|
GetParam(QUIC_PARAM_STREAM.ID, buffer);
|
||||||
|
return *(long*)ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GetParam(
|
||||||
|
QUIC_PARAM_STREAM param,
|
||||||
|
QuicBuffer buf)
|
||||||
|
{
|
||||||
|
MsQuicStatusException.ThrowIfFailed(Api.UnsafeGetParam(
|
||||||
|
_nativeObjPtr,
|
||||||
|
(uint)QUIC_PARAM_LEVEL.STREAM,
|
||||||
|
(uint)param,
|
||||||
|
ref buf));
|
||||||
|
}
|
||||||
|
|
||||||
private void SetParam(
|
private void SetParam(
|
||||||
QUIC_PARAM_STREAM param,
|
QUIC_PARAM_STREAM param,
|
||||||
QuicBuffer buf)
|
QuicBuffer buf)
|
||||||
{
|
{
|
||||||
MsQuicStatusException.ThrowIfFailed(Api.UnsafeSetParam(
|
MsQuicStatusException.ThrowIfFailed(Api.UnsafeSetParam(
|
||||||
_nativeObjPtr,
|
_nativeObjPtr,
|
||||||
(uint)QUIC_PARAM_LEVEL.SESSION,
|
(uint)QUIC_PARAM_LEVEL.STREAM,
|
||||||
(uint)param,
|
(uint)param,
|
||||||
buf));
|
buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
~MsQuicStream()
|
~MsQuicStream()
|
||||||
{
|
{
|
||||||
_log.LogDebug("Destructor");
|
|
||||||
Dispose(false);
|
Dispose(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic
|
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic
|
||||||
{
|
{
|
||||||
public class MsQuicTransportFactory : IConnectionListenerFactory
|
public class MsQuicTransportFactory : IMultiplexedConnectionListenerFactory
|
||||||
{
|
{
|
||||||
private MsQuicTrace _log;
|
private MsQuicTrace _log;
|
||||||
private IHostApplicationLifetime _applicationLifetime;
|
private IHostApplicationLifetime _applicationLifetime;
|
||||||
|
|
|
||||||
|
|
@ -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.AspNetCore.Server.Kestrel.Https;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
|
|
||||||
namespace QuicSampleApp
|
namespace QuicSampleApp
|
||||||
{
|
{
|
||||||
|
|
@ -46,6 +47,7 @@ namespace QuicSampleApp
|
||||||
|
|
||||||
options.Listen(IPAddress.Any, basePort, listenOptions =>
|
options.Listen(IPAddress.Any, basePort, listenOptions =>
|
||||||
{
|
{
|
||||||
|
listenOptions.Protocols = HttpProtocols.Http3;
|
||||||
listenOptions.Use((next) =>
|
listenOptions.Use((next) =>
|
||||||
{
|
{
|
||||||
return async connection =>
|
return async connection =>
|
||||||
|
|
|
||||||
|
|
@ -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()
|
public async Task RunAsync()
|
||||||
{
|
{
|
||||||
|
var start = Console.ReadLine();
|
||||||
|
Console.WriteLine("Starting");
|
||||||
var connectionContext = await _connectionFactory.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 5555));
|
var connectionContext = await _connectionFactory.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 5555));
|
||||||
var createStreamFeature = connectionContext.Features.Get<IQuicCreateStreamFeature>();
|
var createStreamFeature = connectionContext.Features.Get<IQuicCreateStreamFeature>();
|
||||||
var streamContext = await createStreamFeature.StartBidirectionalStreamAsync();
|
var streamContext = await createStreamFeature.StartBidirectionalStreamAsync();
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ namespace CodeGenerator
|
||||||
{
|
{
|
||||||
"Accept-Ranges",
|
"Accept-Ranges",
|
||||||
"Age",
|
"Age",
|
||||||
|
"Alt-Svc",
|
||||||
"ETag",
|
"ETag",
|
||||||
"Location",
|
"Location",
|
||||||
"Proxy-Authenticate",
|
"Proxy-Authenticate",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
@ -89,7 +90,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||||
c.Configure(context.ServerOptions);
|
c.Configure(context.ServerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new KestrelServer(sp.GetRequiredService<IConnectionListenerFactory>(), context);
|
return new KestrelServer(new List<IConnectionListenerFactory>() { sp.GetRequiredService<IConnectionListenerFactory>() }, context);
|
||||||
});
|
});
|
||||||
configureServices(services);
|
configureServices(services);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System.Threading.Channels" />
|
<Reference Include="System.Threading.Channels" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
|
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||||
<Reference Include="Microsoft.Extensions.TypeNameHelper.Sources" />
|
<Reference Include="Microsoft.Extensions.TypeNameHelper.Sources" />
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Hosting.Server;
|
using Microsoft.AspNetCore.Hosting.Server;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -80,7 +82,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTrans
|
||||||
{
|
{
|
||||||
context.ServerOptions.ApplicationServices = sp;
|
context.ServerOptions.ApplicationServices = sp;
|
||||||
configureKestrel(context.ServerOptions);
|
configureKestrel(context.ServerOptions);
|
||||||
return new KestrelServer(_transportFactory, context);
|
return new KestrelServer(new List<IConnectionListenerFactory>() { _transportFactory }, context);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue