HPACK fixes and improvements.

This commit is contained in:
Cesar Blum Silveira 2017-10-04 12:59:41 -07:00 committed by GitHub
parent 156ddfc4e8
commit 11ce1395e5
17 changed files with 1522 additions and 218 deletions

View File

@ -360,4 +360,28 @@
<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>
</root>

View File

@ -344,18 +344,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
public void OnHeader(Span<byte> name, Span<byte> value)
{
_requestHeadersParsed++;
if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
{
ThrowRequestRejected(RequestRejectionReason.TooManyHeaders);
}
var valueString = value.GetAsciiStringNonNullCharacters();
HttpRequestHeaders.Append(name, valueString);
}
protected void EnsureHostHeaderExists()
{
if (_httpVersion == Http.HttpVersion.Http10)

View File

@ -418,6 +418,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
public void OnHeader(Span<byte> name, Span<byte> value)
{
_requestHeadersParsed++;
if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
{
ThrowRequestRejected(RequestRejectionReason.TooManyHeaders);
}
var valueString = value.GetAsciiStringNonNullCharacters();
HttpRequestHeaders.Append(name, valueString);
}
public async Task ProcessRequestsAsync()
{
try

View File

@ -7,8 +7,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{
public class DynamicTable
{
private readonly HeaderField[] _buffer;
private int _maxSize = 4096;
private HeaderField[] _buffer;
private int _maxSize;
private int _size;
private int _count;
private int _insertIndex;
@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
public DynamicTable(int maxSize)
{
_buffer = new HeaderField[maxSize];
_buffer = new HeaderField[maxSize / HeaderField.RfcOverhead];
_maxSize = maxSize;
}
@ -24,6 +24,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
public int Size => _size;
public int MaxSize => _maxSize;
public HeaderField this[int index]
{
get
@ -37,33 +39,53 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
}
}
public void Insert(string name, string value)
public void Insert(Span<byte> name, Span<byte> value)
{
var entrySize = name.Length + value.Length + 32;
EnsureSize(_maxSize - entrySize);
var entryLength = HeaderField.GetLength(name.Length, value.Length);
EnsureAvailable(entryLength);
if (_maxSize < entrySize)
if (entryLength > _maxSize)
{
throw new InvalidOperationException($"Unable to add entry of size {entrySize} to dynamic table of size {_maxSize}.");
// http://httpwg.org/specs/rfc7541.html#rfc.section.4.4
// It is not an error to attempt to add an entry that is larger than the maximum size;
// an attempt to add an entry larger than the maximum size causes the table to be emptied
// of all existing entries and results in an empty table.
return;
}
_buffer[_insertIndex] = new HeaderField(name, value);
var entry = new HeaderField(name, value);
_buffer[_insertIndex] = entry;
_insertIndex = (_insertIndex + 1) % _buffer.Length;
_size += entrySize;
_size += entry.Length;
_count++;
}
public void Resize(int maxSize)
{
_maxSize = maxSize;
EnsureSize(_maxSize);
if (maxSize > _maxSize)
{
var newBuffer = new HeaderField[maxSize / HeaderField.RfcOverhead];
for (var i = 0; i < Count; i++)
{
newBuffer[i] = _buffer[i];
}
_buffer = newBuffer;
_maxSize = maxSize;
}
else
{
_maxSize = maxSize;
EnsureAvailable(0);
}
}
public void EnsureSize(int size)
private void EnsureAvailable(int available)
{
while (_count > 0 && _size > size)
while (_count > 0 && _maxSize - _size < available)
{
_size -= _buffer[_removeIndex].Name.Length + _buffer[_removeIndex].Value.Length + 32;
_size -= _buffer[_removeIndex].Length;
_count--;
_removeIndex = (_removeIndex + 1) % _buffer.Length;
}

View File

@ -2,10 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{
@ -22,14 +19,57 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
HeaderValueLength,
HeaderValueLengthContinue,
HeaderValue,
DynamicTableSize
DynamicTableSizeUpdate
}
// TODO: add new configurable limit
public const int MaxStringOctets = 4096;
// 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 LiteralHeaderFieldWithIncrementalIndexingMask = 0x40;
private const byte LiteralHeaderFieldWithoutIndexingMask = 0x00;
private const byte LiteralHeaderFieldNeverIndexedMask = 0x10;
private const byte DynamicTableSizeUpdateMask = 0x20;
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;
@ -39,44 +79,67 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
private const int DynamicTableSizeUpdatePrefix = 5;
private const int StringLengthPrefix = 7;
private readonly DynamicTable _dynamicTable = new DynamicTable(4096);
private readonly int _maxDynamicTableSize;
private readonly DynamicTable _dynamicTable;
private readonly IntegerDecoder _integerDecoder = new IntegerDecoder();
private readonly byte[] _stringOctets = new byte[MaxStringOctets];
private readonly byte[] _headerNameOctets = new byte[MaxStringOctets];
private readonly byte[] _headerValueOctets = new byte[MaxStringOctets];
private State _state = State.Ready;
// TODO: add new HTTP/2 header size limit and allocate accordingly
private byte[] _stringOctets = new byte[Http2Frame.MinAllowedMaxFrameSize];
private string _headerName = string.Empty;
private string _headerValue = string.Empty;
private int _stringLength;
private byte[] _headerName;
private int _stringIndex;
private int _stringLength;
private int _headerNameLength;
private int _headerValueLength;
private bool _index;
private bool _huffman;
public void Decode(Span<byte> data, IHeaderDictionary headers)
public HPackDecoder(int maxDynamicTableSize)
: this(maxDynamicTableSize, new DynamicTable(maxDynamicTableSize))
{
_maxDynamicTableSize = maxDynamicTableSize;
}
// For testing.
internal HPackDecoder(int maxDynamicTableSize, DynamicTable dynamicTable)
{
_maxDynamicTableSize = maxDynamicTableSize;
_dynamicTable = dynamicTable;
}
public void Decode(Span<byte> data, bool endHeaders, IHttpHeadersHandler handler)
{
for (var i = 0; i < data.Length; i++)
{
OnByte(data[i], headers);
OnByte(data[i], handler);
}
if (endHeaders && _state != State.Ready)
{
throw new HPackDecodingException(CoreStrings.HPackErrorIncompleteHeaderBlock);
}
}
public void OnByte(byte b, IHeaderDictionary headers)
public void OnByte(byte b, IHttpHeadersHandler handler)
{
switch (_state)
{
case State.Ready:
if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldMask)
if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation)
{
if (_integerDecoder.BeginDecode((byte)(b & ~IndexedHeaderFieldMask), IndexedHeaderFieldPrefix))
var val = b & ~IndexedHeaderFieldMask;
if (_integerDecoder.BeginDecode((byte)val, IndexedHeaderFieldPrefix))
{
OnIndexedHeaderField(_integerDecoder.Value, headers);
OnIndexedHeaderField(_integerDecoder.Value, handler);
}
else
{
_state = State.HeaderFieldIndex;
}
}
else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingMask)
else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingRepresentation)
{
_index = true;
var val = b & ~LiteralHeaderFieldWithIncrementalIndexingMask;
@ -94,7 +157,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
_state = State.HeaderNameIndex;
}
}
else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingMask)
else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingRepresentation)
{
_index = false;
var val = b & ~LiteralHeaderFieldWithoutIndexingMask;
@ -112,7 +175,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
_state = State.HeaderNameIndex;
}
}
else if ((b & LiteralHeaderFieldNeverIndexedMask) == LiteralHeaderFieldNeverIndexedMask)
else if ((b & LiteralHeaderFieldNeverIndexedMask) == LiteralHeaderFieldNeverIndexedRepresentation)
{
_index = false;
var val = b & ~LiteralHeaderFieldNeverIndexedMask;
@ -130,7 +193,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
_state = State.HeaderNameIndex;
}
}
else if ((b & DynamicTableSizeUpdateMask) == DynamicTableSizeUpdateMask)
else if ((b & DynamicTableSizeUpdateMask) == DynamicTableSizeUpdateRepresentation)
{
if (_integerDecoder.BeginDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix))
{
@ -139,19 +202,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
}
else
{
_state = State.DynamicTableSize;
_state = State.DynamicTableSizeUpdate;
}
}
else
{
throw new InvalidOperationException();
// Can't happen
throw new HPackDecodingException($"Byte value {b} does not encode a valid header field representation.");
}
break;
case State.HeaderFieldIndex:
if (_integerDecoder.Decode(b))
{
OnIndexedHeaderField(_integerDecoder.Value, headers);
OnIndexedHeaderField(_integerDecoder.Value, handler);
}
break;
@ -163,7 +227,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
break;
case State.HeaderNameLength:
_huffman = (b & HuffmanMask) == HuffmanMask;
_huffman = (b & HuffmanMask) != 0;
if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix))
{
@ -187,12 +251,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
if (_stringIndex == _stringLength)
{
_headerName = OnString(nextState: State.HeaderValueLength);
OnString(nextState: State.HeaderValueLength);
}
break;
case State.HeaderValueLength:
_huffman = (b & HuffmanMask) == HuffmanMask;
_huffman = (b & HuffmanMask) != 0;
if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix))
{
@ -216,20 +280,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
if (_stringIndex == _stringLength)
{
_headerValue = OnString(nextState: State.Ready);
headers.Append(_headerName, _headerValue);
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(_headerName, _headerValue);
_dynamicTable.Insert(headerNameSpan, headerValueSpan);
}
}
break;
case State.DynamicTableSize:
case State.DynamicTableSizeUpdate:
if (_integerDecoder.Decode(b))
{
// TODO: validate that it's less than what's defined via SETTINGS
if (_integerDecoder.Value > _maxDynamicTableSize)
{
throw new HPackDecodingException(
CoreStrings.FormatHPackErrorDynamicTableSizeUpdateTooLarge(_integerDecoder.Value, _maxDynamicTableSize));
}
_dynamicTable.Resize(_integerDecoder.Value);
_state = State.Ready;
}
@ -237,14 +310,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
break;
default:
// Can't happen
throw new InvalidOperationException();
throw new HPackDecodingException("The HPACK decoder reached an invalid state.");
}
}
private void OnIndexedHeaderField(int index, IHeaderDictionary headers)
private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler)
{
var header = GetHeader(index);
headers.Append(header.Name, header.Value);
handler.OnHeader(new Span<byte>(header.Name), new Span<byte>(header.Value));
_state = State.Ready;
}
@ -252,26 +325,69 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{
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 string OnString(State nextState)
private void OnString(State nextState)
{
int Decode(byte[] dst)
{
if (_huffman)
{
return Huffman.Decode(_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;
return _huffman
? Huffman.Decode(_stringOctets, 0, _stringLength)
: Encoding.ASCII.GetString(_stringOctets, 0, _stringLength);
}
private HeaderField GetHeader(int index) => index <= StaticTable.Instance.Length
? StaticTable.Instance[index - 1]
: _dynamicTable[index - StaticTable.Instance.Length - 1];
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);
}
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{
public class HPackDecodingException : Exception
{
public HPackDecodingException(string message)
: base(message)
{
}
public HPackDecodingException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@ -1,17 +1,30 @@
// 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
{
public struct HeaderField
{
public HeaderField(string name, string value)
// http://httpwg.org/specs/rfc7541.html#rfc.section.4.1
public const int RfcOverhead = 32;
public HeaderField(Span<byte> name, Span<byte> value)
{
Name = name;
Value = value;
Name = new byte[name.Length];
name.CopyTo(Name);
Value = new byte[value.Length];
value.CopyTo(Value);
}
public string Name { get; }
public string Value { get; }
public byte[] Name { get; }
public byte[] Value { get; }
public int Length => GetLength(Name.Length, Value.Length);
public static int GetLength(int nameLength, int valueLenth) => nameLength + valueLenth + 32;
}
}

View File

@ -2,8 +2,6 @@
// 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.Text;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{
@ -301,45 +299,108 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
return _encodingTable[data];
}
public static string Decode(byte[] data, int offset, int count)
/// <summary>
/// Decodes a Huffman encoded string from a byte array.
/// </summary>
/// <param name="src">The source byte array containing the encoded data.</param>
/// <param name="offset">The offset in the byte array where the coded data starts.</param>
/// <param name="count">The number of bytes to decode.</param>
/// <param name="dst">The destination byte array to store the decoded data.</param>
/// <returns>The number of decoded symbols.</returns>
public static int Decode(byte[] src, int offset, int count, byte[] dst)
{
var sb = new StringBuilder();
var i = offset;
var j = 0;
var lastDecodedBits = 0;
while (i < count)
{
var next = (uint)(data[i] << 24 + lastDecodedBits);
next |= (i + 1 < data.Length ? (uint)(data[i + 1] << 16 + lastDecodedBits) : 0);
next |= (i + 2 < data.Length ? (uint)(data[i + 2] << 8 + lastDecodedBits) : 0);
next |= (i + 3 < data.Length ? (uint)(data[i + 3] << lastDecodedBits) : 0);
var 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);
var ones = (uint)(int.MinValue >> (8 - lastDecodedBits - 1));
if (i == count - 1 && (next & ones) == ones)
if (i == count - 1 && lastDecodedBits > 0 && (next & ones) == ones)
{
// Padding
// The remaining 7 or less bits are all 1, which is padding.
// We specifically check that lastDecodedBits > 0 because padding
// longer than 7 bits should be treated as a decoding error.
// http://httpwg.org/specs/rfc7541.html#rfc.section.5.2
break;
}
var ch = Decode(next, out var decodedBits);
sb.Append((char)ch);
// 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) + (count - i - 1) * 8);
var ch = Decode(next, validBits, out var decodedBits);
if (ch == -1)
{
// No valid symbol could be decoded with the bits in next
throw new HuffmanDecodingException(CoreStrings.HPackHuffmanErrorIncomplete);
}
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);
}
if (j == dst.Length)
{
throw new HuffmanDecodingException(CoreStrings.HPackHuffmanErrorDestinationTooSmall);
}
dst[j++] = (byte)ch;
// If we crossed a byte boundary, advance i so we start at the next byte that's not fully decoded.
lastDecodedBits += decodedBits;
i += lastDecodedBits / 8;
// Modulo 8 since we only care about how many bits were decoded in the last byte that we processed.
lastDecodedBits %= 8;
}
return sb.ToString();
return j;
}
public static int Decode(uint data, out int decodedBits)
/// <summary>
/// Decodes a single symbol from a 32-bit word.
/// </summary>
/// <param name="data">A 32-bit word containing a Huffman encoded symbol.</param>
/// <param name="validBits">
/// The number of bits in <paramref name="data"/> that may contain an encoded symbol.
/// This is not the exact number of bits that encode the symbol. Instead, it prevents
/// decoding the lower bits of <paramref name="data"/> if they don't contain any
/// encoded data.
/// </param>
/// <param name="decodedBits">The number of bits decoded from <paramref name="data"/>.</param>
/// <returns>The decoded symbol.</returns>
public static int Decode(uint data, int validBits, out int decodedBits)
{
// The code below implements the decoding logic for a canonical Huffman code.
//
// To decode a symbol, we scan the decoding table, which is sorted by ascending symbol bit length.
// For each bit length b, we determine the maximum b-bit encoded value, plus one (that is codeMax).
// This is done with the following logic:
//
// if we're at the first entry in the table,
// codeMax = the # of symbols encoded in b bits
// else,
// left-shift codeMax by the difference between b and the previous entry's bit length,
// then increment codeMax by the # of symbols encoded in b bits
//
// Next, we look at the value v encoded in the highest b bits of data. If v is less than codeMax,
// those bits correspond to a Huffman encoded symbol. We find the corresponding decoded
// symbol in the list of values associated with bit length b in the decoding table by indexing it
// with codeMax - v.
var codeMax = 0;
for (var i = 0; i < _decodingTable.Length; i++)
for (var i = 0; i < _decodingTable.Length && _decodingTable[i].codeLength <= validBits; i++)
{
var (codeLength, codes) = _decodingTable[i];
var mask = int.MinValue >> (codeLength - 1);
if (i > 0)
{
@ -348,6 +409,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
codeMax += codes.Length;
var mask = int.MinValue >> (codeLength - 1);
var masked = (data & mask) >> (32 - codeLength);
if (masked < codeMax)
@ -357,7 +419,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
}
}
throw new Exception();
decodedBits = 0;
return -1;
}
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{
public class HuffmanDecodingException : Exception
{
public HuffmanDecodingException(string message)
: base(message)
{
}
}
}

