Skip to content

HTTP/3: Unit tests for QPACK and validate end #38670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,13 @@ public void OnHeadersComplete(bool endStream)

public void OnStaticIndexedHeader(int index)
{
var knownHeader = H3StaticTable.GetHeaderFieldAt(index);
var knownHeader = H3StaticTable.Get(index);
OnHeader(knownHeader.Name, knownHeader.Value);
}

public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
var knownHeader = H3StaticTable.GetHeaderFieldAt(index);
var knownHeader = H3StaticTable.Get(index);
OnHeader(knownHeader.Name, value);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ private System.Net.Http.QPack.HeaderField GetHeader(int index)
{
try
{
return _s ? H3StaticTable.GetHeaderFieldAt(index) : _dynamicTable[index];
return _s ? H3StaticTable.Get(index) : _dynamicTable[index];
}
catch (IndexOutOfRangeException ex)
{
Expand Down
25 changes: 22 additions & 3 deletions src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,31 @@ public void BeginEncodeHeaders_NonStaticKey_WriteFullNameAndFullValue()
}

[Fact]
public void BeginEncodeHeaders_NoStatus_NonStaticKey_WriteFullNameAndFullValue()
public void BeginEncodeHeaders_NonStaticKey_WriteFullNameAndFullValue_CustomHeader()
{
Span<byte> buffer = new byte[1024 * 16];

var headers = (IHeaderDictionary)new HttpResponseHeaders();
headers.Translate = "private";
headers["new-header"] = "value";

var totalHeaderSize = 0;
var enumerator = new Http3HeadersEnumerator();
enumerator.Initialize(headers);

Assert.True(QPackHeaderWriter.BeginEncodeHeaders(enumerator, buffer, ref totalHeaderSize, out var length));

var result = buffer.Slice(2, length - 2).ToArray(); // trim prefix
var hex = BitConverter.ToString(result);
Assert.Equal("37-03-6E-65-77-2D-68-65-61-64-65-72-05-76-61-6C-75-65", hex);
}

[Fact]
public void BeginEncodeHeaders_StaticKey_WriteStaticNameAndFullValue()
{
Span<byte> buffer = new byte[1024 * 16];

var headers = (IHeaderDictionary)new HttpResponseHeaders();
headers.ContentType = "application/json";

var totalHeaderSize = 0;
var enumerator = new Http3HeadersEnumerator();
Expand All @@ -108,6 +127,6 @@ public void BeginEncodeHeaders_NoStatus_NonStaticKey_WriteFullNameAndFullValue()

var result = buffer.Slice(2, length - 2).ToArray();
var hex = BitConverter.ToString(result);
Assert.Equal("37-02-74-72-61-6E-73-6C-61-74-65-07-70-72-69-76-61-74-65", hex);
Assert.Equal("5F-1D-10-61-70-70-6C-69-63-61-74-69-6F-6E-2F-6A-73-6F-6E", hex);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.key" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
<Compile Include="$(RepoRoot)src\Shared\Buffers.MemoryPool\*.cs" LinkBase="MemoryPool" />
<Compile Include="$(KestrelSharedSourceRoot)\CorrelationIdGenerator.cs" Link="Internal\CorrelationIdGenerator.cs" />
<Compile Include="$(SharedSourceRoot)test\Shared.Tests\runtime\**\*.cs" Link="Shared\runtime\%(Filename)%(Extension)" />
<Compile Include="$(SharedSourceRoot)test\Shared.Tests\runtime\Http2\*.cs" LinkBase="Shared\runtime\Http2" />
<Compile Include="$(SharedSourceRoot)test\Shared.Tests\runtime\Http3\*.cs" LinkBase="Shared\runtime\Http3" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -718,13 +718,13 @@ public void OnHeadersComplete(bool endHeaders)

public void OnStaticIndexedHeader(int index)
{
var knownHeader = H3StaticTable.GetHeaderFieldAt(index);
var knownHeader = H3StaticTable.Get(index);
_headerHandler.DecodedHeaders[((Span<byte>)knownHeader.Name).GetAsciiStringNonNullCharacters()] = HttpUtilities.GetAsciiOrUTF8StringNonNullCharacters((ReadOnlySpan<byte>)knownHeader.Value);
}

public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
_headerHandler.DecodedHeaders[((Span<byte>)H3StaticTable.GetHeaderFieldAt(index).Name).GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters();
_headerHandler.DecodedHeaders[((Span<byte>)H3StaticTable.Get(index).Name).GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters();
}

public void Complete()
Expand Down
2 changes: 1 addition & 1 deletion src/Shared/runtime/Http3/QPack/H3StaticTable.Http3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public static bool TryGetStatusIndex(int status, out int index)
// TODO: just use Dictionary directly to avoid interface dispatch.
public static IReadOnlyDictionary<HttpMethod, int> MethodIndex => s_methodIndex;

public static HeaderField GetHeaderFieldAt(int index) => s_staticTable[index];
public static ref HeaderField Get(int index) => ref s_staticTable[index];

private static readonly HeaderField[] s_staticTable = new HeaderField[]
{
Expand Down
20 changes: 19 additions & 1 deletion src/Shared/runtime/Http3/QPack/QPackDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,18 +180,36 @@ public void Decode(in ReadOnlySequence<byte> headerBlock, bool endHeaders, IHttp
{
foreach (ReadOnlyMemory<byte> segment in headerBlock)
{
Decode(segment.Span, endHeaders: false, handler);
DecodeCore(segment.Span, handler);
}
CheckIncompleteHeaderBlock(endHeaders);
}

public void Decode(ReadOnlySpan<byte> headerBlock, bool endHeaders, IHttpHeadersHandler handler)
{
DecodeCore(headerBlock, handler);
CheckIncompleteHeaderBlock(endHeaders);
}

private void DecodeCore(ReadOnlySpan<byte> headerBlock, IHttpHeadersHandler handler)
{
foreach (byte b in headerBlock)
{
OnByte(b, handler);
}
}

private void CheckIncompleteHeaderBlock(bool endHeaders)
{
if (endHeaders)
{
if (_state != State.CompressedHeaders)
{
throw new QPackDecodingException(SR.net_http_hpack_incomplete_header_block);
}
}
}

private void OnByte(byte b, IHttpHeadersHandler handler)
{
int intResult;
Expand Down
210 changes: 210 additions & 0 deletions src/Shared/test/Shared.Tests/runtime/Http3/QPackDecoderTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using Xunit;
using System.Net.Http.QPack;
using System.Net.Http.HPack;
using HeaderField = System.Net.Http.QPack.HeaderField;
#if KESTREL
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
#endif

namespace System.Net.Http.Unit.Tests.QPack
{
public class QPackDecoderTests
{
private const int MaxHeaderFieldSize = 8192;

// 4.5.2 - Indexed Field Line - Static Table - Index 25 (:method: GET)
private static readonly byte[] _indexedFieldLineStatic = new byte[] { 0xd1 };

// 4.5.4 - Literal Header Field With Name Reference - Static Table - Index 44 (content-type)
private static readonly byte[] _literalHeaderFieldWithNameReferenceStatic = new byte[] { 0x5f, 0x1d };

// 4.5.6 - Literal Field Line With Literal Name - (translate)
private static readonly byte[] _literalFieldLineWithLiteralName = new byte[] { 0x37, 0x02, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65 };

private const string _contentTypeString = "content-type";
private const string _translateString = "translate";

// 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 _headerNameString = "new-header";
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[] _headerNameHuffman = new byte[] { 0x3f, 0x01 }
.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();

private readonly QPackDecoder _decoder;
private readonly TestHttpHeadersHandler _handler = new TestHttpHeadersHandler();

public QPackDecoderTests()
{
_decoder = new QPackDecoder(MaxHeaderFieldSize);
}

[Fact]
public void DecodesIndexedHeaderField_StaticTableWithValue()
{
_decoder.Decode(new byte[] { 0, 0 }, endHeaders: false, handler: _handler);
_decoder.Decode(_indexedFieldLineStatic, endHeaders: true, handler: _handler);
Assert.Equal("GET", _handler.DecodedHeaders[":method"]);

Assert.Equal(":method", _handler.DecodedStaticHeaders[H3StaticTable.MethodGet].Key);
Assert.Equal("GET", _handler.DecodedStaticHeaders[H3StaticTable.MethodGet].Value);
}

[Fact]
public void DecodesIndexedHeaderField_StaticTableLiteralValue()
{
byte[] encoded = _literalHeaderFieldWithNameReferenceStatic
.Concat(_headerValue)
.ToArray();

_decoder.Decode(new byte[] { 0, 0 }, endHeaders: false, handler: _handler);
_decoder.Decode(encoded, endHeaders: true, handler: _handler);
Assert.Equal(_headerValueString, _handler.DecodedHeaders[_contentTypeString]);

Assert.Equal(_contentTypeString, _handler.DecodedStaticHeaders[H3StaticTable.ContentTypeApplicationDnsMessage].Key);
Assert.Equal(_headerValueString, _handler.DecodedStaticHeaders[H3StaticTable.ContentTypeApplicationDnsMessage].Value);
}

[Fact]
public void DecodesLiteralFieldLineWithLiteralName_Value()
{
byte[] encoded = _literalFieldLineWithLiteralName
.Concat(_headerValue)
.ToArray();

TestDecodeWithoutIndexing(encoded, _translateString, _headerValueString);
}

[Fact]
public void DecodesLiteralFieldLineWithLiteralName_HuffmanEncodedValue()
{
byte[] encoded = _literalFieldLineWithLiteralName
.Concat(_headerValueHuffman)
.ToArray();

TestDecodeWithoutIndexing(encoded, _translateString, _headerValueString);
}

[Fact]
public void DecodesLiteralFieldLineWithLiteralName_HuffmanEncodedName()
{
byte[] encoded = _headerNameHuffman
.Concat(_headerValue)
.ToArray();

TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
}

public static readonly TheoryData<byte[]> _incompleteHeaderBlockData = new TheoryData<byte[]>
{
// Incomplete header
new byte[] { },
new byte[] { 0x00 },

// 4.5.4 - Literal Header Field With Name Reference - Static Table - Index 44 (content-type)
new byte[] { 0x00, 0x00, 0x5f },

// 4.5.6 - Literal Field Line With Literal Name - (translate)
new byte[] { 0x00, 0x00, 0x37 },
new byte[] { 0x00, 0x00, 0x37, 0x02 },
new byte[] { 0x00, 0x00, 0x37, 0x02, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74 },
};

[Theory]
[MemberData(nameof(_incompleteHeaderBlockData))]
public void DecodesIncompleteHeaderBlock_Error(byte[] encoded)
{
QPackDecodingException exception = Assert.Throws<QPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: _handler));
Assert.Equal(SR.net_http_hpack_incomplete_header_block, exception.Message);
Assert.Empty(_handler.DecodedHeaders);
}

private static void TestDecodeWithoutIndexing(byte[] encoded, string expectedHeaderName, string expectedHeaderValue)
{
TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: false, byteAtATime: false);
TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: false, byteAtATime: true);
}

private static void TestDecode(byte[] encoded, string expectedHeaderName, string expectedHeaderValue, bool expectDynamicTableEntry, bool byteAtATime)
{
var decoder = new QPackDecoder(MaxHeaderFieldSize);
var handler = new TestHttpHeadersHandler();

// Read past header
decoder.Decode(new byte[] { 0x00, 0x00 }, endHeaders: false, handler: handler);

if (!byteAtATime)
{
decoder.Decode(encoded, endHeaders: true, handler: handler);
}
else
{
// Parse data in 1 byte chunks, separated by empty chunks
for (int i = 0; i < encoded.Length; i++)
{
bool end = i + 1 == encoded.Length;

decoder.Decode(Array.Empty<byte>(), endHeaders: false, handler: handler);
decoder.Decode(new byte[] { encoded[i] }, endHeaders: end, handler: handler);
}
}

Assert.Equal(expectedHeaderValue, handler.DecodedHeaders[expectedHeaderName]);
}
}

public class TestHttpHeadersHandler : IHttpHeadersHandler
{
public Dictionary<string, string> DecodedHeaders { get; } = new Dictionary<string, string>();
public Dictionary<int, KeyValuePair<string, string>> DecodedStaticHeaders { get; } = new Dictionary<int, KeyValuePair<string, string>>();

void IHttpHeadersHandler.OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
string headerName = Encoding.ASCII.GetString(name);
string headerValue = Encoding.ASCII.GetString(value);

DecodedHeaders[headerName] = headerValue;
}

void IHttpHeadersHandler.OnStaticIndexedHeader(int index)
{
ref readonly HeaderField entry = ref H3StaticTable.Get(index);
((IHttpHeadersHandler)this).OnHeader(entry.Name, entry.Value);
DecodedStaticHeaders[index] = new KeyValuePair<string, string>(Encoding.ASCII.GetString(entry.Name), Encoding.ASCII.GetString(entry.Value));
}

void IHttpHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
byte[] name = H3StaticTable.Get(index).Name;
((IHttpHeadersHandler)this).OnHeader(name, value);
DecodedStaticHeaders[index] = new KeyValuePair<string, string>(Encoding.ASCII.GetString(name), Encoding.ASCII.GetString(value));
}

void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }
}
}