Synchronize Http/2 HPack implementation between CoreFx and ASP.NET Core (#13931)

This commit is contained in:
Justin Kotalik 2019-11-01 17:32:21 -07:00 committed by Chris Ross
parent bd917f690d
commit 3ecdc40318
69 changed files with 2796 additions and 1736 deletions

6
.github/CODEOWNERS vendored
View File

@ -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

View File

@ -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);

View File

@ -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>

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.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)
{

View File

@ -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);

View File

@ -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)

View File

@ -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++;

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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)
{
}
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}
}

View File

@ -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));
}
}

View File

@ -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)
};
}
}
}

View File

@ -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));
}

View File

@ -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;

View File

@ -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)
{

View File

@ -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

View File

@ -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

View File

@ -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));

View File

@ -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>

View File

@ -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);
}
}
}
}

View File

@ -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

View File

@ -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 } };
}
}
}

View File

@ -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)
{

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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>

View File

@ -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)

View File

@ -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);

View File

@ -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
{

View File

@ -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;

View File

@ -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;
}

View File

@ -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)
{

View File

@ -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);

View File

@ -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;

View File

@ -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]

View File

@ -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]

View File

@ -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()
{

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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];
}
}
}
}

View File

@ -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)
{

View File

@ -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>";
}
}
}
}

View File

@ -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)
{

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}
}
}

View File

@ -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);
}
}

View File

@ -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`

20
src/Shared/Http2/SR.cs Normal file
View File

@ -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;
}
}
}

156
src/Shared/Http2/SR.resx Normal file
View File

@ -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>

25
src/Shared/Shared.sln Normal file
View File

@ -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

View File

@ -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);
}
}
}
}

View File

@ -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]);

View File

@ -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;
}
}
}
}

View File

@ -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);

View File

@ -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.

View File

@ -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>