Skip to content

Commit 3b6ad60

Browse files
authored
HTTP/3: Unit tests for QPACK and validate end (#38670)
1 parent ba18614 commit 3b6ad60

File tree

8 files changed

+259
-11
lines changed

8 files changed

+259
-11
lines changed

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,13 @@ public void OnHeadersComplete(bool endStream)
191191

192192
public void OnStaticIndexedHeader(int index)
193193
{
194-
var knownHeader = H3StaticTable.GetHeaderFieldAt(index);
194+
var knownHeader = H3StaticTable.Get(index);
195195
OnHeader(knownHeader.Name, knownHeader.Value);
196196
}
197197

198198
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
199199
{
200-
var knownHeader = H3StaticTable.GetHeaderFieldAt(index);
200+
var knownHeader = H3StaticTable.Get(index);
201201
OnHeader(knownHeader.Name, value);
202202
}
203203

src/Servers/Kestrel/Core/src/Internal/Http3/QPack/EncoderStreamReader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ private System.Net.Http.QPack.HeaderField GetHeader(int index)
322322
{
323323
try
324324
{
325-
return _s ? H3StaticTable.GetHeaderFieldAt(index) : _dynamicTable[index];
325+
return _s ? H3StaticTable.Get(index) : _dynamicTable[index];
326326
}
327327
catch (IndexOutOfRangeException ex)
328328
{

src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,31 @@ public void BeginEncodeHeaders_NonStaticKey_WriteFullNameAndFullValue()
9393
}
9494

9595
[Fact]
96-
public void BeginEncodeHeaders_NoStatus_NonStaticKey_WriteFullNameAndFullValue()
96+
public void BeginEncodeHeaders_NonStaticKey_WriteFullNameAndFullValue_CustomHeader()
9797
{
9898
Span<byte> buffer = new byte[1024 * 16];
9999

100100
var headers = (IHeaderDictionary)new HttpResponseHeaders();
101-
headers.Translate = "private";
101+
headers["new-header"] = "value";
102+
103+
var totalHeaderSize = 0;
104+
var enumerator = new Http3HeadersEnumerator();
105+
enumerator.Initialize(headers);
106+
107+
Assert.True(QPackHeaderWriter.BeginEncodeHeaders(enumerator, buffer, ref totalHeaderSize, out var length));
108+
109+
var result = buffer.Slice(2, length - 2).ToArray(); // trim prefix
110+
var hex = BitConverter.ToString(result);
111+
Assert.Equal("37-03-6E-65-77-2D-68-65-61-64-65-72-05-76-61-6C-75-65", hex);
112+
}
113+
114+
[Fact]
115+
public void BeginEncodeHeaders_StaticKey_WriteStaticNameAndFullValue()
116+
{
117+
Span<byte> buffer = new byte[1024 * 16];
118+
119+
var headers = (IHeaderDictionary)new HttpResponseHeaders();
120+
headers.ContentType = "application/json";
102121

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

109128
var result = buffer.Slice(2, length - 2).ToArray();
110129
var hex = BitConverter.ToString(result);
111-
Assert.Equal("37-02-74-72-61-6E-73-6C-61-74-65-07-70-72-69-76-61-74-65", hex);
130+
Assert.Equal("5F-1D-10-61-70-70-6C-69-63-61-74-69-6F-6E-2F-6A-73-6F-6E", hex);
112131
}
113132
}

src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.key" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
1818
<Compile Include="$(RepoRoot)src\Shared\Buffers.MemoryPool\*.cs" LinkBase="MemoryPool" />
1919
<Compile Include="$(KestrelSharedSourceRoot)\CorrelationIdGenerator.cs" Link="Internal\CorrelationIdGenerator.cs" />
20-
<Compile Include="$(SharedSourceRoot)test\Shared.Tests\runtime\**\*.cs" Link="Shared\runtime\%(Filename)%(Extension)" />
20+
<Compile Include="$(SharedSourceRoot)test\Shared.Tests\runtime\Http2\*.cs" LinkBase="Shared\runtime\Http2" />
21+
<Compile Include="$(SharedSourceRoot)test\Shared.Tests\runtime\Http3\*.cs" LinkBase="Shared\runtime\Http3" />
2122
</ItemGroup>
2223

2324
<ItemGroup>

src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -718,13 +718,13 @@ public void OnHeadersComplete(bool endHeaders)
718718

719719
public void OnStaticIndexedHeader(int index)
720720
{
721-
var knownHeader = H3StaticTable.GetHeaderFieldAt(index);
721+
var knownHeader = H3StaticTable.Get(index);
722722
_headerHandler.DecodedHeaders[((Span<byte>)knownHeader.Name).GetAsciiStringNonNullCharacters()] = HttpUtilities.GetAsciiOrUTF8StringNonNullCharacters((ReadOnlySpan<byte>)knownHeader.Value);
723723
}
724724

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

730730
public void Complete()

src/Shared/runtime/Http3/QPack/H3StaticTable.Http3.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public static bool TryGetStatusIndex(int status, out int index)
4848
// TODO: just use Dictionary directly to avoid interface dispatch.
4949
public static IReadOnlyDictionary<HttpMethod, int> MethodIndex => s_methodIndex;
5050

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

5353
private static readonly HeaderField[] s_staticTable = new HeaderField[]
5454
{

src/Shared/runtime/Http3/QPack/QPackDecoder.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,36 @@ public void Decode(in ReadOnlySequence<byte> headerBlock, bool endHeaders, IHttp
180180
{
181181
foreach (ReadOnlyMemory<byte> segment in headerBlock)
182182
{
183-
Decode(segment.Span, endHeaders: false, handler);
183+
DecodeCore(segment.Span, handler);
184184
}
185+
CheckIncompleteHeaderBlock(endHeaders);
185186
}
186187

187188
public void Decode(ReadOnlySpan<byte> headerBlock, bool endHeaders, IHttpHeadersHandler handler)
189+
{
190+
DecodeCore(headerBlock, handler);
191+
CheckIncompleteHeaderBlock(endHeaders);
192+
}
193+
194+
private void DecodeCore(ReadOnlySpan<byte> headerBlock, IHttpHeadersHandler handler)
188195
{
189196
foreach (byte b in headerBlock)
190197
{
191198
OnByte(b, handler);
192199
}
193200
}
194201

202+
private void CheckIncompleteHeaderBlock(bool endHeaders)
203+
{
204+
if (endHeaders)
205+
{
206+
if (_state != State.CompressedHeaders)
207+
{
208+
throw new QPackDecodingException(SR.net_http_hpack_incomplete_header_block);
209+
}
210+
}
211+
}
212+
195213
private void OnByte(byte b, IHttpHeadersHandler handler)
196214
{
197215
int intResult;
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Buffers;
5+
using System.Linq;
6+
using System.Collections.Generic;
7+
using System.Text;
8+
using Xunit;
9+
using System.Net.Http.QPack;
10+
using System.Net.Http.HPack;
11+
using HeaderField = System.Net.Http.QPack.HeaderField;
12+
#if KESTREL
13+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
14+
#endif
15+
16+
namespace System.Net.Http.Unit.Tests.QPack
17+
{
18+
public class QPackDecoderTests
19+
{
20+
private const int MaxHeaderFieldSize = 8192;
21+
22+
// 4.5.2 - Indexed Field Line - Static Table - Index 25 (:method: GET)
23+
private static readonly byte[] _indexedFieldLineStatic = new byte[] { 0xd1 };
24+
25+
// 4.5.4 - Literal Header Field With Name Reference - Static Table - Index 44 (content-type)
26+
private static readonly byte[] _literalHeaderFieldWithNameReferenceStatic = new byte[] { 0x5f, 0x1d };
27+
28+
// 4.5.6 - Literal Field Line With Literal Name - (translate)
29+
private static readonly byte[] _literalFieldLineWithLiteralName = new byte[] { 0x37, 0x02, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65 };
30+
31+
private const string _contentTypeString = "content-type";
32+
private const string _translateString = "translate";
33+
34+
// n e w - h e a d e r *
35+
// 10101000 10111110 00010110 10011100 10100011 10010000 10110110 01111111
36+
private static readonly byte[] _headerNameHuffmanBytes = new byte[] { 0xa8, 0xbe, 0x16, 0x9c, 0xa3, 0x90, 0xb6, 0x7f };
37+
38+
private const string _headerNameString = "new-header";
39+
private const string _headerValueString = "value";
40+
41+
private static readonly byte[] _headerValueBytes = Encoding.ASCII.GetBytes(_headerValueString);
42+
43+
// v a l u e *
44+
// 11101110 00111010 00101101 00101111
45+
private static readonly byte[] _headerValueHuffmanBytes = new byte[] { 0xee, 0x3a, 0x2d, 0x2f };
46+
47+
private static readonly byte[] _headerNameHuffman = new byte[] { 0x3f, 0x01 }
48+
.Concat(_headerNameHuffmanBytes)
49+
.ToArray();
50+
51+
private static readonly byte[] _headerValue = new byte[] { (byte)_headerValueBytes.Length }
52+
.Concat(_headerValueBytes)
53+
.ToArray();
54+
55+
private static readonly byte[] _headerValueHuffman = new byte[] { (byte)(0x80 | _headerValueHuffmanBytes.Length) }
56+
.Concat(_headerValueHuffmanBytes)
57+
.ToArray();
58+
59+
private readonly QPackDecoder _decoder;
60+
private readonly TestHttpHeadersHandler _handler = new TestHttpHeadersHandler();
61+
62+
public QPackDecoderTests()
63+
{
64+
_decoder = new QPackDecoder(MaxHeaderFieldSize);
65+
}
66+
67+
[Fact]
68+
public void DecodesIndexedHeaderField_StaticTableWithValue()
69+
{
70+
_decoder.Decode(new byte[] { 0, 0 }, endHeaders: false, handler: _handler);
71+
_decoder.Decode(_indexedFieldLineStatic, endHeaders: true, handler: _handler);
72+
Assert.Equal("GET", _handler.DecodedHeaders[":method"]);
73+
74+
Assert.Equal(":method", _handler.DecodedStaticHeaders[H3StaticTable.MethodGet].Key);
75+
Assert.Equal("GET", _handler.DecodedStaticHeaders[H3StaticTable.MethodGet].Value);
76+
}
77+
78+
[Fact]
79+
public void DecodesIndexedHeaderField_StaticTableLiteralValue()
80+
{
81+
byte[] encoded = _literalHeaderFieldWithNameReferenceStatic
82+
.Concat(_headerValue)
83+
.ToArray();
84+
85+
_decoder.Decode(new byte[] { 0, 0 }, endHeaders: false, handler: _handler);
86+
_decoder.Decode(encoded, endHeaders: true, handler: _handler);
87+
Assert.Equal(_headerValueString, _handler.DecodedHeaders[_contentTypeString]);
88+
89+
Assert.Equal(_contentTypeString, _handler.DecodedStaticHeaders[H3StaticTable.ContentTypeApplicationDnsMessage].Key);
90+
Assert.Equal(_headerValueString, _handler.DecodedStaticHeaders[H3StaticTable.ContentTypeApplicationDnsMessage].Value);
91+
}
92+
93+
[Fact]
94+
public void DecodesLiteralFieldLineWithLiteralName_Value()
95+
{
96+
byte[] encoded = _literalFieldLineWithLiteralName
97+
.Concat(_headerValue)
98+
.ToArray();
99+
100+
TestDecodeWithoutIndexing(encoded, _translateString, _headerValueString);
101+
}
102+
103+
[Fact]
104+
public void DecodesLiteralFieldLineWithLiteralName_HuffmanEncodedValue()
105+
{
106+
byte[] encoded = _literalFieldLineWithLiteralName
107+
.Concat(_headerValueHuffman)
108+
.ToArray();
109+
110+
TestDecodeWithoutIndexing(encoded, _translateString, _headerValueString);
111+
}
112+
113+
[Fact]
114+
public void DecodesLiteralFieldLineWithLiteralName_HuffmanEncodedName()
115+
{
116+
byte[] encoded = _headerNameHuffman
117+
.Concat(_headerValue)
118+
.ToArray();
119+
120+
TestDecodeWithoutIndexing(encoded, _headerNameString, _headerValueString);
121+
}
122+
123+
public static readonly TheoryData<byte[]> _incompleteHeaderBlockData = new TheoryData<byte[]>
124+
{
125+
// Incomplete header
126+
new byte[] { },
127+
new byte[] { 0x00 },
128+
129+
// 4.5.4 - Literal Header Field With Name Reference - Static Table - Index 44 (content-type)
130+
new byte[] { 0x00, 0x00, 0x5f },
131+
132+
// 4.5.6 - Literal Field Line With Literal Name - (translate)
133+
new byte[] { 0x00, 0x00, 0x37 },
134+
new byte[] { 0x00, 0x00, 0x37, 0x02 },
135+
new byte[] { 0x00, 0x00, 0x37, 0x02, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74 },
136+
};
137+
138+
[Theory]
139+
[MemberData(nameof(_incompleteHeaderBlockData))]
140+
public void DecodesIncompleteHeaderBlock_Error(byte[] encoded)
141+
{
142+
QPackDecodingException exception = Assert.Throws<QPackDecodingException>(() => _decoder.Decode(encoded, endHeaders: true, handler: _handler));
143+
Assert.Equal(SR.net_http_hpack_incomplete_header_block, exception.Message);
144+
Assert.Empty(_handler.DecodedHeaders);
145+
}
146+
147+
private static void TestDecodeWithoutIndexing(byte[] encoded, string expectedHeaderName, string expectedHeaderValue)
148+
{
149+
TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: false, byteAtATime: false);
150+
TestDecode(encoded, expectedHeaderName, expectedHeaderValue, expectDynamicTableEntry: false, byteAtATime: true);
151+
}
152+
153+
private static void TestDecode(byte[] encoded, string expectedHeaderName, string expectedHeaderValue, bool expectDynamicTableEntry, bool byteAtATime)
154+
{
155+
var decoder = new QPackDecoder(MaxHeaderFieldSize);
156+
var handler = new TestHttpHeadersHandler();
157+
158+
// Read past header
159+
decoder.Decode(new byte[] { 0x00, 0x00 }, endHeaders: false, handler: handler);
160+
161+
if (!byteAtATime)
162+
{
163+
decoder.Decode(encoded, endHeaders: true, handler: handler);
164+
}
165+
else
166+
{
167+
// Parse data in 1 byte chunks, separated by empty chunks
168+
for (int i = 0; i < encoded.Length; i++)
169+
{
170+
bool end = i + 1 == encoded.Length;
171+
172+
decoder.Decode(Array.Empty<byte>(), endHeaders: false, handler: handler);
173+
decoder.Decode(new byte[] { encoded[i] }, endHeaders: end, handler: handler);
174+
}
175+
}
176+
177+
Assert.Equal(expectedHeaderValue, handler.DecodedHeaders[expectedHeaderName]);
178+
}
179+
}
180+
181+
public class TestHttpHeadersHandler : IHttpHeadersHandler
182+
{
183+
public Dictionary<string, string> DecodedHeaders { get; } = new Dictionary<string, string>();
184+
public Dictionary<int, KeyValuePair<string, string>> DecodedStaticHeaders { get; } = new Dictionary<int, KeyValuePair<string, string>>();
185+
186+
void IHttpHeadersHandler.OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
187+
{
188+
string headerName = Encoding.ASCII.GetString(name);
189+
string headerValue = Encoding.ASCII.GetString(value);
190+
191+
DecodedHeaders[headerName] = headerValue;
192+
}
193+
194+
void IHttpHeadersHandler.OnStaticIndexedHeader(int index)
195+
{
196+
ref readonly HeaderField entry = ref H3StaticTable.Get(index);
197+
((IHttpHeadersHandler)this).OnHeader(entry.Name, entry.Value);
198+
DecodedStaticHeaders[index] = new KeyValuePair<string, string>(Encoding.ASCII.GetString(entry.Name), Encoding.ASCII.GetString(entry.Value));
199+
}
200+
201+
void IHttpHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
202+
{
203+
byte[] name = H3StaticTable.Get(index).Name;
204+
((IHttpHeadersHandler)this).OnHeader(name, value);
205+
DecodedStaticHeaders[index] = new KeyValuePair<string, string>(Encoding.ASCII.GetString(name), Encoding.ASCII.GetString(value));
206+
}
207+
208+
void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }
209+
}
210+
}

0 commit comments

Comments
 (0)