|
| 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