diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx
index 6fea320658..7127c41905 100644
--- a/src/Kestrel.Core/CoreStrings.resx
+++ b/src/Kestrel.Core/CoreStrings.resx
@@ -360,4 +360,28 @@
HTTP/2 over TLS was not negotiated on an HTTP/2-only endpoint.
+
+ A dynamic table size of {size} octets is greater than the configured maximum size of {maxSize} octets.
+
+
+ Index {index} is outside the bounds of the header field table.
+
+
+ Input data could not be fully decoded.
+
+
+ Input data contains the EOS symbol.
+
+
+ The destination buffer is not large enough to store the decoded data.
+
+
+ Huffman decoding error.
+
+
+ Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets.
+
+
+ The header block was incomplete and could not be fully decoded.
+
\ No newline at end of file
diff --git a/src/Kestrel.Core/Internal/Http/Http1Connection.cs b/src/Kestrel.Core/Internal/Http/Http1Connection.cs
index 9eb06465c1..9357a9d742 100644
--- a/src/Kestrel.Core/Internal/Http/Http1Connection.cs
+++ b/src/Kestrel.Core/Internal/Http/Http1Connection.cs
@@ -344,18 +344,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
- public void OnHeader(Span name, Span 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)
diff --git a/src/Kestrel.Core/Internal/Http/HttpProtocol.cs b/src/Kestrel.Core/Internal/Http/HttpProtocol.cs
index e99f929a1f..7ac194dd6e 100644
--- a/src/Kestrel.Core/Internal/Http/HttpProtocol.cs
+++ b/src/Kestrel.Core/Internal/Http/HttpProtocol.cs
@@ -418,6 +418,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
+ public void OnHeader(Span name, Span value)
+ {
+ _requestHeadersParsed++;
+ if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
+ {
+ ThrowRequestRejected(RequestRejectionReason.TooManyHeaders);
+ }
+ var valueString = value.GetAsciiStringNonNullCharacters();
+
+ HttpRequestHeaders.Append(name, valueString);
+ }
+
public async Task ProcessRequestsAsync()
{
try
diff --git a/src/Kestrel.Core/Internal/Http2/HPack/DynamicTable.cs b/src/Kestrel.Core/Internal/Http2/HPack/DynamicTable.cs
index 0b58c69ba6..6a13e49b82 100644
--- a/src/Kestrel.Core/Internal/Http2/HPack/DynamicTable.cs
+++ b/src/Kestrel.Core/Internal/Http2/HPack/DynamicTable.cs
@@ -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 name, Span 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;
}
diff --git a/src/Kestrel.Core/Internal/Http2/HPack/HPackDecoder.cs b/src/Kestrel.Core/Internal/Http2/HPack/HPackDecoder.cs
index 33a1e014ad..3df0d7af86 100644
--- a/src/Kestrel.Core/Internal/Http2/HPack/HPackDecoder.cs
+++ b/src/Kestrel.Core/Internal/Http2/HPack/HPackDecoder.cs
@@ -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 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 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(_headerName, 0, _headerNameLength);
+ var headerValueSpan = new Span(_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(header.Name), new Span(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);
+ }
+ }
}
}
diff --git a/src/Kestrel.Core/Internal/Http2/HPack/HPackDecodingException.cs b/src/Kestrel.Core/Internal/Http2/HPack/HPackDecodingException.cs
new file mode 100644
index 0000000000..7ae0ddddf5
--- /dev/null
+++ b/src/Kestrel.Core/Internal/Http2/HPack/HPackDecodingException.cs
@@ -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)
+ {
+ }
+ }
+}
diff --git a/src/Kestrel.Core/Internal/Http2/HPack/HeaderField.cs b/src/Kestrel.Core/Internal/Http2/HPack/HeaderField.cs
index 9c3872cad2..73eb4d726e 100644
--- a/src/Kestrel.Core/Internal/Http2/HPack/HeaderField.cs
+++ b/src/Kestrel.Core/Internal/Http2/HPack/HeaderField.cs
@@ -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 name, Span 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;
}
}
diff --git a/src/Kestrel.Core/Internal/Http2/HPack/Huffman.cs b/src/Kestrel.Core/Internal/Http2/HPack/Huffman.cs
index 7c9e52f446..f0d489c952 100644
--- a/src/Kestrel.Core/Internal/Http2/HPack/Huffman.cs
+++ b/src/Kestrel.Core/Internal/Http2/HPack/Huffman.cs
@@ -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)
+ ///
+ /// Decodes a Huffman encoded string from a byte array.
+ ///
+ /// The source byte array containing the encoded data.
+ /// The offset in the byte array where the coded data starts.
+ /// The number of bytes to decode.
+ /// The destination byte array to store the decoded data.
+ /// The number of decoded symbols.
+ 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)
+ ///
+ /// Decodes a single symbol from a 32-bit word.
+ ///
+ /// A 32-bit word containing a Huffman encoded symbol.
+ ///
+ /// The number of bits in 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 if they don't contain any
+ /// encoded data.
+ ///
+ /// The number of bits decoded from .
+ /// The decoded symbol.
+ 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;
}
}
}
diff --git a/src/Kestrel.Core/Internal/Http2/HPack/HuffmanDecodingException.cs b/src/Kestrel.Core/Internal/Http2/HPack/HuffmanDecodingException.cs
new file mode 100644
index 0000000000..3bd992ab4b
--- /dev/null
+++ b/src/Kestrel.Core/Internal/Http2/HPack/HuffmanDecodingException.cs
@@ -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)
+ {
+ }
+ }
+}
diff --git a/src/Kestrel.Core/Internal/Http2/HPack/IntegerDecoder.cs b/src/Kestrel.Core/Internal/Http2/HPack/IntegerDecoder.cs
index c3bac3eb09..5bc051a9a3 100644
--- a/src/Kestrel.Core/Internal/Http2/HPack/IntegerDecoder.cs
+++ b/src/Kestrel.Core/Internal/Http2/HPack/IntegerDecoder.cs
@@ -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
diff --git a/src/Kestrel.Core/Internal/Http2/HPack/StaticTable.cs b/src/Kestrel.Core/Internal/Http2/HPack/StaticTable.cs
index 06612cc778..c28a78ff8d 100644
--- a/src/Kestrel.Core/Internal/Http2/HPack/StaticTable.cs
+++ b/src/Kestrel.Core/Internal/Http2/HPack/StaticTable.cs
@@ -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));
}
}
diff --git a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs
index 7baa89c2c7..b4e54e7b01 100644
--- a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs
+++ b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs
@@ -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 name, Span value)
+ {
+ _currentHeadersStream.OnHeader(name, value);
+ }
+
void ITimeoutControl.SetTimeout(long ticks, TimeoutAction timeoutAction)
{
}
diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs
index 2fa3c085eb..7084b94b0c 100644
--- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs
+++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs
@@ -1144,6 +1144,118 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatEndPointHttp2NotNegotiated()
=> GetString("EndPointHttp2NotNegotiated");
+ ///
+ /// A dynamic table size of {size} octets is greater than the configured maximum size of {maxSize} octets.
+ ///
+ internal static string HPackErrorDynamicTableSizeUpdateTooLarge
+ {
+ get => GetString("HPackErrorDynamicTableSizeUpdateTooLarge");
+ }
+
+ ///
+ /// A dynamic table size of {size} octets is greater than the configured maximum size of {maxSize} octets.
+ ///
+ internal static string FormatHPackErrorDynamicTableSizeUpdateTooLarge(object size, object maxSize)
+ => string.Format(CultureInfo.CurrentCulture, GetString("HPackErrorDynamicTableSizeUpdateTooLarge", "size", "maxSize"), size, maxSize);
+
+ ///
+ /// Index {index} is outside the bounds of the header field table.
+ ///
+ internal static string HPackErrorIndexOutOfRange
+ {
+ get => GetString("HPackErrorIndexOutOfRange");
+ }
+
+ ///
+ /// Index {index} is outside the bounds of the header field table.
+ ///
+ internal static string FormatHPackErrorIndexOutOfRange(object index)
+ => string.Format(CultureInfo.CurrentCulture, GetString("HPackErrorIndexOutOfRange", "index"), index);
+
+ ///
+ /// Input data could not be fully decoded.
+ ///
+ internal static string HPackHuffmanErrorIncomplete
+ {
+ get => GetString("HPackHuffmanErrorIncomplete");
+ }
+
+ ///
+ /// Input data could not be fully decoded.
+ ///
+ internal static string FormatHPackHuffmanErrorIncomplete()
+ => GetString("HPackHuffmanErrorIncomplete");
+
+ ///
+ /// Input data contains the EOS symbol.
+ ///
+ internal static string HPackHuffmanErrorEOS
+ {
+ get => GetString("HPackHuffmanErrorEOS");
+ }
+
+ ///
+ /// Input data contains the EOS symbol.
+ ///
+ internal static string FormatHPackHuffmanErrorEOS()
+ => GetString("HPackHuffmanErrorEOS");
+
+ ///
+ /// The destination buffer is not large enough to store the decoded data.
+ ///
+ internal static string HPackHuffmanErrorDestinationTooSmall
+ {
+ get => GetString("HPackHuffmanErrorDestinationTooSmall");
+ }
+
+ ///
+ /// The destination buffer is not large enough to store the decoded data.
+ ///
+ internal static string FormatHPackHuffmanErrorDestinationTooSmall()
+ => GetString("HPackHuffmanErrorDestinationTooSmall");
+
+ ///
+ /// Huffman decoding error.
+ ///
+ internal static string HPackHuffmanError
+ {
+ get => GetString("HPackHuffmanError");
+ }
+
+ ///
+ /// Huffman decoding error.
+ ///
+ internal static string FormatHPackHuffmanError()
+ => GetString("HPackHuffmanError");
+
+ ///
+ /// Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets.
+ ///
+ internal static string HPackStringLengthTooLarge
+ {
+ get => GetString("HPackStringLengthTooLarge");
+ }
+
+ ///
+ /// Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets.
+ ///
+ internal static string FormatHPackStringLengthTooLarge(object length, object maxStringLength)
+ => string.Format(CultureInfo.CurrentCulture, GetString("HPackStringLengthTooLarge", "length", "maxStringLength"), length, maxStringLength);
+
+ ///
+ /// The header block was incomplete and could not be fully decoded.
+ ///
+ internal static string HPackErrorIncompleteHeaderBlock
+ {
+ get => GetString("HPackErrorIncompleteHeaderBlock");
+ }
+
+ ///
+ /// The header block was incomplete and could not be fully decoded.
+ ///
+ internal static string FormatHPackErrorIncompleteHeaderBlock()
+ => GetString("HPackErrorIncompleteHeaderBlock");
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/test/Kestrel.Core.Tests/DynamicTableTests.cs b/test/Kestrel.Core.Tests/DynamicTableTests.cs
new file mode 100644
index 0000000000..0943272c72
--- /dev/null
+++ b/test/Kestrel.Core.Tests/DynamicTableTests.cs
@@ -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(() => dynamicTable[0]);
+
+ dynamicTable.Insert(_header1.Name, _header1.Value);
+ Assert.Throws(() => 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);
+ }
+ }
+ }
+}
diff --git a/test/Kestrel.Core.Tests/HPackDecoderTests.cs b/test/Kestrel.Core.Tests/HPackDecoderTests.cs
new file mode 100644
index 0000000000..a20c0be120
--- /dev/null
+++ b/test/Kestrel.Core.Tests/HPackDecoderTests.cs
@@ -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 _decodedHeaders = new Dictionary();
+
+ public HPackDecoderTests()
+ {
+ _dynamicTable = new DynamicTable(DynamicTableInitialMaxSize);
+ _decoder = new HPackDecoder(DynamicTableInitialMaxSize, _dynamicTable);
+ }
+
+ void IHttpHeadersHandler.OnHeader(Span name, Span 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(() => _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(() => _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(() => _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(() => _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(() => _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(() => _decoder.Decode(encoded, endHeaders: true, handler: this));
+ Assert.Equal(CoreStrings.FormatHPackStringLengthTooLarge(4097, HPackDecoder.MaxStringOctets), exception.Message);
+ Assert.Empty(_decodedHeaders);
+ }
+
+ public static readonly TheoryData _incompleteHeaderBlockData = new TheoryData
+ {
+ // 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(() => _decoder.Decode(encoded, endHeaders: true, handler: this));
+ Assert.Equal(CoreStrings.HPackErrorIncompleteHeaderBlock, exception.Message);
+ Assert.Empty(_decodedHeaders);
+ }
+
+ public static readonly TheoryData _huffmanDecodingErrorData = new TheoryData
+ {
+ // 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(() => _decoder.Decode(encoded, endHeaders: true, handler: this));
+ Assert.Equal(CoreStrings.HPackHuffmanError, exception.Message);
+ Assert.IsType(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);
+ }
+ }
+ }
+}
diff --git a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs b/test/Kestrel.Core.Tests/Http2ConnectionTests.cs
index 873233df34..3851a86dbd 100644
--- a/test/Kestrel.Core.Tests/Http2ConnectionTests.cs
+++ b/test/Kestrel.Core.Tests/Http2ConnectionTests.cs
@@ -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> _postRequestHeaders = new []
+ private static readonly IEnumerable> _postRequestHeaders = new[]
{
new KeyValuePair(":method", "POST"),
new KeyValuePair(":path", "/"),
@@ -35,7 +33,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
new KeyValuePair(":scheme", "https"),
};
- private static readonly IEnumerable> _browserRequestHeaders = new []
+ private static readonly IEnumerable> _browserRequestHeaders = new[]
{
new KeyValuePair(":method", "GET"),
new KeyValuePair(":path", "/"),
@@ -48,23 +46,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
new KeyValuePair("upgrade-insecure-requests", "1"),
};
- private static readonly IEnumerable> _oneContinuationRequestHeaders = new []
+ private static readonly IEnumerable> _oneContinuationRequestHeaders = new[]
{
new KeyValuePair(":method", "GET"),
new KeyValuePair(":path", "/"),
new KeyValuePair(":authority", "127.0.0.1"),
new KeyValuePair(":scheme", "https"),
- new KeyValuePair("a", _largeHeaderA)
+ new KeyValuePair("a", _largeHeaderValue),
+ new KeyValuePair("b", _largeHeaderValue),
+ new KeyValuePair("c", _largeHeaderValue),
+ new KeyValuePair("d", _largeHeaderValue)
};
- private static readonly IEnumerable> _twoContinuationsRequestHeaders = new []
+ private static readonly IEnumerable> _twoContinuationsRequestHeaders = new[]
{
new KeyValuePair(":method", "GET"),
new KeyValuePair(":path", "/"),
new KeyValuePair(":authority", "127.0.0.1"),
new KeyValuePair(":scheme", "https"),
- new KeyValuePair("a", _largeHeaderA),
- new KeyValuePair("b", _largeHeaderB)
+ new KeyValuePair("a", _largeHeaderValue),
+ new KeyValuePair("b", _largeHeaderValue),
+ new KeyValuePair("c", _largeHeaderValue),
+ new KeyValuePair("d", _largeHeaderValue),
+ new KeyValuePair("e", _largeHeaderValue),
+ new KeyValuePair("f", _largeHeaderValue),
+ new KeyValuePair("g", _largeHeaderValue),
+ new KeyValuePair("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> _runningStreams = new ConcurrentDictionary>();
private readonly Dictionary _receivedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary _decodedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
private readonly HashSet _abortedStreamIds = new HashSet();
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 name, Span 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)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 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 data, bool endStream)
{
var frame = new Http2Frame();
diff --git a/test/Kestrel.Core.Tests/HuffmanTests.cs b/test/Kestrel.Core.Tests/HuffmanTests.cs
index f075bbc7ba..cbe87cd4d9 100644
--- a/test/Kestrel.Core.Tests/HuffmanTests.cs
+++ b/test/Kestrel.Core.Tests/HuffmanTests.cs
@@ -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 _validData = new TheoryData
{
- // 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 _longPaddingData = new TheoryData
+ {
+ // 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(() => Huffman.Decode(encoded, 0, encoded.Length, new byte[encoded.Length * 2]));
+ Assert.Equal(CoreStrings.HPackHuffmanErrorIncomplete, exception.Message);
+ }
+
+ public static readonly TheoryData _eosData = new TheoryData
+ {
+ // 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(() => 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(() => Huffman.Decode(encoded, 0, encoded.Length, new byte[encoded.Length]));
+ Assert.Equal(CoreStrings.HPackHuffmanErrorDestinationTooSmall, exception.Message);
+ }
+
+ public static readonly TheoryData _incompleteSymbolData = new TheoryData
+ {
+ // 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(() => 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);
}