Synchronize Http/2 HPack implementation between CoreFx and ASP.NET Core (#13931)
This commit is contained in:
parent
bd917f690d
commit
3ecdc40318
|
|
@ -14,9 +14,11 @@
|
|||
/src/Hosting/ @tratcher @anurse
|
||||
/src/Http/ @tratcher @jkotalik @anurse
|
||||
/src/Middleware/ @tratcher @anurse
|
||||
/src/Middleware/HttpsPolicy @jkotalik @anurse
|
||||
/src/Middleware/Rewrite @jkotalik @anurse
|
||||
# /src/ProjectTemplates/ @ryanbrandenburg
|
||||
/src/Security/ @tratcher @anurse
|
||||
/src/Servers/ @tratcher @jkotalik @anurse @halter73
|
||||
/src/Middleware/Rewrite @jkotalik @anurse
|
||||
/src/Middleware/HttpsPolicy @jkotalik @anurse
|
||||
/src/Shared/Http2 @aspnet/http2
|
||||
/src/Shared/test/Shared.Tests/Http2 @aspnet/http2
|
||||
/src/SignalR/ @BrennanConroy @halter73 @anurse
|
||||
|
|
|
|||
|
|
@ -204,14 +204,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
Custom = (byte)9,
|
||||
None = (byte)255,
|
||||
}
|
||||
public partial class HttpParser<TRequestHandler> : Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpParser<TRequestHandler> where TRequestHandler : Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpHeadersHandler, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpRequestLineHandler
|
||||
{
|
||||
public HttpParser() { }
|
||||
public HttpParser(bool showErrorDetails) { }
|
||||
bool Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpParser<TRequestHandler>.ParseRequestLine(TRequestHandler handler, in System.Buffers.ReadOnlySequence<byte> buffer, out System.SequencePosition consumed, out System.SequencePosition examined) { throw null; }
|
||||
public bool ParseHeaders(TRequestHandler handler, ref System.Buffers.SequenceReader<byte> reader) { throw null; }
|
||||
public bool ParseRequestLine(TRequestHandler handler, in System.Buffers.ReadOnlySequence<byte> buffer, out System.SequencePosition consumed, out System.SequencePosition examined) { throw null; }
|
||||
}
|
||||
public enum HttpScheme
|
||||
{
|
||||
Unknown = -1,
|
||||
|
|
@ -225,16 +217,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
Http11 = 1,
|
||||
Http2 = 2,
|
||||
}
|
||||
public partial interface IHttpHeadersHandler
|
||||
{
|
||||
void OnHeader(System.Span<byte> name, System.Span<byte> value);
|
||||
void OnHeadersComplete();
|
||||
}
|
||||
public partial interface IHttpParser<TRequestHandler> where TRequestHandler : Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpHeadersHandler, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpRequestLineHandler
|
||||
{
|
||||
bool ParseHeaders(TRequestHandler handler, ref System.Buffers.SequenceReader<byte> reader);
|
||||
bool ParseRequestLine(TRequestHandler handler, in System.Buffers.ReadOnlySequence<byte> buffer, out System.SequencePosition consumed, out System.SequencePosition examined);
|
||||
}
|
||||
public partial interface IHttpRequestLineHandler
|
||||
{
|
||||
void OnStartLine(Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod method, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpVersion version, System.Span<byte> target, System.Span<byte> path, System.Span<byte> query, System.Span<byte> customMethod, bool pathEncoded);
|
||||
|
|
|
|||
|
|
@ -249,9 +249,6 @@
|
|||
<data name="BindingToDefaultAddress" xml:space="preserve">
|
||||
<value>No listening endpoints were configured. Binding to {address} by default.</value>
|
||||
</data>
|
||||
<data name="ConfigureHttpsFromMethodCall" xml:space="preserve">
|
||||
<value>HTTPS endpoints can only be configured using {methodName}.</value>
|
||||
</data>
|
||||
<data name="ConfigurePathBaseFromMethodCall" xml:space="preserve">
|
||||
<value>A path base can only be configured using {methodName}.</value>
|
||||
</data>
|
||||
|
|
@ -261,9 +258,6 @@
|
|||
<data name="EndpointAlreadyInUse" xml:space="preserve">
|
||||
<value>Failed to bind to address {endpoint}: address already in use.</value>
|
||||
</data>
|
||||
<data name="InvalidUrl" xml:space="preserve">
|
||||
<value>Invalid URL: '{url}'.</value>
|
||||
</data>
|
||||
<data name="NetworkInterfaceBindingFailed" xml:space="preserve">
|
||||
<value>Unable to bind to {address} on the {interfaceName} interface: '{error}'.</value>
|
||||
</data>
|
||||
|
|
@ -288,9 +282,6 @@
|
|||
<data name="ParameterReadOnlyAfterResponseStarted" xml:space="preserve">
|
||||
<value>{name} cannot be set because the response has already started.</value>
|
||||
</data>
|
||||
<data name="RequestProcessingAborted" xml:space="preserve">
|
||||
<value>Request processing didn't complete within the shutdown timeout.</value>
|
||||
</data>
|
||||
<data name="TooFewBytesWritten" xml:space="preserve">
|
||||
<value>Response Content-Length mismatch: too few bytes written ({written} of {expected}).</value>
|
||||
</data>
|
||||
|
|
@ -330,9 +321,6 @@
|
|||
<data name="PositiveTimeSpanRequired" xml:space="preserve">
|
||||
<value>Value must be a positive TimeSpan.</value>
|
||||
</data>
|
||||
<data name="NonNegativeTimeSpanRequired" xml:space="preserve">
|
||||
<value>Value must be a non-negative TimeSpan.</value>
|
||||
</data>
|
||||
<data name="MinimumGracePeriodRequired" xml:space="preserve">
|
||||
<value>The request body rate enforcement grace period must be greater than {heartbeatInterval} second.</value>
|
||||
</data>
|
||||
|
|
@ -357,30 +345,6 @@
|
|||
<data name="EndPointHttp2NotNegotiated" xml:space="preserve">
|
||||
<value>HTTP/2 over TLS was not negotiated on an HTTP/2-only endpoint.</value>
|
||||
</data>
|
||||
<data name="HPackErrorDynamicTableSizeUpdateTooLarge" xml:space="preserve">
|
||||
<value>A dynamic table size of {size} octets is greater than the configured maximum size of {maxSize} octets.</value>
|
||||
</data>
|
||||
<data name="HPackErrorIndexOutOfRange" xml:space="preserve">
|
||||
<value>Index {index} is outside the bounds of the header field table.</value>
|
||||
</data>
|
||||
<data name="HPackHuffmanErrorIncomplete" xml:space="preserve">
|
||||
<value>Input data could not be fully decoded.</value>
|
||||
</data>
|
||||
<data name="HPackHuffmanErrorEOS" xml:space="preserve">
|
||||
<value>Input data contains the EOS symbol.</value>
|
||||
</data>
|
||||
<data name="HPackHuffmanErrorDestinationTooSmall" xml:space="preserve">
|
||||
<value>The destination buffer is not large enough to store the decoded data.</value>
|
||||
</data>
|
||||
<data name="HPackHuffmanError" xml:space="preserve">
|
||||
<value>Huffman decoding error.</value>
|
||||
</data>
|
||||
<data name="HPackStringLengthTooLarge" xml:space="preserve">
|
||||
<value>Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets.</value>
|
||||
</data>
|
||||
<data name="HPackErrorIncompleteHeaderBlock" xml:space="preserve">
|
||||
<value>The header block was incomplete and could not be fully decoded.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorStreamIdEven" xml:space="preserve">
|
||||
<value>The client sent a {frameType} frame with even stream ID {streamId}.</value>
|
||||
</data>
|
||||
|
|
@ -459,9 +423,6 @@
|
|||
<data name="Http2ErrorConnectionSpecificHeaderField" xml:space="preserve">
|
||||
<value>Request headers contain connection-specific header field.</value>
|
||||
</data>
|
||||
<data name="UnableToConfigureHttpsBindings" xml:space="preserve">
|
||||
<value>Unable to configure default https bindings because no IDefaultHttpsProvider service was provided.</value>
|
||||
</data>
|
||||
<data name="AuthenticationFailed" xml:space="preserve">
|
||||
<value>Failed to authenticate HTTPS connection.</value>
|
||||
</data>
|
||||
|
|
@ -471,9 +432,6 @@
|
|||
<data name="InvalidServerCertificateEku" xml:space="preserve">
|
||||
<value>Certificate {thumbprint} cannot be used as an SSL server certificate. It has an Extended Key Usage extension but the usages do not include Server Authentication (OID 1.3.6.1.5.5.7.3.1).</value>
|
||||
</data>
|
||||
<data name="PositiveTimeSpanRequired1" xml:space="preserve">
|
||||
<value>Value must be a positive TimeSpan.</value>
|
||||
</data>
|
||||
<data name="ServerCertificateRequired" xml:space="preserve">
|
||||
<value>The server certificate parameter is required.</value>
|
||||
</data>
|
||||
|
|
@ -575,30 +533,12 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
|
|||
<data name="ArgumentOutOfRange" xml:space="preserve">
|
||||
<value>A value between {min} and {max} is required.</value>
|
||||
</data>
|
||||
<data name="HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock" xml:space="preserve">
|
||||
<value>Dynamic tables size update did not occur at the beginning of the first header block.</value>
|
||||
</data>
|
||||
<data name="HPackErrorNotEnoughBuffer" xml:space="preserve">
|
||||
<value>The given buffer was too small to encode any headers.</value>
|
||||
</data>
|
||||
<data name="HPackErrorIntegerTooBig" xml:space="preserve">
|
||||
<value>The decoded integer exceeds the maximum value of Int32.MaxValue.</value>
|
||||
</data>
|
||||
<data name="ConnectionAbortedByClient" xml:space="preserve">
|
||||
<value>The client closed the connection.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorStreamAborted" xml:space="preserve">
|
||||
<value>A frame of type {frameType} was received after stream {streamId} was reset or aborted.</value>
|
||||
</data>
|
||||
<data name="ProtocolSelectionFailed" xml:space="preserve">
|
||||
<value>HTTP protocol selection failed.</value>
|
||||
</data>
|
||||
<data name="ServerShutdownDuringConnectionInitialization" xml:space="preserve">
|
||||
<value>Server shutdown started during connection initialization.</value>
|
||||
</data>
|
||||
<data name="StartAsyncBeforeGetMemory" xml:space="preserve">
|
||||
<value>Cannot call GetMemory() until response has started. Call HttpResponse.StartAsync() before calling GetMemory().</value>
|
||||
</data>
|
||||
<data name="Http2MinDataRateNotSupported" xml:space="preserve">
|
||||
<value>This feature is not supported for HTTP/2 requests except to disable it entirely by setting the rate to null.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
|
|
@ -22,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
Trailers = trailers;
|
||||
}
|
||||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
if (Trailers)
|
||||
{
|
||||
|
|
@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}
|
||||
}
|
||||
|
||||
public void OnHeadersComplete()
|
||||
public void OnHeadersComplete(bool endStream)
|
||||
{
|
||||
if (Trailers)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5768,7 +5768,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public unsafe void Append(Span<byte> name, Span<byte> value)
|
||||
public unsafe void Append(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
ref byte nameStart = ref MemoryMarshal.GetReference(name);
|
||||
ref StringValues values = ref Unsafe.AsRef<StringValues>(null);
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public class HttpParser<TRequestHandler> : IHttpParser<TRequestHandler> where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler
|
||||
internal class HttpParser<TRequestHandler> : IHttpParser<TRequestHandler> where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler
|
||||
{
|
||||
private readonly bool _showErrorDetails;
|
||||
|
||||
|
|
@ -212,7 +213,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}
|
||||
|
||||
// Double CRLF found, so end of headers.
|
||||
handler.OnHeadersComplete();
|
||||
handler.OnHeadersComplete(endStream: false);
|
||||
return true;
|
||||
}
|
||||
else if (readAhead == 1)
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}
|
||||
}
|
||||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
_requestHeadersParsed++;
|
||||
if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
|
||||
|
|
@ -503,7 +503,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
HttpRequestHeaders.Append(name, value);
|
||||
}
|
||||
|
||||
public void OnTrailer(Span<byte> name, Span<byte> value)
|
||||
public void OnTrailer(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
// Trailers still count towards the limit.
|
||||
_requestHeadersParsed++;
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void AppendContentLength(Span<byte> value)
|
||||
private void AppendContentLength(ReadOnlySpan<byte> value)
|
||||
{
|
||||
if (_contentLength.HasValue)
|
||||
{
|
||||
|
|
@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe void AppendUnknownHeaders(Span<byte> name, string valueString)
|
||||
private unsafe void AppendUnknownHeaders(ReadOnlySpan<byte> name, string valueString)
|
||||
{
|
||||
string key = name.GetHeaderName();
|
||||
Unknown.TryGetValue(key, out var existing);
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public interface IHttpHeadersHandler
|
||||
{
|
||||
void OnHeader(Span<byte> name, Span<byte> value);
|
||||
void OnHeadersComplete();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,11 @@
|
|||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public interface IHttpParser<TRequestHandler> where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler
|
||||
internal interface IHttpParser<TRequestHandler> where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler
|
||||
{
|
||||
bool ParseRequestLine(TRequestHandler handler, in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,433 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
internal class HPackDecoder
|
||||
{
|
||||
private enum State
|
||||
{
|
||||
Ready,
|
||||
HeaderFieldIndex,
|
||||
HeaderNameIndex,
|
||||
HeaderNameLength,
|
||||
HeaderNameLengthContinue,
|
||||
HeaderName,
|
||||
HeaderValueLength,
|
||||
HeaderValueLengthContinue,
|
||||
HeaderValue,
|
||||
DynamicTableSizeUpdate
|
||||
}
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.1
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 1 | Index (7+) |
|
||||
// +---+---------------------------+
|
||||
private const byte IndexedHeaderFieldMask = 0x80;
|
||||
private const byte IndexedHeaderFieldRepresentation = 0x80;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 1 | Index (6+) |
|
||||
// +---+---+-----------------------+
|
||||
private const byte LiteralHeaderFieldWithIncrementalIndexingMask = 0xc0;
|
||||
private const byte LiteralHeaderFieldWithIncrementalIndexingRepresentation = 0x40;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 0 | 0 | Index (4+) |
|
||||
// +---+---+-----------------------+
|
||||
private const byte LiteralHeaderFieldWithoutIndexingMask = 0xf0;
|
||||
private const byte LiteralHeaderFieldWithoutIndexingRepresentation = 0x00;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 0 | 1 | Index (4+) |
|
||||
// +---+---+-----------------------+
|
||||
private const byte LiteralHeaderFieldNeverIndexedMask = 0xf0;
|
||||
private const byte LiteralHeaderFieldNeverIndexedRepresentation = 0x10;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.3
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 1 | Max size (5+) |
|
||||
// +---+---------------------------+
|
||||
private const byte DynamicTableSizeUpdateMask = 0xe0;
|
||||
private const byte DynamicTableSizeUpdateRepresentation = 0x20;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.5.2
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | H | String Length (7+) |
|
||||
// +---+---------------------------+
|
||||
private const byte HuffmanMask = 0x80;
|
||||
|
||||
private const int IndexedHeaderFieldPrefix = 7;
|
||||
private const int LiteralHeaderFieldWithIncrementalIndexingPrefix = 6;
|
||||
private const int LiteralHeaderFieldWithoutIndexingPrefix = 4;
|
||||
private const int LiteralHeaderFieldNeverIndexedPrefix = 4;
|
||||
private const int DynamicTableSizeUpdatePrefix = 5;
|
||||
private const int StringLengthPrefix = 7;
|
||||
|
||||
private readonly int _maxDynamicTableSize;
|
||||
private readonly DynamicTable _dynamicTable;
|
||||
private readonly IntegerDecoder _integerDecoder = new IntegerDecoder();
|
||||
private readonly byte[] _stringOctets;
|
||||
private readonly byte[] _headerNameOctets;
|
||||
private readonly byte[] _headerValueOctets;
|
||||
|
||||
private State _state = State.Ready;
|
||||
private byte[] _headerName;
|
||||
private int _stringIndex;
|
||||
private int _stringLength;
|
||||
private int _headerNameLength;
|
||||
private int _headerValueLength;
|
||||
private bool _index;
|
||||
private bool _huffman;
|
||||
private bool _headersObserved;
|
||||
|
||||
public HPackDecoder(int maxDynamicTableSize, int maxRequestHeaderFieldSize)
|
||||
: this(maxDynamicTableSize, maxRequestHeaderFieldSize, new DynamicTable(maxDynamicTableSize)) { }
|
||||
|
||||
// For testing.
|
||||
internal HPackDecoder(int maxDynamicTableSize, int maxRequestHeaderFieldSize, DynamicTable dynamicTable)
|
||||
{
|
||||
_maxDynamicTableSize = maxDynamicTableSize;
|
||||
_dynamicTable = dynamicTable;
|
||||
|
||||
_stringOctets = new byte[maxRequestHeaderFieldSize];
|
||||
_headerNameOctets = new byte[maxRequestHeaderFieldSize];
|
||||
_headerValueOctets = new byte[maxRequestHeaderFieldSize];
|
||||
}
|
||||
|
||||
public void Decode(in ReadOnlySequence<byte> data, bool endHeaders, IHttpHeadersHandler handler)
|
||||
{
|
||||
foreach (var segment in data)
|
||||
{
|
||||
var span = segment.Span;
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
{
|
||||
OnByte(span[i], handler);
|
||||
}
|
||||
}
|
||||
|
||||
if (endHeaders)
|
||||
{
|
||||
if (_state != State.Ready)
|
||||
{
|
||||
throw new HPackDecodingException(CoreStrings.HPackErrorIncompleteHeaderBlock);
|
||||
}
|
||||
|
||||
_headersObserved = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnByte(byte b, IHttpHeadersHandler handler)
|
||||
{
|
||||
int intResult;
|
||||
switch (_state)
|
||||
{
|
||||
case State.Ready:
|
||||
if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation)
|
||||
{
|
||||
_headersObserved = true;
|
||||
var val = b & ~IndexedHeaderFieldMask;
|
||||
|
||||
if (_integerDecoder.BeginTryDecode((byte)val, IndexedHeaderFieldPrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderField(intResult, handler);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderFieldIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingRepresentation)
|
||||
{
|
||||
_headersObserved = true;
|
||||
_index = true;
|
||||
var val = b & ~LiteralHeaderFieldWithIncrementalIndexingMask;
|
||||
|
||||
if (val == 0)
|
||||
{
|
||||
_state = State.HeaderNameLength;
|
||||
}
|
||||
else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithIncrementalIndexingPrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderName(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingRepresentation)
|
||||
{
|
||||
_headersObserved = true;
|
||||
_index = false;
|
||||
var val = b & ~LiteralHeaderFieldWithoutIndexingMask;
|
||||
|
||||
if (val == 0)
|
||||
{
|
||||
_state = State.HeaderNameLength;
|
||||
}
|
||||
else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithoutIndexingPrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderName(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldNeverIndexedMask) == LiteralHeaderFieldNeverIndexedRepresentation)
|
||||
{
|
||||
_headersObserved = true;
|
||||
_index = false;
|
||||
var val = b & ~LiteralHeaderFieldNeverIndexedMask;
|
||||
|
||||
if (val == 0)
|
||||
{
|
||||
_state = State.HeaderNameLength;
|
||||
}
|
||||
else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldNeverIndexedPrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderName(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & DynamicTableSizeUpdateMask) == DynamicTableSizeUpdateRepresentation)
|
||||
{
|
||||
// https://tools.ietf.org/html/rfc7541#section-4.2
|
||||
// This dynamic table size
|
||||
// update MUST occur at the beginning of the first header block
|
||||
// following the change to the dynamic table size.
|
||||
if (_headersObserved)
|
||||
{
|
||||
throw new HPackDecodingException(CoreStrings.HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock);
|
||||
}
|
||||
|
||||
if (_integerDecoder.BeginTryDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix, out intResult))
|
||||
{
|
||||
SetDynamicHeaderTableSize(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.DynamicTableSizeUpdate;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Can't happen
|
||||
throw new HPackDecodingException($"Byte value {b} does not encode a valid header field representation.");
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderFieldIndex:
|
||||
if (_integerDecoder.TryDecode(b, out intResult))
|
||||
{
|
||||
OnIndexedHeaderField(intResult, handler);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderNameIndex:
|
||||
if (_integerDecoder.TryDecode(b, out intResult))
|
||||
{
|
||||
OnIndexedHeaderName(intResult);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderNameLength:
|
||||
_huffman = (b & HuffmanMask) != 0;
|
||||
|
||||
if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult))
|
||||
{
|
||||
OnStringLength(intResult, nextState: State.HeaderName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameLengthContinue;
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderNameLengthContinue:
|
||||
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.HeaderValueLength:
|
||||
_huffman = (b & HuffmanMask) != 0;
|
||||
|
||||
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.DynamicTableSizeUpdate:
|
||||
if (_integerDecoder.TryDecode(b, out intResult))
|
||||
{
|
||||
SetDynamicHeaderTableSize(intResult);
|
||||
_state = State.Ready;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
// Can't happen
|
||||
throw new HPackDecodingException("The HPACK decoder reached an invalid state.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessHeaderValue(IHttpHeadersHandler handler)
|
||||
{
|
||||
OnString(nextState: State.Ready);
|
||||
|
||||
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 OnIndexedHeaderField(int index, IHttpHeadersHandler handler)
|
||||
{
|
||||
var header = GetHeader(index);
|
||||
handler.OnHeader(new Span<byte>(header.Name), new Span<byte>(header.Value));
|
||||
_state = State.Ready;
|
||||
}
|
||||
|
||||
private void OnIndexedHeaderName(int index)
|
||||
{
|
||||
var header = GetHeader(index);
|
||||
_headerName = header.Name;
|
||||
_headerNameLength = header.Name.Length;
|
||||
_state = State.HeaderValueLength;
|
||||
}
|
||||
|
||||
private void OnStringLength(int length, State nextState)
|
||||
{
|
||||
if (length > _stringOctets.Length)
|
||||
{
|
||||
throw new HPackDecodingException(CoreStrings.FormatHPackStringLengthTooLarge(length, _stringOctets.Length));
|
||||
}
|
||||
|
||||
_stringLength = length;
|
||||
_stringIndex = 0;
|
||||
_state = nextState;
|
||||
}
|
||||
|
||||
private void OnString(State nextState)
|
||||
{
|
||||
int Decode(byte[] dst)
|
||||
{
|
||||
if (_huffman)
|
||||
{
|
||||
return Huffman.Decode(new ReadOnlySpan<byte>(_stringOctets, 0, _stringLength), 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 HPackDecodingException(CoreStrings.HPackHuffmanError, ex);
|
||||
}
|
||||
|
||||
_state = nextState;
|
||||
}
|
||||
|
||||
private HeaderField GetHeader(int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
return index <= StaticTable.Instance.Count
|
||||
? StaticTable.Instance[index - 1]
|
||||
: _dynamicTable[index - StaticTable.Instance.Count - 1];
|
||||
}
|
||||
catch (IndexOutOfRangeException ex)
|
||||
{
|
||||
throw new HPackDecodingException(CoreStrings.FormatHPackErrorIndexOutOfRange(index), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetDynamicHeaderTableSize(int size)
|
||||
{
|
||||
if (size > _maxDynamicTableSize)
|
||||
{
|
||||
throw new HPackDecodingException(
|
||||
CoreStrings.FormatHPackErrorDynamicTableSizeUpdateTooLarge(size, _maxDynamicTableSize));
|
||||
}
|
||||
|
||||
_dynamicTable.Resize(size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
internal sealed class HPackDecodingException : Exception
|
||||
{
|
||||
public HPackDecodingException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
public HPackDecodingException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
internal class HPackEncoder
|
||||
{
|
||||
private IEnumerator<KeyValuePair<string, string>> _enumerator;
|
||||
|
||||
public bool BeginEncode(IEnumerable<KeyValuePair<string, string>> headers, Span<byte> buffer, out int length)
|
||||
{
|
||||
_enumerator = headers.GetEnumerator();
|
||||
_enumerator.MoveNext();
|
||||
|
||||
return Encode(buffer, out length);
|
||||
}
|
||||
|
||||
public bool BeginEncode(int statusCode, IEnumerable<KeyValuePair<string, string>> headers, Span<byte> buffer, out int length)
|
||||
{
|
||||
_enumerator = headers.GetEnumerator();
|
||||
_enumerator.MoveNext();
|
||||
|
||||
var statusCodeLength = EncodeStatusCode(statusCode, buffer);
|
||||
var done = Encode(buffer.Slice(statusCodeLength), throwIfNoneEncoded: false, out var headersLength);
|
||||
length = statusCodeLength + headersLength;
|
||||
|
||||
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 HPackEncodingException(CoreStrings.HPackErrorNotEnoughBuffer);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
length += headerLength;
|
||||
} while (_enumerator.MoveNext());
|
||||
|
||||
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:
|
||||
buffer[0] = (byte)(0x80 | 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;
|
||||
((Span<byte>)statusBytes).CopyTo(buffer.Slice(2));
|
||||
|
||||
return 2 + statusBytes.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private bool EncodeHeader(string name, string value, Span<byte> buffer, out int length)
|
||||
{
|
||||
var i = 0;
|
||||
length = 0;
|
||||
|
||||
if (buffer.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer[i++] = 0;
|
||||
|
||||
if (i == buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EncodeString(name, buffer.Slice(i), out var nameLength, lowercase: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
i += nameLength;
|
||||
|
||||
if (i >= buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EncodeString(value, buffer.Slice(i), out var valueLength, lowercase: false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
i += valueLength;
|
||||
|
||||
length = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EncodeString(string s, Span<byte> buffer, out int length, bool lowercase)
|
||||
{
|
||||
const int toLowerMask = 0x20;
|
||||
|
||||
var i = 0;
|
||||
length = 0;
|
||||
|
||||
if (buffer.Length == 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
internal readonly struct HeaderField
|
||||
{
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.4.1
|
||||
public const int RfcOverhead = 32;
|
||||
|
||||
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 + 32;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
internal sealed class HuffmanDecodingException : Exception
|
||||
{
|
||||
public HuffmanDecodingException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum we will decode is Int32.MaxValue, which is also the maximum request header field size.
|
||||
/// </summary>
|
||||
internal class IntegerDecoder
|
||||
{
|
||||
private int _i;
|
||||
private int _m;
|
||||
|
||||
/// <summary>
|
||||
/// Callers must ensure higher bits above the prefix are cleared before calling this method.
|
||||
/// </summary>
|
||||
/// <param name="b"></param>
|
||||
/// <param name="prefixLength"></param>
|
||||
/// <param name="result"></param>
|
||||
/// <returns></returns>
|
||||
public bool BeginTryDecode(byte b, int prefixLength, out int result)
|
||||
{
|
||||
if (b < ((1 << prefixLength) - 1))
|
||||
{
|
||||
result = b;
|
||||
return true;
|
||||
}
|
||||
|
||||
_i = b;
|
||||
_m = 0;
|
||||
result = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryDecode(byte b, out int result)
|
||||
{
|
||||
var m = _m; // Enregister
|
||||
var i = _i + ((b & 0x7f) << m); // Enregister
|
||||
|
||||
if ((b & 0x80) == 0)
|
||||
{
|
||||
// Int32.MaxValue only needs a maximum of 5 bytes to represent and the last byte cannot have any value set larger than 0x7
|
||||
if ((m > 21 && b > 0x7) || i < 0)
|
||||
{
|
||||
ThrowIntegerTooBigException();
|
||||
}
|
||||
|
||||
result = i;
|
||||
return true;
|
||||
}
|
||||
else if (m > 21)
|
||||
{
|
||||
// Int32.MaxValue only needs a maximum of 5 bytes to represent
|
||||
ThrowIntegerTooBigException();
|
||||
}
|
||||
|
||||
_m = m + 7;
|
||||
_i = i;
|
||||
|
||||
result = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void ThrowIntegerTooBigException()
|
||||
=> throw new HPackDecodingException(CoreStrings.HPackErrorIntegerTooBig);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
internal static class IntegerEncoder
|
||||
{
|
||||
public static bool Encode(int i, int n, Span<byte> buffer, out int length)
|
||||
{
|
||||
Debug.Assert(i >= 0);
|
||||
Debug.Assert(n >= 1 && n <= 8);
|
||||
|
||||
var j = 0;
|
||||
length = 0;
|
||||
|
||||
if (buffer.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (i < (1 << n) - 1)
|
||||
{
|
||||
buffer[j] &= MaskHigh(8 - n);
|
||||
buffer[j++] |= (byte)i;
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer[j] &= MaskHigh(8 - n);
|
||||
buffer[j++] |= (byte)((1 << n) - 1);
|
||||
|
||||
if (j == buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
i -= ((1 << n) - 1);
|
||||
while (i >= 128)
|
||||
{
|
||||
var ui = (uint)i; // Use unsigned for optimizations
|
||||
buffer[j++] = (byte)((ui % 128) + 128);
|
||||
|
||||
if (j >= buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
i = (int)(ui / 128); // Jit converts unsigned divide by power-of-2 constant to clean shift
|
||||
}
|
||||
buffer[j++] = (byte)i;
|
||||
}
|
||||
|
||||
length = j;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static byte MaskHigh(int n)
|
||||
{
|
||||
return (byte)(sbyte.MinValue >> (n - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
internal class StaticTable
|
||||
{
|
||||
private static readonly StaticTable _instance = new StaticTable();
|
||||
|
||||
private readonly Dictionary<int, int> _statusIndex = new Dictionary<int, int>
|
||||
{
|
||||
[200] = 8,
|
||||
[204] = 9,
|
||||
[206] = 10,
|
||||
[304] = 11,
|
||||
[400] = 12,
|
||||
[404] = 13,
|
||||
[500] = 14,
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
private readonly HeaderField[] _staticTable = new HeaderField[]
|
||||
{
|
||||
CreateHeaderField(HeaderNames.Authority, ""),
|
||||
CreateHeaderField(HeaderNames.Method, "GET"),
|
||||
CreateHeaderField(HeaderNames.Method, "POST"),
|
||||
CreateHeaderField(HeaderNames.Path, "/"),
|
||||
CreateHeaderField(HeaderNames.Path, "/index.html"),
|
||||
CreateHeaderField(HeaderNames.Scheme, "http"),
|
||||
CreateHeaderField(HeaderNames.Scheme, "https"),
|
||||
CreateHeaderField(HeaderNames.Status, "200"),
|
||||
CreateHeaderField(HeaderNames.Status, "204"),
|
||||
CreateHeaderField(HeaderNames.Status, "206"),
|
||||
CreateHeaderField(HeaderNames.Status, "304"),
|
||||
CreateHeaderField(HeaderNames.Status, "400"),
|
||||
CreateHeaderField(HeaderNames.Status, "404"),
|
||||
CreateHeaderField(HeaderNames.Status, "500"),
|
||||
CreateHeaderField("accept-charset", ""),
|
||||
CreateHeaderField("accept-encoding", "gzip, deflate"),
|
||||
CreateHeaderField("accept-language", ""),
|
||||
CreateHeaderField("accept-ranges", ""),
|
||||
CreateHeaderField("accept", ""),
|
||||
CreateHeaderField("access-control-allow-origin", ""),
|
||||
CreateHeaderField("age", ""),
|
||||
CreateHeaderField("allow", ""),
|
||||
CreateHeaderField("authorization", ""),
|
||||
CreateHeaderField("cache-control", ""),
|
||||
CreateHeaderField("content-disposition", ""),
|
||||
CreateHeaderField("content-encoding", ""),
|
||||
CreateHeaderField("content-language", ""),
|
||||
CreateHeaderField("content-length", ""),
|
||||
CreateHeaderField("content-location", ""),
|
||||
CreateHeaderField("content-range", ""),
|
||||
CreateHeaderField("content-type", ""),
|
||||
CreateHeaderField("cookie", ""),
|
||||
CreateHeaderField("date", ""),
|
||||
CreateHeaderField("etag", ""),
|
||||
CreateHeaderField("expect", ""),
|
||||
CreateHeaderField("expires", ""),
|
||||
CreateHeaderField("from", ""),
|
||||
CreateHeaderField("host", ""),
|
||||
CreateHeaderField("if-match", ""),
|
||||
CreateHeaderField("if-modified-since", ""),
|
||||
CreateHeaderField("if-none-match", ""),
|
||||
CreateHeaderField("if-range", ""),
|
||||
CreateHeaderField("if-unmodifiedsince", ""),
|
||||
CreateHeaderField("last-modified", ""),
|
||||
CreateHeaderField("link", ""),
|
||||
CreateHeaderField("location", ""),
|
||||
CreateHeaderField("max-forwards", ""),
|
||||
CreateHeaderField("proxy-authenticate", ""),
|
||||
CreateHeaderField("proxy-authorization", ""),
|
||||
CreateHeaderField("range", ""),
|
||||
CreateHeaderField("referer", ""),
|
||||
CreateHeaderField("refresh", ""),
|
||||
CreateHeaderField("retry-after", ""),
|
||||
CreateHeaderField("server", ""),
|
||||
CreateHeaderField("set-cookie", ""),
|
||||
CreateHeaderField("strict-transport-security", ""),
|
||||
CreateHeaderField("transfer-encoding", ""),
|
||||
CreateHeaderField("user-agent", ""),
|
||||
CreateHeaderField("vary", ""),
|
||||
CreateHeaderField("via", ""),
|
||||
CreateHeaderField("www-authenticate", "")
|
||||
};
|
||||
|
||||
private static HeaderField CreateHeaderField(string name, string value)
|
||||
=> new HeaderField(Encoding.ASCII.GetBytes(name), Encoding.ASCII.GetBytes(value));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
internal static class StatusCodes
|
||||
{
|
||||
private static readonly byte[] _bytesStatus100 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status100Continue);
|
||||
private static readonly byte[] _bytesStatus101 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status101SwitchingProtocols);
|
||||
private static readonly byte[] _bytesStatus102 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status102Processing);
|
||||
|
||||
private static readonly byte[] _bytesStatus200 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status200OK);
|
||||
private static readonly byte[] _bytesStatus201 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status201Created);
|
||||
private static readonly byte[] _bytesStatus202 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status202Accepted);
|
||||
private static readonly byte[] _bytesStatus203 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status203NonAuthoritative);
|
||||
private static readonly byte[] _bytesStatus204 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status204NoContent);
|
||||
private static readonly byte[] _bytesStatus205 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status205ResetContent);
|
||||
private static readonly byte[] _bytesStatus206 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status206PartialContent);
|
||||
private static readonly byte[] _bytesStatus207 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status207MultiStatus);
|
||||
private static readonly byte[] _bytesStatus208 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status208AlreadyReported);
|
||||
private static readonly byte[] _bytesStatus226 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status226IMUsed);
|
||||
|
||||
private static readonly byte[] _bytesStatus300 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status300MultipleChoices);
|
||||
private static readonly byte[] _bytesStatus301 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status301MovedPermanently);
|
||||
private static readonly byte[] _bytesStatus302 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status302Found);
|
||||
private static readonly byte[] _bytesStatus303 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status303SeeOther);
|
||||
private static readonly byte[] _bytesStatus304 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status304NotModified);
|
||||
private static readonly byte[] _bytesStatus305 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status305UseProxy);
|
||||
private static readonly byte[] _bytesStatus306 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status306SwitchProxy);
|
||||
private static readonly byte[] _bytesStatus307 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status307TemporaryRedirect);
|
||||
private static readonly byte[] _bytesStatus308 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status308PermanentRedirect);
|
||||
|
||||
private static readonly byte[] _bytesStatus400 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest);
|
||||
private static readonly byte[] _bytesStatus401 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status401Unauthorized);
|
||||
private static readonly byte[] _bytesStatus402 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status402PaymentRequired);
|
||||
private static readonly byte[] _bytesStatus403 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status403Forbidden);
|
||||
private static readonly byte[] _bytesStatus404 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status404NotFound);
|
||||
private static readonly byte[] _bytesStatus405 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status405MethodNotAllowed);
|
||||
private static readonly byte[] _bytesStatus406 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status406NotAcceptable);
|
||||
private static readonly byte[] _bytesStatus407 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status407ProxyAuthenticationRequired);
|
||||
private static readonly byte[] _bytesStatus408 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status408RequestTimeout);
|
||||
private static readonly byte[] _bytesStatus409 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status409Conflict);
|
||||
private static readonly byte[] _bytesStatus410 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status410Gone);
|
||||
private static readonly byte[] _bytesStatus411 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status411LengthRequired);
|
||||
private static readonly byte[] _bytesStatus412 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status412PreconditionFailed);
|
||||
private static readonly byte[] _bytesStatus413 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status413PayloadTooLarge);
|
||||
private static readonly byte[] _bytesStatus414 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status414UriTooLong);
|
||||
private static readonly byte[] _bytesStatus415 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status415UnsupportedMediaType);
|
||||
private static readonly byte[] _bytesStatus416 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status416RangeNotSatisfiable);
|
||||
private static readonly byte[] _bytesStatus417 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status417ExpectationFailed);
|
||||
private static readonly byte[] _bytesStatus418 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status418ImATeapot);
|
||||
private static readonly byte[] _bytesStatus419 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status419AuthenticationTimeout);
|
||||
private static readonly byte[] _bytesStatus421 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status421MisdirectedRequest);
|
||||
private static readonly byte[] _bytesStatus422 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status422UnprocessableEntity);
|
||||
private static readonly byte[] _bytesStatus423 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status423Locked);
|
||||
private static readonly byte[] _bytesStatus424 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status424FailedDependency);
|
||||
private static readonly byte[] _bytesStatus426 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status426UpgradeRequired);
|
||||
private static readonly byte[] _bytesStatus428 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status428PreconditionRequired);
|
||||
private static readonly byte[] _bytesStatus429 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status429TooManyRequests);
|
||||
private static readonly byte[] _bytesStatus431 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status431RequestHeaderFieldsTooLarge);
|
||||
private static readonly byte[] _bytesStatus451 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status451UnavailableForLegalReasons);
|
||||
|
||||
private static readonly byte[] _bytesStatus500 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError);
|
||||
private static readonly byte[] _bytesStatus501 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status501NotImplemented);
|
||||
private static readonly byte[] _bytesStatus502 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status502BadGateway);
|
||||
private static readonly byte[] _bytesStatus503 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status503ServiceUnavailable);
|
||||
private static readonly byte[] _bytesStatus504 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status504GatewayTimeout);
|
||||
private static readonly byte[] _bytesStatus505 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status505HttpVersionNotsupported);
|
||||
private static readonly byte[] _bytesStatus506 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status506VariantAlsoNegotiates);
|
||||
private static readonly byte[] _bytesStatus507 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status507InsufficientStorage);
|
||||
private static readonly byte[] _bytesStatus508 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status508LoopDetected);
|
||||
private static readonly byte[] _bytesStatus510 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status510NotExtended);
|
||||
private static readonly byte[] _bytesStatus511 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status511NetworkAuthenticationRequired);
|
||||
|
||||
private static byte[] CreateStatusBytes(int statusCode)
|
||||
{
|
||||
return Encoding.ASCII.GetBytes(statusCode.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
public static byte[] ToStatusBytes(int statusCode)
|
||||
{
|
||||
return statusCode switch
|
||||
{
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status100Continue => _bytesStatus100,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status101SwitchingProtocols => _bytesStatus101,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status102Processing => _bytesStatus102,
|
||||
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status200OK => _bytesStatus200,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status201Created => _bytesStatus201,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status202Accepted => _bytesStatus202,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status203NonAuthoritative => _bytesStatus203,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status204NoContent => _bytesStatus204,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status205ResetContent => _bytesStatus205,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status206PartialContent => _bytesStatus206,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status207MultiStatus => _bytesStatus207,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status208AlreadyReported => _bytesStatus208,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status226IMUsed => _bytesStatus226,
|
||||
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status300MultipleChoices => _bytesStatus300,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status301MovedPermanently => _bytesStatus301,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status302Found => _bytesStatus302,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status303SeeOther => _bytesStatus303,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status304NotModified => _bytesStatus304,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status305UseProxy => _bytesStatus305,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status306SwitchProxy => _bytesStatus306,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status307TemporaryRedirect => _bytesStatus307,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status308PermanentRedirect => _bytesStatus308,
|
||||
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest => _bytesStatus400,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status401Unauthorized => _bytesStatus401,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status402PaymentRequired => _bytesStatus402,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status403Forbidden => _bytesStatus403,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status404NotFound => _bytesStatus404,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status405MethodNotAllowed => _bytesStatus405,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status406NotAcceptable => _bytesStatus406,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status407ProxyAuthenticationRequired => _bytesStatus407,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status408RequestTimeout => _bytesStatus408,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status409Conflict => _bytesStatus409,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status410Gone => _bytesStatus410,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status411LengthRequired => _bytesStatus411,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status412PreconditionFailed => _bytesStatus412,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status413PayloadTooLarge => _bytesStatus413,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status414UriTooLong => _bytesStatus414,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status415UnsupportedMediaType => _bytesStatus415,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status416RangeNotSatisfiable => _bytesStatus416,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status417ExpectationFailed => _bytesStatus417,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status418ImATeapot => _bytesStatus418,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status419AuthenticationTimeout => _bytesStatus419,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status421MisdirectedRequest => _bytesStatus421,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status422UnprocessableEntity => _bytesStatus422,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status423Locked => _bytesStatus423,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status424FailedDependency => _bytesStatus424,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status426UpgradeRequired => _bytesStatus426,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status428PreconditionRequired => _bytesStatus428,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status429TooManyRequests => _bytesStatus429,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status431RequestHeaderFieldsTooLarge => _bytesStatus431,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status451UnavailableForLegalReasons => _bytesStatus451,
|
||||
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError => _bytesStatus500,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status501NotImplemented => _bytesStatus501,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status502BadGateway => _bytesStatus502,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status503ServiceUnavailable => _bytesStatus503,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status504GatewayTimeout => _bytesStatus504,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status505HttpVersionNotsupported => _bytesStatus505,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status506VariantAlsoNegotiates => _bytesStatus506,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status507InsufficientStorage => _bytesStatus507,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status508LoopDetected => _bytesStatus508,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status510NotExtended => _bytesStatus510,
|
||||
Microsoft.AspNetCore.Http.StatusCodes.Status511NetworkAuthenticationRequired => _bytesStatus511,
|
||||
|
||||
_ => CreateStatusBytes(statusCode)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.HPack;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
|
|
@ -19,7 +21,6 @@ using Microsoft.AspNetCore.Hosting.Server;
|
|||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
|
@ -1080,7 +1081,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
// We can't throw a Http2StreamErrorException here, it interrupts the header decompression state and may corrupt subsequent header frames on other streams.
|
||||
// For now these either need to be connection errors or BadRequests. If we want to downgrade any of them to stream errors later then we need to
|
||||
// rework the flow so that the remaining headers are drained and the decompression state is maintained.
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
// https://tools.ietf.org/html/rfc7540#section-6.5.2
|
||||
// "The value is based on the uncompressed size of header fields, including the length of the name and value in octets plus an overhead of 32 octets for each header field.";
|
||||
|
|
@ -1114,10 +1115,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
}
|
||||
}
|
||||
|
||||
public void OnHeadersComplete()
|
||||
public void OnHeadersComplete(bool endStream)
|
||||
=> _currentHeadersStream.OnHeadersComplete();
|
||||
|
||||
private void ValidateHeader(Span<byte> name, Span<byte> value)
|
||||
private void ValidateHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.1
|
||||
/*
|
||||
|
|
@ -1207,7 +1208,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
}
|
||||
}
|
||||
|
||||
private bool IsPseudoHeaderField(Span<byte> name, out PseudoHeaderFields headerField)
|
||||
private bool IsPseudoHeaderField(ReadOnlySpan<byte> name, out PseudoHeaderFields headerField)
|
||||
{
|
||||
headerField = PseudoHeaderFields.None;
|
||||
|
||||
|
|
@ -1244,7 +1245,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool IsConnectionSpecificHeaderField(Span<byte> name, Span<byte> value)
|
||||
private static bool IsConnectionSpecificHeaderField(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
return name.SequenceEqual(_connectionBytes) || (name.SequenceEqual(_teBytes) && !value.SequenceEqual(_trailersBytes));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ using System.Buffers.Binary;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.Http.HPack;
|
||||
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.Http2.FlowControl;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeWriterHelpers;
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
|||
}
|
||||
|
||||
// The same as GetAsciiStringNonNullCharacters but throws BadRequest
|
||||
public static unsafe string GetHeaderName(this Span<byte> span)
|
||||
public static unsafe string GetHeaderName(this ReadOnlySpan<byte> span)
|
||||
{
|
||||
if (span.IsEmpty)
|
||||
{
|
||||
|
|
@ -108,7 +108,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
|||
return asciiString;
|
||||
}
|
||||
|
||||
public static unsafe string GetAsciiStringNonNullCharacters(this Span<byte> span)
|
||||
public static string GetAsciiStringNonNullCharacters(this Span<byte> span)
|
||||
=> GetAsciiStringNonNullCharacters((ReadOnlySpan<byte>)span);
|
||||
|
||||
public static unsafe string GetAsciiStringNonNullCharacters(this ReadOnlySpan<byte> span)
|
||||
{
|
||||
if (span.IsEmpty)
|
||||
{
|
||||
|
|
@ -131,6 +134,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
|||
}
|
||||
|
||||
public static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this Span<byte> span)
|
||||
=> GetAsciiOrUTF8StringNonNullCharacters((ReadOnlySpan<byte>)span);
|
||||
|
||||
public static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this ReadOnlySpan<byte> span)
|
||||
{
|
||||
if (span.IsEmpty)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net.Http.HPack;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net.Http.HPack;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public unsafe static bool BytesOrdinalEqualsStringAndAscii(string previousValue, Span<byte> newValue)
|
||||
public unsafe static bool BytesOrdinalEqualsStringAndAscii(string previousValue, ReadOnlySpan<byte> newValue)
|
||||
{
|
||||
// previousValue is a previously materialized string which *must* have already passed validation.
|
||||
Debug.Assert(IsValidHeaderString(previousValue));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Core components of ASP.NET Core Kestrel cross-platform web server.</Description>
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
<Compile Include="$(SharedSourceRoot)CertificateGeneration\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)UrlDecoder\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)Http2\**\*.cs" Link="Shared\Http2\%(Filename)%(Extension)" />
|
||||
<Compile Include="$(RepoRoot)src\Shared\TaskToApm.cs" Link="Internal\TaskToApm.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
@ -34,6 +35,10 @@
|
|||
<EmbeddedResource Update="CoreStrings.resx">
|
||||
<Generator></Generator>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="$(SharedSourceRoot)Http2\SR.resx" Link="Shared\Http2\SR.resx">
|
||||
<ManifestResourceName>System.Net.Http.SR</ManifestResourceName>
|
||||
<Generator></Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,174 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||
{
|
||||
public class DynamicTableTests
|
||||
{
|
||||
private readonly HeaderField _header1 = new HeaderField(Encoding.ASCII.GetBytes("header-1"), Encoding.ASCII.GetBytes("value1"));
|
||||
private readonly HeaderField _header2 = new HeaderField(Encoding.ASCII.GetBytes("header-02"), Encoding.ASCII.GetBytes("value_2"));
|
||||
|
||||
[Fact]
|
||||
public void DynamicTableIsInitiallyEmpty()
|
||||
{
|
||||
var dynamicTable = new DynamicTable(4096);
|
||||
Assert.Equal(0, dynamicTable.Count);
|
||||
Assert.Equal(0, dynamicTable.Size);
|
||||
Assert.Equal(4096, dynamicTable.MaxSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CountIsNumberOfEntriesInDynamicTable()
|
||||
{
|
||||
var dynamicTable = new DynamicTable(4096);
|
||||
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
Assert.Equal(1, dynamicTable.Count);
|
||||
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
Assert.Equal(2, dynamicTable.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SizeIsCurrentDynamicTableSize()
|
||||
{
|
||||
var dynamicTable = new DynamicTable(4096);
|
||||
Assert.Equal(0, dynamicTable.Size);
|
||||
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
Assert.Equal(_header1.Length, dynamicTable.Size);
|
||||
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
Assert.Equal(_header1.Length + _header2.Length, dynamicTable.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstEntryIsMostRecentEntry()
|
||||
{
|
||||
var dynamicTable = new DynamicTable(4096);
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
|
||||
VerifyTableEntries(dynamicTable, _header2, _header1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WrapsAroundBuffer()
|
||||
{
|
||||
var header3 = new HeaderField(Encoding.ASCII.GetBytes("header-3"), Encoding.ASCII.GetBytes("value3"));
|
||||
var header4 = new HeaderField(Encoding.ASCII.GetBytes("header-4"), Encoding.ASCII.GetBytes("value4"));
|
||||
|
||||
// Make the table small enough that the circular buffer kicks in.
|
||||
var dynamicTable = new DynamicTable(HeaderField.RfcOverhead * 3);
|
||||
dynamicTable.Insert(header4.Name, header4.Value);
|
||||
dynamicTable.Insert(header3.Name, header3.Value);
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
|
||||
VerifyTableEntries(dynamicTable, _header1, _header2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowsIndexOutOfRangeException()
|
||||
{
|
||||
var dynamicTable = new DynamicTable(4096);
|
||||
Assert.Throws<IndexOutOfRangeException>(() => dynamicTable[0]);
|
||||
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
Assert.Throws<IndexOutOfRangeException>(() => dynamicTable[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoOpWhenInsertingEntryLargerThanMaxSize()
|
||||
{
|
||||
var dynamicTable = new DynamicTable(_header1.Length - 1);
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
|
||||
Assert.Equal(0, dynamicTable.Count);
|
||||
Assert.Equal(0, dynamicTable.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoOpWhenInsertingEntryLargerThanRemainingSpace()
|
||||
{
|
||||
var dynamicTable = new DynamicTable(_header1.Length);
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
|
||||
VerifyTableEntries(dynamicTable, _header1);
|
||||
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
|
||||
Assert.Equal(0, dynamicTable.Count);
|
||||
Assert.Equal(0, dynamicTable.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResizingEvictsOldestEntries()
|
||||
{
|
||||
var dynamicTable = new DynamicTable(4096);
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
|
||||
VerifyTableEntries(dynamicTable, _header2, _header1);
|
||||
|
||||
dynamicTable.Resize(_header2.Length);
|
||||
|
||||
VerifyTableEntries(dynamicTable, _header2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResizingToZeroEvictsAllEntries()
|
||||
{
|
||||
var dynamicTable = new DynamicTable(4096);
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
|
||||
dynamicTable.Resize(0);
|
||||
|
||||
Assert.Equal(0, dynamicTable.Count);
|
||||
Assert.Equal(0, dynamicTable.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanBeResizedToLargerMaxSize()
|
||||
{
|
||||
var dynamicTable = new DynamicTable(_header1.Length);
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
|
||||
// _header2 is larger than _header1, so an attempt at inserting it
|
||||
// would first clear the table then return without actually inserting it,
|
||||
// given it is larger than the current max size.
|
||||
Assert.Equal(0, dynamicTable.Count);
|
||||
Assert.Equal(0, dynamicTable.Size);
|
||||
|
||||
dynamicTable.Resize(dynamicTable.MaxSize + _header2.Length);
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
|
||||
VerifyTableEntries(dynamicTable, _header2);
|
||||
}
|
||||
|
||||
private void VerifyTableEntries(DynamicTable dynamicTable, params HeaderField[] entries)
|
||||
{
|
||||
Assert.Equal(entries.Length, dynamicTable.Count);
|
||||
Assert.Equal(entries.Sum(e => e.Length), dynamicTable.Size);
|
||||
|
||||
for (var i = 0; i < entries.Length; i++)
|
||||
{
|
||||
var headerField = dynamicTable[i];
|
||||
|
||||
Assert.NotSame(entries[i].Name, headerField.Name);
|
||||
Assert.Equal(entries[i].Name, headerField.Name);
|
||||
|
||||
Assert.NotSame(entries[i].Value, headerField.Value);
|
||||
Assert.Equal(entries[i].Value, headerField.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using System.Net.Http.HPack;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||
{
|
||||
public class HPackIntegerTests
|
||||
{
|
||||
[Fact]
|
||||
public void IntegerEncoderDecoderRoundtrips()
|
||||
{
|
||||
var decoder = new IntegerDecoder();
|
||||
var range = 1 << 8;
|
||||
|
||||
foreach (var i in Enumerable.Range(0, range).Concat(Enumerable.Range(int.MaxValue - range + 1, range)))
|
||||
{
|
||||
for (int n = 1; n <= 8; n++)
|
||||
{
|
||||
var integerBytes = new byte[6];
|
||||
Assert.True(IntegerEncoder.Encode(i, n, integerBytes, out var length));
|
||||
|
||||
var decodeResult = decoder.BeginTryDecode(integerBytes[0], n, out var intResult);
|
||||
|
||||
for (int j = 1; j < length; j++)
|
||||
{
|
||||
Assert.False(decodeResult);
|
||||
decodeResult = decoder.TryDecode(integerBytes[j], out intResult);
|
||||
}
|
||||
|
||||
Assert.True(decodeResult);
|
||||
Assert.Equal(i, intResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(IntegerCodecSamples))]
|
||||
public void EncodeSamples(int value, int bits, byte[] expectedResult)
|
||||
{
|
||||
Span<byte> actualResult = new byte[64];
|
||||
bool success = IntegerEncoder.Encode(value, bits, actualResult, out int bytesWritten);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.Equal(expectedResult.Length, bytesWritten);
|
||||
Assert.True(actualResult.Slice(0, bytesWritten).SequenceEqual(expectedResult));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(IntegerCodecSamples))]
|
||||
public void EncodeSamplesWithShortBuffer(int value, int bits, byte[] expectedResult)
|
||||
{
|
||||
Span<byte> actualResult = new byte[expectedResult.Length - 1];
|
||||
bool success = IntegerEncoder.Encode(value, bits, actualResult, out int bytesWritten);
|
||||
|
||||
Assert.False(success);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(IntegerCodecSamples))]
|
||||
public void DecodeSamples(int expectedResult, int bits, byte[] encoded)
|
||||
{
|
||||
var integerDecoder = new IntegerDecoder();
|
||||
|
||||
bool finished = integerDecoder.BeginTryDecode(encoded[0], bits, out int actualResult);
|
||||
|
||||
int i = 1;
|
||||
for (; !finished && i < encoded.Length; ++i)
|
||||
{
|
||||
finished = integerDecoder.TryDecode(encoded[i], out actualResult);
|
||||
}
|
||||
|
||||
Assert.True(finished);
|
||||
Assert.Equal(encoded.Length, i);
|
||||
|
||||
Assert.Equal(expectedResult, actualResult);
|
||||
}
|
||||
|
||||
// integer, prefix length, encoded
|
||||
public static IEnumerable<object[]> IntegerCodecSamples()
|
||||
{
|
||||
yield return new object[] { 10, 5, new byte[] { 0x0A } };
|
||||
yield return new object[] { 1337, 5, new byte[] { 0x1F, 0x9A, 0x0A } };
|
||||
yield return new object[] { 42, 8, new byte[] { 0x2A } };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
|
|
@ -13,6 +14,7 @@ using Microsoft.AspNetCore.Testing;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using HttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||
{
|
||||
|
|
@ -505,12 +507,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
|
||||
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>();
|
||||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
Headers[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters();
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeadersComplete() { }
|
||||
void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }
|
||||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||
{
|
||||
public class IntegerDecoderTests
|
||||
{
|
||||
[Theory]
|
||||
[MemberData(nameof(IntegerData))]
|
||||
public void IntegerDecode(int i, int prefixLength, byte[] octets)
|
||||
{
|
||||
var decoder = new IntegerDecoder();
|
||||
var result = decoder.BeginTryDecode(octets[0], prefixLength, out var intResult);
|
||||
|
||||
if (octets.Length == 1)
|
||||
{
|
||||
Assert.True(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
var j = 1;
|
||||
|
||||
for (; j < octets.Length - 1; j++)
|
||||
{
|
||||
Assert.False(decoder.TryDecode(octets[j], out intResult));
|
||||
}
|
||||
|
||||
Assert.True(decoder.TryDecode(octets[j], out intResult));
|
||||
}
|
||||
|
||||
Assert.Equal(i, intResult);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(IntegerData_OverMax))]
|
||||
public void IntegerDecode_Throws_IfMaxExceeded(int prefixLength, byte[] octets)
|
||||
{
|
||||
var decoder = new IntegerDecoder();
|
||||
var result = decoder.BeginTryDecode(octets[0], prefixLength, out var intResult);
|
||||
|
||||
for (var j = 1; j < octets.Length - 1; j++)
|
||||
{
|
||||
Assert.False(decoder.TryDecode(octets[j], out intResult));
|
||||
}
|
||||
|
||||
Assert.Throws<HPackDecodingException>(() => decoder.TryDecode(octets[octets.Length - 1], out intResult));
|
||||
}
|
||||
|
||||
public static TheoryData<int, int, byte[]> IntegerData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<int, int, byte[]>();
|
||||
|
||||
data.Add(10, 5, new byte[] { 10 });
|
||||
data.Add(1337, 5, new byte[] { 0x1f, 0x9a, 0x0a });
|
||||
data.Add(42, 8, new byte[] { 42 });
|
||||
data.Add(7, 3, new byte[] { 0x7, 0x0 });
|
||||
data.Add(int.MaxValue, 1, new byte[] { 0x01, 0xfe, 0xff, 0xff, 0xff, 0x07 });
|
||||
data.Add(int.MaxValue, 8, new byte[] { 0xff, 0x80, 0xfe, 0xff, 0xff, 0x07 });
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public static TheoryData<int, byte[]> IntegerData_OverMax
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<int, byte[]>();
|
||||
|
||||
data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x07 }); // Int32.MaxValue + 1
|
||||
data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x08 }); // MSB exceeds maximum
|
||||
data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x80 }); // Undefined since continuation bit set
|
||||
data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x07 }); // Int32.MaxValue + 1
|
||||
data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x08 }); // MSB exceeds maximum
|
||||
data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x80 }); // Undefined since continuation bit set
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||
{
|
||||
public class IntegerEncoderTests
|
||||
{
|
||||
[Theory]
|
||||
[MemberData(nameof(IntegerData))]
|
||||
public void IntegerEncode(int i, int prefixLength, byte[] expectedOctets)
|
||||
{
|
||||
var buffer = new byte[expectedOctets.Length];
|
||||
|
||||
Assert.True(IntegerEncoder.Encode(i, prefixLength, buffer, out var octets));
|
||||
Assert.Equal(expectedOctets.Length, octets);
|
||||
Assert.Equal(expectedOctets, buffer);
|
||||
}
|
||||
|
||||
public static TheoryData<int, int, byte[]> IntegerData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<int, int, byte[]>();
|
||||
|
||||
data.Add(10, 5, new byte[] { 10 });
|
||||
data.Add(1337, 5, new byte[] { 0x1f, 0x9a, 0x0a });
|
||||
data.Add(42, 8, new byte[] { 42 });
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
<Compile Include="$(RepoRoot)src\Shared\Buffers.MemoryPool\*.cs" LinkBase="MemoryPool" />
|
||||
<Compile Include="$(KestrelSharedSourceRoot)\DuplexPipe.cs" Link="Internal\DuplexPipe.cs" />
|
||||
<Compile Include="$(KestrelSharedSourceRoot)\CorrelationIdGenerator.cs" Link="Internal\CorrelationIdGenerator.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)test\Shared.Tests\Http2\**\*.cs" Link="Shared\Http2\%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.Http;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using HttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
||||
{
|
||||
|
|
@ -104,10 +106,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
RequestHandler = requestHandler;
|
||||
}
|
||||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
=> RequestHandler.Connection.OnHeader(name, value);
|
||||
|
||||
public void OnHeadersComplete()
|
||||
public void OnHeadersComplete(bool endStream)
|
||||
=> RequestHandler.Connection.OnHeadersComplete();
|
||||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Net.Http;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using HttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
||||
{
|
||||
|
|
@ -69,11 +71,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
{
|
||||
}
|
||||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnHeadersComplete()
|
||||
public void OnHeadersComplete(bool endStream)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -86,11 +88,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
RequestHandler = requestHandler;
|
||||
}
|
||||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
=> RequestHandler.OnHeader(name, value);
|
||||
|
||||
public void OnHeadersComplete()
|
||||
=> RequestHandler.OnHeadersComplete();
|
||||
public void OnHeadersComplete(bool endStream)
|
||||
=> RequestHandler.OnHeadersComplete(endStream);
|
||||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
=> RequestHandler.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Net.Http.HPack;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net.Http.HPack;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using HttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
||||
{
|
||||
|
|
@ -26,7 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
handler.OnHeader(new Span<byte>(_hostHeaderName), new Span<byte>(_hostHeaderValue));
|
||||
handler.OnHeader(new Span<byte>(_acceptHeaderName), new Span<byte>(_acceptHeaderValue));
|
||||
handler.OnHeader(new Span<byte>(_connectionHeaderName), new Span<byte>(_connectionHeaderValue));
|
||||
handler.OnHeadersComplete();
|
||||
handler.OnHeadersComplete(endStream: false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,13 +9,14 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.HPack;
|
||||
using System.Text;
|
||||
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.Http2;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
|
|
@ -119,12 +120,12 @@ namespace http2cat
|
|||
_pair = new DuplexPipe.DuplexPipePair(transport: null, application: clientConnectionContext.Transport);
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeader(Span<byte> name, Span<byte> value)
|
||||
void IHttpHeadersHandler.OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters();
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeadersComplete() { }
|
||||
void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }
|
||||
|
||||
public async Task InitializeConnectionAsync(int expectedSettingsCount = 3)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -951,7 +951,7 @@ $@" private void Clear(long bitsToClear)
|
|||
}} while (tempBits != 0);
|
||||
}}" : "")}{(loop.ClassName == "HttpRequestHeaders" ? $@"
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public unsafe void Append(Span<byte> name, Span<byte> value)
|
||||
public unsafe void Append(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{{
|
||||
ref byte nameStart = ref MemoryMarshal.GetReference(name);
|
||||
ref StringValues values = ref Unsafe.AsRef<StringValues>(null);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net.Http.HPack;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ using System.Buffers;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.HPack;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
|
@ -1564,7 +1565,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
ignoreNonGoAwayFrames: false,
|
||||
expectedLastStreamId: 1,
|
||||
expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR,
|
||||
expectedErrorMessage: CoreStrings.HPackErrorIncompleteHeaderBlock);
|
||||
expectedErrorMessage: SR.net_http_hpack_incomplete_header_block);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -1592,7 +1593,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
ignoreNonGoAwayFrames: false,
|
||||
expectedLastStreamId: 1,
|
||||
expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR,
|
||||
expectedErrorMessage: CoreStrings.HPackErrorIntegerTooBig);
|
||||
expectedErrorMessage: SR.net_http_hpack_bad_integer);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -3431,7 +3432,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
ignoreNonGoAwayFrames: false,
|
||||
expectedLastStreamId: 1,
|
||||
expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR,
|
||||
expectedErrorMessage: CoreStrings.HPackErrorIncompleteHeaderBlock);
|
||||
expectedErrorMessage: SR.net_http_hpack_incomplete_header_block);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ using System.Buffers;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.HPack;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
|
@ -15,7 +17,6 @@ using Microsoft.AspNetCore.Http;
|
|||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
|
@ -2041,7 +2042,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
await _connectionTask;
|
||||
|
||||
var message = Assert.Single(TestApplicationErrorLogger.Messages, m => m.Exception is HPackEncodingException);
|
||||
Assert.Contains(CoreStrings.HPackErrorNotEnoughBuffer, message.Exception.Message);
|
||||
Assert.Contains(SR.net_http_hpack_encode_failure, message.Exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -2614,7 +2615,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
|
||||
|
||||
var message = await appFinished.Task.DefaultTimeout();
|
||||
Assert.Equal(CoreStrings.HPackErrorNotEnoughBuffer, message);
|
||||
Assert.Equal(SR.net_http_hpack_encode_failure, message);
|
||||
|
||||
// Just the StatusCode gets written before aborting in the continuation frame
|
||||
await ExpectAsync(Http2FrameType.HEADERS,
|
||||
|
|
@ -2625,7 +2626,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
_pair.Application.Output.Complete();
|
||||
|
||||
await WaitForConnectionErrorAsync<HPackEncodingException>(ignoreNonGoAwayFrames: false, expectedLastStreamId: int.MaxValue, Http2ErrorCode.INTERNAL_ERROR,
|
||||
CoreStrings.HPackErrorNotEnoughBuffer);
|
||||
SR.net_http_hpack_encode_failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.HPack;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
|
@ -21,7 +23,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
|||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -400,12 +401,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
base.Dispose();
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeader(Span<byte> name, Span<byte> value)
|
||||
void IHttpHeadersHandler.OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters();
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeadersComplete() { }
|
||||
void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }
|
||||
|
||||
protected void CreateConnection()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
@ECHO OFF
|
||||
SETLOCAL
|
||||
|
||||
if not [%1] == [] (set remote_repo=%1) else (set remote_repo=%ASPNETCORE_REPO%)
|
||||
|
||||
IF [%remote_repo%] == [] (
|
||||
echo The 'ASPNETCORE_REPO' environment variable or command line paramter is not set, aborting.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ASPNETCORE_REPO: %remote_repo%
|
||||
|
||||
robocopy . %remote_repo%\src\Shared\Http2 /MIR
|
||||
robocopy .\..\..\..\..\..\tests\Tests\System\Net\Http2\ %remote_repo%\src\Shared\test\Shared.Tests\Http2 /MIR
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
@ECHO OFF
|
||||
SETLOCAL
|
||||
|
||||
if not [%1] == [] (set remote_repo=%1) else (set remote_repo=%COREFX_REPO%)
|
||||
|
||||
IF [%remote_repo%] == [] (
|
||||
echo The 'COREFX_REPO' environment variable or command line paramter is not set, aborting.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo COREFX_REPO: %remote_repo%
|
||||
|
||||
robocopy . %remote_repo%\src\Common\src\System\Net\Http\Http2 /MIR
|
||||
robocopy .\..\test\Shared.Tests\Http2 %remote_repo%\src\Common\tests\Tests\System\Net\Http2 /MIR
|
||||
|
|
@ -1,12 +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.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
namespace System.Net.Http.HPack
|
||||
{
|
||||
// The dynamic table is defined as a queue where items are inserted at the front and removed from the back.
|
||||
// It's implemented as a circular buffer that appends to the end and trims from the front. Thus index are reversed.
|
||||
internal class DynamicTable
|
||||
{
|
||||
private HeaderField[] _buffer;
|
||||
|
|
@ -37,19 +34,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
|
||||
var modIndex = _insertIndex - index - 1;
|
||||
if (modIndex < 0)
|
||||
index = _insertIndex - index - 1;
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
modIndex += _buffer.Length;
|
||||
// _buffer is circular; wrap the index back around.
|
||||
index += _buffer.Length;
|
||||
}
|
||||
|
||||
return _buffer[modIndex];
|
||||
return _buffer[index];
|
||||
}
|
||||
}
|
||||
|
||||
public void Insert(Span<byte> name, Span<byte> value)
|
||||
public void Insert(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
var entryLength = HeaderField.GetLength(name.Length, value.Length);
|
||||
int entryLength = HeaderField.GetLength(name.Length, value.Length);
|
||||
EnsureAvailable(entryLength);
|
||||
|
||||
if (entryLength > _maxSize)
|
||||
|
|
@ -74,12 +73,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
{
|
||||
var newBuffer = new HeaderField[maxSize / HeaderField.RfcOverhead];
|
||||
|
||||
for (var i = 0; i < Count; i++)
|
||||
{
|
||||
newBuffer[i] = _buffer[i];
|
||||
}
|
||||
int headCount = Math.Min(_buffer.Length - _removeIndex, _count);
|
||||
int tailCount = _count - headCount;
|
||||
|
||||
Array.Copy(_buffer, _removeIndex, newBuffer, 0, headCount);
|
||||
Array.Copy(_buffer, 0, newBuffer, headCount, tailCount);
|
||||
|
||||
_buffer = newBuffer;
|
||||
_removeIndex = 0;
|
||||
_insertIndex = _count;
|
||||
_maxSize = maxSize;
|
||||
}
|
||||
else
|
||||
|
|
@ -93,7 +95,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
{
|
||||
while (_count > 0 && _maxSize - _size < available)
|
||||
{
|
||||
_size -= _buffer[_removeIndex].Length;
|
||||
ref HeaderField field = ref _buffer[_removeIndex];
|
||||
_size -= field.Length;
|
||||
field = default;
|
||||
|
||||
_count--;
|
||||
_removeIndex = (_removeIndex + 1) % _buffer.Length;
|
||||
}
|
||||
|
|
@ -0,0 +1,491 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace System.Net.Http.HPack
|
||||
{
|
||||
internal class HPackDecoder
|
||||
{
|
||||
private enum State
|
||||
{
|
||||
Ready,
|
||||
HeaderFieldIndex,
|
||||
HeaderNameIndex,
|
||||
HeaderNameLength,
|
||||
HeaderNameLengthContinue,
|
||||
HeaderName,
|
||||
HeaderValueLength,
|
||||
HeaderValueLengthContinue,
|
||||
HeaderValue,
|
||||
DynamicTableSizeUpdate
|
||||
}
|
||||
|
||||
public const int DefaultHeaderTableSize = 4096;
|
||||
public const int DefaultStringOctetsSize = 4096;
|
||||
public const int DefaultMaxHeadersLength = 64 * 1024;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.1
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 1 | Index (7+) |
|
||||
// +---+---------------------------+
|
||||
private const byte IndexedHeaderFieldMask = 0x80;
|
||||
private const byte IndexedHeaderFieldRepresentation = 0x80;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 1 | Index (6+) |
|
||||
// +---+---+-----------------------+
|
||||
private const byte LiteralHeaderFieldWithIncrementalIndexingMask = 0xc0;
|
||||
private const byte LiteralHeaderFieldWithIncrementalIndexingRepresentation = 0x40;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 0 | 0 | Index (4+) |
|
||||
// +---+---+-----------------------+
|
||||
private const byte LiteralHeaderFieldWithoutIndexingMask = 0xf0;
|
||||
private const byte LiteralHeaderFieldWithoutIndexingRepresentation = 0x00;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 0 | 1 | Index (4+) |
|
||||
// +---+---+-----------------------+
|
||||
private const byte LiteralHeaderFieldNeverIndexedMask = 0xf0;
|
||||
private const byte LiteralHeaderFieldNeverIndexedRepresentation = 0x10;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.3
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 1 | Max size (5+) |
|
||||
// +---+---------------------------+
|
||||
private const byte DynamicTableSizeUpdateMask = 0xe0;
|
||||
private const byte DynamicTableSizeUpdateRepresentation = 0x20;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.5.2
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | H | String Length (7+) |
|
||||
// +---+---------------------------+
|
||||
private const byte HuffmanMask = 0x80;
|
||||
|
||||
private const int IndexedHeaderFieldPrefix = 7;
|
||||
private const int LiteralHeaderFieldWithIncrementalIndexingPrefix = 6;
|
||||
private const int LiteralHeaderFieldWithoutIndexingPrefix = 4;
|
||||
private const int LiteralHeaderFieldNeverIndexedPrefix = 4;
|
||||
private const int DynamicTableSizeUpdatePrefix = 5;
|
||||
private const int StringLengthPrefix = 7;
|
||||
|
||||
private readonly int _maxDynamicTableSize;
|
||||
private readonly int _maxHeadersLength;
|
||||
private readonly DynamicTable _dynamicTable;
|
||||
private readonly IntegerDecoder _integerDecoder = new IntegerDecoder();
|
||||
private byte[] _stringOctets;
|
||||
private byte[] _headerNameOctets;
|
||||
private byte[] _headerValueOctets;
|
||||
|
||||
private State _state = State.Ready;
|
||||
private byte[] _headerName;
|
||||
private int _stringIndex;
|
||||
private int _stringLength;
|
||||
private int _headerNameLength;
|
||||
private int _headerValueLength;
|
||||
private bool _index;
|
||||
private bool _huffman;
|
||||
private bool _headersObserved;
|
||||
|
||||
public HPackDecoder(int maxDynamicTableSize = DefaultHeaderTableSize, int maxHeadersLength = DefaultMaxHeadersLength)
|
||||
: this(maxDynamicTableSize, maxHeadersLength, new DynamicTable(maxDynamicTableSize))
|
||||
{
|
||||
}
|
||||
|
||||
// For testing.
|
||||
internal HPackDecoder(int maxDynamicTableSize, int maxHeadersLength, DynamicTable dynamicTable)
|
||||
{
|
||||
_maxDynamicTableSize = maxDynamicTableSize;
|
||||
_maxHeadersLength = maxHeadersLength;
|
||||
_dynamicTable = dynamicTable;
|
||||
|
||||
_stringOctets = new byte[DefaultStringOctetsSize];
|
||||
_headerNameOctets = new byte[DefaultStringOctetsSize];
|
||||
_headerValueOctets = new byte[DefaultStringOctetsSize];
|
||||
}
|
||||
|
||||
public void Decode(in ReadOnlySequence<byte> data, bool endHeaders, IHttpHeadersHandler handler)
|
||||
{
|
||||
foreach (ReadOnlyMemory<byte> segment in data)
|
||||
{
|
||||
DecodeInternal(segment.Span, endHeaders, handler);
|
||||
}
|
||||
|
||||
CheckIncompleteHeaderBlock(endHeaders);
|
||||
}
|
||||
|
||||
public void Decode(ReadOnlySpan<byte> data, bool endHeaders, IHttpHeadersHandler handler)
|
||||
{
|
||||
DecodeInternal(data, endHeaders, handler);
|
||||
CheckIncompleteHeaderBlock(endHeaders);
|
||||
}
|
||||
|
||||
private void DecodeInternal(ReadOnlySpan<byte> data, bool endHeaders, IHttpHeadersHandler handler)
|
||||
{
|
||||
int intResult;
|
||||
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
byte b = data[i];
|
||||
switch (_state)
|
||||
{
|
||||
case State.Ready:
|
||||
// TODO: Instead of masking and comparing each prefix value,
|
||||
// consider doing a 16-way switch on the first four bits (which is the max prefix size).
|
||||
// Look at this once we have more concrete perf data.
|
||||
if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation)
|
||||
{
|
||||
_headersObserved = true;
|
||||
|
||||
int val = b & ~IndexedHeaderFieldMask;
|
||||
|
||||
if (_integerDecoder.BeginTryDecode((byte)val, IndexedHeaderFieldPrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderField(intResult, handler);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderFieldIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingRepresentation)
|
||||
{
|
||||
_headersObserved = true;
|
||||
|
||||
_index = true;
|
||||
int val = b & ~LiteralHeaderFieldWithIncrementalIndexingMask;
|
||||
|
||||
if (val == 0)
|
||||
{
|
||||
_state = State.HeaderNameLength;
|
||||
}
|
||||
else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithIncrementalIndexingPrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderName(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingRepresentation)
|
||||
{
|
||||
_headersObserved = true;
|
||||
|
||||
_index = false;
|
||||
int val = b & ~LiteralHeaderFieldWithoutIndexingMask;
|
||||
|
||||
if (val == 0)
|
||||
{
|
||||
_state = State.HeaderNameLength;
|
||||
}
|
||||
else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithoutIndexingPrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderName(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldNeverIndexedMask) == LiteralHeaderFieldNeverIndexedRepresentation)
|
||||
{
|
||||
_headersObserved = true;
|
||||
|
||||
_index = false;
|
||||
int val = b & ~LiteralHeaderFieldNeverIndexedMask;
|
||||
|
||||
if (val == 0)
|
||||
{
|
||||
_state = State.HeaderNameLength;
|
||||
}
|
||||
else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldNeverIndexedPrefix, out intResult))
|
||||
{
|
||||
OnIndexedHeaderName(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & DynamicTableSizeUpdateMask) == DynamicTableSizeUpdateRepresentation)
|
||||
{
|
||||
// https://tools.ietf.org/html/rfc7541#section-4.2
|
||||
// This dynamic table size
|
||||
// update MUST occur at the beginning of the first header block
|
||||
// following the change to the dynamic table size.
|
||||
if (_headersObserved)
|
||||
{
|
||||
throw new HPackDecodingException(SR.net_http_hpack_late_dynamic_table_size_update);
|
||||
}
|
||||
|
||||
if (_integerDecoder.BeginTryDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix, out intResult))
|
||||
{
|
||||
SetDynamicHeaderTableSize(intResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.DynamicTableSizeUpdate;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Can't happen
|
||||
Debug.Fail("Unreachable code");
|
||||
throw new InvalidOperationException("Unreachable code.");
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderFieldIndex:
|
||||
if (_integerDecoder.TryDecode(b, out intResult))
|
||||
{
|
||||
OnIndexedHeaderField(intResult, handler);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderNameIndex:
|
||||
if (_integerDecoder.TryDecode(b, out intResult))
|
||||
{
|
||||
OnIndexedHeaderName(intResult);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderNameLength:
|
||||
_huffman = (b & HuffmanMask) != 0;
|
||||
|
||||
if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult))
|
||||
{
|
||||
if (intResult == 0)
|
||||
{
|
||||
throw new HPackDecodingException(SR.Format(SR.net_http_invalid_header_name, ""));
|
||||
}
|
||||
|
||||
OnStringLength(intResult, nextState: State.HeaderName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameLengthContinue;
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderNameLengthContinue:
|
||||
if (_integerDecoder.TryDecode(b, out intResult))
|
||||
{
|
||||
// IntegerDecoder disallows overlong encodings, where an integer is encoded with more bytes than is strictly required.
|
||||
// 0 should always be represented by a single byte, so we shouldn't need to check for it in the continuation case.
|
||||
Debug.Assert(intResult != 0, "A header name length of 0 should never be encoded with a continuation byte.");
|
||||
|
||||
OnStringLength(intResult, nextState: State.HeaderName);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderName:
|
||||
_stringOctets[_stringIndex++] = b;
|
||||
|
||||
if (_stringIndex == _stringLength)
|
||||
{
|
||||
OnString(nextState: State.HeaderValueLength);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderValueLength:
|
||||
_huffman = (b & HuffmanMask) != 0;
|
||||
|
||||
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))
|
||||
{
|
||||
// IntegerDecoder disallows overlong encodings where an integer is encoded with more bytes than is strictly required.
|
||||
// 0 should always be represented by a single byte, so we shouldn't need to check for it in the continuation case.
|
||||
Debug.Assert(intResult != 0, "A header value length of 0 should never be encoded with a continuation byte.");
|
||||
|
||||
OnStringLength(intResult, nextState: State.HeaderValue);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderValue:
|
||||
_stringOctets[_stringIndex++] = b;
|
||||
|
||||
if (_stringIndex == _stringLength)
|
||||
{
|
||||
ProcessHeaderValue(handler);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.DynamicTableSizeUpdate:
|
||||
if (_integerDecoder.TryDecode(b, out intResult))
|
||||
{
|
||||
SetDynamicHeaderTableSize(intResult);
|
||||
_state = State.Ready;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
// Can't happen
|
||||
Debug.Fail("HPACK decoder reach an invalid state");
|
||||
throw new NotImplementedException(_state.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckIncompleteHeaderBlock(bool endHeaders)
|
||||
{
|
||||
if (endHeaders)
|
||||
{
|
||||
if (_state != State.Ready)
|
||||
{
|
||||
throw new HPackDecodingException(SR.net_http_hpack_incomplete_header_block);
|
||||
}
|
||||
|
||||
_headersObserved = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessHeaderValue(IHttpHeadersHandler handler)
|
||||
{
|
||||
OnString(nextState: State.Ready);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public void CompleteDecode()
|
||||
{
|
||||
if (_state != State.Ready)
|
||||
{
|
||||
// Incomplete header block
|
||||
throw new HPackDecodingException(SR.net_http_hpack_unexpected_end);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler)
|
||||
{
|
||||
HeaderField header = GetHeader(index);
|
||||
handler?.OnHeader(header.Name, header.Value);
|
||||
_state = State.Ready;
|
||||
}
|
||||
|
||||
private void OnIndexedHeaderName(int index)
|
||||
{
|
||||
HeaderField header = GetHeader(index);
|
||||
_headerName = header.Name;
|
||||
_headerNameLength = header.Name.Length;
|
||||
_state = State.HeaderValueLength;
|
||||
}
|
||||
|
||||
private void OnStringLength(int length, State nextState)
|
||||
{
|
||||
if (length > _stringOctets.Length)
|
||||
{
|
||||
if (length > _maxHeadersLength)
|
||||
{
|
||||
throw new HPackDecodingException(SR.Format(SR.net_http_headers_exceeded_length, _maxHeadersLength));
|
||||
}
|
||||
|
||||
_stringOctets = new byte[Math.Max(length, _stringOctets.Length * 2)];
|
||||
}
|
||||
|
||||
_stringLength = length;
|
||||
_stringIndex = 0;
|
||||
_state = nextState;
|
||||
}
|
||||
|
||||
private void OnString(State nextState)
|
||||
{
|
||||
int Decode(ref byte[] dst)
|
||||
{
|
||||
if (_huffman)
|
||||
{
|
||||
return Huffman.Decode(new ReadOnlySpan<byte>(_stringOctets, 0, _stringLength), ref dst);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dst.Length < _stringLength)
|
||||
{
|
||||
dst = new byte[Math.Max(_stringLength, dst.Length * 2)];
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength);
|
||||
return _stringLength;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_state == State.HeaderName)
|
||||
{
|
||||
_headerNameLength = Decode(ref _headerNameOctets);
|
||||
_headerName = _headerNameOctets;
|
||||
}
|
||||
else
|
||||
{
|
||||
_headerValueLength = Decode(ref _headerValueOctets);
|
||||
}
|
||||
}
|
||||
catch (HuffmanDecodingException ex)
|
||||
{
|
||||
throw new HPackDecodingException(SR.net_http_hpack_huffman_decode_failed, ex);
|
||||
}
|
||||
|
||||
_state = nextState;
|
||||
}
|
||||
|
||||
private HeaderField GetHeader(int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
return index <= StaticTable.Count
|
||||
? StaticTable.Get(index - 1)
|
||||
: _dynamicTable[index - StaticTable.Count - 1];
|
||||
}
|
||||
catch (IndexOutOfRangeException)
|
||||
{
|
||||
// Header index out of range.
|
||||
throw new HPackDecodingException(SR.Format(SR.net_http_hpack_invalid_index, index));
|
||||
}
|
||||
}
|
||||
|
||||
private void SetDynamicHeaderTableSize(int size)
|
||||
{
|
||||
if (size > _maxDynamicTableSize)
|
||||
{
|
||||
throw new HPackDecodingException(SR.Format(SR.net_http_hpack_large_table_size_update, size, _maxDynamicTableSize));
|
||||
}
|
||||
|
||||
_dynamicTable.Resize(size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace System.Net.Http.HPack
|
||||
{
|
||||
// TODO: Should this be public?
|
||||
[Serializable]
|
||||
internal class HPackDecodingException : Exception
|
||||
{
|
||||
public HPackDecodingException()
|
||||
{
|
||||
}
|
||||
|
||||
public HPackDecodingException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public HPackDecodingException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public HPackDecodingException(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,530 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace System.Net.Http.HPack
|
||||
{
|
||||
internal class HPackEncoder
|
||||
{
|
||||
private IEnumerator<KeyValuePair<string, string>> _enumerator;
|
||||
|
||||
public bool BeginEncode(IEnumerable<KeyValuePair<string, string>> headers, Span<byte> buffer, out int length)
|
||||
{
|
||||
_enumerator = headers.GetEnumerator();
|
||||
_enumerator.MoveNext();
|
||||
|
||||
return Encode(buffer, out length);
|
||||
}
|
||||
|
||||
public bool BeginEncode(int statusCode, IEnumerable<KeyValuePair<string, string>> headers, Span<byte> buffer, out int length)
|
||||
{
|
||||
_enumerator = headers.GetEnumerator();
|
||||
_enumerator.MoveNext();
|
||||
|
||||
int statusCodeLength = EncodeStatusCode(statusCode, buffer);
|
||||
bool done = Encode(buffer.Slice(statusCodeLength), throwIfNoneEncoded: false, out int headersLength);
|
||||
length = statusCodeLength + headersLength;
|
||||
|
||||
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)
|
||||
{
|
||||
int currentLength = 0;
|
||||
do
|
||||
{
|
||||
if (!EncodeHeader(_enumerator.Current.Key, _enumerator.Current.Value, buffer.Slice(currentLength), out int headerLength))
|
||||
{
|
||||
if (currentLength == 0 && throwIfNoneEncoded)
|
||||
{
|
||||
throw new HPackEncodingException(SR.net_http_hpack_encode_failure);
|
||||
}
|
||||
|
||||
length = currentLength;
|
||||
return false;
|
||||
}
|
||||
|
||||
currentLength += headerLength;
|
||||
}
|
||||
while (_enumerator.MoveNext());
|
||||
|
||||
length = currentLength;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private int EncodeStatusCode(int statusCode, Span<byte> buffer)
|
||||
{
|
||||
switch (statusCode)
|
||||
{
|
||||
// Status codes which exist in the HTTP/2 StaticTable.
|
||||
case 200:
|
||||
case 204:
|
||||
case 206:
|
||||
case 304:
|
||||
case 400:
|
||||
case 404:
|
||||
case 500:
|
||||
buffer[0] = (byte)(0x80 | StaticTable.StatusIndex[statusCode]);
|
||||
return 1;
|
||||
default:
|
||||
// Send as Literal Header Field Without Indexing - Indexed Name
|
||||
buffer[0] = 0x08;
|
||||
|
||||
ReadOnlySpan<byte> statusBytes = StatusCodes.ToStatusBytes(statusCode);
|
||||
buffer[1] = (byte)statusBytes.Length;
|
||||
statusBytes.CopyTo(buffer.Slice(2));
|
||||
|
||||
return 2 + statusBytes.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private bool EncodeHeader(string name, string value, Span<byte> buffer, out int length)
|
||||
{
|
||||
int i = 0;
|
||||
length = 0;
|
||||
|
||||
if (buffer.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer[i++] = 0;
|
||||
|
||||
if (i == buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EncodeString(name, buffer.Slice(i), out int nameLength, lowercase: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
i += nameLength;
|
||||
|
||||
if (i >= buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EncodeString(value, buffer.Slice(i), out int valueLength, lowercase: false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
i += valueLength;
|
||||
|
||||
length = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EncodeString(string value, Span<byte> destination, out int bytesWritten, bool lowercase)
|
||||
{
|
||||
// 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) |
|
||||
// +-------------------------------+
|
||||
const int toLowerMask = 0x20;
|
||||
|
||||
if (destination.Length != 0)
|
||||
{
|
||||
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)(lowercase && (uint)(c - 'A') <= ('Z' - 'A') ? c | toLowerMask : c);
|
||||
}
|
||||
|
||||
bytesWritten = integerLength + value.Length;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Things we should add:
|
||||
// * Huffman encoding
|
||||
//
|
||||
// Things we should consider adding:
|
||||
// * Dynamic table encoding:
|
||||
// This would make the encoder stateful, which complicates things significantly.
|
||||
// Additionally, it's not clear exactly what strings we would add to the dynamic table
|
||||
// without some additional guidance from the user about this.
|
||||
// So for now, don't do dynamic encoding.
|
||||
|
||||
/// <summary>Encodes an "Indexed Header Field".</summary>
|
||||
public static bool EncodeIndexedHeaderField(int index, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
// From https://tools.ietf.org/html/rfc7541#section-6.1
|
||||
// ----------------------------------------------------
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 1 | Index (7+) |
|
||||
// +---+---------------------------+
|
||||
|
||||
if (destination.Length != 0)
|
||||
{
|
||||
destination[0] = 0x80;
|
||||
return IntegerEncoder.Encode(index, 7, destination, out bytesWritten);
|
||||
}
|
||||
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Encodes a "Literal Header Field without Indexing".</summary>
|
||||
public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, string value, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
// From https://tools.ietf.org/html/rfc7541#section-6.2.2
|
||||
// ------------------------------------------------------
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 0 | 0 | Index (4+) |
|
||||
// +---+---+-----------------------+
|
||||
// | H | Value Length (7+) |
|
||||
// +---+---------------------------+
|
||||
// | Value String (Length octets) |
|
||||
// +-------------------------------+
|
||||
|
||||
if ((uint)destination.Length >= 2)
|
||||
{
|
||||
destination[0] = 0;
|
||||
if (IntegerEncoder.Encode(index, 4, destination, out int indexLength))
|
||||
{
|
||||
Debug.Assert(indexLength >= 1);
|
||||
if (EncodeStringLiteral(value, destination.Slice(indexLength), out int nameLength))
|
||||
{
|
||||
bytesWritten = indexLength + nameLength;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a "Literal Header Field without Indexing", but only the index portion;
|
||||
/// a subsequent call to <see cref="EncodeStringLiteral"/> must be used to encode the associated value.
|
||||
/// </summary>
|
||||
public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
// From https://tools.ietf.org/html/rfc7541#section-6.2.2
|
||||
// ------------------------------------------------------
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 0 | 0 | Index (4+) |
|
||||
// +---+---+-----------------------+
|
||||
//
|
||||
// ... expected after this:
|
||||
//
|
||||
// | H | Value Length (7+) |
|
||||
// +---+---------------------------+
|
||||
// | Value String (Length octets) |
|
||||
// +-------------------------------+
|
||||
|
||||
if ((uint)destination.Length != 0)
|
||||
{
|
||||
destination[0] = 0;
|
||||
if (IntegerEncoder.Encode(index, 4, destination, out int indexLength))
|
||||
{
|
||||
Debug.Assert(indexLength >= 1);
|
||||
bytesWritten = indexLength;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Encodes a "Literal Header Field without Indexing - New Name".</summary>
|
||||
public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, ReadOnlySpan<string> values, string separator, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
// From https://tools.ietf.org/html/rfc7541#section-6.2.2
|
||||
// ------------------------------------------------------
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 0 | 0 | 0 |
|
||||
// +---+---+-----------------------+
|
||||
// | H | Name Length (7+) |
|
||||
// +---+---------------------------+
|
||||
// | Name String (Length octets) |
|
||||
// +---+---------------------------+
|
||||
// | H | Value Length (7+) |
|
||||
// +---+---------------------------+
|
||||
// | Value String (Length octets) |
|
||||
// +-------------------------------+
|
||||
|
||||
if ((uint)destination.Length >= 3)
|
||||
{
|
||||
destination[0] = 0;
|
||||
if (EncodeLiteralHeaderName(name, destination.Slice(1), out int nameLength) &&
|
||||
EncodeStringLiterals(values, separator, destination.Slice(1 + nameLength), out int valueLength))
|
||||
{
|
||||
bytesWritten = 1 + nameLength + valueLength;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a "Literal Header Field without Indexing - New Name", but only the name portion;
|
||||
/// a subsequent call to <see cref="EncodeStringLiteral"/> must be used to encode the associated value.
|
||||
/// </summary>
|
||||
public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
// From https://tools.ietf.org/html/rfc7541#section-6.2.2
|
||||
// ------------------------------------------------------
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 0 | 0 | 0 |
|
||||
// +---+---+-----------------------+
|
||||
// | H | Name Length (7+) |
|
||||
// +---+---------------------------+
|
||||
// | Name String (Length octets) |
|
||||
// +---+---------------------------+
|
||||
//
|
||||
// ... expected after this:
|
||||
//
|
||||
// | H | Value Length (7+) |
|
||||
// +---+---------------------------+
|
||||
// | Value String (Length octets) |
|
||||
// +-------------------------------+
|
||||
|
||||
if ((uint)destination.Length >= 2)
|
||||
{
|
||||
destination[0] = 0;
|
||||
if (EncodeLiteralHeaderName(name, destination.Slice(1), out int nameLength))
|
||||
{
|
||||
bytesWritten = 1 + nameLength;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
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.Length != 0)
|
||||
{
|
||||
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(SR.net_http_request_invalid_char_encoding);
|
||||
}
|
||||
|
||||
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.Length != 0)
|
||||
{
|
||||
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(ReadOnlySpan<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.Length != 0)
|
||||
{
|
||||
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));
|
||||
|
||||
destination[0] = 0;
|
||||
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 = EncodeLiteralHeaderFieldWithoutIndexing(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();
|
||||
}
|
||||
|
||||
/// <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 (EncodeLiteralHeaderFieldWithoutIndexing(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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,20 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
namespace System.Net.Http.HPack
|
||||
{
|
||||
internal sealed class HPackEncodingException : Exception
|
||||
{
|
||||
public HPackEncodingException()
|
||||
{
|
||||
}
|
||||
|
||||
public HPackEncodingException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public HPackEncodingException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace System.Net.Http.HPack
|
||||
{
|
||||
internal readonly struct HeaderField
|
||||
{
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.4.1
|
||||
public const int RfcOverhead = 32;
|
||||
|
||||
public HeaderField(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
Debug.Assert(name.Length > 0);
|
||||
|
||||
// TODO: We're allocating here on every new table entry.
|
||||
// That means a poorly-behaved server could cause us to allocate repeatedly.
|
||||
// We should revisit our allocation strategy here so we don't need to allocate per entry
|
||||
// and we have a cap to how much allocation can happen per dynamic table
|
||||
// (without limiting the number of table entries a server can provide within the table size limit).
|
||||
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 + RfcOverhead;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Name != null)
|
||||
{
|
||||
return Encoding.ASCII.GetString(Name) + ": " + Encoding.ASCII.GetString(Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return "<empty>";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
namespace System.Net.Http.HPack
|
||||
{
|
||||
internal class Huffman
|
||||
{
|
||||
|
|
@ -303,25 +304,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
/// Decodes a Huffman encoded string from a byte array.
|
||||
/// </summary>
|
||||
/// <param name="src">The source byte array containing the encoded data.</param>
|
||||
/// <param name="dst">The destination byte array to store the decoded data.</param>
|
||||
/// <param name="dstArray">The destination byte array to store the decoded data. This may grow if its size is insufficient.</param>
|
||||
/// <returns>The number of decoded symbols.</returns>
|
||||
public static int Decode(ReadOnlySpan<byte> src, Span<byte> dst)
|
||||
public static int Decode(ReadOnlySpan<byte> src, ref byte[] dstArray)
|
||||
{
|
||||
var i = 0;
|
||||
var j = 0;
|
||||
var lastDecodedBits = 0;
|
||||
Span<byte> dst = dstArray;
|
||||
Debug.Assert(dst != null && dst.Length > 0);
|
||||
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int lastDecodedBits = 0;
|
||||
while (i < src.Length)
|
||||
{
|
||||
// Note that if lastDecodeBits is 3 or more, then we will only get 5 bits (or less)
|
||||
// from src[i]. Thus we need to read 5 bytes here to ensure that we always have
|
||||
// at least 30 bits available for decoding.
|
||||
var next = (uint)(src[i] << 24 + lastDecodedBits);
|
||||
// TODO ISSUE 31751: Rework this as part of Huffman perf improvements
|
||||
uint next = (uint)(src[i] << 24 + lastDecodedBits);
|
||||
next |= (i + 1 < src.Length ? (uint)(src[i + 1] << 16 + lastDecodedBits) : 0);
|
||||
next |= (i + 2 < src.Length ? (uint)(src[i + 2] << 8 + lastDecodedBits) : 0);
|
||||
next |= (i + 3 < src.Length ? (uint)(src[i + 3] << lastDecodedBits) : 0);
|
||||
next |= (i + 4 < src.Length ? (uint)(src[i + 4] >> (8 - lastDecodedBits)) : 0);
|
||||
|
||||
var ones = (uint)(int.MinValue >> (8 - lastDecodedBits - 1));
|
||||
uint ones = (uint)(int.MinValue >> (8 - lastDecodedBits - 1));
|
||||
if (i == src.Length - 1 && lastDecodedBits > 0 && (next & ones) == ones)
|
||||
{
|
||||
// The remaining 7 or less bits are all 1, which is padding.
|
||||
|
|
@ -334,24 +339,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
// The longest possible symbol size is 30 bits. If we're at the last 4 bytes
|
||||
// of the input, we need to make sure we pass the correct number of valid bits
|
||||
// left, otherwise the trailing 0s in next may form a valid symbol.
|
||||
var validBits = Math.Min(30, (8 - lastDecodedBits) + (src.Length - i - 1) * 8);
|
||||
var ch = DecodeValue(next, validBits, out var decodedBits);
|
||||
int validBits = Math.Min(30, (8 - lastDecodedBits) + (src.Length - i - 1) * 8);
|
||||
int ch = DecodeValue(next, validBits, out int decodedBits);
|
||||
|
||||
if (ch == -1)
|
||||
{
|
||||
// No valid symbol could be decoded with the bits in next
|
||||
throw new HuffmanDecodingException(CoreStrings.HPackHuffmanErrorIncomplete);
|
||||
throw new HuffmanDecodingException(SR.net_http_hpack_huffman_decode_failed);
|
||||
}
|
||||
else if (ch == 256)
|
||||
{
|
||||
// A Huffman-encoded string literal containing the EOS symbol MUST be treated as a decoding error.
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.5.2
|
||||
throw new HuffmanDecodingException(CoreStrings.HPackHuffmanErrorEOS);
|
||||
throw new HuffmanDecodingException(SR.net_http_hpack_huffman_decode_failed);
|
||||
}
|
||||
|
||||
if (j == dst.Length)
|
||||
{
|
||||
throw new HuffmanDecodingException(CoreStrings.HPackHuffmanErrorDestinationTooSmall);
|
||||
Array.Resize(ref dstArray, dst.Length * 2);
|
||||
dst = dstArray;
|
||||
}
|
||||
|
||||
dst[j++] = (byte)ch;
|
||||
|
|
@ -398,11 +404,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
// symbol in the list of values associated with bit length b in the decoding table by indexing it
|
||||
// with codeMax - v.
|
||||
|
||||
var codeMax = 0;
|
||||
int codeMax = 0;
|
||||
|
||||
for (var i = 0; i < _decodingTable.Length && _decodingTable[i].codeLength <= validBits; i++)
|
||||
for (int i = 0; i < _decodingTable.Length && _decodingTable[i].codeLength <= validBits; i++)
|
||||
{
|
||||
var (codeLength, codes) = _decodingTable[i];
|
||||
(int codeLength, int[] codes) = _decodingTable[i];
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
|
|
@ -411,8 +417,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
|
||||
codeMax += codes.Length;
|
||||
|
||||
var mask = int.MinValue >> (codeLength - 1);
|
||||
var masked = (data & mask) >> (32 - codeLength);
|
||||
int mask = int.MinValue >> (codeLength - 1);
|
||||
long masked = (data & mask) >> (32 - codeLength);
|
||||
|
||||
if (masked < codeMax)
|
||||
{
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace System.Net.Http.HPack
|
||||
{
|
||||
// TODO: Should this be public?
|
||||
[Serializable]
|
||||
internal class HuffmanDecodingException : Exception, ISerializable
|
||||
{
|
||||
public HuffmanDecodingException()
|
||||
{
|
||||
}
|
||||
|
||||
public HuffmanDecodingException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
protected HuffmanDecodingException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
|
||||
void ISerializable.GetObjectData(SerializationInfo serializationInfo, StreamingContext streamingContext)
|
||||
{
|
||||
base.GetObjectData(serializationInfo, streamingContext);
|
||||
}
|
||||
|
||||
public override void GetObjectData(SerializationInfo serializationInfo, StreamingContext streamingContext)
|
||||
{
|
||||
base.GetObjectData(serializationInfo, streamingContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
|
||||
namespace System.Net.Http.HPack
|
||||
{
|
||||
internal class IntegerDecoder
|
||||
{
|
||||
private int _i;
|
||||
private int _m;
|
||||
|
||||
/// <summary>
|
||||
/// Decodes the first byte of the integer.
|
||||
/// </summary>
|
||||
/// <param name="b">
|
||||
/// The first byte of the variable-length encoded integer.
|
||||
/// </param>
|
||||
/// <param name="prefixLength">
|
||||
/// The number of lower bits in this prefix byte that the
|
||||
/// integer has been encoded into. Must be between 1 and 8.
|
||||
/// Upper bits must be zero.
|
||||
/// </param>
|
||||
/// <param name="result">
|
||||
/// If decoded successfully, contains the decoded integer.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// If the integer has been fully decoded, true.
|
||||
/// Otherwise, false -- <see cref="TryDecode(byte, out int)"/> must be called on subsequent bytes.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// The term "prefix" can be confusing. From the HPACK spec:
|
||||
/// An integer is represented in two parts: a prefix that fills the current octet and an
|
||||
/// optional list of octets that are used if the integer value does not fit within the prefix.
|
||||
/// </remarks>
|
||||
public bool BeginTryDecode(byte b, int prefixLength, out int result)
|
||||
{
|
||||
Debug.Assert(prefixLength >= 1 && prefixLength <= 8);
|
||||
Debug.Assert((b & ~((1 << prefixLength) - 1)) == 0, "bits other than prefix data must be set to 0.");
|
||||
|
||||
if (b < ((1 << prefixLength) - 1))
|
||||
{
|
||||
result = b;
|
||||
return true;
|
||||
}
|
||||
|
||||
_i = b;
|
||||
_m = 0;
|
||||
result = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes subsequent bytes of an integer.
|
||||
/// </summary>
|
||||
/// <param name="b">The next byte.</param>
|
||||
/// <param name="result">
|
||||
/// If decoded successfully, contains the decoded integer.
|
||||
/// </param>
|
||||
/// <returns>If the integer has been fully decoded, true. Otherwise, false -- <see cref="TryDecode(byte, out int)"/> must be called on subsequent bytes.</returns>
|
||||
public bool TryDecode(byte b, out int result)
|
||||
{
|
||||
// Check if shifting b by _m would result in > 31 bits.
|
||||
// No masking is required: if the 8th bit is set, it indicates there is a
|
||||
// bit set in a future byte, so it is fine to check that here as if it were
|
||||
// bit 0 on the next byte.
|
||||
// This is a simplified form of:
|
||||
// int additionalBitsRequired = 32 - BitOperations.LeadingZeroCount((uint)b);
|
||||
// if (_m + additionalBitsRequired > 31)
|
||||
if (BitOperations.LeadingZeroCount((uint)b) <= _m)
|
||||
{
|
||||
throw new HPackDecodingException(SR.net_http_hpack_bad_integer);
|
||||
}
|
||||
|
||||
_i = _i + ((b & 0x7f) << _m);
|
||||
|
||||
// If the addition overflowed, the result will be negative.
|
||||
if (_i < 0)
|
||||
{
|
||||
throw new HPackDecodingException(SR.net_http_hpack_bad_integer);
|
||||
}
|
||||
|
||||
_m = _m + 7;
|
||||
|
||||
if ((b & 128) == 0)
|
||||
{
|
||||
if (b == 0 && _m / 7 > 1)
|
||||
{
|
||||
// Do not accept overlong encodings.
|
||||
throw new HPackDecodingException(SR.net_http_hpack_bad_integer);
|
||||
}
|
||||
|
||||
result = _i;
|
||||
return true;
|
||||
}
|
||||
|
||||
result = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace System.Net.Http.HPack
|
||||
{
|
||||
internal static class IntegerEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// Encodes an integer into one or more bytes.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to encode. Must not be negative.</param>
|
||||
/// <param name="numBits">The length of the prefix, in bits, to encode <paramref name="value"/> within. Must be between 1 and 8.</param>
|
||||
/// <param name="destination">The destination span to encode <paramref name="value"/> to.</param>
|
||||
/// <param name="bytesWritten">The number of bytes used to encode <paramref name="value"/>.</param>
|
||||
/// <returns>If <paramref name="destination"/> had enough storage to encode <paramref name="value"/>, true. Otherwise, false.</returns>
|
||||
public static bool Encode(int value, int numBits, Span<byte> destination, out int bytesWritten)
|
||||
{
|
||||
Debug.Assert(value >= 0);
|
||||
Debug.Assert(numBits >= 1 && numBits <= 8);
|
||||
|
||||
if (destination.Length == 0)
|
||||
{
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
destination[0] &= MaskHigh(8 - numBits);
|
||||
|
||||
if (value < (1 << numBits) - 1)
|
||||
{
|
||||
destination[0] |= (byte)value;
|
||||
|
||||
bytesWritten = 1;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
destination[0] |= (byte)((1 << numBits) - 1);
|
||||
|
||||
if (1 == destination.Length)
|
||||
{
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = value - ((1 << numBits) - 1);
|
||||
int i = 1;
|
||||
|
||||
while (value >= 128)
|
||||
{
|
||||
destination[i++] = (byte)(value % 128 + 128);
|
||||
|
||||
if (i >= destination.Length)
|
||||
{
|
||||
bytesWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = value / 128;
|
||||
}
|
||||
destination[i++] = (byte)value;
|
||||
|
||||
bytesWritten = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte MaskHigh(int n) => (byte)(sbyte.MinValue >> (n - 1));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace System.Net.Http.HPack
|
||||
{
|
||||
internal static class StaticTable
|
||||
{
|
||||
// Index of status code into s_staticDecoderTable
|
||||
private static readonly Dictionary<int, int> s_statusIndex = new Dictionary<int, int>
|
||||
{
|
||||
[200] = 8,
|
||||
[204] = 9,
|
||||
[206] = 10,
|
||||
[304] = 11,
|
||||
[400] = 12,
|
||||
[404] = 13,
|
||||
[500] = 14,
|
||||
};
|
||||
|
||||
public static int Count => s_staticDecoderTable.Length;
|
||||
|
||||
public static HeaderField Get(int index) => s_staticDecoderTable[index];
|
||||
|
||||
public static IReadOnlyDictionary<int, int> StatusIndex => s_statusIndex;
|
||||
|
||||
private static readonly HeaderField[] s_staticDecoderTable = new HeaderField[]
|
||||
{
|
||||
CreateHeaderField(":authority", ""),
|
||||
CreateHeaderField(":method", "GET"),
|
||||
CreateHeaderField(":method", "POST"),
|
||||
CreateHeaderField(":path", "/"),
|
||||
CreateHeaderField(":path", "/index.html"),
|
||||
CreateHeaderField(":scheme", "http"),
|
||||
CreateHeaderField(":scheme", "https"),
|
||||
CreateHeaderField(":status", "200"),
|
||||
CreateHeaderField(":status", "204"),
|
||||
CreateHeaderField(":status", "206"),
|
||||
CreateHeaderField(":status", "304"),
|
||||
CreateHeaderField(":status", "400"),
|
||||
CreateHeaderField(":status", "404"),
|
||||
CreateHeaderField(":status", "500"),
|
||||
CreateHeaderField("accept-charset", ""),
|
||||
CreateHeaderField("accept-encoding", "gzip, deflate"),
|
||||
CreateHeaderField("accept-language", ""),
|
||||
CreateHeaderField("accept-ranges", ""),
|
||||
CreateHeaderField("accept", ""),
|
||||
CreateHeaderField("access-control-allow-origin", ""),
|
||||
CreateHeaderField("age", ""),
|
||||
CreateHeaderField("allow", ""),
|
||||
CreateHeaderField("authorization", ""),
|
||||
CreateHeaderField("cache-control", ""),
|
||||
CreateHeaderField("content-disposition", ""),
|
||||
CreateHeaderField("content-encoding", ""),
|
||||
CreateHeaderField("content-language", ""),
|
||||
CreateHeaderField("content-length", ""),
|
||||
CreateHeaderField("content-location", ""),
|
||||
CreateHeaderField("content-range", ""),
|
||||
CreateHeaderField("content-type", ""),
|
||||
CreateHeaderField("cookie", ""),
|
||||
CreateHeaderField("date", ""),
|
||||
CreateHeaderField("etag", ""),
|
||||
CreateHeaderField("expect", ""),
|
||||
CreateHeaderField("expires", ""),
|
||||
CreateHeaderField("from", ""),
|
||||
CreateHeaderField("host", ""),
|
||||
CreateHeaderField("if-match", ""),
|
||||
CreateHeaderField("if-modified-since", ""),
|
||||
CreateHeaderField("if-none-match", ""),
|
||||
CreateHeaderField("if-range", ""),
|
||||
CreateHeaderField("if-unmodified-since", ""),
|
||||
CreateHeaderField("last-modified", ""),
|
||||
CreateHeaderField("link", ""),
|
||||
CreateHeaderField("location", ""),
|
||||
CreateHeaderField("max-forwards", ""),
|
||||
CreateHeaderField("proxy-authenticate", ""),
|
||||
CreateHeaderField("proxy-authorization", ""),
|
||||
CreateHeaderField("range", ""),
|
||||
CreateHeaderField("referer", ""),
|
||||
CreateHeaderField("refresh", ""),
|
||||
CreateHeaderField("retry-after", ""),
|
||||
CreateHeaderField("server", ""),
|
||||
CreateHeaderField("set-cookie", ""),
|
||||
CreateHeaderField("strict-transport-security", ""),
|
||||
CreateHeaderField("transfer-encoding", ""),
|
||||
CreateHeaderField("user-agent", ""),
|
||||
CreateHeaderField("vary", ""),
|
||||
CreateHeaderField("via", ""),
|
||||
CreateHeaderField("www-authenticate", "")
|
||||
};
|
||||
|
||||
// TODO: The HeaderField constructor will allocate and copy again. We should avoid this.
|
||||
// Tackle as part of header table allocation strategy in general (see note in HeaderField constructor).
|
||||
|
||||
private static HeaderField CreateHeaderField(string name, string value) =>
|
||||
new HeaderField(
|
||||
Encoding.ASCII.GetBytes(name),
|
||||
value.Length != 0 ? Encoding.ASCII.GetBytes(value) : Array.Empty<byte>());
|
||||
|
||||
// Values for encoding.
|
||||
// Unused values are omitted.
|
||||
public const int Authority = 1;
|
||||
public const int MethodGet = 2;
|
||||
public const int MethodPost = 3;
|
||||
public const int PathSlash = 4;
|
||||
public const int SchemeHttp = 6;
|
||||
public const int SchemeHttps = 7;
|
||||
public const int AcceptCharset = 15;
|
||||
public const int AcceptEncoding = 16;
|
||||
public const int AcceptLanguage = 17;
|
||||
public const int AcceptRanges = 18;
|
||||
public const int Accept = 19;
|
||||
public const int AccessControlAllowOrigin = 20;
|
||||
public const int Age = 21;
|
||||
public const int Allow = 22;
|
||||
public const int Authorization = 23;
|
||||
public const int CacheControl = 24;
|
||||
public const int ContentDisposition = 25;
|
||||
public const int ContentEncoding = 26;
|
||||
public const int ContentLanguage = 27;
|
||||
public const int ContentLength = 28;
|
||||
public const int ContentLocation = 29;
|
||||
public const int ContentRange = 30;
|
||||
public const int ContentType = 31;
|
||||
public const int Cookie = 32;
|
||||
public const int Date = 33;
|
||||
public const int ETag = 34;
|
||||
public const int Expect = 35;
|
||||
public const int Expires = 36;
|
||||
public const int From = 37;
|
||||
public const int Host = 38;
|
||||
public const int IfMatch = 39;
|
||||
public const int IfModifiedSince = 40;
|
||||
public const int IfNoneMatch = 41;
|
||||
public const int IfRange = 42;
|
||||
public const int IfUnmodifiedSince = 43;
|
||||
public const int LastModified = 44;
|
||||
public const int Link = 45;
|
||||
public const int Location = 46;
|
||||
public const int MaxForwards = 47;
|
||||
public const int ProxyAuthenticate = 48;
|
||||
public const int ProxyAuthorization = 49;
|
||||
public const int Range = 50;
|
||||
public const int Referer = 51;
|
||||
public const int Refresh = 52;
|
||||
public const int RetryAfter = 53;
|
||||
public const int Server = 54;
|
||||
public const int SetCookie = 55;
|
||||
public const int StrictTransportSecurity = 56;
|
||||
public const int TransferEncoding = 57;
|
||||
public const int UserAgent = 58;
|
||||
public const int Vary = 59;
|
||||
public const int Via = 60;
|
||||
public const int WwwAuthenticate = 61;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace System.Net.Http.HPack
|
||||
{
|
||||
internal static class StatusCodes
|
||||
{
|
||||
private static ReadOnlySpan<byte> BytesStatus100 => new byte[] { (byte)'1', (byte)'0', (byte)'0' };
|
||||
private static ReadOnlySpan<byte> BytesStatus101 => new byte[] { (byte)'1', (byte)'0', (byte)'1' };
|
||||
private static ReadOnlySpan<byte> BytesStatus102 => new byte[] { (byte)'1', (byte)'0', (byte)'2' };
|
||||
|
||||
private static ReadOnlySpan<byte> BytesStatus200 => new byte[] { (byte)'2', (byte)'0', (byte)'0' };
|
||||
private static ReadOnlySpan<byte> BytesStatus201 => new byte[] { (byte)'2', (byte)'0', (byte)'1' };
|
||||
private static ReadOnlySpan<byte> BytesStatus202 => new byte[] { (byte)'2', (byte)'0', (byte)'2' };
|
||||
private static ReadOnlySpan<byte> BytesStatus203 => new byte[] { (byte)'2', (byte)'0', (byte)'3' };
|
||||
private static ReadOnlySpan<byte> BytesStatus204 => new byte[] { (byte)'2', (byte)'0', (byte)'4' };
|
||||
private static ReadOnlySpan<byte> BytesStatus205 => new byte[] { (byte)'2', (byte)'0', (byte)'5' };
|
||||
private static ReadOnlySpan<byte> BytesStatus206 => new byte[] { (byte)'2', (byte)'0', (byte)'6' };
|
||||
private static ReadOnlySpan<byte> BytesStatus207 => new byte[] { (byte)'2', (byte)'0', (byte)'7' };
|
||||
private static ReadOnlySpan<byte> BytesStatus208 => new byte[] { (byte)'2', (byte)'0', (byte)'8' };
|
||||
private static ReadOnlySpan<byte> BytesStatus226 => new byte[] { (byte)'2', (byte)'2', (byte)'6' };
|
||||
|
||||
private static ReadOnlySpan<byte> BytesStatus300 => new byte[] { (byte)'3', (byte)'0', (byte)'0' };
|
||||
private static ReadOnlySpan<byte> BytesStatus301 => new byte[] { (byte)'3', (byte)'0', (byte)'1' };
|
||||
private static ReadOnlySpan<byte> BytesStatus302 => new byte[] { (byte)'3', (byte)'0', (byte)'2' };
|
||||
private static ReadOnlySpan<byte> BytesStatus303 => new byte[] { (byte)'3', (byte)'0', (byte)'3' };
|
||||
private static ReadOnlySpan<byte> BytesStatus304 => new byte[] { (byte)'3', (byte)'0', (byte)'4' };
|
||||
private static ReadOnlySpan<byte> BytesStatus305 => new byte[] { (byte)'3', (byte)'0', (byte)'5' };
|
||||
private static ReadOnlySpan<byte> BytesStatus306 => new byte[] { (byte)'3', (byte)'0', (byte)'6' };
|
||||
private static ReadOnlySpan<byte> BytesStatus307 => new byte[] { (byte)'3', (byte)'0', (byte)'7' };
|
||||
private static ReadOnlySpan<byte> BytesStatus308 => new byte[] { (byte)'3', (byte)'0', (byte)'8' };
|
||||
|
||||
private static ReadOnlySpan<byte> BytesStatus400 => new byte[] { (byte)'4', (byte)'0', (byte)'0' };
|
||||
private static ReadOnlySpan<byte> BytesStatus401 => new byte[] { (byte)'4', (byte)'0', (byte)'1' };
|
||||
private static ReadOnlySpan<byte> BytesStatus402 => new byte[] { (byte)'4', (byte)'0', (byte)'2' };
|
||||
private static ReadOnlySpan<byte> BytesStatus403 => new byte[] { (byte)'4', (byte)'0', (byte)'3' };
|
||||
private static ReadOnlySpan<byte> BytesStatus404 => new byte[] { (byte)'4', (byte)'0', (byte)'4' };
|
||||
private static ReadOnlySpan<byte> BytesStatus405 => new byte[] { (byte)'4', (byte)'0', (byte)'5' };
|
||||
private static ReadOnlySpan<byte> BytesStatus406 => new byte[] { (byte)'4', (byte)'0', (byte)'6' };
|
||||
private static ReadOnlySpan<byte> BytesStatus407 => new byte[] { (byte)'4', (byte)'0', (byte)'7' };
|
||||
private static ReadOnlySpan<byte> BytesStatus408 => new byte[] { (byte)'4', (byte)'0', (byte)'8' };
|
||||
private static ReadOnlySpan<byte> BytesStatus409 => new byte[] { (byte)'4', (byte)'0', (byte)'9' };
|
||||
private static ReadOnlySpan<byte> BytesStatus410 => new byte[] { (byte)'4', (byte)'1', (byte)'0' };
|
||||
private static ReadOnlySpan<byte> BytesStatus411 => new byte[] { (byte)'4', (byte)'1', (byte)'1' };
|
||||
private static ReadOnlySpan<byte> BytesStatus412 => new byte[] { (byte)'4', (byte)'1', (byte)'2' };
|
||||
private static ReadOnlySpan<byte> BytesStatus413 => new byte[] { (byte)'4', (byte)'1', (byte)'3' };
|
||||
private static ReadOnlySpan<byte> BytesStatus414 => new byte[] { (byte)'4', (byte)'1', (byte)'4' };
|
||||
private static ReadOnlySpan<byte> BytesStatus415 => new byte[] { (byte)'4', (byte)'1', (byte)'5' };
|
||||
private static ReadOnlySpan<byte> BytesStatus416 => new byte[] { (byte)'4', (byte)'1', (byte)'6' };
|
||||
private static ReadOnlySpan<byte> BytesStatus417 => new byte[] { (byte)'4', (byte)'1', (byte)'7' };
|
||||
private static ReadOnlySpan<byte> BytesStatus418 => new byte[] { (byte)'4', (byte)'1', (byte)'8' };
|
||||
private static ReadOnlySpan<byte> BytesStatus419 => new byte[] { (byte)'4', (byte)'1', (byte)'9' };
|
||||
private static ReadOnlySpan<byte> BytesStatus421 => new byte[] { (byte)'4', (byte)'2', (byte)'1' };
|
||||
private static ReadOnlySpan<byte> BytesStatus422 => new byte[] { (byte)'4', (byte)'2', (byte)'2' };
|
||||
private static ReadOnlySpan<byte> BytesStatus423 => new byte[] { (byte)'4', (byte)'2', (byte)'3' };
|
||||
private static ReadOnlySpan<byte> BytesStatus424 => new byte[] { (byte)'4', (byte)'2', (byte)'4' };
|
||||
private static ReadOnlySpan<byte> BytesStatus426 => new byte[] { (byte)'4', (byte)'2', (byte)'6' };
|
||||
private static ReadOnlySpan<byte> BytesStatus428 => new byte[] { (byte)'4', (byte)'2', (byte)'8' };
|
||||
private static ReadOnlySpan<byte> BytesStatus429 => new byte[] { (byte)'4', (byte)'2', (byte)'9' };
|
||||
private static ReadOnlySpan<byte> BytesStatus431 => new byte[] { (byte)'4', (byte)'3', (byte)'1' };
|
||||
private static ReadOnlySpan<byte> BytesStatus451 => new byte[] { (byte)'4', (byte)'5', (byte)'1' };
|
||||
|
||||
private static ReadOnlySpan<byte> BytesStatus500 => new byte[] { (byte)'5', (byte)'0', (byte)'0' };
|
||||
private static ReadOnlySpan<byte> BytesStatus501 => new byte[] { (byte)'5', (byte)'0', (byte)'1' };
|
||||
private static ReadOnlySpan<byte> BytesStatus502 => new byte[] { (byte)'5', (byte)'0', (byte)'2' };
|
||||
private static ReadOnlySpan<byte> BytesStatus503 => new byte[] { (byte)'5', (byte)'0', (byte)'3' };
|
||||
private static ReadOnlySpan<byte> BytesStatus504 => new byte[] { (byte)'5', (byte)'0', (byte)'4' };
|
||||
private static ReadOnlySpan<byte> BytesStatus505 => new byte[] { (byte)'5', (byte)'0', (byte)'5' };
|
||||
private static ReadOnlySpan<byte> BytesStatus506 => new byte[] { (byte)'5', (byte)'0', (byte)'6' };
|
||||
private static ReadOnlySpan<byte> BytesStatus507 => new byte[] { (byte)'5', (byte)'0', (byte)'7' };
|
||||
private static ReadOnlySpan<byte> BytesStatus508 => new byte[] { (byte)'5', (byte)'0', (byte)'8' };
|
||||
private static ReadOnlySpan<byte> BytesStatus510 => new byte[] { (byte)'5', (byte)'1', (byte)'0' };
|
||||
private static ReadOnlySpan<byte> BytesStatus511 => new byte[] { (byte)'5', (byte)'1', (byte)'1' };
|
||||
|
||||
public static ReadOnlySpan<byte> ToStatusBytes(int statusCode)
|
||||
{
|
||||
switch (statusCode)
|
||||
{
|
||||
case (int)HttpStatusCode.Continue:
|
||||
return BytesStatus100;
|
||||
case (int)HttpStatusCode.SwitchingProtocols:
|
||||
return BytesStatus101;
|
||||
case (int)HttpStatusCode.Processing:
|
||||
return BytesStatus102;
|
||||
|
||||
case (int)HttpStatusCode.OK:
|
||||
return BytesStatus200;
|
||||
case (int)HttpStatusCode.Created:
|
||||
return BytesStatus201;
|
||||
case (int)HttpStatusCode.Accepted:
|
||||
return BytesStatus202;
|
||||
case (int)HttpStatusCode.NonAuthoritativeInformation:
|
||||
return BytesStatus203;
|
||||
case (int)HttpStatusCode.NoContent:
|
||||
return BytesStatus204;
|
||||
case (int)HttpStatusCode.ResetContent:
|
||||
return BytesStatus205;
|
||||
case (int)HttpStatusCode.PartialContent:
|
||||
return BytesStatus206;
|
||||
case (int)HttpStatusCode.MultiStatus:
|
||||
return BytesStatus207;
|
||||
case (int)HttpStatusCode.AlreadyReported:
|
||||
return BytesStatus208;
|
||||
case (int)HttpStatusCode.IMUsed:
|
||||
return BytesStatus226;
|
||||
|
||||
case (int)HttpStatusCode.MultipleChoices:
|
||||
return BytesStatus300;
|
||||
case (int)HttpStatusCode.MovedPermanently:
|
||||
return BytesStatus301;
|
||||
case (int)HttpStatusCode.Found:
|
||||
return BytesStatus302;
|
||||
case (int)HttpStatusCode.SeeOther:
|
||||
return BytesStatus303;
|
||||
case (int)HttpStatusCode.NotModified:
|
||||
return BytesStatus304;
|
||||
case (int)HttpStatusCode.UseProxy:
|
||||
return BytesStatus305;
|
||||
case (int)HttpStatusCode.Unused:
|
||||
return BytesStatus306;
|
||||
case (int)HttpStatusCode.TemporaryRedirect:
|
||||
return BytesStatus307;
|
||||
case (int)HttpStatusCode.PermanentRedirect:
|
||||
return BytesStatus308;
|
||||
|
||||
case (int)HttpStatusCode.BadRequest:
|
||||
return BytesStatus400;
|
||||
case (int)HttpStatusCode.Unauthorized:
|
||||
return BytesStatus401;
|
||||
case (int)HttpStatusCode.PaymentRequired:
|
||||
return BytesStatus402;
|
||||
case (int)HttpStatusCode.Forbidden:
|
||||
return BytesStatus403;
|
||||
case (int)HttpStatusCode.NotFound:
|
||||
return BytesStatus404;
|
||||
case (int)HttpStatusCode.MethodNotAllowed:
|
||||
return BytesStatus405;
|
||||
case (int)HttpStatusCode.NotAcceptable:
|
||||
return BytesStatus406;
|
||||
case (int)HttpStatusCode.ProxyAuthenticationRequired:
|
||||
return BytesStatus407;
|
||||
case (int)HttpStatusCode.RequestTimeout:
|
||||
return BytesStatus408;
|
||||
case (int)HttpStatusCode.Conflict:
|
||||
return BytesStatus409;
|
||||
case (int)HttpStatusCode.Gone:
|
||||
return BytesStatus410;
|
||||
case (int)HttpStatusCode.LengthRequired:
|
||||
return BytesStatus411;
|
||||
case (int)HttpStatusCode.PreconditionFailed:
|
||||
return BytesStatus412;
|
||||
case (int)HttpStatusCode.RequestEntityTooLarge:
|
||||
return BytesStatus413;
|
||||
case (int)HttpStatusCode.RequestUriTooLong:
|
||||
return BytesStatus414;
|
||||
case (int)HttpStatusCode.UnsupportedMediaType:
|
||||
return BytesStatus415;
|
||||
case (int)HttpStatusCode.RequestedRangeNotSatisfiable:
|
||||
return BytesStatus416;
|
||||
case (int)HttpStatusCode.ExpectationFailed:
|
||||
return BytesStatus417;
|
||||
case (int)418:
|
||||
return BytesStatus418;
|
||||
case (int)419:
|
||||
return BytesStatus419;
|
||||
case (int)HttpStatusCode.MisdirectedRequest:
|
||||
return BytesStatus421;
|
||||
case (int)HttpStatusCode.UnprocessableEntity:
|
||||
return BytesStatus422;
|
||||
case (int)HttpStatusCode.Locked:
|
||||
return BytesStatus423;
|
||||
case (int)HttpStatusCode.FailedDependency:
|
||||
return BytesStatus424;
|
||||
case (int)HttpStatusCode.UpgradeRequired:
|
||||
return BytesStatus426;
|
||||
case (int)HttpStatusCode.PreconditionRequired:
|
||||
return BytesStatus428;
|
||||
case (int)HttpStatusCode.TooManyRequests:
|
||||
return BytesStatus429;
|
||||
case (int)HttpStatusCode.RequestHeaderFieldsTooLarge:
|
||||
return BytesStatus431;
|
||||
case (int)HttpStatusCode.UnavailableForLegalReasons:
|
||||
return BytesStatus451;
|
||||
|
||||
case (int)HttpStatusCode.InternalServerError:
|
||||
return BytesStatus500;
|
||||
case (int)HttpStatusCode.NotImplemented:
|
||||
return BytesStatus501;
|
||||
case (int)HttpStatusCode.BadGateway:
|
||||
return BytesStatus502;
|
||||
case (int)HttpStatusCode.ServiceUnavailable:
|
||||
return BytesStatus503;
|
||||
case (int)HttpStatusCode.GatewayTimeout:
|
||||
return BytesStatus504;
|
||||
case (int)HttpStatusCode.HttpVersionNotSupported:
|
||||
return BytesStatus505;
|
||||
case (int)HttpStatusCode.VariantAlsoNegotiates:
|
||||
return BytesStatus506;
|
||||
case (int)HttpStatusCode.InsufficientStorage:
|
||||
return BytesStatus507;
|
||||
case (int)HttpStatusCode.LoopDetected:
|
||||
return BytesStatus508;
|
||||
case (int)HttpStatusCode.NotExtended:
|
||||
return BytesStatus510;
|
||||
case (int)HttpStatusCode.NetworkAuthenticationRequired:
|
||||
return BytesStatus511;
|
||||
|
||||
default:
|
||||
return Encoding.ASCII.GetBytes(statusCode.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace System.Net.Http
|
||||
{
|
||||
internal interface IHttpHeadersHandler
|
||||
{
|
||||
void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value);
|
||||
void OnHeadersComplete(bool endStream);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
The code in this directory is shared between CoreFx and AspNetCore. This contains HTTP/2 protocol infrastructure such as an HPACK implementation. Any changes to this dir need to be checked into both repositories.
|
||||
|
||||
Corefx code paths:
|
||||
- corefx\src\Common\src\System\Net\Http\Http2
|
||||
- corefx\src\Common\tests\Tests\System\Net\Http2
|
||||
AspNetCore code paths:
|
||||
- AspNetCore\src\Shared\Http2
|
||||
- AspNetCore\src\Shared\test\Shared.Tests\Http2
|
||||
|
||||
## Copying code
|
||||
To copy code from CoreFx to AspNetCore set ASPNETCORE_REPO to the AspNetCore repo root and then run CopyToAspNetCore.cmd.
|
||||
To copy code from AspNetCore to CoreFx set COREFX_REPO to the CoreFx repo root and then run CopyToCoreFx.cmd.
|
||||
|
||||
## Building CoreFx code:
|
||||
- https://github.com/dotnet/corefx/blob/master/Documentation/building/windows-instructions.md
|
||||
- https://github.com/dotnet/corefx/blob/master/Documentation/project-docs/developer-guide.md
|
||||
- Run build.cmd from the root once: `PS D:\github\corefx> .\build.cmd`
|
||||
- Build the individual projects:
|
||||
- `PS D:\github\corefx\src\Common\tests> dotnet msbuild /t:rebuild`
|
||||
- `PS D:\github\corefx\src\System.Net.Http\src> dotnet msbuild /t:rebuild`
|
||||
|
||||
### Running CoreFx tests:
|
||||
- `PS D:\github\corefx\src\Common\tests> dotnet msbuild /t:rebuildandtest`
|
||||
- `PS D:\github\corefx\src\System.Net.Http\tests\UnitTests> dotnet msbuild /t:rebuildandtest`
|
||||
|
||||
## Building AspNetCore code:
|
||||
- https://github.com/aspnet/AspNetCore/blob/master/docs/BuildFromSource.md
|
||||
- Run restore in the root once: `PS D:\github\AspNetCore> .\restore.cmd`
|
||||
- Activate to use the repo local runtime: `PS D:\github\AspNetCore> . .\activate.ps1`
|
||||
- Build the individual projects:
|
||||
- `(AspNetCore) PS D:\github\AspNetCore\src\Shared\test\Shared.Tests> dotnet msbuild`
|
||||
- `(AspNetCore) PS D:\github\AspNetCore\src\servers\Kestrel\core\src> dotnet msbuild`
|
||||
|
||||
### Running AspNetCore tests:
|
||||
- `(AspNetCore) PS D:\github\AspNetCore\src\Shared\test\Shared.Tests> dotnet test`
|
||||
- `(AspNetCore) PS D:\github\AspNetCore\src\servers\Kestrel\core\test> dotnet test`
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace System.Net.Http
|
||||
{
|
||||
internal static partial class SR
|
||||
{
|
||||
// The resource generator used in AspNetCore does not create this method. This file fills in that functional gap
|
||||
// so we don't have to modify the shared source.
|
||||
internal static string Format(string resourceFormat, params object[] args)
|
||||
{
|
||||
if (args != null)
|
||||
{
|
||||
return string.Format(resourceFormat, args);
|
||||
}
|
||||
|
||||
return resourceFormat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="net_http_headers_exceeded_length" xml:space="preserve">
|
||||
<value>The HTTP headers length exceeded the set limit of {0} bytes.</value>
|
||||
</data>
|
||||
<data name="net_http_headers_invalid_header_name" xml:space="preserve">
|
||||
<value>The header name format is invalid.</value>
|
||||
</data>
|
||||
<data name="net_http_hpack_bad_integer" xml:space="preserve">
|
||||
<value>HPACK integer exceeds limits or has an overlong encoding.</value>
|
||||
</data>
|
||||
<data name="net_http_hpack_encode_failure" xml:space="preserve">
|
||||
<value>Failed to HPACK encode the headers.</value>
|
||||
</data>
|
||||
<data name="net_http_hpack_huffman_decode_failed" xml:space="preserve">
|
||||
<value>Huffman-coded literal string failed to decode.</value>
|
||||
</data>
|
||||
<data name="net_http_hpack_incomplete_header_block" xml:space="preserve">
|
||||
<value>Incomplete header block received.</value>
|
||||
</data>
|
||||
<data name="net_http_hpack_invalid_index" xml:space="preserve">
|
||||
<value>Invalid header index: {0} is outside of static table and no dynamic table entry found.</value>
|
||||
</data>
|
||||
<data name="net_http_hpack_large_table_size_update" xml:space="preserve">
|
||||
<value>A dynamic table size update of {0} octets is greater than the configured maximum size of {1} octets.</value>
|
||||
</data>
|
||||
<data name="net_http_hpack_late_dynamic_table_size_update" xml:space="preserve">
|
||||
<value>Dynamic table size update received after beginning of header block.</value>
|
||||
</data>
|
||||
<data name="net_http_hpack_unexpected_end" xml:space="preserve">
|
||||
<value>End of headers reached with incomplete token.</value>
|
||||
</data>
|
||||
<data name="net_http_invalid_header_name" xml:space="preserve">
|
||||
<value>Received an invalid header name: '{0}'.</value>
|
||||
</data>
|
||||
<data name="net_http_request_invalid_char_encoding" xml:space="preserve">
|
||||
<value>Request headers must contain only ASCII characters.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 16
|
||||
VisualStudioVersion = 16.0.0.0
|
||||
MinimumVisualStudioVersion = 16.0.0.0
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Shared.Tests", "test\Shared.Tests\Microsoft.AspNetCore.Shared.Tests.csproj", "{06CD38EF-7733-4284-B3E4-825B6B63E1DD}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{06CD38EF-7733-4284-B3E4-825B6B63E1DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{06CD38EF-7733-4284-B3E4-825B6B63E1DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{06CD38EF-7733-4284-B3E4-825B6B63E1DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{06CD38EF-7733-4284-B3E4-825B6B63E1DD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {9B7E5B1E-6E6D-4185-9088-2C7C779C6AB2}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.Http.HPack;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace System.Net.Http.Unit.Tests.HPack
|
||||
{
|
||||
public class DynamicTableTest
|
||||
{
|
||||
private readonly HeaderField _header1 = new HeaderField(Encoding.ASCII.GetBytes("header-1"), Encoding.ASCII.GetBytes("value1"));
|
||||
private readonly HeaderField _header2 = new HeaderField(Encoding.ASCII.GetBytes("header-02"), Encoding.ASCII.GetBytes("value_2"));
|
||||
|
||||
[Fact]
|
||||
public void DynamicTable_IsInitiallyEmpty()
|
||||
{
|
||||
DynamicTable dynamicTable = new DynamicTable(4096);
|
||||
Assert.Equal(0, dynamicTable.Count);
|
||||
Assert.Equal(0, dynamicTable.Size);
|
||||
Assert.Equal(4096, dynamicTable.MaxSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DynamicTable_Count_IsNumberOfEntriesInDynamicTable()
|
||||
{
|
||||
DynamicTable dynamicTable = new DynamicTable(4096);
|
||||
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
Assert.Equal(1, dynamicTable.Count);
|
||||
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
Assert.Equal(2, dynamicTable.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DynamicTable_Size_IsCurrentDynamicTableSize()
|
||||
{
|
||||
DynamicTable dynamicTable = new DynamicTable(4096);
|
||||
Assert.Equal(0, dynamicTable.Size);
|
||||
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
Assert.Equal(_header1.Length, dynamicTable.Size);
|
||||
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
Assert.Equal(_header1.Length + _header2.Length, dynamicTable.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DynamicTable_FirstEntry_IsMostRecentEntry()
|
||||
{
|
||||
DynamicTable dynamicTable = new DynamicTable(4096);
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
|
||||
VerifyTableEntries(dynamicTable, _header2, _header1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoundsCheck_ThrowsIndexOutOfRangeException()
|
||||
{
|
||||
DynamicTable dynamicTable = new DynamicTable(4096);
|
||||
Assert.Throws<IndexOutOfRangeException>(() => dynamicTable[0]);
|
||||
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
Assert.Throws<IndexOutOfRangeException>(() => dynamicTable[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DynamicTable_InsertEntryLargerThanMaxSize_NoOp()
|
||||
{
|
||||
DynamicTable dynamicTable = new DynamicTable(_header1.Length - 1);
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
|
||||
Assert.Equal(0, dynamicTable.Count);
|
||||
Assert.Equal(0, dynamicTable.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DynamicTable_InsertEntryLargerThanRemainingSpace_NoOp()
|
||||
{
|
||||
DynamicTable dynamicTable = new DynamicTable(_header1.Length);
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
|
||||
VerifyTableEntries(dynamicTable, _header1);
|
||||
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
|
||||
Assert.Equal(0, dynamicTable.Count);
|
||||
Assert.Equal(0, dynamicTable.Size);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
public void DynamicTable_WrapsRingBuffer_Success(int targetInsertIndex)
|
||||
{
|
||||
FieldInfo insertIndexField = typeof(DynamicTable).GetField("_insertIndex", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
DynamicTable table = new DynamicTable(maxSize: 256);
|
||||
Stack<byte[]> insertedHeaders = new Stack<byte[]>();
|
||||
|
||||
// Insert into dynamic table until its insert index into its ring buffer loops back to 0.
|
||||
do
|
||||
{
|
||||
InsertOne();
|
||||
}
|
||||
while ((int)insertIndexField.GetValue(table) != 0);
|
||||
|
||||
// Finally loop until the insert index reaches the target.
|
||||
while ((int)insertIndexField.GetValue(table) != targetInsertIndex)
|
||||
{
|
||||
InsertOne();
|
||||
}
|
||||
|
||||
void InsertOne()
|
||||
{
|
||||
byte[] data = Encoding.ASCII.GetBytes($"header-{insertedHeaders.Count}");
|
||||
|
||||
insertedHeaders.Push(data);
|
||||
table.Insert(data, data);
|
||||
}
|
||||
|
||||
// Now check to see that we can retrieve the remaining headers.
|
||||
// Some headers will have been evacuated from the table during this process, so we don't exhaust the entire insertedHeaders stack.
|
||||
Assert.True(table.Count > 0);
|
||||
Assert.True(table.Count < insertedHeaders.Count);
|
||||
|
||||
for (int i = 0; i < table.Count; ++i)
|
||||
{
|
||||
HeaderField dynamicField = table[i];
|
||||
byte[] expectedData = insertedHeaders.Pop();
|
||||
|
||||
Assert.True(expectedData.AsSpan().SequenceEqual(dynamicField.Name));
|
||||
Assert.True(expectedData.AsSpan().SequenceEqual(dynamicField.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CreateResizeData))]
|
||||
public void DynamicTable_Resize_Success(int initialMaxSize, int finalMaxSize, int insertSize)
|
||||
{
|
||||
// This is purely to make it simple to perfectly reach our initial max size to test growing a full but non-wrapping buffer.
|
||||
Debug.Assert((insertSize % 64) == 0, $"{nameof(insertSize)} must be a multiple of 64 ({nameof(HeaderField)}.{nameof(HeaderField.RfcOverhead)} * 2)");
|
||||
|
||||
DynamicTable dynamicTable = new DynamicTable(maxSize: initialMaxSize);
|
||||
int insertedSize = 0;
|
||||
|
||||
while (insertedSize != insertSize)
|
||||
{
|
||||
byte[] data = Encoding.ASCII.GetBytes($"header-{dynamicTable.Size}".PadRight(16, ' '));
|
||||
Debug.Assert(data.Length == 16);
|
||||
|
||||
dynamicTable.Insert(data, data);
|
||||
insertedSize += data.Length * 2 + HeaderField.RfcOverhead;
|
||||
}
|
||||
|
||||
List<HeaderField> headers = new List<HeaderField>();
|
||||
|
||||
for (int i = 0; i < dynamicTable.Count; ++i)
|
||||
{
|
||||
headers.Add(dynamicTable[i]);
|
||||
}
|
||||
|
||||
dynamicTable.Resize(finalMaxSize);
|
||||
|
||||
int expectedCount = Math.Min(finalMaxSize / 64, headers.Count);
|
||||
Assert.Equal(expectedCount, dynamicTable.Count);
|
||||
|
||||
for (int i = 0; i < dynamicTable.Count; ++i)
|
||||
{
|
||||
Assert.True(headers[i].Name.AsSpan().SequenceEqual(dynamicTable[i].Name));
|
||||
Assert.True(headers[i].Value.AsSpan().SequenceEqual(dynamicTable[i].Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DynamicTable_ResizingEvictsOldestEntries()
|
||||
{
|
||||
DynamicTable dynamicTable = new DynamicTable(4096);
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
|
||||
VerifyTableEntries(dynamicTable, _header2, _header1);
|
||||
|
||||
dynamicTable.Resize(_header2.Length);
|
||||
|
||||
VerifyTableEntries(dynamicTable, _header2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DynamicTable_ResizingToZeroEvictsAllEntries()
|
||||
{
|
||||
DynamicTable dynamicTable = new DynamicTable(4096);
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
|
||||
dynamicTable.Resize(0);
|
||||
|
||||
Assert.Equal(0, dynamicTable.Count);
|
||||
Assert.Equal(0, dynamicTable.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DynamicTable_CanBeResizedToLargerMaxSize()
|
||||
{
|
||||
DynamicTable dynamicTable = new DynamicTable(_header1.Length);
|
||||
dynamicTable.Insert(_header1.Name, _header1.Value);
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
|
||||
// _header2 is larger than _header1, so an attempt at inserting it
|
||||
// would first clear the table then return without actually inserting it,
|
||||
// given it is larger than the current max size.
|
||||
Assert.Equal(0, dynamicTable.Count);
|
||||
Assert.Equal(0, dynamicTable.Size);
|
||||
|
||||
dynamicTable.Resize(dynamicTable.MaxSize + _header2.Length);
|
||||
dynamicTable.Insert(_header2.Name, _header2.Value);
|
||||
|
||||
VerifyTableEntries(dynamicTable, _header2);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> CreateResizeData()
|
||||
{
|
||||
int[] values = new[] { 128, 256, 384, 512 };
|
||||
return from initialMaxSize in values
|
||||
from finalMaxSize in values
|
||||
from insertSize in values
|
||||
select new object[] { initialMaxSize, finalMaxSize, insertSize };
|
||||
}
|
||||
|
||||
private void VerifyTableEntries(DynamicTable dynamicTable, params HeaderField[] entries)
|
||||
{
|
||||
Assert.Equal(entries.Length, dynamicTable.Count);
|
||||
Assert.Equal(entries.Sum(e => e.Length), dynamicTable.Size);
|
||||
|
||||
for (int i = 0; i < entries.Length; i++)
|
||||
{
|
||||
HeaderField headerField = dynamicTable[i];
|
||||
|
||||
Assert.NotSame(entries[i].Name, headerField.Name);
|
||||
Assert.Equal(entries[i].Name, headerField.Name);
|
||||
|
||||
Assert.NotSame(entries[i].Value, headerField.Value);
|
||||
Assert.Equal(entries[i].Value, headerField.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,20 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
// Licensed under the Apache License, Version 2.0.
|
||||
// See THIRD-PARTY-NOTICES.TXT in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using System.Net.Http.HPack;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||
namespace System.Net.Http.Unit.Tests.HPack
|
||||
{
|
||||
public class HPackDecoderTests : IHttpHeadersHandler
|
||||
{
|
||||
private const int DynamicTableInitialMaxSize = 4096;
|
||||
private const int MaxRequestHeaderFieldSize = 8192;
|
||||
private const int MaxHeaderFieldSize = 8192;
|
||||
|
||||
// Indexed Header Field Representation - Static Table - Index 2 (:method: GET)
|
||||
private static readonly byte[] _indexedHeaderStatic = new byte[] { 0x82 };
|
||||
|
|
@ -61,7 +58,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
|
||||
// v a l u e *
|
||||
// 11101110 00111010 00101101 00101111
|
||||
private static readonly byte[] _headerValueHuffmanBytes = new byte [] { 0xee, 0x3a, 0x2d, 0x2f };
|
||||
private static readonly byte[] _headerValueHuffmanBytes = new byte[] { 0xee, 0x3a, 0x2d, 0x2f };
|
||||
|
||||
private static readonly byte[] _headerName = new byte[] { (byte)_headerNameBytes.Length }
|
||||
.Concat(_headerNameBytes)
|
||||
|
|
@ -95,21 +92,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
public HPackDecoderTests()
|
||||
{
|
||||
_dynamicTable = new DynamicTable(DynamicTableInitialMaxSize);
|
||||
_decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxRequestHeaderFieldSize, _dynamicTable);
|
||||
_decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxHeaderFieldSize, _dynamicTable);
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeader(Span<byte> name, Span<byte> value)
|
||||
void IHttpHeadersHandler.OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
|
||||
{
|
||||
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters();
|
||||
string headerName = Encoding.ASCII.GetString(name);
|
||||
string headerValue = Encoding.ASCII.GetString(value);
|
||||
|
||||
_decodedHeaders[headerName] = headerValue;
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeadersComplete() { }
|
||||
void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }
|
||||
|
||||
[Fact]
|
||||
public void DecodesIndexedHeaderField_StaticTable()
|
||||
{
|
||||
_decoder.Decode(new ReadOnlySequence<byte>(_indexedHeaderStatic), endHeaders: true, handler: this);
|
||||
Assert.Equal("GET", _decodedHeaders[HeaderNames.Method]);
|
||||
_decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: this);
|
||||
Assert.Equal("GET", _decodedHeaders[":method"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -119,23 +119,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
_dynamicTable.Insert(_headerNameBytes, _headerValueBytes);
|
||||
|
||||
// Index it
|
||||
_decoder.Decode(new ReadOnlySequence<byte>(_indexedHeaderDynamic), endHeaders: true, handler: this);
|
||||
_decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: this);
|
||||
Assert.Equal(_headerValueString, _decodedHeaders[_headerNameString]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodesIndexedHeaderField_OutOfRange_Error()
|
||||
{
|
||||
var exception = Assert.Throws<HPackDecodingException>(() =>
|
||||
_decoder.Decode(new ReadOnlySequence<byte>(_indexedHeaderDynamic), endHeaders: true, handler: this));
|
||||
Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message);
|
||||
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() =>
|
||||
_decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: this));
|
||||
Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message);
|
||||
Assert.Empty(_decodedHeaders);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName()
|
||||
{
|
||||
var encoded = _literalHeaderFieldWithIndexingNewName
|
||||
byte[] encoded = _literalHeaderFieldWithIndexingNewName
|
||||
.Concat(_headerName)
|
||||
.Concat(_headerValue)
|
||||
.ToArray();
|
||||
|
|
@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedName()
|
||||
{
|
||||
var encoded = _literalHeaderFieldWithIndexingNewName
|
||||
byte[] encoded = _literalHeaderFieldWithIndexingNewName
|
||||
.Concat(_headerNameHuffman)
|
||||
.Concat(_headerValue)
|
||||
.ToArray();
|
||||
|
|
@ -157,7 +157,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedValue()
|
||||
{
|
||||
var encoded = _literalHeaderFieldWithIndexingNewName
|
||||
byte[] encoded = _literalHeaderFieldWithIndexingNewName
|
||||
.Concat(_headerName)
|
||||
.Concat(_headerValueHuffman)
|
||||
.ToArray();
|
||||
|
|
@ -168,7 +168,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedNameAndValue()
|
||||
{
|
||||
var encoded = _literalHeaderFieldWithIndexingNewName
|
||||
byte[] encoded = _literalHeaderFieldWithIndexingNewName
|
||||
.Concat(_headerNameHuffman)
|
||||
.Concat(_headerValueHuffman)
|
||||
.ToArray();
|
||||
|
|
@ -179,7 +179,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldWithIncrementalIndexing_IndexedName()
|
||||
{
|
||||
var encoded = _literalHeaderFieldWithIndexingIndexedName
|
||||
byte[] encoded = _literalHeaderFieldWithIndexingIndexedName
|
||||
.Concat(_headerValue)
|
||||
.ToArray();
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldWithIncrementalIndexing_IndexedName_HuffmanEncodedValue()
|
||||
{
|
||||
var encoded = _literalHeaderFieldWithIndexingIndexedName
|
||||
byte[] encoded = _literalHeaderFieldWithIndexingIndexedName
|
||||
.Concat(_headerValueHuffman)
|
||||
.ToArray();
|
||||
|
||||
|
|
@ -203,15 +203,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
// 11 1110 (Indexed Name - Index 62 encoded with 6-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation)
|
||||
// Index 62 is the first entry in the dynamic table. If there's nothing there, the decoder should throw.
|
||||
|
||||
var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new ReadOnlySequence<byte>(new byte[] { 0x7e }), endHeaders: true, handler: this));
|
||||
Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message);
|
||||
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new byte[] { 0x7e }, endHeaders: true, handler: this));
|
||||
Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message);
|
||||
Assert.Empty(_decodedHeaders);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldWithoutIndexing_NewName()
|
||||
{
|
||||
var encoded = _literalHeaderFieldWithoutIndexingNewName
|
||||
byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
|
||||
.Concat(_headerName)
|
||||
.Concat(_headerValue)
|
||||
.ToArray();
|
||||
|
|
@ -222,7 +222,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedName()
|
||||
{
|
||||
var encoded = _literalHeaderFieldWithoutIndexingNewName
|
||||
byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
|
||||
.Concat(_headerNameHuffman)
|
||||
.Concat(_headerValue)
|
||||
.ToArray();
|
||||
|
|
@ -233,7 +233,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedValue()
|
||||
{
|
||||
var encoded = _literalHeaderFieldWithoutIndexingNewName
|
||||
byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
|
||||
.Concat(_headerName)
|
||||
.Concat(_headerValueHuffman)
|
||||
.ToArray();
|
||||
|
|
@ -244,7 +244,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedNameAndValue()
|
||||
{
|
||||
var encoded = _literalHeaderFieldWithoutIndexingNewName
|
||||
byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
|
||||
.Concat(_headerNameHuffman)
|
||||
.Concat(_headerValueHuffman)
|
||||
.ToArray();
|
||||
|
|
@ -255,7 +255,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldWithoutIndexing_IndexedName()
|
||||
{
|
||||
var encoded = _literalHeaderFieldWithoutIndexingIndexedName
|
||||
byte[] encoded = _literalHeaderFieldWithoutIndexingIndexedName
|
||||
.Concat(_headerValue)
|
||||
.ToArray();
|
||||
|
||||
|
|
@ -265,7 +265,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldWithoutIndexing_IndexedName_HuffmanEncodedValue()
|
||||
{
|
||||
var encoded = _literalHeaderFieldWithoutIndexingIndexedName
|
||||
byte[] encoded = _literalHeaderFieldWithoutIndexingIndexedName
|
||||
.Concat(_headerValueHuffman)
|
||||
.ToArray();
|
||||
|
||||
|
|
@ -279,15 +279,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
// 1111 0010 1111 (Indexed Name - Index 62 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation)
|
||||
// Index 62 is the first entry in the dynamic table. If there's nothing there, the decoder should throw.
|
||||
|
||||
var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new ReadOnlySequence<byte>(new byte[] { 0x0f, 0x2f }), endHeaders: true, handler: this));
|
||||
Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message);
|
||||
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new byte[] { 0x0f, 0x2f }, endHeaders: true, handler: this));
|
||||
Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message);
|
||||
Assert.Empty(_decodedHeaders);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldNeverIndexed_NewName()
|
||||
{
|
||||
var encoded = _literalHeaderFieldNeverIndexedNewName
|
||||
byte[] encoded = _literalHeaderFieldNeverIndexedNewName
|
||||
.Concat(_headerName)
|
||||
.Concat(_headerValue)
|
||||
.ToArray();
|
||||
|
|
@ -298,7 +298,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedName()
|
||||
{
|
||||
var encoded = _literalHeaderFieldNeverIndexedNewName
|
||||
byte[] encoded = _literalHeaderFieldNeverIndexedNewName
|
||||
.Concat(_headerNameHuffman)
|
||||
.Concat(_headerValue)
|
||||
.ToArray();
|
||||
|
|
@ -309,7 +309,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedValue()
|
||||
{
|
||||
var encoded = _literalHeaderFieldNeverIndexedNewName
|
||||
byte[] encoded = _literalHeaderFieldNeverIndexedNewName
|
||||
.Concat(_headerName)
|
||||
.Concat(_headerValueHuffman)
|
||||
.ToArray();
|
||||
|
|
@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Fact]
|
||||
public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedNameAndValue()
|
||||
{
|
||||
var encoded = _literalHeaderFieldNeverIndexedNewName
|
||||
byte[] encoded = _literalHeaderFieldNeverIndexedNewName
|
||||
.Concat(_headerNameHuffman)
|
||||
.Concat(_headerValueHuffman)
|
||||
.ToArray();
|
||||
|
|
@ -334,7 +334,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
// 0001 (Literal Header Field Never Indexed Representation)
|
||||
// 1111 0010 1011 (Indexed Name - Index 58 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation)
|
||||
// Concatenated with value bytes
|
||||
var encoded = _literalHeaderFieldNeverIndexedIndexedName
|
||||
byte[] encoded = _literalHeaderFieldNeverIndexedIndexedName
|
||||
.Concat(_headerValue)
|
||||
.ToArray();
|
||||
|
||||
|
|
@ -347,7 +347,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
// 0001 (Literal Header Field Never Indexed Representation)
|
||||
// 1111 0010 1011 (Indexed Name - Index 58 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation)
|
||||
// Concatenated with Huffman encoded value bytes
|
||||
var encoded = _literalHeaderFieldNeverIndexedIndexedName
|
||||
byte[] encoded = _literalHeaderFieldNeverIndexedIndexedName
|
||||
.Concat(_headerValueHuffman)
|
||||
.ToArray();
|
||||
|
||||
|
|
@ -361,8 +361,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
// 1111 0010 1111 (Indexed Name - Index 62 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation)
|
||||
// Index 62 is the first entry in the dynamic table. If there's nothing there, the decoder should throw.
|
||||
|
||||
var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new ReadOnlySequence<byte>(new byte[] { 0x1f, 0x2f }), endHeaders: true, handler: this));
|
||||
Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message);
|
||||
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new byte[] { 0x1f, 0x2f }, endHeaders: true, handler: this));
|
||||
Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message);
|
||||
Assert.Empty(_decodedHeaders);
|
||||
}
|
||||
|
||||
|
|
@ -374,7 +374,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
|
||||
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
|
||||
|
||||
_decoder.Decode(new ReadOnlySequence<byte>(new byte[] { 0x3e }), endHeaders: true, handler: this);
|
||||
_decoder.Decode(new byte[] { 0x3e }, endHeaders: true, handler: this);
|
||||
|
||||
Assert.Equal(30, _dynamicTable.MaxSize);
|
||||
Assert.Empty(_decodedHeaders);
|
||||
|
|
@ -388,9 +388,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
|
||||
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
|
||||
|
||||
var data = new ReadOnlySequence<byte>(_indexedHeaderStatic.Concat(new byte[] { 0x3e }).ToArray());
|
||||
var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(data, endHeaders: true, handler: this));
|
||||
Assert.Equal(CoreStrings.HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock, exception.Message);
|
||||
byte[] data = _indexedHeaderStatic.Concat(new byte[] { 0x3e }).ToArray();
|
||||
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(data, endHeaders: true, handler: this));
|
||||
Assert.Equal(SR.net_http_hpack_late_dynamic_table_size_update, exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -398,14 +398,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
{
|
||||
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
|
||||
|
||||
_decoder.Decode(new ReadOnlySequence<byte>(_indexedHeaderStatic), endHeaders: false, handler: this);
|
||||
Assert.Equal("GET", _decodedHeaders[HeaderNames.Method]);
|
||||
_decoder.Decode(_indexedHeaderStatic, endHeaders: false, handler: this);
|
||||
Assert.Equal("GET", _decodedHeaders[":method"]);
|
||||
|
||||
// 001 (Dynamic Table Size Update)
|
||||
// 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation)
|
||||
var data = new ReadOnlySequence<byte>(new byte[] { 0x3e });
|
||||
var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(data, endHeaders: true, handler: this));
|
||||
Assert.Equal(CoreStrings.HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock, exception.Message);
|
||||
byte[] data = new byte[] { 0x3e };
|
||||
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(data, endHeaders: true, handler: this));
|
||||
Assert.Equal(SR.net_http_hpack_late_dynamic_table_size_update, exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -413,12 +413,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
{
|
||||
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
|
||||
|
||||
_decoder.Decode(new ReadOnlySequence<byte>(_indexedHeaderStatic), endHeaders: true, handler: this);
|
||||
Assert.Equal("GET", _decodedHeaders[HeaderNames.Method]);
|
||||
_decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: this);
|
||||
Assert.Equal("GET", _decodedHeaders[":method"]);
|
||||
|
||||
// 001 (Dynamic Table Size Update)
|
||||
// 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation)
|
||||
_decoder.Decode(new ReadOnlySequence<byte>(new byte[] { 0x3e }), endHeaders: true, handler: this);
|
||||
_decoder.Decode(new byte[] { 0x3e }, endHeaders: true, handler: this);
|
||||
|
||||
Assert.Equal(30, _dynamicTable.MaxSize);
|
||||
}
|
||||
|
|
@ -431,38 +431,38 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
|
||||
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
|
||||
|
||||
var exception = Assert.Throws<HPackDecodingException>(() =>
|
||||
_decoder.Decode(new ReadOnlySequence<byte>(new byte[] { 0x3f, 0xe2, 0x1f }), endHeaders: true, handler: this));
|
||||
Assert.Equal(CoreStrings.FormatHPackErrorDynamicTableSizeUpdateTooLarge(4097, DynamicTableInitialMaxSize), exception.Message);
|
||||
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() =>
|
||||
_decoder.Decode(new byte[] { 0x3f, 0xe2, 0x1f }, endHeaders: true, handler: this));
|
||||
Assert.Equal(SR.Format(SR.net_http_hpack_large_table_size_update, 4097, DynamicTableInitialMaxSize), exception.Message);
|
||||
Assert.Empty(_decodedHeaders);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodesStringLength_GreaterThanLimit_Error()
|
||||
{
|
||||
var encoded = _literalHeaderFieldWithoutIndexingNewName
|
||||
byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
|
||||
.Concat(new byte[] { 0xff, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix
|
||||
.ToArray();
|
||||
|
||||
var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new ReadOnlySequence<byte>(encoded), endHeaders: true, handler: this));
|
||||
Assert.Equal(CoreStrings.FormatHPackStringLengthTooLarge(MaxRequestHeaderFieldSize + 1, MaxRequestHeaderFieldSize), exception.Message);
|
||||
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: this));
|
||||
Assert.Equal(SR.Format(SR.net_http_headers_exceeded_length, MaxHeaderFieldSize), exception.Message);
|
||||
Assert.Empty(_decodedHeaders);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodesStringLength_LimitConfigurable()
|
||||
{
|
||||
var decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxRequestHeaderFieldSize + 1);
|
||||
var string8193 = new string('a', MaxRequestHeaderFieldSize + 1);
|
||||
HPackDecoder decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxHeaderFieldSize + 1);
|
||||
string string8193 = new string('a', MaxHeaderFieldSize + 1);
|
||||
|
||||
var encoded = _literalHeaderFieldWithoutIndexingNewName
|
||||
byte[] encoded = _literalHeaderFieldWithoutIndexingNewName
|
||||
.Concat(new byte[] { 0x7f, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix, no Huffman encoding
|
||||
.Concat(Encoding.ASCII.GetBytes(string8193))
|
||||
.Concat(new byte[] { 0x7f, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix, no Huffman encoding
|
||||
.Concat(Encoding.ASCII.GetBytes(string8193))
|
||||
.ToArray();
|
||||
|
||||
decoder.Decode(new ReadOnlySequence<byte>(encoded), endHeaders: true, handler: this);
|
||||
decoder.Decode(encoded, endHeaders: true, handler: this);
|
||||
|
||||
Assert.Equal(string8193, _decodedHeaders[string8193]);
|
||||
}
|
||||
|
|
@ -552,8 +552,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[MemberData(nameof(_incompleteHeaderBlockData))]
|
||||
public void DecodesIncompleteHeaderBlock_Error(byte[] encoded)
|
||||
{
|
||||
var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new ReadOnlySequence<byte>(encoded), endHeaders: true, handler: this));
|
||||
Assert.Equal(CoreStrings.HPackErrorIncompleteHeaderBlock, exception.Message);
|
||||
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: this));
|
||||
Assert.Equal(SR.net_http_hpack_incomplete_header_block, exception.Message);
|
||||
Assert.Empty(_decodedHeaders);
|
||||
}
|
||||
|
||||
|
|
@ -586,8 +586,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[MemberData(nameof(_huffmanDecodingErrorData))]
|
||||
public void WrapsHuffmanDecodingExceptionInHPackDecodingException(byte[] encoded)
|
||||
{
|
||||
var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new ReadOnlySequence<byte>(encoded), endHeaders: true, handler: this));
|
||||
Assert.Equal(CoreStrings.HPackHuffmanError, exception.Message);
|
||||
HPackDecodingException exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: this));
|
||||
Assert.Equal(SR.net_http_hpack_huffman_decode_failed, exception.Message);
|
||||
Assert.IsType<HuffmanDecodingException>(exception.InnerException);
|
||||
Assert.Empty(_decodedHeaders);
|
||||
}
|
||||
|
|
@ -607,7 +607,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
Assert.Equal(0, _dynamicTable.Count);
|
||||
Assert.Equal(0, _dynamicTable.Size);
|
||||
|
||||
_decoder.Decode(new ReadOnlySequence<byte>(encoded), endHeaders: true, handler: this);
|
||||
_decoder.Decode(encoded, endHeaders: true, handler: this);
|
||||
|
||||
Assert.Equal(expectedHeaderValue, _decodedHeaders[expectedHeaderName]);
|
||||
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http.HPack;
|
||||
using Xunit;
|
||||
|
||||
namespace System.Net.Http.Unit.Tests.HPack
|
||||
{
|
||||
public class HPackIntegerTest
|
||||
{
|
||||
[Theory]
|
||||
[MemberData(nameof(IntegerCodecExactSamples))]
|
||||
public void HPack_IntegerEncode(int value, int bits, byte[] expectedResult)
|
||||
{
|
||||
Span<byte> actualResult = new byte[64];
|
||||
bool success = IntegerEncoder.Encode(value, bits, actualResult, out int bytesWritten);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.Equal(expectedResult.Length, bytesWritten);
|
||||
Assert.True(actualResult.Slice(0, bytesWritten).SequenceEqual(expectedResult));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(IntegerCodecExactSamples))]
|
||||
public void HPack_IntegerEncode_ShortBuffer(int value, int bits, byte[] expectedResult)
|
||||
{
|
||||
Span<byte> actualResult = new byte[expectedResult.Length - 1];
|
||||
bool success = IntegerEncoder.Encode(value, bits, actualResult, out int bytesWritten);
|
||||
|
||||
Assert.False(success);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(IntegerCodecExactSamples))]
|
||||
public void HPack_IntegerDecode(int expectedResult, int bits, byte[] encoded)
|
||||
{
|
||||
IntegerDecoder integerDecoder = new IntegerDecoder();
|
||||
|
||||
bool finished = integerDecoder.BeginTryDecode(encoded[0], bits, out int actualResult);
|
||||
|
||||
int i = 1;
|
||||
for (; !finished && i < encoded.Length; ++i)
|
||||
{
|
||||
finished = integerDecoder.TryDecode(encoded[i], out actualResult);
|
||||
}
|
||||
|
||||
Assert.True(finished);
|
||||
Assert.Equal(encoded.Length, i);
|
||||
|
||||
Assert.Equal(expectedResult, actualResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntegerEncoderDecoderRoundtrips()
|
||||
{
|
||||
IntegerDecoder decoder = new IntegerDecoder();
|
||||
|
||||
for (int i = 0; i < 2048; ++i)
|
||||
{
|
||||
for (int prefixLength = 1; prefixLength <= 8; ++prefixLength)
|
||||
{
|
||||
Span<byte> integerBytes = stackalloc byte[5];
|
||||
Assert.True(IntegerEncoder.Encode(i, prefixLength, integerBytes, out int length));
|
||||
|
||||
bool decodeResult = decoder.BeginTryDecode(integerBytes[0], prefixLength, out int intResult);
|
||||
|
||||
for (int j = 1; j < length; j++)
|
||||
{
|
||||
Assert.False(decodeResult);
|
||||
decodeResult = decoder.TryDecode(integerBytes[j], out intResult);
|
||||
}
|
||||
|
||||
Assert.True(decodeResult);
|
||||
Assert.Equal(i, intResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> IntegerCodecExactSamples()
|
||||
{
|
||||
yield return new object[] { 10, 5, new byte[] { 0x0A } };
|
||||
yield return new object[] { 1337, 5, new byte[] { 0x1F, 0x9A, 0x0A } };
|
||||
yield return new object[] { 42, 8, new byte[] { 0x2A } };
|
||||
yield return new object[] { 7, 3, new byte[] { 0x7, 0x0 } };
|
||||
yield return new object[] { int.MaxValue, 1, new byte[] { 0x01, 0xfe, 0xff, 0xff, 0xff, 0x07 } };
|
||||
yield return new object[] { int.MaxValue, 8, new byte[] { 0xff, 0x80, 0xfe, 0xff, 0xff, 0x07 } };
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(IntegerData_OverMax))]
|
||||
public void IntegerDecode_Throws_IfMaxExceeded(int prefixLength, byte[] octets)
|
||||
{
|
||||
var decoder = new IntegerDecoder();
|
||||
var result = decoder.BeginTryDecode(octets[0], prefixLength, out var intResult);
|
||||
|
||||
for (var j = 1; j < octets.Length - 1; j++)
|
||||
{
|
||||
Assert.False(decoder.TryDecode(octets[j], out intResult));
|
||||
}
|
||||
|
||||
Assert.Throws<HPackDecodingException>(() => decoder.TryDecode(octets[octets.Length - 1], out intResult));
|
||||
}
|
||||
|
||||
public static TheoryData<int, byte[]> IntegerData_OverMax
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<int, byte[]>();
|
||||
|
||||
data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x07 }); // Int32.MaxValue + 1
|
||||
data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x08 }); // MSB exceeds maximum
|
||||
data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x80 }); // Undefined since continuation bit set
|
||||
|
||||
data.Add(7, new byte[] { 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x08 }); // 1 bit too large
|
||||
data.Add(7, new byte[] { 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F });
|
||||
data.Add(7, new byte[] { 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80 }); // A continuation byte (0x80) where the byte after it would be too large.
|
||||
data.Add(7, new byte[] { 0x7F, 0xFF, 0x00 }); // Encoded with 1 byte too many.
|
||||
|
||||
data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x07 }); // Int32.MaxValue + 1
|
||||
data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x08 }); // MSB exceeds maximum
|
||||
data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x80 }); // Undefined since continuation bit set
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,193 @@
|
|||
// 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 to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http.HPack;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||
namespace System.Net.Http.Unit.Tests.HPack
|
||||
{
|
||||
public class HuffmanTests
|
||||
public class HuffmanDecodingTests
|
||||
{
|
||||
// Encoded values are 30 bits at most, so are stored in the table in a uint.
|
||||
// Convert to ulong here and put the encoded value in the most significant bits.
|
||||
// This makes the encoding logic below simpler.
|
||||
private static (ulong code, int bitLength) GetEncodedValue(byte b)
|
||||
{
|
||||
(uint code, int bitLength) = Huffman.Encode(b);
|
||||
return (((ulong)code) << 32, bitLength);
|
||||
}
|
||||
|
||||
private static int Encode(byte[] source, byte[] destination, bool injectEOS)
|
||||
{
|
||||
ulong currentBits = 0; // We can have 7 bits of rollover plus 30 bits for the next encoded value, so use a ulong
|
||||
int currentBitCount = 0;
|
||||
int dstOffset = 0;
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
(ulong code, int bitLength) = GetEncodedValue(source[i]);
|
||||
|
||||
// inject EOS if instructed to
|
||||
if (injectEOS)
|
||||
{
|
||||
code |= (ulong)0b11111111_11111111_11111111_11111100 << (32 - bitLength);
|
||||
bitLength += 30;
|
||||
injectEOS = false;
|
||||
}
|
||||
|
||||
currentBits |= code >> currentBitCount;
|
||||
currentBitCount += bitLength;
|
||||
|
||||
while (currentBitCount >= 8)
|
||||
{
|
||||
destination[dstOffset++] = (byte)(currentBits >> 56);
|
||||
currentBits = currentBits << 8;
|
||||
currentBitCount -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill any trailing bits with ones, per RFC
|
||||
if (currentBitCount > 0)
|
||||
{
|
||||
currentBits |= 0xFFFFFFFFFFFFFFFF >> currentBitCount;
|
||||
destination[dstOffset++] = (byte)(currentBits >> 56);
|
||||
}
|
||||
|
||||
return dstOffset;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HuffmanDecoding_ValidEncoding_Succeeds()
|
||||
{
|
||||
foreach (byte[] input in TestData())
|
||||
{
|
||||
// Worst case encoding is 30 bits per input byte, so make the encoded buffer 4 times as big
|
||||
byte[] encoded = new byte[input.Length * 4];
|
||||
int encodedByteCount = Encode(input, encoded, false);
|
||||
|
||||
// Worst case decoding is an output byte per 5 input bits, so make the decoded buffer 2 times as big
|
||||
byte[] decoded = new byte[encoded.Length * 2];
|
||||
|
||||
int decodedByteCount = Huffman.Decode(new ReadOnlySpan<byte>(encoded, 0, encodedByteCount), ref decoded);
|
||||
|
||||
Assert.Equal(input.Length, decodedByteCount);
|
||||
Assert.Equal(input, decoded.Take(decodedByteCount));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HuffmanDecoding_InvalidEncoding_Throws()
|
||||
{
|
||||
foreach (byte[] encoded in InvalidEncodingData())
|
||||
{
|
||||
// Worst case decoding is an output byte per 5 input bits, so make the decoded buffer 2 times as big
|
||||
byte[] decoded = new byte[encoded.Length * 2];
|
||||
|
||||
Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(encoded, ref decoded));
|
||||
}
|
||||
}
|
||||
|
||||
// This input sequence will encode to 17 bits, thus offsetting the next character to encode
|
||||
// by exactly one bit. We use this below to generate a prefix that encodes all of the possible starting
|
||||
// bit offsets for a character, from 0 to 7.
|
||||
private static readonly byte[] s_offsetByOneBit = new byte[] { (byte)'c', (byte)'l', (byte)'r' };
|
||||
|
||||
public static IEnumerable<byte[]> TestData()
|
||||
{
|
||||
// Single byte data
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
yield return new byte[] { (byte)i };
|
||||
}
|
||||
|
||||
// Ensure that decoding every possible value leaves the decoder in a correct state so that
|
||||
// a subsequent value can be decoded (here, 'a')
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
yield return new byte[] { (byte)i, (byte)'a' };
|
||||
}
|
||||
|
||||
// Ensure that every possible bit starting position for every value is encoded properly
|
||||
// s_offsetByOneBit encodes to exactly 17 bits, leaving 1 bit for the next byte
|
||||
// So by repeating this sequence, we can generate any starting bit position we want.
|
||||
byte[] currentPrefix = new byte[0];
|
||||
for (int prefixBits = 1; prefixBits <= 8; prefixBits++)
|
||||
{
|
||||
currentPrefix = currentPrefix.Concat(s_offsetByOneBit).ToArray();
|
||||
|
||||
// Make sure we're actually getting the correct number of prefix bits
|
||||
int encodedBits = currentPrefix.Select(b => Huffman.Encode(b).bitLength).Sum();
|
||||
Assert.Equal(prefixBits % 8, encodedBits % 8);
|
||||
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
yield return currentPrefix.Concat(new byte[] { (byte)i }.Concat(currentPrefix)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, one really big chunk of randomly generated data.
|
||||
byte[] data = new byte[1024 * 1024];
|
||||
new Random(42).NextBytes(data);
|
||||
yield return data;
|
||||
}
|
||||
|
||||
private static IEnumerable<byte[]> InvalidEncodingData()
|
||||
{
|
||||
// For encodings greater than 8 bits, truncate one or more bytes to generate an invalid encoding
|
||||
byte[] source = new byte[1];
|
||||
byte[] destination = new byte[10];
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
source[0] = (byte)i;
|
||||
int encodedByteCount = Encode(source, destination, false);
|
||||
if (encodedByteCount > 1)
|
||||
{
|
||||
yield return destination.Take(encodedByteCount - 1).ToArray();
|
||||
if (encodedByteCount > 2)
|
||||
{
|
||||
yield return destination.Take(encodedByteCount - 2).ToArray();
|
||||
if (encodedByteCount > 3)
|
||||
{
|
||||
yield return destination.Take(encodedByteCount - 3).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pad encodings with invalid trailing one bits. This is disallowed.
|
||||
byte[] pad1 = new byte[] { 0xFF };
|
||||
byte[] pad2 = new byte[] { 0xFF, 0xFF, };
|
||||
byte[] pad3 = new byte[] { 0xFF, 0xFF, 0xFF };
|
||||
byte[] pad4 = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
source[0] = (byte)i;
|
||||
int encodedByteCount = Encode(source, destination, false);
|
||||
yield return destination.Take(encodedByteCount).Concat(pad1).ToArray();
|
||||
yield return destination.Take(encodedByteCount).Concat(pad2).ToArray();
|
||||
yield return destination.Take(encodedByteCount).Concat(pad3).ToArray();
|
||||
yield return destination.Take(encodedByteCount).Concat(pad4).ToArray();
|
||||
}
|
||||
|
||||
// send single EOS
|
||||
yield return new byte[] { 0b11111111, 0b11111111, 0b11111111, 0b11111100 };
|
||||
|
||||
// send combinations with EOS in the middle
|
||||
source = new byte[2];
|
||||
destination = new byte[24];
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
source[0] = source[1] = (byte)i;
|
||||
int encodedByteCount = Encode(source, destination, true);
|
||||
yield return destination.Take(encodedByteCount).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly TheoryData<byte[], byte[]> _validData = new TheoryData<byte[], byte[]>
|
||||
{
|
||||
// Single 5-bit symbol
|
||||
|
|
@ -67,8 +245,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[MemberData(nameof(_validData))]
|
||||
public void HuffmanDecodeArray(byte[] encoded, byte[] expected)
|
||||
{
|
||||
var dst = new byte[expected.Length];
|
||||
Assert.Equal(expected.Length, Huffman.Decode(new ReadOnlySpan<byte>(encoded), dst));
|
||||
byte[] dst = new byte[expected.Length];
|
||||
Assert.Equal(expected.Length, Huffman.Decode(new ReadOnlySpan<byte>(encoded), ref dst));
|
||||
Assert.Equal(expected, dst);
|
||||
}
|
||||
|
||||
|
|
@ -88,8 +266,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[MemberData(nameof(_longPaddingData))]
|
||||
public void ThrowsOnPaddingLongerThanSevenBits(byte[] encoded)
|
||||
{
|
||||
var exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(new ReadOnlySpan<byte>(encoded), new byte[encoded.Length * 2]));
|
||||
Assert.Equal(CoreStrings.HPackHuffmanErrorIncomplete, exception.Message);
|
||||
byte[] dst = new byte[encoded.Length * 2];
|
||||
Exception exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(new ReadOnlySpan<byte>(encoded), ref dst));
|
||||
Assert.Equal(SR.net_http_hpack_huffman_decode_failed, exception.Message);
|
||||
}
|
||||
|
||||
public static readonly TheoryData<byte[]> _eosData = new TheoryData<byte[]>
|
||||
|
|
@ -104,17 +283,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[MemberData(nameof(_eosData))]
|
||||
public void ThrowsOnEOS(byte[] encoded)
|
||||
{
|
||||
var exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(new ReadOnlySpan<byte>(encoded), new byte[encoded.Length * 2]));
|
||||
Assert.Equal(CoreStrings.HPackHuffmanErrorEOS, exception.Message);
|
||||
byte[] dst = new byte[encoded.Length * 2];
|
||||
Exception exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(new ReadOnlySpan<byte>(encoded), ref dst));
|
||||
Assert.Equal(SR.net_http_hpack_huffman_decode_failed, exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowsOnDestinationBufferTooSmall()
|
||||
public void ResizesOnDestinationBufferTooSmall()
|
||||
{
|
||||
// h e l l o *
|
||||
var encoded = new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1111 };
|
||||
var exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(new ReadOnlySpan<byte>(encoded), new byte[encoded.Length]));
|
||||
Assert.Equal(CoreStrings.HPackHuffmanErrorDestinationTooSmall, exception.Message);
|
||||
byte[] encoded = new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1111 };
|
||||
byte[] originalDestination = new byte[encoded.Length];
|
||||
byte[] actualDestination = originalDestination;
|
||||
int decodedCount = Huffman.Decode(new ReadOnlySpan<byte>(encoded), ref actualDestination);
|
||||
Assert.Equal(5, decodedCount);
|
||||
Assert.NotSame(originalDestination, actualDestination);
|
||||
}
|
||||
|
||||
public static readonly TheoryData<byte[]> _incompleteSymbolData = new TheoryData<byte[]>
|
||||
|
|
@ -145,28 +328,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[MemberData(nameof(_incompleteSymbolData))]
|
||||
public void ThrowsOnIncompleteSymbol(byte[] encoded)
|
||||
{
|
||||
var exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(new ReadOnlySpan<byte>(encoded), new byte[encoded.Length * 2]));
|
||||
Assert.Equal(CoreStrings.HPackHuffmanErrorIncomplete, exception.Message);
|
||||
byte[] dst = new byte[encoded.Length * 2];
|
||||
Exception exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(new ReadOnlySpan<byte>(encoded), ref dst));
|
||||
Assert.Equal(SR.net_http_hpack_huffman_decode_failed, exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodeCharactersThatSpans5Octets()
|
||||
{
|
||||
var expectedLength = 2;
|
||||
var decodedBytes = new byte[expectedLength];
|
||||
int expectedLength = 2;
|
||||
byte[] decodedBytes = new byte[expectedLength];
|
||||
// B LF EOS
|
||||
var encoded = new byte[] { 0b1011101_1, 0b11111111, 0b11111111, 0b11111111, 0b11100_111 };
|
||||
var decodedLength = Huffman.Decode(new ReadOnlySpan<byte>(encoded, 0, encoded.Length), decodedBytes);
|
||||
byte[] encoded = new byte[] { 0b1011101_1, 0b11111111, 0b11111111, 0b11111111, 0b11100_111 };
|
||||
int decodedLength = Huffman.Decode(new ReadOnlySpan<byte>(encoded, 0, encoded.Length), ref decodedBytes);
|
||||
|
||||
Assert.Equal(expectedLength, decodedLength);
|
||||
Assert.Equal(new byte [] { (byte)'B', (byte)'\n' }, decodedBytes);
|
||||
Assert.Equal(new byte[] { (byte)'B', (byte)'\n' }, decodedBytes);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(HuffmanData))]
|
||||
public void HuffmanEncode(int code, uint expectedEncoded, int expectedBitLength)
|
||||
{
|
||||
var (encoded, bitLength) = Huffman.Encode(code);
|
||||
(uint encoded, int bitLength) = Huffman.Encode(code);
|
||||
Assert.Equal(expectedEncoded, encoded);
|
||||
Assert.Equal(expectedBitLength, bitLength);
|
||||
}
|
||||
|
|
@ -175,7 +359,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[MemberData(nameof(HuffmanData))]
|
||||
public void HuffmanDecode(int code, uint encoded, int bitLength)
|
||||
{
|
||||
Assert.Equal(code, Huffman.DecodeValue(encoded, bitLength, out var decodedBits));
|
||||
Assert.Equal(code, Huffman.DecodeValue(encoded, bitLength, out int decodedBits));
|
||||
Assert.Equal(bitLength, decodedBits);
|
||||
}
|
||||
|
||||
|
|
@ -183,14 +367,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[MemberData(nameof(HuffmanData))]
|
||||
public void HuffmanEncodeDecode(
|
||||
int code,
|
||||
// Suppresses the warning about an unused theory parameter because
|
||||
// this test shares data with other methods
|
||||
// Suppresses the warning about an unused theory parameter because
|
||||
// this test shares data with other methods
|
||||
#pragma warning disable xUnit1026
|
||||
uint encoded,
|
||||
#pragma warning restore xUnit1026
|
||||
int bitLength)
|
||||
{
|
||||
Assert.Equal(code, Huffman.DecodeValue(Huffman.Encode(code).encoded, bitLength, out var decodedBits));
|
||||
Assert.Equal(code, Huffman.DecodeValue(Huffman.Encode(code).encoded, bitLength, out int decodedBits));
|
||||
Assert.Equal(bitLength, decodedBits);
|
||||
}
|
||||
|
||||
|
|
@ -198,7 +382,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<int, uint, int>();
|
||||
TheoryData<int, uint, int> data = new TheoryData<int, uint, int>();
|
||||
|
||||
data.Add(0, 0b11111111_11000000_00000000_00000000, 13);
|
||||
data.Add(1, 0b11111111_11111111_10110000_00000000, 23);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
The code in this directory is shared between CoreFx and AspNetCore. This contains tests for HTTP/2 protocol infrastructure such as HPACK. Any changes to this dir need to be checked into both repositories.
|
||||
|
||||
See corefx\src\Common\src\System\Net\Http\Http2\ReadMe.SharedCode.md or AspNetCore\src\Shared\Http2\ReadMe.SharedCode.md for additional details.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(DefaultNetCoreTargetFramework)</TargetFrameworks>
|
||||
|
|
@ -7,15 +7,16 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(SharedSourceRoot)ClosedGenericMatcher\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)CopyOnWriteDictionary\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)HttpSys\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)ObjectMethodExecutor\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)PropertyActivator\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)PropertyHelper\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)SecurityHelper\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)StackTrace\StackFrame\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)WebEncoders\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)ClosedGenericMatcher\*.cs" Link="Shared\ClosedGenericMatcher\%(Filename)%(Extension)"/>
|
||||
<Compile Include="$(SharedSourceRoot)CopyOnWriteDictionary\*.cs" Link="Shared\CopyOnWriteDictionary\%(Filename)%(Extension)"/>
|
||||
<Compile Include="$(SharedSourceRoot)Http2\**\*.cs" Link="Shared\Http2\%(Filename)%(Extension)"/>
|
||||
<Compile Include="$(SharedSourceRoot)HttpSys\**\*.cs" Link="Shared\HttpSys\%(Filename)%(Extension)"/>
|
||||
<Compile Include="$(SharedSourceRoot)ObjectMethodExecutor\*.cs" Link="Shared\ObjectMethodExecutor\%(Filename)%(Extension)"/>
|
||||
<Compile Include="$(SharedSourceRoot)PropertyActivator\*.cs" Link="Shared\PropertyActivator\%(Filename)%(Extension)"/>
|
||||
<Compile Include="$(SharedSourceRoot)PropertyHelper\*.cs" Link="Shared\PropertyHelper\%(Filename)%(Extension)"/>
|
||||
<Compile Include="$(SharedSourceRoot)SecurityHelper\**\*.cs" Link="Shared\SecurityHelper\%(Filename)%(Extension)"/>
|
||||
<Compile Include="$(SharedSourceRoot)StackTrace\StackFrame\**\*.cs" Link="Shared\StackTrace\%(Filename)%(Extension)"/>
|
||||
<Compile Include="$(SharedSourceRoot)WebEncoders\**\*.cs" Link="Shared\WebEncoders\%(Filename)%(Extension)"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -30,6 +31,10 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\testassets\ThrowingLibrary\ThrowingLibrary.csproj" />
|
||||
<EmbeddedResource Include="$(SharedSourceRoot)Http2\SR.resx" Link="Shared\Http2\SR.resx">
|
||||
<ManifestResourceName>System.Net.Http.SR</ManifestResourceName>
|
||||
<Generator></Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
Loading…
Reference in New Issue