View File

@ -1,9 +1,6 @@
// 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
{
public class IntegerDecoder

View File

@ -1,8 +1,8 @@
// 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.Text;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{
@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
public static StaticTable Instance => _instance;
public int Length => _staticTable.Length;
public int Count => _staticTable.Length;
public HeaderField this[int index] => _staticTable[index];
@ -35,67 +35,70 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
private readonly HeaderField[] _staticTable = new HeaderField[]
{
new HeaderField(":authority", ""),
new HeaderField(":method", "GET"),
new HeaderField(":method", "POST"),
new HeaderField(":path", "/"),
new HeaderField(":path", "/index.html"),
new HeaderField(":scheme", "http"),
new HeaderField(":scheme", "https"),
new HeaderField(":status", "200"),
new HeaderField(":status", "204"),
new HeaderField(":status", "206"),
new HeaderField(":status", "304"),
new HeaderField(":status", "400"),
new HeaderField(":status", "404"),
new HeaderField(":status", "500"),
new HeaderField("accept-charset", ""),
new HeaderField("accept-encoding", "gzip, deflate"),
new HeaderField("accept-language", ""),
new HeaderField("accept-ranges", ""),
new HeaderField("accept", ""),
new HeaderField("access-control-allow-origin", ""),
new HeaderField("age", ""),
new HeaderField("allow", ""),
new HeaderField("authorization", ""),
new HeaderField("cache-control", ""),
new HeaderField("content-disposition", ""),
new HeaderField("content-encoding", ""),
new HeaderField("content-language", ""),
new HeaderField("content-length", ""),
new HeaderField("content-location", ""),
new HeaderField("content-range", ""),
new HeaderField("content-type", ""),
new HeaderField("cookie", ""),
new HeaderField("date", ""),
new HeaderField("etag", ""),
new HeaderField("expect", ""),
new HeaderField("expires", ""),
new HeaderField("from", ""),
new HeaderField("host", ""),
new HeaderField("if-match", ""),
new HeaderField("if-modified-since", ""),
new HeaderField("if-none-match", ""),
new HeaderField("if-range", ""),
new HeaderField("if-unmodifiedsince", ""),
new HeaderField("last-modified", ""),
new HeaderField("link", ""),
new HeaderField("location", ""),
new HeaderField("max-forwards", ""),
new HeaderField("proxy-authenticate", ""),
new HeaderField("proxy-authorization", ""),
new HeaderField("range", ""),
new HeaderField("referer", ""),
new HeaderField("refresh", ""),
new HeaderField("retry-after", ""),
new HeaderField("server", ""),
new HeaderField("set-cookie", ""),
new HeaderField("strict-transport-security", ""),
new HeaderField("transfer-encoding", ""),
new HeaderField("user-agent", ""),
new HeaderField("vary", ""),
new HeaderField("via", ""),
new HeaderField("www-authenticate", "")
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-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

@ -16,7 +16,7 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
public class Http2Connection : ITimeoutControl, IHttp2StreamLifetimeHandler
public class Http2Connection : ITimeoutControl, IHttp2StreamLifetimeHandler, IHttpHeadersHandler
{
public static byte[] ClientPreface { get; } = Encoding.ASCII.GetBytes("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
_context = context;
_frameWriter = new Http2FrameWriter(context.Transport.Output, context.Application.Input);
_hpackDecoder = new HPackDecoder();
_hpackDecoder = new HPackDecoder((int)_serverSettings.HeaderTableSize);
}
public string ConnectionId => _context.ConnectionId;
@ -141,6 +141,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
error = ex;
errorCode = ex.ErrorCode;
}
catch (HPackDecodingException ex)
{
// TODO: log
error = ex;
errorCode = Http2ErrorCode.COMPRESSION_ERROR;
}
catch (Exception ex)
{
// TODO: log
@ -350,9 +356,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_streams[_incomingFrame.StreamId] = _currentHeadersStream;
_hpackDecoder.Decode(_incomingFrame.HeadersPayload, _currentHeadersStream.RequestHeaders);
var endHeaders = (_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS;
_hpackDecoder.Decode(_incomingFrame.HeadersPayload, endHeaders, handler: this);
if ((_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS)
if (endHeaders)
{
_highestOpenedStreamId = _incomingFrame.StreamId;
_ = _currentHeadersStream.ProcessRequestsAsync();
@ -525,9 +532,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR);
}
_hpackDecoder.Decode(_incomingFrame.HeadersPayload, _currentHeadersStream.RequestHeaders);
var endHeaders = (_incomingFrame.ContinuationFlags & Http2ContinuationFrameFlags.END_HEADERS) == Http2ContinuationFrameFlags.END_HEADERS;
_hpackDecoder.Decode(_incomingFrame.HeadersPayload, endHeaders, handler: this);
if ((_incomingFrame.ContinuationFlags & Http2ContinuationFrameFlags.END_HEADERS) == Http2ContinuationFrameFlags.END_HEADERS)
if (endHeaders)
{
_highestOpenedStreamId = _currentHeadersStream.StreamId;
_ = _currentHeadersStream.ProcessRequestsAsync();
@ -571,6 +579,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_streams.TryRemove(streamId, out _);
}
public void OnHeader(Span<byte> name, Span<byte> value)
{
_currentHeadersStream.OnHeader(name, value);
}
void ITimeoutControl.SetTimeout(long ticks, TimeoutAction timeoutAction)
{
}

View File

@ -1144,6 +1144,118 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatEndPointHttp2NotNegotiated()
=> GetString("EndPointHttp2NotNegotiated");
/// <summary>
/// A dynamic table size of {size} octets is greater than the configured maximum size of {maxSize} octets.
/// </summary>
internal static string HPackErrorDynamicTableSizeUpdateTooLarge
{
get => GetString("HPackErrorDynamicTableSizeUpdateTooLarge");
}
/// <summary>
/// A dynamic table size of {size} octets is greater than the configured maximum size of {maxSize} octets.
/// </summary>
internal static string FormatHPackErrorDynamicTableSizeUpdateTooLarge(object size, object maxSize)
=> string.Format(CultureInfo.CurrentCulture, GetString("HPackErrorDynamicTableSizeUpdateTooLarge", "size", "maxSize"), size, maxSize);
/// <summary>
/// Index {index} is outside the bounds of the header field table.
/// </summary>
internal static string HPackErrorIndexOutOfRange
{
get => GetString("HPackErrorIndexOutOfRange");
}
/// <summary>
/// Index {index} is outside the bounds of the header field table.
/// </summary>
internal static string FormatHPackErrorIndexOutOfRange(object index)
=> string.Format(CultureInfo.CurrentCulture, GetString("HPackErrorIndexOutOfRange", "index"), index);
/// <summary>
/// Input data could not be fully decoded.
/// </summary>
internal static string HPackHuffmanErrorIncomplete
{
get => GetString("HPackHuffmanErrorIncomplete");
}
/// <summary>
/// Input data could not be fully decoded.
/// </summary>
internal static string FormatHPackHuffmanErrorIncomplete()
=> GetString("HPackHuffmanErrorIncomplete");
/// <summary>
/// Input data contains the EOS symbol.
/// </summary>
internal static string HPackHuffmanErrorEOS
{
get => GetString("HPackHuffmanErrorEOS");
}
/// <summary>
/// Input data contains the EOS symbol.
/// </summary>
internal static string FormatHPackHuffmanErrorEOS()
=> GetString("HPackHuffmanErrorEOS");
/// <summary>
/// The destination buffer is not large enough to store the decoded data.
/// </summary>
internal static string HPackHuffmanErrorDestinationTooSmall
{
get => GetString("HPackHuffmanErrorDestinationTooSmall");
}
/// <summary>
/// The destination buffer is not large enough to store the decoded data.
/// </summary>
internal static string FormatHPackHuffmanErrorDestinationTooSmall()
=> GetString("HPackHuffmanErrorDestinationTooSmall");
/// <summary>
/// Huffman decoding error.
/// </summary>
internal static string HPackHuffmanError
{
get => GetString("HPackHuffmanError");
}
/// <summary>
/// Huffman decoding error.
/// </summary>
internal static string FormatHPackHuffmanError()
=> GetString("HPackHuffmanError");
/// <summary>
/// Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets.
/// </summary>
internal static string HPackStringLengthTooLarge
{
get => GetString("HPackStringLengthTooLarge");
}
/// <summary>
/// Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets.
/// </summary>
internal static string FormatHPackStringLengthTooLarge(object length, object maxStringLength)
=> string.Format(CultureInfo.CurrentCulture, GetString("HPackStringLengthTooLarge", "length", "maxStringLength"), length, maxStringLength);
/// <summary>
/// The header block was incomplete and could not be fully decoded.
/// </summary>
internal static string HPackErrorIncompleteHeaderBlock
{
get => GetString("HPackErrorIncompleteHeaderBlock");
}
/// <summary>
/// The header block was incomplete and could not be fully decoded.
/// </summary>
internal static string FormatHPackErrorIncompleteHeaderBlock()
=> GetString("HPackErrorIncompleteHeaderBlock");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -0,0 +1,158 @@
// 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 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

@ -0,0 +1,560 @@
// 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 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 Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class HPackDecoderTests : IHttpHeadersHandler
{
private const int DynamicTableInitialMaxSize = 4096;
// Indexed Header Field Representation - Static Table - Index 2 (:method: GET)
private static readonly byte[] _indexedHeaderStatic = new byte[] { 0x82 };
// Indexed Header Field Representation - Dynamic Table - Index 62 (first index in dynamic table)
private static readonly byte[] _indexedHeaderDynamic = new byte[] { 0xbe };
// Literal Header Field with Incremental Indexing Representation - New Name
private static readonly byte[] _literalHeaderFieldWithIndexingNewName = new byte[] { 0x40 };
// Literal Header Field with Incremental Indexing Representation - Indexed Name - Index 58 (user-agent)
private static readonly byte[] _literalHeaderFieldWithIndexingIndexedName = new byte[] { 0x7a };
// Literal Header Field without Indexing Representation - New Name
private static readonly byte[] _literalHeaderFieldWithoutIndexingNewName = new byte[] { 0x00 };
// Literal Header Field without Indexing Representation - Indexed Name - Index 58 (user-agent)
private static readonly byte[] _literalHeaderFieldWithoutIndexingIndexedName = new byte[] { 0x0f, 0x2b };
// Literal Header Field Never Indexed Representation - New Name
private static readonly byte[] _literalHeaderFieldNeverIndexedNewName = new byte[] { 0x10 };
// Literal Header Field Never Indexed Representation - Indexed Name - Index 58 (user-agent)
private static readonly byte[] _literalHeaderFieldNeverIndexedIndexedName = new byte[] { 0x1f, 0x2b };
private const string _userAgentString = "user-agent";
private static readonly byte[] _userAgentBytes = Encoding.ASCII.GetBytes(_userAgentString);
private const string _headerNameString = "new-header";
private static readonly byte[] _headerNameBytes = Encoding.ASCII.GetBytes(_headerNameString);
// n e w - h e a d e r *
// 10101000 10111110 00010110 10011100 10100011 10010000 10110110 01111111
private static readonly byte[] _headerNameHuffmanBytes = new byte[] { 0xa8, 0xbe, 0x16, 0x9c, 0xa3, 0x90, 0xb6, 0x7f };
private const string _headerValueString = "value";
private static readonly byte[] _headerValueBytes = Encoding.ASCII.GetBytes(_headerValueString);
// v a l u e *
// 11101110 00111010 00101101 00101111
private static readonly byte[] _headerValueHuffmanBytes = new byte [] { 0xee, 0x3a, 0x2d, 0x2f };
private static readonly byte[] _headerName = new byte[] { (byte)_headerNameBytes.Length }
.Concat(_headerNameBytes)
.ToArray();
private static readonly byte[] _headerNameHuffman = new byte[] { (byte)(0x80 | _headerNameHuffmanBytes.Length) }
.Concat(_headerNameHuffmanBytes)
.ToArray();
private static readonly byte[] _headerValue = new byte[] { (byte)_headerValueBytes.Length }
.Concat(_headerValueBytes)
.ToArray();
private static readonly byte[] _headerValueHuffman = new byte[] { (byte)(0x80 | _headerValueHuffmanBytes.Length) }
.Concat(_headerValueHuffmanBytes)
.ToArray();
// & *
// 11111000 11111111
private static readonly byte[] _huffmanLongPadding = new byte[] { 0x82, 0xf8, 0xff };
// EOS *
// 11111111 11111111 11111111 11111111
private static readonly byte[] _huffmanEos = new byte[] { 0x84, 0xff, 0xff, 0xff, 0xff };
private readonly DynamicTable _dynamicTable;
private readonly HPackDecoder _decoder;
private readonly Dictionary<string, string> _decodedHeaders = new Dictionary<string, string>();
public HPackDecoderTests()
{
_dynamicTable = new DynamicTable(DynamicTableInitialMaxSize);
_decoder = new HPackDecoder(DynamicTableInitialMaxSize, _dynamicTable);
}
void IHttpHeadersHandler.OnHeader(Span<byte> name, Span<byte> value)
{
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters();
}
[Fact]
public void DecodesIndexedHeaderField_StaticTable()
{
_decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: this);
Assert.Equal("GET", _decodedHeaders[":method"]);
}
[Fact]
public void DecodesIndexedHeaderField_DynamicTable()
{
// Add the header to the dynamic table
_dynamicTable.Insert(_headerNameBytes, _headerValueBytes);
// Index it
_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(_indexedHeaderDynamic, endHeaders: true, handler: this));
Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message);
Assert.Empty(_decodedHeaders);
}
[Fact]
public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName()
{
var encoded = _literalHeaderFieldWithIndexingNewName
.Concat(_headerName)
.Concat(_headerValue)
.ToArray();
TestDecodeWithIndexing(encoded, _headerNameString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedName()
{
var encoded = _literalHeaderFieldWithIndexingNewName
.Concat(_headerNameHuffman)
.Concat(_headerValue)
.ToArray();
TestDecodeWithIndexing(encoded, _headerNameString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedValue()
{
var encoded = _literalHeaderFieldWithIndexingNewName
.Concat(_headerName)
.Concat(_headerValueHuffman)
.ToArray();
TestDecodeWithIndexing(encoded, _headerNameString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedNameAndValue()
{
var encoded = _literalHeaderFieldWithIndexingNewName
.Concat(_headerNameHuffman)
.Concat(_headerValueHuffman)
.ToArray();
TestDecodeWithIndexing(encoded, _headerNameString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldWithIncrementalIndexing_IndexedName()
{
var encoded = _literalHeaderFieldWithIndexingIndexedName
.Concat(_headerValue)
.ToArray();
TestDecodeWithIndexing(encoded, _userAgentString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldWithIncrementalIndexing_IndexedName_HuffmanEncodedValue()
{
var encoded = _literalHeaderFieldWithIndexingIndexedName
.Concat(_headerValueHuffman)
.ToArray();
TestDecodeWithIndexing(encoded, _userAgentString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldWithIncrementalIndexing_IndexedName_OutOfRange_Error()
{
// 01 (Literal Header Field without Indexing Representation)
// 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 byte[] { 0x7e }, endHeaders: true, handler: this));
Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message);
Assert.Empty(_decodedHeaders);
}
[Fact]
public void DecodesLiteralHeaderFieldWithoutIndexing_NewName()
{
var encoded = _literalHeaderFieldWithoutIndexingNewName
.Concat(_headerName)
.Concat(_headerValue)
.ToArray();
TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedName()
{
var encoded = _literalHeaderFieldWithoutIndexingNewName
.Concat(_headerNameHuffman)
.Concat(_headerValue)
.ToArray();
TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedValue()
{
var encoded = _literalHeaderFieldWithoutIndexingNewName
.Concat(_headerName)
.Concat(_headerValueHuffman)
.ToArray();
TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedNameAndValue()
{
var encoded = _literalHeaderFieldWithoutIndexingNewName
.Concat(_headerNameHuffman)
.Concat(_headerValueHuffman)
.ToArray();
TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldWithoutIndexing_IndexedName()
{
var encoded = _literalHeaderFieldWithoutIndexingIndexedName
.Concat(_headerValue)
.ToArray();
TestDecodeWithoutIndexing(encoded, _userAgentString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldWithoutIndexing_IndexedName_HuffmanEncodedValue()
{
var encoded = _literalHeaderFieldWithoutIndexingIndexedName
.Concat(_headerValueHuffman)
.ToArray();
TestDecodeWithoutIndexing(encoded, _userAgentString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldWithoutIndexing_IndexedName_OutOfRange_Error()
{
// 0000 (Literal Header Field without Indexing Representation)
// 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 byte[] { 0x0f, 0x2f }, endHeaders: true, handler: this));
Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message);
Assert.Empty(_decodedHeaders);
}
[Fact]
public void DecodesLiteralHeaderFieldNeverIndexed_NewName()
{
var encoded = _literalHeaderFieldNeverIndexedNewName
.Concat(_headerName)
.Concat(_headerValue)
.ToArray();
TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedName()
{
var encoded = _literalHeaderFieldNeverIndexedNewName
.Concat(_headerNameHuffman)
.Concat(_headerValue)
.ToArray();
TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedValue()
{
var encoded = _literalHeaderFieldNeverIndexedNewName
.Concat(_headerName)
.Concat(_headerValueHuffman)
.ToArray();
TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedNameAndValue()
{
var encoded = _literalHeaderFieldNeverIndexedNewName
.Concat(_headerNameHuffman)
.Concat(_headerValueHuffman)
.ToArray();
TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldNeverIndexed_IndexedName()
{
// 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
.Concat(_headerValue)
.ToArray();
TestDecodeWithoutIndexing(encoded, _userAgentString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldNeverIndexed_IndexedName_HuffmanEncodedValue()
{
// 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
.Concat(_headerValueHuffman)
.ToArray();
TestDecodeWithoutIndexing(encoded, _userAgentString, _headerValueString);
}
[Fact]
public void DecodesLiteralHeaderFieldNeverIndexed_IndexedName_OutOfRange_Error()
{
// 0001 (Literal Header Field Never Indexed Representation)
// 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 byte[] { 0x1f, 0x2f }, endHeaders: true, handler: this));
Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message);
Assert.Empty(_decodedHeaders);
}
[Fact]
public void DecodesDynamicTableSizeUpdate()
{
// 001 (Dynamic Table Size Update)
// 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation)
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
_decoder.Decode(new byte[] { 0x3e }, endHeaders: true, handler: this);
Assert.Equal(30, _dynamicTable.MaxSize);
Assert.Empty(_decodedHeaders);
}
[Fact]
public void DecodesDynamicTableSizeUpdate_GreaterThanLimit_Error()
{
// 001 (Dynamic Table Size Update)
// 11111 11100010 00011111 (4097 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation)
Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize);
var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(new byte[] { 0x3f, 0xe2, 0x1f }, endHeaders: true, handler: this));
Assert.Equal(CoreStrings.FormatHPackErrorDynamicTableSizeUpdateTooLarge(4097, DynamicTableInitialMaxSize), exception.Message);
Assert.Empty(_decodedHeaders);
}
[Fact]
public void DecodesStringLength_GreaterThanLimit_Error()
{
var encoded = _literalHeaderFieldWithoutIndexingNewName
.Concat(new byte[] { 0xff, 0x82, 0x1f }) // 4097 encoded with 7-bit prefix
.ToArray();
var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: this));
Assert.Equal(CoreStrings.FormatHPackStringLengthTooLarge(4097, HPackDecoder.MaxStringOctets), exception.Message);
Assert.Empty(_decodedHeaders);
}
public static readonly TheoryData<byte[]> _incompleteHeaderBlockData = new TheoryData<byte[]>
{
// Indexed Header Field Representation - incomplete index encoding
new byte[] { 0xff },
// Literal Header Field with Incremental Indexing Representation - New Name - incomplete header name length encoding
new byte[] { 0x40, 0x7f },
// Literal Header Field with Incremental Indexing Representation - New Name - incomplete header name
new byte[] { 0x40, 0x01 },
new byte[] { 0x40, 0x02, 0x61 },
// Literal Header Field with Incremental Indexing Representation - New Name - incomplete header value length encoding
new byte[] { 0x40, 0x01, 0x61, 0x7f },
// Literal Header Field with Incremental Indexing Representation - New Name - incomplete header value
new byte[] { 0x40, 0x01, 0x61, 0x01 },
new byte[] { 0x40, 0x01, 0x61, 0x02, 0x61 },
// Literal Header Field with Incremental Indexing Representation - Indexed Name - incomplete index encoding
new byte[] { 0x7f },
// Literal Header Field with Incremental Indexing Representation - Indexed Name - incomplete header value length encoding
new byte[] { 0x7a, 0xff },
// Literal Header Field with Incremental Indexing Representation - Indexed Name - incomplete header value
new byte[] { 0x7a, 0x01 },
new byte[] { 0x7a, 0x02, 0x61 },
// Literal Header Field without Indexing - New Name - incomplete header name length encoding
new byte[] { 0x00, 0xff },
// Literal Header Field without Indexing - New Name - incomplete header name
new byte[] { 0x00, 0x01 },
new byte[] { 0x00, 0x02, 0x61 },
// Literal Header Field without Indexing - New Name - incomplete header value length encoding
new byte[] { 0x00, 0x01, 0x61, 0xff },
// Literal Header Field without Indexing - New Name - incomplete header value
new byte[] { 0x00, 0x01, 0x61, 0x01 },
new byte[] { 0x00, 0x01, 0x61, 0x02, 0x61 },
// Literal Header Field without Indexing Representation - Indexed Name - incomplete index encoding
new byte[] { 0x0f },
// Literal Header Field without Indexing Representation - Indexed Name - incomplete header value length encoding
new byte[] { 0x02, 0xff },
// Literal Header Field without Indexing Representation - Indexed Name - incomplete header value
new byte[] { 0x02, 0x01 },
new byte[] { 0x02, 0x02, 0x61 },
// Literal Header Field Never Indexed - New Name - incomplete header name length encoding
new byte[] { 0x10, 0xff },
// Literal Header Field Never Indexed - New Name - incomplete header name
new byte[] { 0x10, 0x01 },
new byte[] { 0x10, 0x02, 0x61 },
// Literal Header Field Never Indexed - New Name - incomplete header value length encoding
new byte[] { 0x10, 0x01, 0x61, 0xff },
// Literal Header Field Never Indexed - New Name - incomplete header value
new byte[] { 0x10, 0x01, 0x61, 0x01 },
new byte[] { 0x10, 0x01, 0x61, 0x02, 0x61 },
// Literal Header Field Never Indexed Representation - Indexed Name - incomplete index encoding
new byte[] { 0x1f },
// Literal Header Field Never Indexed Representation - Indexed Name - incomplete header value length encoding
new byte[] { 0x12, 0xff },
// Literal Header Field Never Indexed Representation - Indexed Name - incomplete header value
new byte[] { 0x12, 0x01 },
new byte[] { 0x12, 0x02, 0x61 },
// Dynamic Table Size Update - incomplete max size encoding
new byte[] { 0x3f }
};
[Theory]
[MemberData(nameof(_incompleteHeaderBlockData))]
public void DecodesIncompleteHeaderBlock_Error(byte[] encoded)
{
var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: this));
Assert.Equal(CoreStrings.HPackErrorIncompleteHeaderBlock, exception.Message);
Assert.Empty(_decodedHeaders);
}
public static readonly TheoryData<byte[]> _huffmanDecodingErrorData = new TheoryData<byte[]>
{
// Invalid Huffman encoding in header name
_literalHeaderFieldWithIndexingNewName.Concat(_huffmanLongPadding).ToArray(),
_literalHeaderFieldWithIndexingNewName.Concat(_huffmanEos).ToArray(),
_literalHeaderFieldWithoutIndexingNewName.Concat(_huffmanLongPadding).ToArray(),
_literalHeaderFieldWithoutIndexingNewName.Concat(_huffmanEos).ToArray(),
_literalHeaderFieldNeverIndexedNewName.Concat(_huffmanLongPadding).ToArray(),
_literalHeaderFieldNeverIndexedNewName.Concat(_huffmanEos).ToArray(),
// Invalid Huffman encoding in header value
_literalHeaderFieldWithIndexingIndexedName.Concat(_huffmanLongPadding).ToArray(),
_literalHeaderFieldWithIndexingIndexedName.Concat(_huffmanEos).ToArray(),
_literalHeaderFieldWithoutIndexingIndexedName.Concat(_huffmanLongPadding).ToArray(),
_literalHeaderFieldWithoutIndexingIndexedName.Concat(_huffmanEos).ToArray(),
_literalHeaderFieldNeverIndexedIndexedName.Concat(_huffmanLongPadding).ToArray(),
_literalHeaderFieldNeverIndexedIndexedName.Concat(_huffmanEos).ToArray()
};
[Theory]
[MemberData(nameof(_huffmanDecodingErrorData))]
public void WrapsHuffmanDecodingExceptionInHPackDecodingException(byte[] encoded)
{
var exception = Assert.Throws<HPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: this));
Assert.Equal(CoreStrings.HPackHuffmanError, exception.Message);
Assert.IsType<HuffmanDecodingException>(exception.InnerException);
Assert.Empty(_decodedHeaders);
}
private void TestDecodeWithIndexing(byte[] encoded, string expectedHeaderName, string expectedHeaderValue)
{
TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: true);
}
private void TestDecodeWithoutIndexing(byte[] encoded, string expectedHeaderName, string expectedHeaderValue)
{
TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: false);
}
private void TestDecode(byte[] encoded, string expectedHeaderName, string expectedHeaderValue, bool expectDynamicTableEntry)
{
Assert.Equal(0, _dynamicTable.Count);
Assert.Equal(0, _dynamicTable.Size);
_decoder.Decode(encoded, endHeaders: true, handler: this);
Assert.Equal(expectedHeaderValue, _decodedHeaders[expectedHeaderName]);
if (expectDynamicTableEntry)
{
Assert.Equal(1, _dynamicTable.Count);
Assert.Equal(expectedHeaderName, Encoding.ASCII.GetString(_dynamicTable[0].Name));
Assert.Equal(expectedHeaderValue, Encoding.ASCII.GetString(_dynamicTable[0].Value));
Assert.Equal(expectedHeaderName.Length + expectedHeaderValue.Length + 32, _dynamicTable.Size);
}
else
{
Assert.Equal(0, _dynamicTable.Count);
Assert.Equal(0, _dynamicTable.Size);
}
}
}
}

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Text;
@ -15,19 +14,18 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class Http2ConnectionTests : IDisposable
public class Http2ConnectionTests : IDisposable, IHttpHeadersHandler
{
private static readonly string _largeHeaderA = new string('a', Http2Frame.MinAllowedMaxFrameSize - Http2Frame.HeaderLength - 8);
private static readonly string _largeHeaderValue = new string('a', HPackDecoder.MaxStringOctets);
private static readonly string _largeHeaderB = new string('b', Http2Frame.MinAllowedMaxFrameSize - Http2Frame.HeaderLength - 8);
private static readonly IEnumerable<KeyValuePair<string, string>> _postRequestHeaders = new []
private static readonly IEnumerable<KeyValuePair<string, string>> _postRequestHeaders = new[]
{
new KeyValuePair<string, string>(":method", "POST"),
new KeyValuePair<string, string>(":path", "/"),
@ -35,7 +33,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
new KeyValuePair<string, string>(":scheme", "https"),
};
private static readonly IEnumerable<KeyValuePair<string, string>> _browserRequestHeaders = new []
private static readonly IEnumerable<KeyValuePair<string, string>> _browserRequestHeaders = new[]
{
new KeyValuePair<string, string>(":method", "GET"),
new KeyValuePair<string, string>(":path", "/"),
@ -48,23 +46,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
new KeyValuePair<string, string>("upgrade-insecure-requests", "1"),
};
private static readonly IEnumerable<KeyValuePair<string, string>> _oneContinuationRequestHeaders = new []
private static readonly IEnumerable<KeyValuePair<string, string>> _oneContinuationRequestHeaders = new[]
{
new KeyValuePair<string, string>(":method", "GET"),
new KeyValuePair<string, string>(":path", "/"),
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
new KeyValuePair<string, string>(":scheme", "https"),
new KeyValuePair<string, string>("a", _largeHeaderA)
new KeyValuePair<string, string>("a", _largeHeaderValue),
new KeyValuePair<string, string>("b", _largeHeaderValue),
new KeyValuePair<string, string>("c", _largeHeaderValue),
new KeyValuePair<string, string>("d", _largeHeaderValue)
};
private static readonly IEnumerable<KeyValuePair<string, string>> _twoContinuationsRequestHeaders = new []
private static readonly IEnumerable<KeyValuePair<string, string>> _twoContinuationsRequestHeaders = new[]
{
new KeyValuePair<string, string>(":method", "GET"),
new KeyValuePair<string, string>(":path", "/"),
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
new KeyValuePair<string, string>(":scheme", "https"),
new KeyValuePair<string, string>("a", _largeHeaderA),
new KeyValuePair<string, string>("b", _largeHeaderB)
new KeyValuePair<string, string>("a", _largeHeaderValue),
new KeyValuePair<string, string>("b", _largeHeaderValue),
new KeyValuePair<string, string>("c", _largeHeaderValue),
new KeyValuePair<string, string>("d", _largeHeaderValue),
new KeyValuePair<string, string>("e", _largeHeaderValue),
new KeyValuePair<string, string>("f", _largeHeaderValue),
new KeyValuePair<string, string>("g", _largeHeaderValue),
new KeyValuePair<string, string>("h", _largeHeaderValue)
};
private static readonly byte[] _helloBytes = Encoding.ASCII.GetBytes("hello");
@ -77,12 +84,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
private readonly (IPipeConnection Transport, IPipeConnection Application) _pair;
private readonly Http2ConnectionContext _connectionContext;
private readonly Http2Connection _connection;
private readonly HPackEncoder _hpackEncoder = new HPackEncoder();
private readonly HPackDecoder _hpackDecoder = new HPackDecoder();
private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings();
private readonly HPackEncoder _hpackEncoder = new HPackEncoder();
private readonly HPackDecoder _hpackDecoder;
private readonly ConcurrentDictionary<int, TaskCompletionSource<object>> _runningStreams = new ConcurrentDictionary<int, TaskCompletionSource<object>>();
private readonly Dictionary<string, string> _receivedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, string> _decodedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<int> _abortedStreamIds = new HashSet<int>();
private readonly object _abortedStreamIdsLock = new object();
@ -160,8 +168,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_largeHeadersApplication = context =>
{
context.Response.Headers["a"] = _largeHeaderA;
context.Response.Headers["b"] = _largeHeaderB;
foreach (var name in new[] { "a", "b", "c", "d", "e", "f", "g", "h" })
{
context.Response.Headers[name] = _largeHeaderValue;
}
return Task.CompletedTask;
};
@ -208,6 +218,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_runningStreams[streamIdFeature.StreamId].TrySetResult(null);
};
_hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize);
_connectionContext = new Http2ConnectionContext
{
ServiceContext = new TestServiceContext(),
@ -223,6 +235,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_pipeFactory.Dispose();
}
void IHttpHeadersHandler.OnHeader(Span<byte> name, Span<byte> value)
{
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters();
}
[Fact]
public async Task DATA_Received_ReadByStream()
{
@ -752,6 +769,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task HEADERS_Received_IncompleteHeaderBlockFragment_ConnectionError()
{
await InitializeConnectionAsync(_noopApplication);
await SendIncompleteHeadersFrameAsync(streamId: 1);
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task PRIORITY_Received_StreamIdZero_ConnectionError()
{
@ -1192,6 +1219,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task CONTINUATION_Received_IncompleteHeaderBlockFragment_ConnectionError()
{
await InitializeConnectionAsync(_noopApplication);
await SendHeadersAsync(1, Http2HeadersFrameFlags.NONE, _postRequestHeaders);
await SendIncompleteContinuationFrameAsync(streamId: 1);
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task CONTINUATION_Sent_WhenHeadersLargerThanFrameLength()
{
@ -1200,15 +1238,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55,
withLength: 12361,
withFlags: (byte)Http2HeadersFrameFlags.NONE,
withStreamId: 1);
var continuationFrame1 = await ExpectAsync(Http2FrameType.CONTINUATION,
withLength: 16373,
withLength: 12306,
withFlags: (byte)Http2ContinuationFrameFlags.NONE,
withStreamId: 1);
var continuationFrame2 = await ExpectAsync(Http2FrameType.CONTINUATION,
withLength: 16373,
withLength: 8204,
withFlags: (byte)Http2ContinuationFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@ -1218,18 +1256,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
var responseHeaders = new HttpResponseHeaders();
_hpackDecoder.Decode(headersFrame.HeadersPayload, responseHeaders);
_hpackDecoder.Decode(continuationFrame1.HeadersPayload, responseHeaders);
_hpackDecoder.Decode(continuationFrame2.HeadersPayload, responseHeaders);
_hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
_hpackDecoder.Decode(continuationFrame1.HeadersPayload, endHeaders: false, handler: this);
_hpackDecoder.Decode(continuationFrame2.HeadersPayload, endHeaders: true, handler: this);
var responseHeadersDictionary = (IDictionary<string, StringValues>)responseHeaders;
Assert.Equal(5, responseHeadersDictionary.Count);
Assert.Contains("date", responseHeadersDictionary.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", responseHeadersDictionary[":status"]);
Assert.Equal("0", responseHeadersDictionary["content-length"]);
Assert.Equal(_largeHeaderA, responseHeadersDictionary["a"]);
Assert.Equal(_largeHeaderB, responseHeadersDictionary["b"]);
Assert.Equal(11, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[":status"]);
Assert.Equal("0", _decodedHeaders["content-length"]);
Assert.Equal(_largeHeaderValue, _decodedHeaders["a"]);
Assert.Equal(_largeHeaderValue, _decodedHeaders["b"]);
Assert.Equal(_largeHeaderValue, _decodedHeaders["c"]);
Assert.Equal(_largeHeaderValue, _decodedHeaders["d"]);
Assert.Equal(_largeHeaderValue, _decodedHeaders["e"]);
Assert.Equal(_largeHeaderValue, _decodedHeaders["f"]);
Assert.Equal(_largeHeaderValue, _decodedHeaders["g"]);
Assert.Equal(_largeHeaderValue, _decodedHeaders["h"]);
}
[Fact]
@ -1525,12 +1567,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
return SendAsync(frame.Raw);
}
private Task SendIncompleteHeadersFrameAsync(int streamId)
{
var frame = new Http2Frame();
frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, streamId);
frame.Length = 3;
// Set up an incomplete Literal Header Field w/ Incremental Indexing frame,
// with an incomplete new name
frame.Payload[0] = 0;
frame.Payload[1] = 2;
frame.Payload[2] = (byte)'a';
return SendAsync(frame.Raw);
}
private async Task<bool> SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags)
{
var frame = new Http2Frame();
frame.PrepareContinuation(flags, streamId);
var done =_hpackEncoder.Encode(frame.Payload, out var length);
var done = _hpackEncoder.Encode(frame.Payload, out var length);
frame.Length = length;
await SendAsync(frame.Raw);
@ -1538,6 +1596,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
return done;
}
private Task SendIncompleteContinuationFrameAsync(int streamId)
{
var frame = new Http2Frame();
frame.PrepareContinuation(Http2ContinuationFrameFlags.END_HEADERS, streamId);
frame.Length = 3;
// Set up an incomplete Literal Header Field w/ Incremental Indexing frame,
// with an incomplete new name
frame.Payload[0] = 0;
frame.Payload[1] = 2;
frame.Payload[2] = (byte)'a';
return SendAsync(frame.Raw);
}
private Task SendDataAsync(int streamId, Span<byte> data, bool endStream)
{
var frame = new Http2Frame();

View File

@ -1,7 +1,7 @@
// 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.Text;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
using Xunit;
@ -9,28 +9,143 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class HuffmanTests
{
[Fact]
public void HuffmanDecodeString()
public static readonly TheoryData<byte[], byte[]> _validData = new TheoryData<byte[], byte[]>
{
// h e.......e l........l l o.......o
var encodedHello = new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1111 };
// Single 5-bit symbol
{ new byte[] { 0x07 }, Encoding.ASCII.GetBytes("0") },
// Single 6-bit symbol
{ new byte[] { 0x57 }, Encoding.ASCII.GetBytes("%") },
// Single 7-bit symbol
{ new byte[] { 0xb9 }, Encoding.ASCII.GetBytes(":") },
// Single 8-bit symbol
{ new byte[] { 0xf8 }, Encoding.ASCII.GetBytes("&") },
// Single 10-bit symbol
{ new byte[] { 0xfe, 0x3f }, Encoding.ASCII.GetBytes("!") },
// Single 11-bit symbol
{ new byte[] { 0xff, 0x7f }, Encoding.ASCII.GetBytes("+") },
// Single 12-bit symbol
{ new byte[] { 0xff, 0xaf }, Encoding.ASCII.GetBytes("#") },
// Single 13-bit symbol
{ new byte[] { 0xff, 0xcf }, Encoding.ASCII.GetBytes("$") },
// Single 14-bit symbol
{ new byte[] { 0xff, 0xf3 }, Encoding.ASCII.GetBytes("^") },
// Single 15-bit symbol
{ new byte[] { 0xff, 0xf9 }, Encoding.ASCII.GetBytes("<") },
// Single 19-bit symbol
{ new byte[] { 0xff, 0xfe, 0x1f }, Encoding.ASCII.GetBytes("\\") },
// Single 20-bit symbol
{ new byte[] { 0xff, 0xfe, 0x6f }, new byte[] { 0x80 } },
// Single 21-bit symbol
{ new byte[] { 0xff, 0xfe, 0xe7 }, new byte[] { 0x99 } },
// Single 22-bit symbol
{ new byte[] { 0xff, 0xff, 0x4b }, new byte[] { 0x81 } },
// Single 23-bit symbol
{ new byte[] { 0xff, 0xff, 0xb1 }, new byte[] { 0x01 } },
// Single 24-bit symbol
{ new byte[] { 0xff, 0xff, 0xea }, new byte[] { 0x09 } },
// Single 25-bit symbol
{ new byte[] { 0xff, 0xff, 0xf6, 0x7f }, new byte[] { 0xc7 } },
// Single 26-bit symbol
{ new byte[] { 0xff, 0xff, 0xf8, 0x3f }, new byte[] { 0xc0 } },
// Single 27-bit symbol
{ new byte[] { 0xff, 0xff, 0xfb, 0xdf }, new byte[] { 0xcb } },
// Single 28-bit symbol
{ new byte[] { 0xff, 0xff, 0xfe, 0x2f }, new byte[] { 0x02 } },
// Single 30-bit symbol
{ new byte[] { 0xff, 0xff, 0xff, 0xf3 }, new byte[] { 0x0a } },
Assert.Equal("hello", Huffman.Decode(encodedHello, 0, encodedHello.Length));
// h e l l o *
{ new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1111 }, Encoding.ASCII.GetBytes("hello") },
var encodedHeader = new byte[]
{
0xb6, 0xb9, 0xac, 0x1c, 0x85, 0x58, 0xd5, 0x20, 0xa4, 0xb6, 0xc2, 0xad, 0x61, 0x7b, 0x5a, 0x54, 0x25, 0x1f
};
// Sequences that uncovered errors
{ new byte[] { 0xb6, 0xb9, 0xac, 0x1c, 0x85, 0x58, 0xd5, 0x20, 0xa4, 0xb6, 0xc2, 0xad, 0x61, 0x7b, 0x5a, 0x54, 0x25, 0x1f }, Encoding.ASCII.GetBytes("upgrade-insecure-requests") },
{ new byte[] { 0xfe, 0x53 }, Encoding.ASCII.GetBytes("\"t") }
};
Assert.Equal("upgrade-insecure-requests", Huffman.Decode(encodedHeader, 0, encodedHeader.Length));
[Theory]
[MemberData(nameof(_validData))]
public void HuffmanDecodeArray(byte[] encoded, byte[] expected)
{
var dst = new byte[expected.Length];
Assert.Equal(expected.Length, Huffman.Decode(encoded, 0, encoded.Length, dst));
Assert.Equal(expected, dst);
}
encodedHeader = new byte[]
{
// "t
0xfe, 0x53
};
public static readonly TheoryData<byte[]> _longPaddingData = new TheoryData<byte[]>
{
// h e l l o *
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1111, 0b11111111 },
Assert.Equal("\"t", Huffman.Decode(encodedHeader, 0, encodedHeader.Length));
// '&' (8 bits) + 8 bit padding
new byte[] { 0xf8, 0xff },
// ':' (7 bits) + 9 bit padding
new byte[] { 0xb9, 0xff }
};
[Theory]
[MemberData(nameof(_longPaddingData))]
public void ThrowsOnPaddingLongerThanSevenBits(byte[] encoded)
{
var exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(encoded, 0, encoded.Length, new byte[encoded.Length * 2]));
Assert.Equal(CoreStrings.HPackHuffmanErrorIncomplete, exception.Message);
}
public static readonly TheoryData<byte[]> _eosData = new TheoryData<byte[]>
{
// EOS
new byte[] { 0xff, 0xff, 0xff, 0xff },
// '&' + EOS + '0'
new byte[] { 0xf8, 0xff, 0xff, 0xff, 0xfc, 0x1f }
};
[Theory]
[MemberData(nameof(_eosData))]
public void ThrowsOnEOS(byte[] encoded)
{
var exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(encoded, 0, encoded.Length, new byte[encoded.Length * 2]));
Assert.Equal(CoreStrings.HPackHuffmanErrorEOS, exception.Message);
}
[Fact]
public void ThrowsOnDestinationBufferTooSmall()
{
// 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(encoded, 0, encoded.Length, new byte[encoded.Length]));
Assert.Equal(CoreStrings.HPackHuffmanErrorDestinationTooSmall, exception.Message);
}
public static readonly TheoryData<byte[]> _incompleteSymbolData = new TheoryData<byte[]>
{
// h e l l o (incomplete)
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0 },
// Non-zero padding will be seen as incomplete symbol
// h e l l o *
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0000 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0001 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0010 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0011 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0100 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0101 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0110 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0111 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1000 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1001 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1010 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1011 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1100 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1101 },
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1110 }
};
[Theory]
[MemberData(nameof(_incompleteSymbolData))]
public void ThrowsOnIncompleteSymbol(byte[] encoded)
{
var exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(encoded, 0, encoded.Length, new byte[encoded.Length * 2]));
Assert.Equal(CoreStrings.HPackHuffmanErrorIncomplete, exception.Message);
}
[Theory]
@ -46,7 +161,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[MemberData(nameof(HuffmanData))]
public void HuffmanDecode(int code, uint encoded, int bitLength)
{
Assert.Equal(code, Huffman.Decode(encoded, out var decodedBits));
Assert.Equal(code, Huffman.Decode(encoded, bitLength, out var decodedBits));
Assert.Equal(bitLength, decodedBits);
}
@ -61,7 +176,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
#pragma warning restore xUnit1026
int bitLength)
{
Assert.Equal(code, Huffman.Decode(Huffman.Encode(code).encoded, out var decodedBits));
Assert.Equal(code, Huffman.Decode(Huffman.Encode(code).encoded, bitLength, out var decodedBits));
Assert.Equal(bitLength, decodedBits);
}