HPACK fixes and improvements.
This commit is contained in:
parent
156ddfc4e8
commit
11ce1395e5
|
|
@ -360,4 +360,28 @@
|
|||
<data name="EndPointHttp2NotNegotiated" xml:space="preserve">
|
||||
<value>HTTP/2 over TLS was not negotiated on an HTTP/2-only endpoint.</value>
|
||||
</data>
|
||||
<data name="HPackErrorDynamicTableSizeUpdateTooLarge" xml:space="preserve">
|
||||
<value>A dynamic table size of {size} octets is greater than the configured maximum size of {maxSize} octets.</value>
|
||||
</data>
|
||||
<data name="HPackErrorIndexOutOfRange" xml:space="preserve">
|
||||
<value>Index {index} is outside the bounds of the header field table.</value>
|
||||
</data>
|
||||
<data name="HPackHuffmanErrorIncomplete" xml:space="preserve">
|
||||
<value>Input data could not be fully decoded.</value>
|
||||
</data>
|
||||
<data name="HPackHuffmanErrorEOS" xml:space="preserve">
|
||||
<value>Input data contains the EOS symbol.</value>
|
||||
</data>
|
||||
<data name="HPackHuffmanErrorDestinationTooSmall" xml:space="preserve">
|
||||
<value>The destination buffer is not large enough to store the decoded data.</value>
|
||||
</data>
|
||||
<data name="HPackHuffmanError" xml:space="preserve">
|
||||
<value>Huffman decoding error.</value>
|
||||
</data>
|
||||
<data name="HPackStringLengthTooLarge" xml:space="preserve">
|
||||
<value>Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets.</value>
|
||||
</data>
|
||||
<data name="HPackErrorIncompleteHeaderBlock" xml:space="preserve">
|
||||
<value>The header block was incomplete and could not be fully decoded.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -344,18 +344,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}
|
||||
}
|
||||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
{
|
||||
_requestHeadersParsed++;
|
||||
if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
|
||||
{
|
||||
ThrowRequestRejected(RequestRejectionReason.TooManyHeaders);
|
||||
}
|
||||
var valueString = value.GetAsciiStringNonNullCharacters();
|
||||
|
||||
HttpRequestHeaders.Append(name, valueString);
|
||||
}
|
||||
|
||||
protected void EnsureHostHeaderExists()
|
||||
{
|
||||
if (_httpVersion == Http.HttpVersion.Http10)
|
||||
|
|
|
|||
|
|
@ -418,6 +418,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}
|
||||
}
|
||||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
{
|
||||
_requestHeadersParsed++;
|
||||
if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
|
||||
{
|
||||
ThrowRequestRejected(RequestRejectionReason.TooManyHeaders);
|
||||
}
|
||||
var valueString = value.GetAsciiStringNonNullCharacters();
|
||||
|
||||
HttpRequestHeaders.Append(name, valueString);
|
||||
}
|
||||
|
||||
public async Task ProcessRequestsAsync()
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
{
|
||||
public class DynamicTable
|
||||
{
|
||||
private readonly HeaderField[] _buffer;
|
||||
private int _maxSize = 4096;
|
||||
private HeaderField[] _buffer;
|
||||
private int _maxSize;
|
||||
private int _size;
|
||||
private int _count;
|
||||
private int _insertIndex;
|
||||
|
|
@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
|
||||
public DynamicTable(int maxSize)
|
||||
{
|
||||
_buffer = new HeaderField[maxSize];
|
||||
_buffer = new HeaderField[maxSize / HeaderField.RfcOverhead];
|
||||
_maxSize = maxSize;
|
||||
}
|
||||
|
||||
|
|
@ -24,6 +24,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
|
||||
public int Size => _size;
|
||||
|
||||
public int MaxSize => _maxSize;
|
||||
|
||||
public HeaderField this[int index]
|
||||
{
|
||||
get
|
||||
|
|
@ -37,33 +39,53 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
}
|
||||
}
|
||||
|
||||
public void Insert(string name, string value)
|
||||
public void Insert(Span<byte> name, Span<byte> value)
|
||||
{
|
||||
var entrySize = name.Length + value.Length + 32;
|
||||
EnsureSize(_maxSize - entrySize);
|
||||
var entryLength = HeaderField.GetLength(name.Length, value.Length);
|
||||
EnsureAvailable(entryLength);
|
||||
|
||||
if (_maxSize < entrySize)
|
||||
if (entryLength > _maxSize)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to add entry of size {entrySize} to dynamic table of size {_maxSize}.");
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.4.4
|
||||
// It is not an error to attempt to add an entry that is larger than the maximum size;
|
||||
// an attempt to add an entry larger than the maximum size causes the table to be emptied
|
||||
// of all existing entries and results in an empty table.
|
||||
return;
|
||||
}
|
||||
|
||||
_buffer[_insertIndex] = new HeaderField(name, value);
|
||||
var entry = new HeaderField(name, value);
|
||||
_buffer[_insertIndex] = entry;
|
||||
_insertIndex = (_insertIndex + 1) % _buffer.Length;
|
||||
_size += entrySize;
|
||||
_size += entry.Length;
|
||||
_count++;
|
||||
}
|
||||
|
||||
public void Resize(int maxSize)
|
||||
{
|
||||
_maxSize = maxSize;
|
||||
EnsureSize(_maxSize);
|
||||
if (maxSize > _maxSize)
|
||||
{
|
||||
var newBuffer = new HeaderField[maxSize / HeaderField.RfcOverhead];
|
||||
|
||||
for (var i = 0; i < Count; i++)
|
||||
{
|
||||
newBuffer[i] = _buffer[i];
|
||||
}
|
||||
|
||||
_buffer = newBuffer;
|
||||
_maxSize = maxSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
_maxSize = maxSize;
|
||||
EnsureAvailable(0);
|
||||
}
|
||||
}
|
||||
|
||||
public void EnsureSize(int size)
|
||||
private void EnsureAvailable(int available)
|
||||
{
|
||||
while (_count > 0 && _size > size)
|
||||
while (_count > 0 && _maxSize - _size < available)
|
||||
{
|
||||
_size -= _buffer[_removeIndex].Name.Length + _buffer[_removeIndex].Value.Length + 32;
|
||||
_size -= _buffer[_removeIndex].Length;
|
||||
_count--;
|
||||
_removeIndex = (_removeIndex + 1) % _buffer.Length;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
|
|
@ -22,14 +19,57 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
HeaderValueLength,
|
||||
HeaderValueLengthContinue,
|
||||
HeaderValue,
|
||||
DynamicTableSize
|
||||
DynamicTableSizeUpdate
|
||||
}
|
||||
|
||||
// TODO: add new configurable limit
|
||||
public const int MaxStringOctets = 4096;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.1
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 1 | Index (7+) |
|
||||
// +---+---------------------------+
|
||||
private const byte IndexedHeaderFieldMask = 0x80;
|
||||
private const byte LiteralHeaderFieldWithIncrementalIndexingMask = 0x40;
|
||||
private const byte LiteralHeaderFieldWithoutIndexingMask = 0x00;
|
||||
private const byte LiteralHeaderFieldNeverIndexedMask = 0x10;
|
||||
private const byte DynamicTableSizeUpdateMask = 0x20;
|
||||
private const byte IndexedHeaderFieldRepresentation = 0x80;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 1 | Index (6+) |
|
||||
// +---+---+-----------------------+
|
||||
private const byte LiteralHeaderFieldWithIncrementalIndexingMask = 0xc0;
|
||||
private const byte LiteralHeaderFieldWithIncrementalIndexingRepresentation = 0x40;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 0 | 0 | Index (4+) |
|
||||
// +---+---+-----------------------+
|
||||
private const byte LiteralHeaderFieldWithoutIndexingMask = 0xf0;
|
||||
private const byte LiteralHeaderFieldWithoutIndexingRepresentation = 0x00;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 0 | 1 | Index (4+) |
|
||||
// +---+---+-----------------------+
|
||||
private const byte LiteralHeaderFieldNeverIndexedMask = 0xf0;
|
||||
private const byte LiteralHeaderFieldNeverIndexedRepresentation = 0x10;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.3
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 1 | Max size (5+) |
|
||||
// +---+---------------------------+
|
||||
private const byte DynamicTableSizeUpdateMask = 0xe0;
|
||||
private const byte DynamicTableSizeUpdateRepresentation = 0x20;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.5.2
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | H | String Length (7+) |
|
||||
// +---+---------------------------+
|
||||
private const byte HuffmanMask = 0x80;
|
||||
|
||||
private const int IndexedHeaderFieldPrefix = 7;
|
||||
|
|
@ -39,44 +79,67 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
private const int DynamicTableSizeUpdatePrefix = 5;
|
||||
private const int StringLengthPrefix = 7;
|
||||
|
||||
private readonly DynamicTable _dynamicTable = new DynamicTable(4096);
|
||||
private readonly int _maxDynamicTableSize;
|
||||
private readonly DynamicTable _dynamicTable;
|
||||
private readonly IntegerDecoder _integerDecoder = new IntegerDecoder();
|
||||
private readonly byte[] _stringOctets = new byte[MaxStringOctets];
|
||||
private readonly byte[] _headerNameOctets = new byte[MaxStringOctets];
|
||||
private readonly byte[] _headerValueOctets = new byte[MaxStringOctets];
|
||||
|
||||
private State _state = State.Ready;
|
||||
// TODO: add new HTTP/2 header size limit and allocate accordingly
|
||||
private byte[] _stringOctets = new byte[Http2Frame.MinAllowedMaxFrameSize];
|
||||
private string _headerName = string.Empty;
|
||||
private string _headerValue = string.Empty;
|
||||
private int _stringLength;
|
||||
private byte[] _headerName;
|
||||
private int _stringIndex;
|
||||
private int _stringLength;
|
||||
private int _headerNameLength;
|
||||
private int _headerValueLength;
|
||||
private bool _index;
|
||||
private bool _huffman;
|
||||
|
||||
public void Decode(Span<byte> data, IHeaderDictionary headers)
|
||||
public HPackDecoder(int maxDynamicTableSize)
|
||||
: this(maxDynamicTableSize, new DynamicTable(maxDynamicTableSize))
|
||||
{
|
||||
_maxDynamicTableSize = maxDynamicTableSize;
|
||||
}
|
||||
|
||||
// For testing.
|
||||
internal HPackDecoder(int maxDynamicTableSize, DynamicTable dynamicTable)
|
||||
{
|
||||
_maxDynamicTableSize = maxDynamicTableSize;
|
||||
_dynamicTable = dynamicTable;
|
||||
}
|
||||
|
||||
public void Decode(Span<byte> data, bool endHeaders, IHttpHeadersHandler handler)
|
||||
{
|
||||
for (var i = 0; i < data.Length; i++)
|
||||
{
|
||||
OnByte(data[i], headers);
|
||||
OnByte(data[i], handler);
|
||||
}
|
||||
|
||||
if (endHeaders && _state != State.Ready)
|
||||
{
|
||||
throw new HPackDecodingException(CoreStrings.HPackErrorIncompleteHeaderBlock);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnByte(byte b, IHeaderDictionary headers)
|
||||
public void OnByte(byte b, IHttpHeadersHandler handler)
|
||||
{
|
||||
switch (_state)
|
||||
{
|
||||
case State.Ready:
|
||||
if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldMask)
|
||||
if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation)
|
||||
{
|
||||
if (_integerDecoder.BeginDecode((byte)(b & ~IndexedHeaderFieldMask), IndexedHeaderFieldPrefix))
|
||||
var val = b & ~IndexedHeaderFieldMask;
|
||||
|
||||
if (_integerDecoder.BeginDecode((byte)val, IndexedHeaderFieldPrefix))
|
||||
{
|
||||
OnIndexedHeaderField(_integerDecoder.Value, headers);
|
||||
OnIndexedHeaderField(_integerDecoder.Value, handler);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderFieldIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingMask)
|
||||
else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingRepresentation)
|
||||
{
|
||||
_index = true;
|
||||
var val = b & ~LiteralHeaderFieldWithIncrementalIndexingMask;
|
||||
|
|
@ -94,7 +157,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingMask)
|
||||
else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingRepresentation)
|
||||
{
|
||||
_index = false;
|
||||
var val = b & ~LiteralHeaderFieldWithoutIndexingMask;
|
||||
|
|
@ -112,7 +175,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldNeverIndexedMask) == LiteralHeaderFieldNeverIndexedMask)
|
||||
else if ((b & LiteralHeaderFieldNeverIndexedMask) == LiteralHeaderFieldNeverIndexedRepresentation)
|
||||
{
|
||||
_index = false;
|
||||
var val = b & ~LiteralHeaderFieldNeverIndexedMask;
|
||||
|
|
@ -130,7 +193,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & DynamicTableSizeUpdateMask) == DynamicTableSizeUpdateMask)
|
||||
else if ((b & DynamicTableSizeUpdateMask) == DynamicTableSizeUpdateRepresentation)
|
||||
{
|
||||
if (_integerDecoder.BeginDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix))
|
||||
{
|
||||
|
|
@ -139,19 +202,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
}
|
||||
else
|
||||
{
|
||||
_state = State.DynamicTableSize;
|
||||
_state = State.DynamicTableSizeUpdate;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
// Can't happen
|
||||
throw new HPackDecodingException($"Byte value {b} does not encode a valid header field representation.");
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderFieldIndex:
|
||||
if (_integerDecoder.Decode(b))
|
||||
{
|
||||
OnIndexedHeaderField(_integerDecoder.Value, headers);
|
||||
OnIndexedHeaderField(_integerDecoder.Value, handler);
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -163,7 +227,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
|
||||
break;
|
||||
case State.HeaderNameLength:
|
||||
_huffman = (b & HuffmanMask) == HuffmanMask;
|
||||
_huffman = (b & HuffmanMask) != 0;
|
||||
|
||||
if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix))
|
||||
{
|
||||
|
|
@ -187,12 +251,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
|
||||
if (_stringIndex == _stringLength)
|
||||
{
|
||||
_headerName = OnString(nextState: State.HeaderValueLength);
|
||||
OnString(nextState: State.HeaderValueLength);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderValueLength:
|
||||
_huffman = (b & HuffmanMask) == HuffmanMask;
|
||||
_huffman = (b & HuffmanMask) != 0;
|
||||
|
||||
if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix))
|
||||
{
|
||||
|
|
@ -216,20 +280,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
|
||||
if (_stringIndex == _stringLength)
|
||||
{
|
||||
_headerValue = OnString(nextState: State.Ready);
|
||||
headers.Append(_headerName, _headerValue);
|
||||
OnString(nextState: State.Ready);
|
||||
|
||||
var headerNameSpan = new Span<byte>(_headerName, 0, _headerNameLength);
|
||||
var headerValueSpan = new Span<byte>(_headerValueOctets, 0, _headerValueLength);
|
||||
|
||||
handler.OnHeader(headerNameSpan, headerValueSpan);
|
||||
|
||||
if (_index)
|
||||
{
|
||||
_dynamicTable.Insert(_headerName, _headerValue);
|
||||
_dynamicTable.Insert(headerNameSpan, headerValueSpan);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case State.DynamicTableSize:
|
||||
case State.DynamicTableSizeUpdate:
|
||||
if (_integerDecoder.Decode(b))
|
||||
{
|
||||
// TODO: validate that it's less than what's defined via SETTINGS
|
||||
if (_integerDecoder.Value > _maxDynamicTableSize)
|
||||
{
|
||||
throw new HPackDecodingException(
|
||||
CoreStrings.FormatHPackErrorDynamicTableSizeUpdateTooLarge(_integerDecoder.Value, _maxDynamicTableSize));
|
||||
}
|
||||
|
||||
_dynamicTable.Resize(_integerDecoder.Value);
|
||||
_state = State.Ready;
|
||||
}
|
||||
|
|
@ -237,14 +310,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
break;
|
||||
default:
|
||||
// Can't happen
|
||||
throw new InvalidOperationException();
|
||||
throw new HPackDecodingException("The HPACK decoder reached an invalid state.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnIndexedHeaderField(int index, IHeaderDictionary headers)
|
||||
private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler)
|
||||
{
|
||||
var header = GetHeader(index);
|
||||
headers.Append(header.Name, header.Value);
|
||||
handler.OnHeader(new Span<byte>(header.Name), new Span<byte>(header.Value));
|
||||
_state = State.Ready;
|
||||
}
|
||||
|
||||
|
|
@ -252,26 +325,69 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
{
|
||||
var header = GetHeader(index);
|
||||
_headerName = header.Name;
|
||||
_headerNameLength = header.Name.Length;
|
||||
_state = State.HeaderValueLength;
|
||||
}
|
||||
|
||||
private void OnStringLength(int length, State nextState)
|
||||
{
|
||||
if (length > _stringOctets.Length)
|
||||
{
|
||||
throw new HPackDecodingException(CoreStrings.FormatHPackStringLengthTooLarge(length, _stringOctets.Length));
|
||||
}
|
||||
|
||||
_stringLength = length;
|
||||
_stringIndex = 0;
|
||||
_state = nextState;
|
||||
}
|
||||
|
||||
private string OnString(State nextState)
|
||||
private void OnString(State nextState)
|
||||
{
|
||||
int Decode(byte[] dst)
|
||||
{
|
||||
if (_huffman)
|
||||
{
|
||||
return Huffman.Decode(_stringOctets, 0, _stringLength, dst);
|
||||
}
|
||||
else
|
||||
{
|
||||
Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength);
|
||||
return _stringLength;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_state == State.HeaderName)
|
||||
{
|
||||
_headerName = _headerNameOctets;
|
||||
_headerNameLength = Decode(_headerNameOctets);
|
||||
}
|
||||
else
|
||||
{
|
||||
_headerValueLength = Decode(_headerValueOctets);
|
||||
}
|
||||
}
|
||||
catch (HuffmanDecodingException ex)
|
||||
{
|
||||
throw new HPackDecodingException(CoreStrings.HPackHuffmanError, ex);
|
||||
}
|
||||
|
||||
_state = nextState;
|
||||
return _huffman
|
||||
? Huffman.Decode(_stringOctets, 0, _stringLength)
|
||||
: Encoding.ASCII.GetString(_stringOctets, 0, _stringLength);
|
||||
}
|
||||
|
||||
private HeaderField GetHeader(int index) => index <= StaticTable.Instance.Length
|
||||
? StaticTable.Instance[index - 1]
|
||||
: _dynamicTable[index - StaticTable.Instance.Length - 1];
|
||||
private HeaderField GetHeader(int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
return index <= StaticTable.Instance.Count
|
||||
? StaticTable.Instance[index - 1]
|
||||
: _dynamicTable[index - StaticTable.Instance.Count - 1];
|
||||
}
|
||||
catch (IndexOutOfRangeException ex)
|
||||
{
|
||||
throw new HPackDecodingException(CoreStrings.FormatHPackErrorIndexOutOfRange(index), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,30 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
public struct HeaderField
|
||||
{
|
||||
public HeaderField(string name, string value)
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.4.1
|
||||
public const int RfcOverhead = 32;
|
||||
|
||||
public HeaderField(Span<byte> name, Span<byte> value)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
Name = new byte[name.Length];
|
||||
name.CopyTo(Name);
|
||||
|
||||
Value = new byte[value.Length];
|
||||
value.CopyTo(Value);
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string Value { get; }
|
||||
public byte[] Name { get; }
|
||||
|
||||
public byte[] Value { get; }
|
||||
|
||||
public int Length => GetLength(Name.Length, Value.Length);
|
||||
|
||||
public static int GetLength(int nameLength, int valueLenth) => nameLength + valueLenth + 32;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
|
|
@ -301,45 +299,108 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
return _encodingTable[data];
|
||||
}
|
||||
|
||||
public static string Decode(byte[] data, int offset, int count)
|
||||
/// <summary>
|
||||
/// Decodes a Huffman encoded string from a byte array.
|
||||
/// </summary>
|
||||
/// <param name="src">The source byte array containing the encoded data.</param>
|
||||
/// <param name="offset">The offset in the byte array where the coded data starts.</param>
|
||||
/// <param name="count">The number of bytes to decode.</param>
|
||||
/// <param name="dst">The destination byte array to store the decoded data.</param>
|
||||
/// <returns>The number of decoded symbols.</returns>
|
||||
public static int Decode(byte[] src, int offset, int count, byte[] dst)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var i = offset;
|
||||
var j = 0;
|
||||
var lastDecodedBits = 0;
|
||||
while (i < count)
|
||||
{
|
||||
var next = (uint)(data[i] << 24 + lastDecodedBits);
|
||||
next |= (i + 1 < data.Length ? (uint)(data[i + 1] << 16 + lastDecodedBits) : 0);
|
||||
next |= (i + 2 < data.Length ? (uint)(data[i + 2] << 8 + lastDecodedBits) : 0);
|
||||
next |= (i + 3 < data.Length ? (uint)(data[i + 3] << lastDecodedBits) : 0);
|
||||
var next = (uint)(src[i] << 24 + lastDecodedBits);
|
||||
next |= (i + 1 < src.Length ? (uint)(src[i + 1] << 16 + lastDecodedBits) : 0);
|
||||
next |= (i + 2 < src.Length ? (uint)(src[i + 2] << 8 + lastDecodedBits) : 0);
|
||||
next |= (i + 3 < src.Length ? (uint)(src[i + 3] << lastDecodedBits) : 0);
|
||||
|
||||
var ones = (uint)(int.MinValue >> (8 - lastDecodedBits - 1));
|
||||
if (i == count - 1 && (next & ones) == ones)
|
||||
if (i == count - 1 && lastDecodedBits > 0 && (next & ones) == ones)
|
||||
{
|
||||
// Padding
|
||||
// The remaining 7 or less bits are all 1, which is padding.
|
||||
// We specifically check that lastDecodedBits > 0 because padding
|
||||
// longer than 7 bits should be treated as a decoding error.
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.5.2
|
||||
break;
|
||||
}
|
||||
|
||||
var ch = Decode(next, out var decodedBits);
|
||||
sb.Append((char)ch);
|
||||
// The longest possible symbol size is 30 bits. If we're at the last 4 bytes
|
||||
// of the input, we need to make sure we pass the correct number of valid bits
|
||||
// left, otherwise the trailing 0s in next may form a valid symbol.
|
||||
var validBits = Math.Min(30, (8 - lastDecodedBits) + (count - i - 1) * 8);
|
||||
var ch = Decode(next, validBits, out var decodedBits);
|
||||
|
||||
if (ch == -1)
|
||||
{
|
||||
// No valid symbol could be decoded with the bits in next
|
||||
throw new HuffmanDecodingException(CoreStrings.HPackHuffmanErrorIncomplete);
|
||||
}
|
||||
else if (ch == 256)
|
||||
{
|
||||
// A Huffman-encoded string literal containing the EOS symbol MUST be treated as a decoding error.
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.5.2
|
||||
throw new HuffmanDecodingException(CoreStrings.HPackHuffmanErrorEOS);
|
||||
}
|
||||
|
||||
if (j == dst.Length)
|
||||
{
|
||||
throw new HuffmanDecodingException(CoreStrings.HPackHuffmanErrorDestinationTooSmall);
|
||||
}
|
||||
|
||||
dst[j++] = (byte)ch;
|
||||
|
||||
// If we crossed a byte boundary, advance i so we start at the next byte that's not fully decoded.
|
||||
lastDecodedBits += decodedBits;
|
||||
i += lastDecodedBits / 8;
|
||||
|
||||
// Modulo 8 since we only care about how many bits were decoded in the last byte that we processed.
|
||||
lastDecodedBits %= 8;
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
return j;
|
||||
}
|
||||
|
||||
public static int Decode(uint data, out int decodedBits)
|
||||
/// <summary>
|
||||
/// Decodes a single symbol from a 32-bit word.
|
||||
/// </summary>
|
||||
/// <param name="data">A 32-bit word containing a Huffman encoded symbol.</param>
|
||||
/// <param name="validBits">
|
||||
/// The number of bits in <paramref name="data"/> that may contain an encoded symbol.
|
||||
/// This is not the exact number of bits that encode the symbol. Instead, it prevents
|
||||
/// decoding the lower bits of <paramref name="data"/> if they don't contain any
|
||||
/// encoded data.
|
||||
/// </param>
|
||||
/// <param name="decodedBits">The number of bits decoded from <paramref name="data"/>.</param>
|
||||
/// <returns>The decoded symbol.</returns>
|
||||
public static int Decode(uint data, int validBits, out int decodedBits)
|
||||
{
|
||||
// The code below implements the decoding logic for a canonical Huffman code.
|
||||
//
|
||||
// To decode a symbol, we scan the decoding table, which is sorted by ascending symbol bit length.
|
||||
// For each bit length b, we determine the maximum b-bit encoded value, plus one (that is codeMax).
|
||||
// This is done with the following logic:
|
||||
//
|
||||
// if we're at the first entry in the table,
|
||||
// codeMax = the # of symbols encoded in b bits
|
||||
// else,
|
||||
// left-shift codeMax by the difference between b and the previous entry's bit length,
|
||||
// then increment codeMax by the # of symbols encoded in b bits
|
||||
//
|
||||
// Next, we look at the value v encoded in the highest b bits of data. If v is less than codeMax,
|
||||
// those bits correspond to a Huffman encoded symbol. We find the corresponding decoded
|
||||
// symbol in the list of values associated with bit length b in the decoding table by indexing it
|
||||
// with codeMax - v.
|
||||
|
||||
var codeMax = 0;
|
||||
|
||||
for (var i = 0; i < _decodingTable.Length; i++)
|
||||
for (var i = 0; i < _decodingTable.Length && _decodingTable[i].codeLength <= validBits; i++)
|
||||
{
|
||||
var (codeLength, codes) = _decodingTable[i];
|
||||
var mask = int.MinValue >> (codeLength - 1);
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
|
|
@ -348,6 +409,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
|
||||
codeMax += codes.Length;
|
||||
|
||||
var mask = int.MinValue >> (codeLength - 1);
|
||||
var masked = (data & mask) >> (32 - codeLength);
|
||||
|
||||
if (masked < codeMax)
|
||||
|
|
@ -357,7 +419,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
|||
}
|
||||
}
|
||||
|
||||
throw new Exception();
|
||||
decodedBits = 0;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||
{
|
||||
public class Http2Connection : ITimeoutControl, IHttp2StreamLifetimeHandler
|
||||
public class Http2Connection : ITimeoutControl, IHttp2StreamLifetimeHandler, IHttpHeadersHandler
|
||||
{
|
||||
public static byte[] ClientPreface { get; } = Encoding.ASCII.GetBytes("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
{
|
||||
_context = context;
|
||||
_frameWriter = new Http2FrameWriter(context.Transport.Output, context.Application.Input);
|
||||
_hpackDecoder = new HPackDecoder();
|
||||
_hpackDecoder = new HPackDecoder((int)_serverSettings.HeaderTableSize);
|
||||
}
|
||||
|
||||
public string ConnectionId => _context.ConnectionId;
|
||||
|
|
@ -141,6 +141,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
error = ex;
|
||||
errorCode = ex.ErrorCode;
|
||||
}
|
||||
catch (HPackDecodingException ex)
|
||||
{
|
||||
// TODO: log
|
||||
error = ex;
|
||||
errorCode = Http2ErrorCode.COMPRESSION_ERROR;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO: log
|
||||
|
|
@ -350,9 +356,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
|
||||
_streams[_incomingFrame.StreamId] = _currentHeadersStream;
|
||||
|
||||
_hpackDecoder.Decode(_incomingFrame.HeadersPayload, _currentHeadersStream.RequestHeaders);
|
||||
var endHeaders = (_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS;
|
||||
_hpackDecoder.Decode(_incomingFrame.HeadersPayload, endHeaders, handler: this);
|
||||
|
||||
if ((_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS)
|
||||
if (endHeaders)
|
||||
{
|
||||
_highestOpenedStreamId = _incomingFrame.StreamId;
|
||||
_ = _currentHeadersStream.ProcessRequestsAsync();
|
||||
|
|
@ -525,9 +532,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR);
|
||||
}
|
||||
|
||||
_hpackDecoder.Decode(_incomingFrame.HeadersPayload, _currentHeadersStream.RequestHeaders);
|
||||
var endHeaders = (_incomingFrame.ContinuationFlags & Http2ContinuationFrameFlags.END_HEADERS) == Http2ContinuationFrameFlags.END_HEADERS;
|
||||
_hpackDecoder.Decode(_incomingFrame.HeadersPayload, endHeaders, handler: this);
|
||||
|
||||
if ((_incomingFrame.ContinuationFlags & Http2ContinuationFrameFlags.END_HEADERS) == Http2ContinuationFrameFlags.END_HEADERS)
|
||||
if (endHeaders)
|
||||
{
|
||||
_highestOpenedStreamId = _currentHeadersStream.StreamId;
|
||||
_ = _currentHeadersStream.ProcessRequestsAsync();
|
||||
|
|
@ -571,6 +579,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
_streams.TryRemove(streamId, out _);
|
||||
}
|
||||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
{
|
||||
_currentHeadersStream.OnHeader(name, value);
|
||||
}
|
||||
|
||||
void ITimeoutControl.SetTimeout(long ticks, TimeoutAction timeoutAction)
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1144,6 +1144,118 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
internal static string FormatEndPointHttp2NotNegotiated()
|
||||
=> GetString("EndPointHttp2NotNegotiated");
|
||||
|
||||
/// <summary>
|
||||
/// A dynamic table size of {size} octets is greater than the configured maximum size of {maxSize} octets.
|
||||
/// </summary>
|
||||
internal static string HPackErrorDynamicTableSizeUpdateTooLarge
|
||||
{
|
||||
get => GetString("HPackErrorDynamicTableSizeUpdateTooLarge");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A dynamic table size of {size} octets is greater than the configured maximum size of {maxSize} octets.
|
||||
/// </summary>
|
||||
internal static string FormatHPackErrorDynamicTableSizeUpdateTooLarge(object size, object maxSize)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("HPackErrorDynamicTableSizeUpdateTooLarge", "size", "maxSize"), size, maxSize);
|
||||
|
||||
/// <summary>
|
||||
/// Index {index} is outside the bounds of the header field table.
|
||||
/// </summary>
|
||||
internal static string HPackErrorIndexOutOfRange
|
||||
{
|
||||
get => GetString("HPackErrorIndexOutOfRange");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Index {index} is outside the bounds of the header field table.
|
||||
/// </summary>
|
||||
internal static string FormatHPackErrorIndexOutOfRange(object index)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("HPackErrorIndexOutOfRange", "index"), index);
|
||||
|
||||
/// <summary>
|
||||
/// Input data could not be fully decoded.
|
||||
/// </summary>
|
||||
internal static string HPackHuffmanErrorIncomplete
|
||||
{
|
||||
get => GetString("HPackHuffmanErrorIncomplete");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input data could not be fully decoded.
|
||||
/// </summary>
|
||||
internal static string FormatHPackHuffmanErrorIncomplete()
|
||||
=> GetString("HPackHuffmanErrorIncomplete");
|
||||
|
||||
/// <summary>
|
||||
/// Input data contains the EOS symbol.
|
||||
/// </summary>
|
||||
internal static string HPackHuffmanErrorEOS
|
||||
{
|
||||
get => GetString("HPackHuffmanErrorEOS");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input data contains the EOS symbol.
|
||||
/// </summary>
|
||||
internal static string FormatHPackHuffmanErrorEOS()
|
||||
=> GetString("HPackHuffmanErrorEOS");
|
||||
|
||||
/// <summary>
|
||||
/// The destination buffer is not large enough to store the decoded data.
|
||||
/// </summary>
|
||||
internal static string HPackHuffmanErrorDestinationTooSmall
|
||||
{
|
||||
get => GetString("HPackHuffmanErrorDestinationTooSmall");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The destination buffer is not large enough to store the decoded data.
|
||||
/// </summary>
|
||||
internal static string FormatHPackHuffmanErrorDestinationTooSmall()
|
||||
=> GetString("HPackHuffmanErrorDestinationTooSmall");
|
||||
|
||||
/// <summary>
|
||||
/// Huffman decoding error.
|
||||
/// </summary>
|
||||
internal static string HPackHuffmanError
|
||||
{
|
||||
get => GetString("HPackHuffmanError");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Huffman decoding error.
|
||||
/// </summary>
|
||||
internal static string FormatHPackHuffmanError()
|
||||
=> GetString("HPackHuffmanError");
|
||||
|
||||
/// <summary>
|
||||
/// Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets.
|
||||
/// </summary>
|
||||
internal static string HPackStringLengthTooLarge
|
||||
{
|
||||
get => GetString("HPackStringLengthTooLarge");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets.
|
||||
/// </summary>
|
||||
internal static string FormatHPackStringLengthTooLarge(object length, object maxStringLength)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("HPackStringLengthTooLarge", "length", "maxStringLength"), length, maxStringLength);
|
||||
|
||||
/// <summary>
|
||||
/// The header block was incomplete and could not be fully decoded.
|
||||
/// </summary>
|
||||
internal static string HPackErrorIncompleteHeaderBlock
|
||||
{
|
||||
get => GetString("HPackErrorIncompleteHeaderBlock");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The header block was incomplete and could not be fully decoded.
|
||||
/// </summary>
|
||||
internal static string FormatHPackErrorIncompleteHeaderBlock()
|
||||
=> GetString("HPackErrorIncompleteHeaderBlock");
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
|
@ -15,19 +14,18 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
|||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||
{
|
||||
public class Http2ConnectionTests : IDisposable
|
||||
public class Http2ConnectionTests : IDisposable, IHttpHeadersHandler
|
||||
{
|
||||
private static readonly string _largeHeaderA = new string('a', Http2Frame.MinAllowedMaxFrameSize - Http2Frame.HeaderLength - 8);
|
||||
private static readonly string _largeHeaderValue = new string('a', HPackDecoder.MaxStringOctets);
|
||||
|
||||
private static readonly string _largeHeaderB = new string('b', Http2Frame.MinAllowedMaxFrameSize - Http2Frame.HeaderLength - 8);
|
||||
|
||||
private static readonly IEnumerable<KeyValuePair<string, string>> _postRequestHeaders = new []
|
||||
private static readonly IEnumerable<KeyValuePair<string, string>> _postRequestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(":method", "POST"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
|
|
@ -35,7 +33,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
new KeyValuePair<string, string>(":scheme", "https"),
|
||||
};
|
||||
|
||||
private static readonly IEnumerable<KeyValuePair<string, string>> _browserRequestHeaders = new []
|
||||
private static readonly IEnumerable<KeyValuePair<string, string>> _browserRequestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
|
|
@ -48,23 +46,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
new KeyValuePair<string, string>("upgrade-insecure-requests", "1"),
|
||||
};
|
||||
|
||||
private static readonly IEnumerable<KeyValuePair<string, string>> _oneContinuationRequestHeaders = new []
|
||||
private static readonly IEnumerable<KeyValuePair<string, string>> _oneContinuationRequestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
||||
new KeyValuePair<string, string>(":scheme", "https"),
|
||||
new KeyValuePair<string, string>("a", _largeHeaderA)
|
||||
new KeyValuePair<string, string>("a", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("b", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("c", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("d", _largeHeaderValue)
|
||||
};
|
||||
|
||||
private static readonly IEnumerable<KeyValuePair<string, string>> _twoContinuationsRequestHeaders = new []
|
||||
private static readonly IEnumerable<KeyValuePair<string, string>> _twoContinuationsRequestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(":method", "GET"),
|
||||
new KeyValuePair<string, string>(":path", "/"),
|
||||
new KeyValuePair<string, string>(":authority", "127.0.0.1"),
|
||||
new KeyValuePair<string, string>(":scheme", "https"),
|
||||
new KeyValuePair<string, string>("a", _largeHeaderA),
|
||||
new KeyValuePair<string, string>("b", _largeHeaderB)
|
||||
new KeyValuePair<string, string>("a", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("b", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("c", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("d", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("e", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("f", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("g", _largeHeaderValue),
|
||||
new KeyValuePair<string, string>("h", _largeHeaderValue)
|
||||
};
|
||||
|
||||
private static readonly byte[] _helloBytes = Encoding.ASCII.GetBytes("hello");
|
||||
|
|
@ -77,12 +84,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
private readonly (IPipeConnection Transport, IPipeConnection Application) _pair;
|
||||
private readonly Http2ConnectionContext _connectionContext;
|
||||
private readonly Http2Connection _connection;
|
||||
private readonly HPackEncoder _hpackEncoder = new HPackEncoder();
|
||||
private readonly HPackDecoder _hpackDecoder = new HPackDecoder();
|
||||
private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings();
|
||||
private readonly HPackEncoder _hpackEncoder = new HPackEncoder();
|
||||
private readonly HPackDecoder _hpackDecoder;
|
||||
|
||||
private readonly ConcurrentDictionary<int, TaskCompletionSource<object>> _runningStreams = new ConcurrentDictionary<int, TaskCompletionSource<object>>();
|
||||
private readonly Dictionary<string, string> _receivedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, string> _decodedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<int> _abortedStreamIds = new HashSet<int>();
|
||||
private readonly object _abortedStreamIdsLock = new object();
|
||||
|
||||
|
|
@ -160,8 +168,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
|
||||
_largeHeadersApplication = context =>
|
||||
{
|
||||
context.Response.Headers["a"] = _largeHeaderA;
|
||||
context.Response.Headers["b"] = _largeHeaderB;
|
||||
foreach (var name in new[] { "a", "b", "c", "d", "e", "f", "g", "h" })
|
||||
{
|
||||
context.Response.Headers[name] = _largeHeaderValue;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
|
@ -208,6 +218,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
_runningStreams[streamIdFeature.StreamId].TrySetResult(null);
|
||||
};
|
||||
|
||||
_hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize);
|
||||
|
||||
_connectionContext = new Http2ConnectionContext
|
||||
{
|
||||
ServiceContext = new TestServiceContext(),
|
||||
|
|
@ -223,6 +235,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
_pipeFactory.Dispose();
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeader(Span<byte> name, Span<byte> value)
|
||||
{
|
||||
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DATA_Received_ReadByStream()
|
||||
{
|
||||
|
|
@ -752,6 +769,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HEADERS_Received_IncompleteHeaderBlockFragment_ConnectionError()
|
||||
{
|
||||
await InitializeConnectionAsync(_noopApplication);
|
||||
|
||||
await SendIncompleteHeadersFrameAsync(streamId: 1);
|
||||
|
||||
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, ignoreNonGoAwayFrames: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PRIORITY_Received_StreamIdZero_ConnectionError()
|
||||
{
|
||||
|
|
@ -1192,6 +1219,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CONTINUATION_Received_IncompleteHeaderBlockFragment_ConnectionError()
|
||||
{
|
||||
await InitializeConnectionAsync(_noopApplication);
|
||||
|
||||
await SendHeadersAsync(1, Http2HeadersFrameFlags.NONE, _postRequestHeaders);
|
||||
await SendIncompleteContinuationFrameAsync(streamId: 1);
|
||||
|
||||
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, ignoreNonGoAwayFrames: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CONTINUATION_Sent_WhenHeadersLargerThanFrameLength()
|
||||
{
|
||||
|
|
@ -1200,15 +1238,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
|
||||
|
||||
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
|
||||
withLength: 55,
|
||||
withLength: 12361,
|
||||
withFlags: (byte)Http2HeadersFrameFlags.NONE,
|
||||
withStreamId: 1);
|
||||
var continuationFrame1 = await ExpectAsync(Http2FrameType.CONTINUATION,
|
||||
withLength: 16373,
|
||||
withLength: 12306,
|
||||
withFlags: (byte)Http2ContinuationFrameFlags.NONE,
|
||||
withStreamId: 1);
|
||||
var continuationFrame2 = await ExpectAsync(Http2FrameType.CONTINUATION,
|
||||
withLength: 16373,
|
||||
withLength: 8204,
|
||||
withFlags: (byte)Http2ContinuationFrameFlags.END_HEADERS,
|
||||
withStreamId: 1);
|
||||
await ExpectAsync(Http2FrameType.DATA,
|
||||
|
|
@ -1218,18 +1256,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
|
||||
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
|
||||
|
||||
var responseHeaders = new HttpResponseHeaders();
|
||||
_hpackDecoder.Decode(headersFrame.HeadersPayload, responseHeaders);
|
||||
_hpackDecoder.Decode(continuationFrame1.HeadersPayload, responseHeaders);
|
||||
_hpackDecoder.Decode(continuationFrame2.HeadersPayload, responseHeaders);
|
||||
_hpackDecoder.Decode(headersFrame.HeadersPayload, endHeaders: false, handler: this);
|
||||
_hpackDecoder.Decode(continuationFrame1.HeadersPayload, endHeaders: false, handler: this);
|
||||
_hpackDecoder.Decode(continuationFrame2.HeadersPayload, endHeaders: true, handler: this);
|
||||
|
||||
var responseHeadersDictionary = (IDictionary<string, StringValues>)responseHeaders;
|
||||
Assert.Equal(5, responseHeadersDictionary.Count);
|
||||
Assert.Contains("date", responseHeadersDictionary.Keys, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Equal("200", responseHeadersDictionary[":status"]);
|
||||
Assert.Equal("0", responseHeadersDictionary["content-length"]);
|
||||
Assert.Equal(_largeHeaderA, responseHeadersDictionary["a"]);
|
||||
Assert.Equal(_largeHeaderB, responseHeadersDictionary["b"]);
|
||||
Assert.Equal(11, _decodedHeaders.Count);
|
||||
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
|
||||
Assert.Equal("200", _decodedHeaders[":status"]);
|
||||
Assert.Equal("0", _decodedHeaders["content-length"]);
|
||||
Assert.Equal(_largeHeaderValue, _decodedHeaders["a"]);
|
||||
Assert.Equal(_largeHeaderValue, _decodedHeaders["b"]);
|
||||
Assert.Equal(_largeHeaderValue, _decodedHeaders["c"]);
|
||||
Assert.Equal(_largeHeaderValue, _decodedHeaders["d"]);
|
||||
Assert.Equal(_largeHeaderValue, _decodedHeaders["e"]);
|
||||
Assert.Equal(_largeHeaderValue, _decodedHeaders["f"]);
|
||||
Assert.Equal(_largeHeaderValue, _decodedHeaders["g"]);
|
||||
Assert.Equal(_largeHeaderValue, _decodedHeaders["h"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -1525,12 +1567,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
return SendAsync(frame.Raw);
|
||||
}
|
||||
|
||||
private Task SendIncompleteHeadersFrameAsync(int streamId)
|
||||
{
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, streamId);
|
||||
frame.Length = 3;
|
||||
|
||||
// Set up an incomplete Literal Header Field w/ Incremental Indexing frame,
|
||||
// with an incomplete new name
|
||||
frame.Payload[0] = 0;
|
||||
frame.Payload[1] = 2;
|
||||
frame.Payload[2] = (byte)'a';
|
||||
|
||||
return SendAsync(frame.Raw);
|
||||
}
|
||||
|
||||
private async Task<bool> SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags)
|
||||
{
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareContinuation(flags, streamId);
|
||||
var done =_hpackEncoder.Encode(frame.Payload, out var length);
|
||||
var done = _hpackEncoder.Encode(frame.Payload, out var length);
|
||||
frame.Length = length;
|
||||
|
||||
await SendAsync(frame.Raw);
|
||||
|
|
@ -1538,6 +1596,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
return done;
|
||||
}
|
||||
|
||||
private Task SendIncompleteContinuationFrameAsync(int streamId)
|
||||
{
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareContinuation(Http2ContinuationFrameFlags.END_HEADERS, streamId);
|
||||
frame.Length = 3;
|
||||
|
||||
// Set up an incomplete Literal Header Field w/ Incremental Indexing frame,
|
||||
// with an incomplete new name
|
||||
frame.Payload[0] = 0;
|
||||
frame.Payload[1] = 2;
|
||||
frame.Payload[2] = (byte)'a';
|
||||
|
||||
return SendAsync(frame.Raw);
|
||||
}
|
||||
|
||||
private Task SendDataAsync(int streamId, Span<byte> data, bool endStream)
|
||||
{
|
||||
var frame = new Http2Frame();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -9,28 +9,143 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
{
|
||||
public class HuffmanTests
|
||||
{
|
||||
[Fact]
|
||||
public void HuffmanDecodeString()
|
||||
public static readonly TheoryData<byte[], byte[]> _validData = new TheoryData<byte[], byte[]>
|
||||
{
|
||||
// h e.......e l........l l o.......o
|
||||
var encodedHello = new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1111 };
|
||||
// Single 5-bit symbol
|
||||
{ new byte[] { 0x07 }, Encoding.ASCII.GetBytes("0") },
|
||||
// Single 6-bit symbol
|
||||
{ new byte[] { 0x57 }, Encoding.ASCII.GetBytes("%") },
|
||||
// Single 7-bit symbol
|
||||
{ new byte[] { 0xb9 }, Encoding.ASCII.GetBytes(":") },
|
||||
// Single 8-bit symbol
|
||||
{ new byte[] { 0xf8 }, Encoding.ASCII.GetBytes("&") },
|
||||
// Single 10-bit symbol
|
||||
{ new byte[] { 0xfe, 0x3f }, Encoding.ASCII.GetBytes("!") },
|
||||
// Single 11-bit symbol
|
||||
{ new byte[] { 0xff, 0x7f }, Encoding.ASCII.GetBytes("+") },
|
||||
// Single 12-bit symbol
|
||||
{ new byte[] { 0xff, 0xaf }, Encoding.ASCII.GetBytes("#") },
|
||||
// Single 13-bit symbol
|
||||
{ new byte[] { 0xff, 0xcf }, Encoding.ASCII.GetBytes("$") },
|
||||
// Single 14-bit symbol
|
||||
{ new byte[] { 0xff, 0xf3 }, Encoding.ASCII.GetBytes("^") },
|
||||
// Single 15-bit symbol
|
||||
{ new byte[] { 0xff, 0xf9 }, Encoding.ASCII.GetBytes("<") },
|
||||
// Single 19-bit symbol
|
||||
{ new byte[] { 0xff, 0xfe, 0x1f }, Encoding.ASCII.GetBytes("\\") },
|
||||
// Single 20-bit symbol
|
||||
{ new byte[] { 0xff, 0xfe, 0x6f }, new byte[] { 0x80 } },
|
||||
// Single 21-bit symbol
|
||||
{ new byte[] { 0xff, 0xfe, 0xe7 }, new byte[] { 0x99 } },
|
||||
// Single 22-bit symbol
|
||||
{ new byte[] { 0xff, 0xff, 0x4b }, new byte[] { 0x81 } },
|
||||
// Single 23-bit symbol
|
||||
{ new byte[] { 0xff, 0xff, 0xb1 }, new byte[] { 0x01 } },
|
||||
// Single 24-bit symbol
|
||||
{ new byte[] { 0xff, 0xff, 0xea }, new byte[] { 0x09 } },
|
||||
// Single 25-bit symbol
|
||||
{ new byte[] { 0xff, 0xff, 0xf6, 0x7f }, new byte[] { 0xc7 } },
|
||||
// Single 26-bit symbol
|
||||
{ new byte[] { 0xff, 0xff, 0xf8, 0x3f }, new byte[] { 0xc0 } },
|
||||
// Single 27-bit symbol
|
||||
{ new byte[] { 0xff, 0xff, 0xfb, 0xdf }, new byte[] { 0xcb } },
|
||||
// Single 28-bit symbol
|
||||
{ new byte[] { 0xff, 0xff, 0xfe, 0x2f }, new byte[] { 0x02 } },
|
||||
// Single 30-bit symbol
|
||||
{ new byte[] { 0xff, 0xff, 0xff, 0xf3 }, new byte[] { 0x0a } },
|
||||
|
||||
Assert.Equal("hello", Huffman.Decode(encodedHello, 0, encodedHello.Length));
|
||||
// h e l l o *
|
||||
{ new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1111 }, Encoding.ASCII.GetBytes("hello") },
|
||||
|
||||
var encodedHeader = new byte[]
|
||||
{
|
||||
0xb6, 0xb9, 0xac, 0x1c, 0x85, 0x58, 0xd5, 0x20, 0xa4, 0xb6, 0xc2, 0xad, 0x61, 0x7b, 0x5a, 0x54, 0x25, 0x1f
|
||||
};
|
||||
// Sequences that uncovered errors
|
||||
{ new byte[] { 0xb6, 0xb9, 0xac, 0x1c, 0x85, 0x58, 0xd5, 0x20, 0xa4, 0xb6, 0xc2, 0xad, 0x61, 0x7b, 0x5a, 0x54, 0x25, 0x1f }, Encoding.ASCII.GetBytes("upgrade-insecure-requests") },
|
||||
{ new byte[] { 0xfe, 0x53 }, Encoding.ASCII.GetBytes("\"t") }
|
||||
};
|
||||
|
||||
Assert.Equal("upgrade-insecure-requests", Huffman.Decode(encodedHeader, 0, encodedHeader.Length));
|
||||
[Theory]
|
||||
[MemberData(nameof(_validData))]
|
||||
public void HuffmanDecodeArray(byte[] encoded, byte[] expected)
|
||||
{
|
||||
var dst = new byte[expected.Length];
|
||||
Assert.Equal(expected.Length, Huffman.Decode(encoded, 0, encoded.Length, dst));
|
||||
Assert.Equal(expected, dst);
|
||||
}
|
||||
|
||||
encodedHeader = new byte[]
|
||||
{
|
||||
// "t
|
||||
0xfe, 0x53
|
||||
};
|
||||
public static readonly TheoryData<byte[]> _longPaddingData = new TheoryData<byte[]>
|
||||
{
|
||||
// h e l l o *
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1111, 0b11111111 },
|
||||
|
||||
Assert.Equal("\"t", Huffman.Decode(encodedHeader, 0, encodedHeader.Length));
|
||||
// '&' (8 bits) + 8 bit padding
|
||||
new byte[] { 0xf8, 0xff },
|
||||
|
||||
// ':' (7 bits) + 9 bit padding
|
||||
new byte[] { 0xb9, 0xff }
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(_longPaddingData))]
|
||||
public void ThrowsOnPaddingLongerThanSevenBits(byte[] encoded)
|
||||
{
|
||||
var exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(encoded, 0, encoded.Length, new byte[encoded.Length * 2]));
|
||||
Assert.Equal(CoreStrings.HPackHuffmanErrorIncomplete, exception.Message);
|
||||
}
|
||||
|
||||
public static readonly TheoryData<byte[]> _eosData = new TheoryData<byte[]>
|
||||
{
|
||||
// EOS
|
||||
new byte[] { 0xff, 0xff, 0xff, 0xff },
|
||||
// '&' + EOS + '0'
|
||||
new byte[] { 0xf8, 0xff, 0xff, 0xff, 0xfc, 0x1f }
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(_eosData))]
|
||||
public void ThrowsOnEOS(byte[] encoded)
|
||||
{
|
||||
var exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(encoded, 0, encoded.Length, new byte[encoded.Length * 2]));
|
||||
Assert.Equal(CoreStrings.HPackHuffmanErrorEOS, exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowsOnDestinationBufferTooSmall()
|
||||
{
|
||||
// h e l l o *
|
||||
var encoded = new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1111 };
|
||||
var exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(encoded, 0, encoded.Length, new byte[encoded.Length]));
|
||||
Assert.Equal(CoreStrings.HPackHuffmanErrorDestinationTooSmall, exception.Message);
|
||||
}
|
||||
|
||||
public static readonly TheoryData<byte[]> _incompleteSymbolData = new TheoryData<byte[]>
|
||||
{
|
||||
// h e l l o (incomplete)
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0 },
|
||||
|
||||
// Non-zero padding will be seen as incomplete symbol
|
||||
// h e l l o *
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0000 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0001 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0010 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0011 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0100 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0101 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0110 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_0111 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1000 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1001 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1010 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1011 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1100 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1101 },
|
||||
new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1110 }
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(_incompleteSymbolData))]
|
||||
public void ThrowsOnIncompleteSymbol(byte[] encoded)
|
||||
{
|
||||
var exception = Assert.Throws<HuffmanDecodingException>(() => Huffman.Decode(encoded, 0, encoded.Length, new byte[encoded.Length * 2]));
|
||||
Assert.Equal(CoreStrings.HPackHuffmanErrorIncomplete, exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -46,7 +161,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[MemberData(nameof(HuffmanData))]
|
||||
public void HuffmanDecode(int code, uint encoded, int bitLength)
|
||||
{
|
||||
Assert.Equal(code, Huffman.Decode(encoded, out var decodedBits));
|
||||
Assert.Equal(code, Huffman.Decode(encoded, bitLength, out var decodedBits));
|
||||
Assert.Equal(bitLength, decodedBits);
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +176,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
#pragma warning restore xUnit1026
|
||||
int bitLength)
|
||||
{
|
||||
Assert.Equal(code, Huffman.Decode(Huffman.Encode(code).encoded, out var decodedBits));
|
||||
Assert.Equal(code, Huffman.Decode(Huffman.Encode(code).encoded, bitLength, out var decodedBits));
|
||||
Assert.Equal(bitLength, decodedBits);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue