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"> <data name="EndPointHttp2NotNegotiated" xml:space="preserve">
<value>HTTP/2 over TLS was not negotiated on an HTTP/2-only endpoint.</value> <value>HTTP/2 over TLS was not negotiated on an HTTP/2-only endpoint.</value>
</data> </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> </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() protected void EnsureHostHeaderExists()
{ {
if (_httpVersion == Http.HttpVersion.Http10) 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() public async Task ProcessRequestsAsync()
{ {
try try

View File

@ -7,8 +7,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{ {
public class DynamicTable public class DynamicTable
{ {
private readonly HeaderField[] _buffer; private HeaderField[] _buffer;
private int _maxSize = 4096; private int _maxSize;
private int _size; private int _size;
private int _count; private int _count;
private int _insertIndex; private int _insertIndex;
@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
public DynamicTable(int maxSize) public DynamicTable(int maxSize)
{ {
_buffer = new HeaderField[maxSize]; _buffer = new HeaderField[maxSize / HeaderField.RfcOverhead];
_maxSize = maxSize; _maxSize = maxSize;
} }
@ -24,6 +24,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
public int Size => _size; public int Size => _size;
public int MaxSize => _maxSize;
public HeaderField this[int index] public HeaderField this[int index]
{ {
get 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; var entryLength = HeaderField.GetLength(name.Length, value.Length);
EnsureSize(_maxSize - entrySize); 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; _insertIndex = (_insertIndex + 1) % _buffer.Length;
_size += entrySize; _size += entry.Length;
_count++; _count++;
} }
public void Resize(int maxSize) public void Resize(int maxSize)
{ {
_maxSize = maxSize; if (maxSize > _maxSize)
EnsureSize(_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--; _count--;
_removeIndex = (_removeIndex + 1) % _buffer.Length; _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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{ {
@ -22,14 +19,57 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
HeaderValueLength, HeaderValueLength,
HeaderValueLengthContinue, HeaderValueLengthContinue,
HeaderValue, 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 IndexedHeaderFieldMask = 0x80;
private const byte LiteralHeaderFieldWithIncrementalIndexingMask = 0x40; private const byte IndexedHeaderFieldRepresentation = 0x80;
private const byte LiteralHeaderFieldWithoutIndexingMask = 0x00;
private const byte LiteralHeaderFieldNeverIndexedMask = 0x10; // http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1
private const byte DynamicTableSizeUpdateMask = 0x20; // 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 byte HuffmanMask = 0x80;
private const int IndexedHeaderFieldPrefix = 7; 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 DynamicTableSizeUpdatePrefix = 5;
private const int StringLengthPrefix = 7; 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 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; private State _state = State.Ready;
// TODO: add new HTTP/2 header size limit and allocate accordingly private byte[] _headerName;
private byte[] _stringOctets = new byte[Http2Frame.MinAllowedMaxFrameSize];
private string _headerName = string.Empty;
private string _headerValue = string.Empty;
private int _stringLength;
private int _stringIndex; private int _stringIndex;
private int _stringLength;
private int _headerNameLength;
private int _headerValueLength;
private bool _index; private bool _index;
private bool _huffman; 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++) 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) switch (_state)
{ {
case State.Ready: 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 else
{ {
_state = State.HeaderFieldIndex; _state = State.HeaderFieldIndex;
} }
} }
else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingMask) else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingRepresentation)
{ {
_index = true; _index = true;
var val = b & ~LiteralHeaderFieldWithIncrementalIndexingMask; var val = b & ~LiteralHeaderFieldWithIncrementalIndexingMask;
@ -94,7 +157,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
_state = State.HeaderNameIndex; _state = State.HeaderNameIndex;
} }
} }
else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingMask) else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingRepresentation)
{ {
_index = false; _index = false;
var val = b & ~LiteralHeaderFieldWithoutIndexingMask; var val = b & ~LiteralHeaderFieldWithoutIndexingMask;
@ -112,7 +175,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
_state = State.HeaderNameIndex; _state = State.HeaderNameIndex;
} }
} }
else if ((b & LiteralHeaderFieldNeverIndexedMask) == LiteralHeaderFieldNeverIndexedMask) else if ((b & LiteralHeaderFieldNeverIndexedMask) == LiteralHeaderFieldNeverIndexedRepresentation)
{ {
_index = false; _index = false;
var val = b & ~LiteralHeaderFieldNeverIndexedMask; var val = b & ~LiteralHeaderFieldNeverIndexedMask;
@ -130,7 +193,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
_state = State.HeaderNameIndex; _state = State.HeaderNameIndex;
} }
} }
else if ((b & DynamicTableSizeUpdateMask) == DynamicTableSizeUpdateMask) else if ((b & DynamicTableSizeUpdateMask) == DynamicTableSizeUpdateRepresentation)
{ {
if (_integerDecoder.BeginDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix)) if (_integerDecoder.BeginDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix))
{ {
@ -139,19 +202,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
} }
else else
{ {
_state = State.DynamicTableSize; _state = State.DynamicTableSizeUpdate;
} }
} }
else else
{ {
throw new InvalidOperationException(); // Can't happen
throw new HPackDecodingException($"Byte value {b} does not encode a valid header field representation.");
} }
break; break;
case State.HeaderFieldIndex: case State.HeaderFieldIndex:
if (_integerDecoder.Decode(b)) if (_integerDecoder.Decode(b))
{ {
OnIndexedHeaderField(_integerDecoder.Value, headers); OnIndexedHeaderField(_integerDecoder.Value, handler);
} }
break; break;
@ -163,7 +227,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
break; break;
case State.HeaderNameLength: case State.HeaderNameLength:
_huffman = (b & HuffmanMask) == HuffmanMask; _huffman = (b & HuffmanMask) != 0;
if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix)) if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix))
{ {
@ -187,12 +251,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
if (_stringIndex == _stringLength) if (_stringIndex == _stringLength)
{ {
_headerName = OnString(nextState: State.HeaderValueLength); OnString(nextState: State.HeaderValueLength);
} }
break; break;
case State.HeaderValueLength: case State.HeaderValueLength:
_huffman = (b & HuffmanMask) == HuffmanMask; _huffman = (b & HuffmanMask) != 0;
if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix)) if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix))
{ {
@ -216,20 +280,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
if (_stringIndex == _stringLength) if (_stringIndex == _stringLength)
{ {
_headerValue = OnString(nextState: State.Ready); OnString(nextState: State.Ready);
headers.Append(_headerName, _headerValue);
var headerNameSpan = new Span<byte>(_headerName, 0, _headerNameLength);
var headerValueSpan = new Span<byte>(_headerValueOctets, 0, _headerValueLength);
handler.OnHeader(headerNameSpan, headerValueSpan);
if (_index) if (_index)
{ {
_dynamicTable.Insert(_headerName, _headerValue); _dynamicTable.Insert(headerNameSpan, headerValueSpan);
} }
} }
break; break;
case State.DynamicTableSize: case State.DynamicTableSizeUpdate:
if (_integerDecoder.Decode(b)) 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); _dynamicTable.Resize(_integerDecoder.Value);
_state = State.Ready; _state = State.Ready;
} }
@ -237,14 +310,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
break; break;
default: default:
// Can't happen // 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); 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; _state = State.Ready;
} }
@ -252,26 +325,69 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{ {
var header = GetHeader(index); var header = GetHeader(index);
_headerName = header.Name; _headerName = header.Name;
_headerNameLength = header.Name.Length;
_state = State.HeaderValueLength; _state = State.HeaderValueLength;
} }
private void OnStringLength(int length, State nextState) private void OnStringLength(int length, State nextState)
{ {
if (length > _stringOctets.Length)
{
throw new HPackDecodingException(CoreStrings.FormatHPackStringLengthTooLarge(length, _stringOctets.Length));
}
_stringLength = length; _stringLength = length;
_stringIndex = 0; _stringIndex = 0;
_state = nextState; _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; _state = nextState;
return _huffman
? Huffman.Decode(_stringOctets, 0, _stringLength)
: Encoding.ASCII.GetString(_stringOctets, 0, _stringLength);
} }
private HeaderField GetHeader(int index) => index <= StaticTable.Instance.Length private HeaderField GetHeader(int index)
? StaticTable.Instance[index - 1] {
: _dynamicTable[index - StaticTable.Instance.Length - 1]; 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. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{ {
public struct HeaderField 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; Name = new byte[name.Length];
Value = value; name.CopyTo(Name);
Value = new byte[value.Length];
value.CopyTo(Value);
} }
public string Name { get; } public byte[] Name { get; }
public string Value { 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack 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]; 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 i = offset;
var j = 0;
var lastDecodedBits = 0; var lastDecodedBits = 0;
while (i < count) while (i < count)
{ {
var next = (uint)(data[i] << 24 + lastDecodedBits); var next = (uint)(src[i] << 24 + lastDecodedBits);
next |= (i + 1 < data.Length ? (uint)(data[i + 1] << 16 + lastDecodedBits) : 0); next |= (i + 1 < src.Length ? (uint)(src[i + 1] << 16 + lastDecodedBits) : 0);
next |= (i + 2 < data.Length ? (uint)(data[i + 2] << 8 + lastDecodedBits) : 0); next |= (i + 2 < src.Length ? (uint)(src[i + 2] << 8 + lastDecodedBits) : 0);
next |= (i + 3 < data.Length ? (uint)(data[i + 3] << lastDecodedBits) : 0); next |= (i + 3 < src.Length ? (uint)(src[i + 3] << lastDecodedBits) : 0);
var ones = (uint)(int.MinValue >> (8 - lastDecodedBits - 1)); 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; break;
} }
var ch = Decode(next, out var decodedBits); // The longest possible symbol size is 30 bits. If we're at the last 4 bytes
sb.Append((char)ch); // 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; lastDecodedBits += decodedBits;
i += lastDecodedBits / 8; i += lastDecodedBits / 8;
// Modulo 8 since we only care about how many bits were decoded in the last byte that we processed.
lastDecodedBits %= 8; 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; 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 (codeLength, codes) = _decodingTable[i];
var mask = int.MinValue >> (codeLength - 1);
if (i > 0) if (i > 0)
{ {
@ -348,6 +409,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
codeMax += codes.Length; codeMax += codes.Length;
var mask = int.MinValue >> (codeLength - 1);
var masked = (data & mask) >> (32 - codeLength); var masked = (data & mask) >> (32 - codeLength);
if (masked < codeMax) 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. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{ {
public class IntegerDecoder public class IntegerDecoder

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. // 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.Collections.Generic;
using System.Text;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack 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 static StaticTable Instance => _instance;
public int Length => _staticTable.Length; public int Count => _staticTable.Length;
public HeaderField this[int index] => _staticTable[index]; 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[] private readonly HeaderField[] _staticTable = new HeaderField[]
{ {
new HeaderField(":authority", ""), CreateHeaderField(":authority", ""),
new HeaderField(":method", "GET"), CreateHeaderField(":method", "GET"),
new HeaderField(":method", "POST"), CreateHeaderField(":method", "POST"),
new HeaderField(":path", "/"), CreateHeaderField(":path", "/"),
new HeaderField(":path", "/index.html"), CreateHeaderField(":path", "/index.html"),
new HeaderField(":scheme", "http"), CreateHeaderField(":scheme", "http"),
new HeaderField(":scheme", "https"), CreateHeaderField(":scheme", "https"),
new HeaderField(":status", "200"), CreateHeaderField(":status", "200"),
new HeaderField(":status", "204"), CreateHeaderField(":status", "204"),
new HeaderField(":status", "206"), CreateHeaderField(":status", "206"),
new HeaderField(":status", "304"), CreateHeaderField(":status", "304"),
new HeaderField(":status", "400"), CreateHeaderField(":status", "400"),
new HeaderField(":status", "404"), CreateHeaderField(":status", "404"),
new HeaderField(":status", "500"), CreateHeaderField(":status", "500"),
new HeaderField("accept-charset", ""), CreateHeaderField("accept-charset", ""),
new HeaderField("accept-encoding", "gzip, deflate"), CreateHeaderField("accept-encoding", "gzip, deflate"),
new HeaderField("accept-language", ""), CreateHeaderField("accept-language", ""),
new HeaderField("accept-ranges", ""), CreateHeaderField("accept-ranges", ""),
new HeaderField("accept", ""), CreateHeaderField("accept", ""),
new HeaderField("access-control-allow-origin", ""), CreateHeaderField("access-control-allow-origin", ""),
new HeaderField("age", ""), CreateHeaderField("age", ""),
new HeaderField("allow", ""), CreateHeaderField("allow", ""),
new HeaderField("authorization", ""), CreateHeaderField("authorization", ""),
new HeaderField("cache-control", ""), CreateHeaderField("cache-control", ""),
new HeaderField("content-disposition", ""), CreateHeaderField("content-disposition", ""),
new HeaderField("content-encoding", ""), CreateHeaderField("content-encoding", ""),
new HeaderField("content-language", ""), CreateHeaderField("content-language", ""),
new HeaderField("content-length", ""), CreateHeaderField("content-length", ""),
new HeaderField("content-location", ""), CreateHeaderField("content-location", ""),
new HeaderField("content-range", ""), CreateHeaderField("content-range", ""),
new HeaderField("content-type", ""), CreateHeaderField("content-type", ""),
new HeaderField("cookie", ""), CreateHeaderField("cookie", ""),
new HeaderField("date", ""), CreateHeaderField("date", ""),
new HeaderField("etag", ""), CreateHeaderField("etag", ""),
new HeaderField("expect", ""), CreateHeaderField("expect", ""),
new HeaderField("expires", ""), CreateHeaderField("expires", ""),
new HeaderField("from", ""), CreateHeaderField("from", ""),
new HeaderField("host", ""), CreateHeaderField("host", ""),
new HeaderField("if-match", ""), CreateHeaderField("if-match", ""),
new HeaderField("if-modified-since", ""), CreateHeaderField("if-modified-since", ""),
new HeaderField("if-none-match", ""), CreateHeaderField("if-none-match", ""),
new HeaderField("if-range", ""), CreateHeaderField("if-range", ""),
new HeaderField("if-unmodifiedsince", ""), CreateHeaderField("if-unmodifiedsince", ""),
new HeaderField("last-modified", ""), CreateHeaderField("last-modified", ""),
new HeaderField("link", ""), CreateHeaderField("link", ""),
new HeaderField("location", ""), CreateHeaderField("location", ""),
new HeaderField("max-forwards", ""), CreateHeaderField("max-forwards", ""),
new HeaderField("proxy-authenticate", ""), CreateHeaderField("proxy-authenticate", ""),
new HeaderField("proxy-authorization", ""), CreateHeaderField("proxy-authorization", ""),
new HeaderField("range", ""), CreateHeaderField("range", ""),
new HeaderField("referer", ""), CreateHeaderField("referer", ""),
new HeaderField("refresh", ""), CreateHeaderField("refresh", ""),
new HeaderField("retry-after", ""), CreateHeaderField("retry-after", ""),
new HeaderField("server", ""), CreateHeaderField("server", ""),
new HeaderField("set-cookie", ""), CreateHeaderField("set-cookie", ""),
new HeaderField("strict-transport-security", ""), CreateHeaderField("strict-transport-security", ""),
new HeaderField("transfer-encoding", ""), CreateHeaderField("transfer-encoding", ""),
new HeaderField("user-agent", ""), CreateHeaderField("user-agent", ""),
new HeaderField("vary", ""), CreateHeaderField("vary", ""),
new HeaderField("via", ""), CreateHeaderField("via", ""),
new HeaderField("www-authenticate", "") 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 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"); 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; _context = context;
_frameWriter = new Http2FrameWriter(context.Transport.Output, context.Application.Input); _frameWriter = new Http2FrameWriter(context.Transport.Output, context.Application.Input);
_hpackDecoder = new HPackDecoder(); _hpackDecoder = new HPackDecoder((int)_serverSettings.HeaderTableSize);
} }
public string ConnectionId => _context.ConnectionId; public string ConnectionId => _context.ConnectionId;
@ -141,6 +141,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
error = ex; error = ex;
errorCode = ex.ErrorCode; errorCode = ex.ErrorCode;
} }
catch (HPackDecodingException ex)
{
// TODO: log
error = ex;
errorCode = Http2ErrorCode.COMPRESSION_ERROR;
}
catch (Exception ex) catch (Exception ex)
{ {
// TODO: log // TODO: log
@ -350,9 +356,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_streams[_incomingFrame.StreamId] = _currentHeadersStream; _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; _highestOpenedStreamId = _incomingFrame.StreamId;
_ = _currentHeadersStream.ProcessRequestsAsync(); _ = _currentHeadersStream.ProcessRequestsAsync();
@ -525,9 +532,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR); 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; _highestOpenedStreamId = _currentHeadersStream.StreamId;
_ = _currentHeadersStream.ProcessRequestsAsync(); _ = _currentHeadersStream.ProcessRequestsAsync();
@ -571,6 +579,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_streams.TryRemove(streamId, out _); _streams.TryRemove(streamId, out _);
} }
public void OnHeader(Span<byte> name, Span<byte> value)
{
_currentHeadersStream.OnHeader(name, value);
}
void ITimeoutControl.SetTimeout(long ticks, TimeoutAction timeoutAction) void ITimeoutControl.SetTimeout(long ticks, TimeoutAction timeoutAction)
{ {
} }

View File

@ -1144,6 +1144,118 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatEndPointHttp2NotNegotiated() internal static string FormatEndPointHttp2NotNegotiated()
=> GetString("EndPointHttp2NotNegotiated"); => 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); 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;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Linq; using System.Linq;
using System.Text; 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.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests 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>(":method", "POST"),
new KeyValuePair<string, string>(":path", "/"), new KeyValuePair<string, string>(":path", "/"),
@ -35,7 +33,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
new KeyValuePair<string, string>(":scheme", "https"), 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>(":method", "GET"),
new KeyValuePair<string, string>(":path", "/"), new KeyValuePair<string, string>(":path", "/"),
@ -48,23 +46,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
new KeyValuePair<string, string>("upgrade-insecure-requests", "1"), 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>(":method", "GET"),
new KeyValuePair<string, string>(":path", "/"), new KeyValuePair<string, string>(":path", "/"),
new KeyValuePair<string, string>(":authority", "127.0.0.1"), new KeyValuePair<string, string>(":authority", "127.0.0.1"),
new KeyValuePair<string, string>(":scheme", "https"), 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>(":method", "GET"),
new KeyValuePair<string, string>(":path", "/"), new KeyValuePair<string, string>(":path", "/"),
new KeyValuePair<string, string>(":authority", "127.0.0.1"), new KeyValuePair<string, string>(":authority", "127.0.0.1"),
new KeyValuePair<string, string>(":scheme", "https"), new KeyValuePair<string, string>(":scheme", "https"),
new KeyValuePair<string, string>("a", _largeHeaderA), new KeyValuePair<string, string>("a", _largeHeaderValue),
new KeyValuePair<string, string>("b", _largeHeaderB) 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"); 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 (IPipeConnection Transport, IPipeConnection Application) _pair;
private readonly Http2ConnectionContext _connectionContext; private readonly Http2ConnectionContext _connectionContext;
private readonly Http2Connection _connection; private readonly Http2Connection _connection;
private readonly HPackEncoder _hpackEncoder = new HPackEncoder();
private readonly HPackDecoder _hpackDecoder = new HPackDecoder();
private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); 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 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> _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 HashSet<int> _abortedStreamIds = new HashSet<int>();
private readonly object _abortedStreamIdsLock = new object(); private readonly object _abortedStreamIdsLock = new object();
@ -160,8 +168,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_largeHeadersApplication = context => _largeHeadersApplication = context =>
{ {
context.Response.Headers["a"] = _largeHeaderA; foreach (var name in new[] { "a", "b", "c", "d", "e", "f", "g", "h" })
context.Response.Headers["b"] = _largeHeaderB; {
context.Response.Headers[name] = _largeHeaderValue;
}
return Task.CompletedTask; return Task.CompletedTask;
}; };
@ -208,6 +218,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_runningStreams[streamIdFeature.StreamId].TrySetResult(null); _runningStreams[streamIdFeature.StreamId].TrySetResult(null);
}; };
_hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize);
_connectionContext = new Http2ConnectionContext _connectionContext = new Http2ConnectionContext
{ {
ServiceContext = new TestServiceContext(), ServiceContext = new TestServiceContext(),
@ -223,6 +235,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_pipeFactory.Dispose(); _pipeFactory.Dispose();
} }
void IHttpHeadersHandler.OnHeader(Span<byte> name, Span<byte> value)
{
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters();
}
[Fact] [Fact]
public async Task DATA_Received_ReadByStream() 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); 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] [Fact]
public async Task PRIORITY_Received_StreamIdZero_ConnectionError() 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); 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] [Fact]
public async Task CONTINUATION_Sent_WhenHeadersLargerThanFrameLength() public async Task CONTINUATION_Sent_WhenHeadersLargerThanFrameLength()
{ {
@ -1200,15 +1238,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55, withLength: 12361,
withFlags: (byte)Http2HeadersFrameFlags.NONE, withFlags: (byte)Http2HeadersFrameFlags.NONE,
withStreamId: 1); withStreamId: 1);
var continuationFrame1 = await ExpectAsync(Http2FrameType.CONTINUATION, var continuationFrame1 = await ExpectAsync(Http2FrameType.CONTINUATION,
withLength: 16373, withLength: 12306,
withFlags: (byte)Http2ContinuationFrameFlags.NONE, withFlags: (byte)Http2ContinuationFrameFlags.NONE,
withStreamId: 1); withStreamId: 1);
var continuationFrame2 = await ExpectAsync(Http2FrameType.CONTINUATION, var continuationFrame2 = await ExpectAsync(Http2FrameType.CONTINUATION,
withLength: 16373, withLength: 8204,
withFlags: (byte)Http2ContinuationFrameFlags.END_HEADERS, withFlags: (byte)Http2ContinuationFrameFlags.END_HEADERS,
withStreamId: 1); withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA, await ExpectAsync(Http2FrameType.DATA,
@ -1218,18 +1256,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
var responseHeaders = new HttpResponseHeaders(); _hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
_hpackDecoder.Decode(headersFrame.HeadersPayload, responseHeaders); _hpackDecoder.Decode(continuationFrame1.HeadersPayload, endHeaders: false, handler: this);
_hpackDecoder.Decode(continuationFrame1.HeadersPayload, responseHeaders); _hpackDecoder.Decode(continuationFrame2.HeadersPayload, endHeaders: true, handler: this);
_hpackDecoder.Decode(continuationFrame2.HeadersPayload, responseHeaders);
var responseHeadersDictionary = (IDictionary<string, StringValues>)responseHeaders; Assert.Equal(11, _decodedHeaders.Count);
Assert.Equal(5, responseHeadersDictionary.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Contains("date", responseHeadersDictionary.Keys, StringComparer.OrdinalIgnoreCase); Assert.Equal("200", _decodedHeaders[":status"]);
Assert.Equal("200", responseHeadersDictionary[":status"]); Assert.Equal("0", _decodedHeaders["content-length"]);
Assert.Equal("0", responseHeadersDictionary["content-length"]); Assert.Equal(_largeHeaderValue, _decodedHeaders["a"]);
Assert.Equal(_largeHeaderA, responseHeadersDictionary["a"]); Assert.Equal(_largeHeaderValue, _decodedHeaders["b"]);
Assert.Equal(_largeHeaderB, responseHeadersDictionary["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] [Fact]
@ -1525,12 +1567,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
return SendAsync(frame.Raw); 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) private async Task<bool> SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags)
{ {
var frame = new Http2Frame(); var frame = new Http2Frame();
frame.PrepareContinuation(flags, streamId); 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; frame.Length = length;
await SendAsync(frame.Raw); await SendAsync(frame.Raw);
@ -1538,6 +1596,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
return done; 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) private Task SendDataAsync(int streamId, Span<byte> data, bool endStream)
{ {
var frame = new Http2Frame(); var frame = new Http2Frame();

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System.Text;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
using Xunit; using Xunit;
@ -9,28 +9,143 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
public class HuffmanTests public class HuffmanTests
{ {
[Fact] public static readonly TheoryData<byte[], byte[]> _validData = new TheoryData<byte[], byte[]>
public void HuffmanDecodeString()
{ {
// h e.......e l........l l o.......o // Single 5-bit symbol
var encodedHello = new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1111 }; { 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[] // 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") },
0xb6, 0xb9, 0xac, 0x1c, 0x85, 0x58, 0xd5, 0x20, 0xa4, 0xb6, 0xc2, 0xad, 0x61, 0x7b, 0x5a, 0x54, 0x25, 0x1f { 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[] public static readonly TheoryData<byte[]> _longPaddingData = new TheoryData<byte[]>
{ {
// "t // h e l l o *
0xfe, 0x53 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] [Theory]
@ -46,7 +161,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[MemberData(nameof(HuffmanData))] [MemberData(nameof(HuffmanData))]
public void HuffmanDecode(int code, uint encoded, int bitLength) 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); Assert.Equal(bitLength, decodedBits);
} }
@ -61,7 +176,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
#pragma warning restore xUnit1026 #pragma warning restore xUnit1026
int bitLength) 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); Assert.Equal(bitLength, decodedBits);
} }