--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using System.Text;
+
+namespace System.Net.Test.Common
+{
+ public static class HPackEncoder
+ {
+ /// <summary>
+ /// Encodes a dynamic table size update.
+ /// </summary>
+ /// <param name="newMaximumSize">The new maximum size of the dynamic table. This must be less than or equal to the connection's maximum table size setting, which defaults to 4096 bytes.</param>
+ /// <param name="headerBlock">A span to write the encoded header to.</param>
+ /// <returns>The number of bytes written to <paramref name="headerBlock"/>.</returns>
+ public static int EncodeDynamicTableSizeUpdate(int newMaximumSize, Span<byte> headerBlock)
+ {
+ return EncodeInteger(newMaximumSize, 0b00100000, 0b11100000, headerBlock);
+ }
+
+ /// <summary>
+ /// Encodes a header using an index for both name and value.
+ /// </summary>
+ /// <param name="headerIndex">The header index to encode.</param>
+ /// <param name="headerBlock">A span to write the encoded header to.</param>
+ /// <returns>The number of bytes written to <paramref name="headerBlock"/>.</returns>
+ public static int EncodeHeader(int headerIndex, Span<byte> headerBlock)
+ {
+ Debug.Assert(headerIndex > 0);
+ return EncodeInteger(headerIndex, 0b10000000, 0b10000000, headerBlock);
+ }
+
+ /// <summary>
+ /// Encodes a header using an indexed name and literal value.
+ /// </summary>
+ /// <param name="nameIdx">An index of a header containing the name for this header.</param>
+ /// <param name="value">A literal value to encode for this header.</param>
+ /// <param name="headerBlock">A span to write the encoded header to.</param>
+ /// <returns>The number of bytes written to <paramref name="headerBlock"/>.</returns>
+ public static int EncodeHeader(int nameIdx, string value, HPackFlags flags, Span<byte> headerBlock)
+ {
+ Debug.Assert(nameIdx > 0);
+ return EncodeHeaderImpl(nameIdx, null, value, flags, headerBlock);
+ }
+
+ /// <summary>
+ /// Encodes a header using a literal name and value.
+ /// </summary>
+ /// <param name="name">A literal name to encode for this header.</param>
+ /// <param name="value">A literal value to encode for this header.</param>
+ /// <param name="headerBlock">A span to write the encoded header to.</param>
+ /// <returns>The number of bytes written to <paramref name="headerBlock"/>.</returns>
+ public static int EncodeHeader(string name, string value, HPackFlags flags, Span<byte> headerBlock)
+ {
+ return EncodeHeaderImpl(0, name, value, flags, headerBlock);
+ }
+
+ private static int EncodeHeaderImpl(int nameIdx, string name, string value, HPackFlags flags, Span<byte> headerBlock)
+ {
+ const HPackFlags IndexingMask = HPackFlags.NeverIndexed | HPackFlags.NewIndexed | HPackFlags.WithoutIndexing;
+
+ Debug.Assert((nameIdx != 0) != (name != null), $"Only one of {nameof(nameIdx)} or {nameof(name)} can be used.");
+ Debug.Assert(name != null || (flags & HPackFlags.HuffmanEncodeName) == 0, "An indexed name can not be huffman encoded.");
+
+ byte prefix, prefixMask;
+
+ switch (flags & IndexingMask)
+ {
+ case HPackFlags.WithoutIndexing:
+ prefix = 0;
+ prefixMask = 0b11110000;
+ break;
+ case HPackFlags.NewIndexed:
+ prefix = 0b01000000;
+ prefixMask = 0b11000000;
+ break;
+ case HPackFlags.NeverIndexed:
+ prefix = 0b00010000;
+ prefixMask = 0b11110000;
+ break;
+ default:
+ throw new Exception("invalid indexing flag");
+ }
+
+ int bytesGenerated = EncodeInteger(nameIdx, prefix, prefixMask, headerBlock);
+
+ if (name != null)
+ {
+ bytesGenerated += EncodeString(name, headerBlock.Slice(bytesGenerated), (flags & HPackFlags.HuffmanEncodeName) != 0);
+ }
+
+ bytesGenerated += EncodeString(value, headerBlock.Slice(bytesGenerated), (flags & HPackFlags.HuffmanEncodeValue) != 0);
+ return bytesGenerated;
+ }
+
+ private static int EncodeString(string value, Span<byte> headerBlock, bool huffmanEncode)
+ {
+ byte[] data = Encoding.ASCII.GetBytes(value);
+ byte prefix;
+
+ if (!huffmanEncode)
+ {
+ prefix = 0;
+ }
+ else
+ {
+ int len = HuffmanEncoder.GetEncodedLength(data);
+
+ byte[] huffmanData = new byte[len];
+ HuffmanEncoder.Encode(data, huffmanData);
+
+ data = huffmanData;
+ prefix = 0x80;
+ }
+
+ int bytesGenerated = 0;
+
+ bytesGenerated += EncodeInteger(data.Length, prefix, 0x80, headerBlock);
+
+ data.AsSpan().CopyTo(headerBlock.Slice(bytesGenerated));
+ bytesGenerated += data.Length;
+
+ return bytesGenerated;
+ }
+
+ public static int EncodeInteger(int value, byte prefix, byte prefixMask, Span<byte> headerBlock)
+ {
+ byte prefixLimit = (byte)(~prefixMask);
+
+ if (value < prefixLimit)
+ {
+ headerBlock[0] = (byte)(prefix | value);
+ return 1;
+ }
+
+ headerBlock[0] = (byte)(prefix | prefixLimit);
+ int bytesGenerated = 1;
+
+ value -= prefixLimit;
+
+ while (value >= 0x80)
+ {
+ headerBlock[bytesGenerated] = (byte)((value & 0x7F) | 0x80);
+ value >>= 7;
+ bytesGenerated++;
+ }
+
+ headerBlock[bytesGenerated] = (byte)value;
+ bytesGenerated++;
+
+ return bytesGenerated;
+ }
+ }
+
+ public enum HPackFlags
+ {
+ /// <summary>
+ /// Encodes a header literal without indexing and without huffman encoding.
+ /// </summary>
+ None = 0,
+
+ /// <summary>
+ /// Applies Huffman encoding to the header's name.
+ /// </summary>
+ HuffmanEncodeName = 1,
+
+ /// <summary>
+ /// Applies Huffman encoding to the header's value.
+ /// </summary>
+ HuffmanEncodeValue = 2,
+
+ /// <summary>
+ /// Applies Huffman encoding to both the name and the value of the header.
+ /// </summary>
+ HuffmanEncode = HuffmanEncodeName | HuffmanEncodeValue,
+
+ /// <summary>
+ /// Encode a literal value without adding a new dynamic index. Intermediaries (such as a proxy) are still allowed to index the value when forwarding the header.
+ /// </summary>
+ WithoutIndexing = 0,
+
+ /// <summary>
+ /// Encode a literal value to a new dynamic index.
+ /// </summary>
+ NewIndexed = 4,
+
+ /// <summary>
+ /// Encode a literal value without adding a new dynamic index. Intermediaries (such as a proxy) must not index the value when forwarding the header.
+ /// </summary>
+ NeverIndexed = 8
+ }
+}
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Net.Http.Functional.Tests;
using System.Net.Security;
return (2, prefixMask + b);
}
- private static int EncodeInteger(int value, byte prefix, byte prefixMask, Span<byte> headerBlock)
- {
- byte prefixLimit = (byte)(~prefixMask);
-
- if (value < prefixLimit)
- {
- headerBlock[0] = (byte)(prefix | value);
- return 1;
- }
-
- headerBlock[0] = (byte)(prefix | prefixLimit);
- int bytesGenerated = 1;
-
- value -= prefixLimit;
-
- while (value >= 0x80)
- {
- headerBlock[bytesGenerated] = (byte)((value & 0x7F) | 0x80);
- value = value >> 7;
- bytesGenerated++;
- }
-
- headerBlock[bytesGenerated] = (byte)value;
- bytesGenerated++;
-
- return bytesGenerated;
- }
-
private static (int bytesConsumed, string value) DecodeString(ReadOnlySpan<byte> headerBlock)
{
(int bytesConsumed, int stringLength) = DecodeInteger(headerBlock, 0b01111111);
}
}
- private static int EncodeString(string value, Span<byte> headerBlock, bool huffmanEncode)
- {
- byte[] data = Encoding.ASCII.GetBytes(value);
- byte prefix;
-
- if (!huffmanEncode)
- {
- prefix = 0;
- }
- else
- {
- int len = HuffmanEncoder.GetEncodedLength(data);
-
- byte[] huffmanData = new byte[len];
- HuffmanEncoder.Encode(data, huffmanData);
-
- data = huffmanData;
- prefix = 0x80;
- }
-
- int bytesGenerated = 0;
-
- bytesGenerated += EncodeInteger(data.Length, prefix, 0x80, headerBlock);
-
- data.AsSpan().CopyTo(headerBlock.Slice(bytesGenerated));
- bytesGenerated += data.Length;
-
- return bytesGenerated;
- }
-
private static readonly HttpHeaderData[] s_staticTable = new HttpHeaderData[]
{
new HttpHeaderData(":authority", ""),
}
}
- public static int EncodeHeader(HttpHeaderData headerData, Span<byte> headerBlock)
- {
- // Always encode as literal, no indexing.
- int bytesGenerated = EncodeInteger(0, 0, 0b11110000, headerBlock);
- bytesGenerated += EncodeString(headerData.Name, headerBlock.Slice(bytesGenerated), headerData.HuffmanEncoded);
- bytesGenerated += EncodeString(headerData.Value, headerBlock.Slice(bytesGenerated), headerData.HuffmanEncoded);
- return bytesGenerated;
- }
-
- public static int EncodeDynamicTableSizeUpdate(int newMaximumSize, Span<byte> headerBlock)
- {
- return EncodeInteger(newMaximumSize, 0b00100000, 0b11100000, headerBlock);
- }
-
public async Task<byte[]> ReadBodyAsync()
{
byte[] body = null;
if (!isTrailingHeader)
{
string statusCodeString = ((int)statusCode).ToString();
- bytesGenerated += EncodeHeader(new HttpHeaderData(":status", statusCodeString), headerBlock.AsSpan());
+ bytesGenerated += HPackEncoder.EncodeHeader(":status", statusCodeString, HPackFlags.None, headerBlock.AsSpan());
}
if (headers != null)
{
foreach (HttpHeaderData headerData in headers)
{
- bytesGenerated += EncodeHeader(headerData, headerBlock.AsSpan().Slice(bytesGenerated));
+ bytesGenerated += HPackEncoder.EncodeHeader(headerData.Name, headerData.Value, headerData.HuffmanEncoded ? HPackFlags.HuffmanEncode : HPackFlags.None, headerBlock.AsSpan(bytesGenerated));
}
}
throw new IndexOutOfRangeException();
}
- return _buffer[_insertIndex == 0 ? _buffer.Length - 1 : _insertIndex - index - 1];
+ index = _insertIndex - index - 1;
+
+ if (index < 0)
+ {
+ // _buffer is circular; wrap the index back around.
+ index += _buffer.Length;
+ }
+
+ return _buffer[index];
}
}
if (_integerDecoder.StartDecode((byte)(b & ~HuffmanMask), StringLengthPrefix))
{
+ if (_integerDecoder.Value == 0)
+ {
+ throw new HPackDecodingException(SR.Format(SR.net_http_invalid_response_header_name, ""));
+ }
+
OnStringLength(_integerDecoder.Value, nextState: State.HeaderName);
}
else
case State.HeaderNameLengthContinue:
if (_integerDecoder.Decode(b))
{
+ // IntegerDecoder disallows overlong encodings, where an integer is encoded with more bytes than is strictly required.
+ // 0 should always be represented by a single byte, so we shouldn't need to check for it in the continuation case.
+ Debug.Assert(_integerDecoder.Value != 0, "A header name length of 0 should never be encoded with a continuation byte.");
+
OnStringLength(_integerDecoder.Value, nextState: State.HeaderName);
}
case State.HeaderValueLengthContinue:
if (_integerDecoder.Decode(b))
{
+ // IntegerDecoder disallows overlong encodings where an integer is encoded with more bytes than is strictly required.
+ // 0 should always be represented by a single byte, so we shouldn't need to check for it in the continuation case.
+ Debug.Assert(_integerDecoder.Value != 0, "A header value length of 0 should never be encoded with a continuation byte.");
+
OnStringLength(_integerDecoder.Value, nextState: State.HeaderValue);
}
public int Length => GetLength(Name.Length, Value.Length);
- public static int GetLength(int nameLength, int valueLenth) => nameLength + valueLenth + 32;
+ public static int GetLength(int nameLength, int valueLenth) => nameLength + valueLenth + RfcOverhead;
}
}
await Assert.ThrowsAsync<HttpRequestException>(() => client.SendAsync(m));
}
}
+
+ [Fact]
+ public async Task SendAsync_WithZeroLengthHeaderName_Throws()
+ {
+ await LoopbackServerFactory.CreateClientAndServerAsync(
+ async uri =>
+ {
+ using HttpClient client = CreateHttpClient();
+ await Assert.ThrowsAsync<HttpRequestException>(() => client.GetAsync(uri));
+ },
+ async server =>
+ {
+ await server.HandleRequestAsync(headers: new[]
+ {
+ new HttpHeaderData("", "foo")
+ });
+ });
+ }
}
}
private static Frame MakeSimpleContinuationFrame(int streamId, bool endHeaders = false)
{
Memory<byte> headerBlock = new byte[Frame.MaxFrameLength];
- int bytesGenerated = Http2LoopbackConnection.EncodeHeader(new HttpHeaderData("foo", "bar"), headerBlock.Span);
+ int bytesGenerated = HPackEncoder.EncodeHeader("foo", "bar", HPackFlags.None, headerBlock.Span);
return new ContinuationFrame(headerBlock.Slice(0, bytesGenerated),
(endHeaders ? FrameFlags.EndHeaders : FrameFlags.None),
}
byte[] headerData = new byte[16];
- int headersLen = Http2LoopbackConnection.EncodeDynamicTableSizeUpdate(headerTableSize + 1, headerData);
+ int headersLen = HPackEncoder.EncodeDynamicTableSizeUpdate(headerTableSize + 1, headerData);
HeadersFrame frame = new HeadersFrame(headerData.AsMemory(0, headersLen), FrameFlags.EndHeaders | FrameFlags.EndStream, 0, 0, 0, streamId);
await con.WriteFrameAsync(frame);
<Compile Include="$(CommonTestPath)\System\Net\Http\Http2Frames.cs">
<Link>Common\System\Net\Http\Http2Frames.cs</Link>
</Compile>
+ <Compile Include="$(CommonTestPath)\System\Net\Http\HPackEncoder.cs">
+ <Link>Common\System\Net\Http\HPackEncoder.cs</Link>
+ </Compile>
<Compile Include="$(CommonTestPath)\System\Net\Http\Http2LoopbackServer.cs">
<Link>Common\System\Net\Http\Http2LoopbackServer.cs</Link>
</Compile>
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Net.Http.HPack;
+using System.Reflection;
+using System.Text;
+using Xunit;
+
+namespace System.Net.Http.Unit.Tests.HPack
+{
+ public class DynamicTableTest
+ {
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(3)]
+ public void DynamicTable_WrapsRingBuffer_Success(int targetInsertIndex)
+ {
+ FieldInfo insertIndexField = typeof(DynamicTable).GetField("_insertIndex", BindingFlags.NonPublic | BindingFlags.Instance);
+ var table = new DynamicTable(maxSize: 256);
+ var insertedHeaders = new Stack<byte[]>();
+
+ // Insert into dynamic table until its insert index into its ring buffer loops back to 0.
+ do
+ {
+ InsertOne();
+ }
+ while ((int)insertIndexField.GetValue(table) != 0);
+
+ // Finally loop until the insert index reaches the target.
+ while ((int)insertIndexField.GetValue(table) != targetInsertIndex)
+ {
+ InsertOne();
+ }
+
+ void InsertOne()
+ {
+ byte[] data = Encoding.ASCII.GetBytes($"header-{insertedHeaders.Count}");
+
+ insertedHeaders.Push(data);
+ table.Insert(data, data);
+ }
+
+ // Now check to see that we can retrieve the remaining headers.
+ // Some headers will have been evacuated from the table during this process, so we don't exhaust the entire insertedHeaders stack.
+ Assert.True(table.Count > 0);
+ Assert.True(table.Count < insertedHeaders.Count);
+
+ for (int i = 0; i < table.Count; ++i)
+ {
+ HeaderField dynamicField = table[i];
+ byte[] expectedData = insertedHeaders.Pop();
+
+ Assert.True(expectedData.AsSpan().SequenceEqual(dynamicField.Name));
+ Assert.True(expectedData.AsSpan().SequenceEqual(dynamicField.Value));
+ }
+ }
+ }
+}
using Xunit;
using System.Buffers;
+using HPackEncoder = System.Net.Test.Common.HPackEncoder;
+
namespace System.Net.Http.Unit.Tests.HPack
{
public class HPackDecoderTest
<Link>ProductionCode\Common\CoreLib\System\IO\StreamHelpers.CopyValidation.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\System\Net\InternalException.cs">
- <Link>Common\System\Net\InternalException.cs</Link>
+ <Link>ProductionCode\Common\System\Net\InternalException.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\System\Net\HttpDateParser.cs">
- <Link>Common\System\Net\HttpDateParser.cs</Link>
+ <Link>ProductionCode\Common\System\Net\HttpDateParser.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\System\Net\HttpKnownHeaderNames.cs">
<Link>ProductionCode\Common\System\Net\HttpKnownHeaderNames.cs</Link>
<Link>ProductionCode\Common\System\Net\Logging\NetEventSource.Common.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\System\Net\SecurityProtocol.cs">
- <Link>Common\System\Net\SecurityProtocol.cs</Link>
+ <Link>ProductionCode\Common\System\Net\SecurityProtocol.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\System\Net\UriScheme.cs">
- <Link>Common\System\Net\UriScheme.cs</Link>
+ <Link>ProductionCode\Common\System\Net\UriScheme.cs</Link>
</Compile>
<Compile Include="$(CommonTestPath)\System\ShouldNotBeInvokedException.cs">
<Link>Common\System\ShouldNotBeInvokedException.cs</Link>
</Compile>
+ <Compile Include="$(CommonTestPath)\System\Net\Http\HPackEncoder.cs">
+ <Link>Common\System\Net\Http\HPackEncoder.cs</Link>
+ </Compile>
+ <Compile Include="$(CommonTestPath)\System\Net\Http\HuffmanEncoder.cs">
+ <Link>Common\System\Net\Http\HuffmanEncoder.cs</Link>
+ </Compile>
<Compile Include="$(CommonPath)\System\Net\Mail\MailAddress.cs">
<Link>ProductionCode\Common\src\System\Net\Mail\MailAddress.cs</Link>
</Compile>
<Compile Include="Headers\UriHeaderParserTest.cs" />
<Compile Include="Headers\ViaHeaderValueTest.cs" />
<Compile Include="Headers\WarningHeaderValueTest.cs" />
+ <Compile Include="HPack\DynamicTableTest.cs" />
<Compile Include="HPack\HPackDecoderTest.cs" />
<Compile Include="HPack\HPackIntegerTest.cs" />
<Compile Include="HPack\HuffmanDecodingTests.cs" />
<Link>ProductionCode\System\Net\Http\WinHttpTraceHelper.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Windows\Interop.Libraries.cs">
- <Link>Common\Interop\Windows\Interop.Libraries.cs</Link>
+ <Link>ProductionCode\Common\Interop\Windows\Interop.Libraries.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Windows\Crypt32\Interop.CertEnumCertificatesInStore.cs">
- <Link>Common\Interop\Windows\Crypt32\Interop.CertEnumCertificatesInStore.cs</Link>
+ <Link>ProductionCode\Common\Interop\Windows\Crypt32\Interop.CertEnumCertificatesInStore.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Windows\Crypt32\Interop.certificates_types.cs">
- <Link>Common\Interop\Windows\Crypt32\Interop.certificates_types.cs</Link>
+ <Link>ProductionCode\Common\Interop\Windows\Crypt32\Interop.certificates_types.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Windows\Interop.HRESULT_FROM_WIN32.cs">
- <Link>Common\Interop\Windows\Interop.HRESULT_FROM_WIN32.cs</Link>
+ <Link>ProductionCode\Common\Interop\Windows\Interop.HRESULT_FROM_WIN32.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Windows\WinHttp\Interop.SafeWinHttpHandle.cs">
- <Link>Common\Interop\Windows\WinHttp\Interop.SafeWinHttpHandle.cs</Link>
+ <Link>ProductionCode\Common\Interop\Windows\WinHttp\Interop.SafeWinHttpHandle.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\System\Runtime\ExceptionServices\ExceptionStackTrace.cs">
- <Link>Common\System\Runtime\ExceptionServices\ExceptionStackTrace.cs</Link>
+ <Link>ProductionCode\Common\System\Runtime\ExceptionServices\ExceptionStackTrace.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Windows\WinHttp\Interop.winhttp_types.cs">
- <Link>Common\Interop\Windows\WinHttp\Interop.winhttp_types.cs</Link>
+ <Link>ProductionCode\Common\Interop\Windows\WinHttp\Interop.winhttp_types.cs</Link>
</Compile>
<Compile Include="..\..\..\System.Net.Http.WinHttpHandler\tests\UnitTests\FakeInterop.cs">
<Link>WinHttpHandler\UnitTests\FakeInterop.cs</Link>