diff --git a/src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/H2StaticTable.cs b/src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/H2StaticTable.cs index 52de5ee8a84dc2..7f3b77558248a7 100644 --- a/src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/H2StaticTable.cs +++ b/src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/H2StaticTable.cs @@ -108,6 +108,7 @@ private static HeaderField CreateHeaderField(string name, string value) => public const int PathSlash = 4; public const int SchemeHttp = 6; public const int SchemeHttps = 7; + public const int Status200 = 8; public const int AcceptCharset = 15; public const int AcceptEncoding = 16; public const int AcceptLanguage = 17; diff --git a/src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/HPackEncoder.cs b/src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/HPackEncoder.cs index ac321b6f0d0be5..15aec6cfa6599a 100644 --- a/src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/HPackEncoder.cs +++ b/src/libraries/Common/src/System/Net/Http/aspnetcore/Http2/Hpack/HPackEncoder.cs @@ -3,172 +3,11 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; -#if KESTREL -using HeadersEnumerator = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.Http2HeadersEnumerator; -#else -using HeadersEnumerator = System.Collections.Generic.IEnumerator>; -#endif namespace System.Net.Http.HPack { - internal class HPackEncoder + internal static class HPackEncoder { - private HeadersEnumerator _enumerator; - - public bool BeginEncode(HeadersEnumerator enumerator, Span buffer, out int length) - { - _enumerator = enumerator; - _enumerator.MoveNext(); - - return Encode(buffer, out length); - } - - public bool BeginEncode(int statusCode, HeadersEnumerator enumerator, Span buffer, out int length) - { - _enumerator = enumerator; - _enumerator.MoveNext(); - - int statusCodeLength = EncodeStatusCode(statusCode, buffer); - bool done = Encode(buffer.Slice(statusCodeLength), throwIfNoneEncoded: false, out int headersLength); - length = statusCodeLength + headersLength; - - return done; - } - - public bool Encode(Span buffer, out int length) - { - return Encode(buffer, throwIfNoneEncoded: true, out length); - } - - private bool Encode(Span buffer, bool throwIfNoneEncoded, out int length) - { - int currentLength = 0; - do - { - if (!EncodeHeader(_enumerator.Current.Key, _enumerator.Current.Value, buffer.Slice(currentLength), out int headerLength)) - { - if (currentLength == 0 && throwIfNoneEncoded) - { - throw new HPackEncodingException(SR.net_http_hpack_encode_failure); - } - - length = currentLength; - return false; - } - - currentLength += headerLength; - } - while (_enumerator.MoveNext()); - - length = currentLength; - - return true; - } - - private int EncodeStatusCode(int statusCode, Span buffer) - { - switch (statusCode) - { - // Status codes which exist in the HTTP/2 StaticTable. - case 200: - case 204: - case 206: - case 304: - case 400: - case 404: - case 500: - buffer[0] = (byte)(0x80 | H2StaticTable.StatusIndex[statusCode]); - return 1; - default: - // Send as Literal Header Field Without Indexing - Indexed Name - buffer[0] = 0x08; - - ReadOnlySpan statusBytes = StatusCodes.ToStatusBytes(statusCode); - buffer[1] = (byte)statusBytes.Length; - statusBytes.CopyTo(buffer.Slice(2)); - - return 2 + statusBytes.Length; - } - } - - private bool EncodeHeader(string name, string value, Span buffer, out int length) - { - int i = 0; - length = 0; - - if (buffer.Length == 0) - { - return false; - } - - buffer[i++] = 0; - - if (i == buffer.Length) - { - return false; - } - - if (!EncodeString(name, buffer.Slice(i), out int nameLength, lowercase: true)) - { - return false; - } - - i += nameLength; - - if (i >= buffer.Length) - { - return false; - } - - if (!EncodeString(value, buffer.Slice(i), out int valueLength, lowercase: false)) - { - return false; - } - - i += valueLength; - - length = i; - return true; - } - - private bool EncodeString(string value, Span destination, out int bytesWritten, bool lowercase) - { - // From https://tools.ietf.org/html/rfc7541#section-5.2 - // ------------------------------------------------------ - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | H | String Length (7+) | - // +---+---------------------------+ - // | String Data (Length octets) | - // +-------------------------------+ - const int toLowerMask = 0x20; - - if (destination.Length != 0) - { - destination[0] = 0; // TODO: Use Huffman encoding - if (IntegerEncoder.Encode(value.Length, 7, destination, out int integerLength)) - { - Debug.Assert(integerLength >= 1); - - destination = destination.Slice(integerLength); - if (value.Length <= destination.Length) - { - for (int i = 0; i < value.Length; i++) - { - char c = value[i]; - destination[i] = (byte)(lowercase && (uint)(c - 'A') <= ('Z' - 'A') ? c | toLowerMask : c); - } - - bytesWritten = integerLength + value.Length; - return true; - } - } - } - - bytesWritten = 0; - return false; - } - // Things we should add: // * Huffman encoding // @@ -199,6 +38,43 @@ public static bool EncodeIndexedHeaderField(int index, Span destination, o return false; } + /// Encodes the status code of a response to the :status field. + public static bool EncodeStatusHeader(int statusCode, Span destination, out int bytesWritten) + { + // Bytes written depend on whether the status code value maps directly to an index + switch (statusCode) + { + case 200: + case 204: + case 206: + case 304: + case 400: + case 404: + case 500: + // Status codes which exist in the HTTP/2 StaticTable. + return EncodeIndexedHeaderField(H2StaticTable.StatusIndex[statusCode], destination, out bytesWritten); + default: + // If the status code doesn't have a static index then we need to include the full value. + // Write a status index and then the number bytes as a string literal. + if (!EncodeLiteralHeaderFieldWithoutIndexing(H2StaticTable.Status200, destination, out var nameLength)) + { + bytesWritten = 0; + return false; + } + + var statusBytes = StatusCodes.ToStatusBytes(statusCode); + + if (!EncodeStringLiteral(statusBytes, destination.Slice(nameLength), out var valueLength)) + { + bytesWritten = 0; + return false; + } + + bytesWritten = nameLength + valueLength; + return true; + } + } + /// Encodes a "Literal Header Field without Indexing". public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, string value, Span destination, out int bytesWritten) { @@ -233,7 +109,7 @@ public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, string val /// /// Encodes a "Literal Header Field without Indexing", but only the index portion; - /// a subsequent call to must be used to encode the associated value. + /// a subsequent call to EncodeStringLiteral must be used to encode the associated value. /// public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, Span destination, out int bytesWritten) { @@ -266,6 +142,39 @@ public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, Span return false; } + /// Encodes a "Literal Header Field without Indexing - New Name". + public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, string value, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.2.2 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | 0 | + // +---+---+-----------------------+ + // | H | Name Length (7+) | + // +---+---------------------------+ + // | Name String (Length octets) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + + if ((uint)destination.Length >= 3) + { + destination[0] = 0; + if (EncodeLiteralHeaderName(name, destination.Slice(1), out int nameLength) && + EncodeStringLiteral(value, destination.Slice(1 + nameLength), out int valueLength)) + { + bytesWritten = 1 + nameLength + valueLength; + return true; + } + } + + bytesWritten = 0; + return false; + } + /// Encodes a "Literal Header Field without Indexing - New Name". public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, ReadOnlySpan values, string separator, Span destination, out int bytesWritten) { @@ -301,7 +210,7 @@ public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, R /// /// Encodes a "Literal Header Field without Indexing - New Name", but only the name portion; - /// a subsequent call to must be used to encode the associated value. + /// a subsequent call to EncodeStringLiteral must be used to encode the associated value. /// public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, Span destination, out int bytesWritten) { @@ -397,6 +306,40 @@ private static bool EncodeStringLiteralValue(string value, Span destinatio return false; } + public static bool EncodeStringLiteral(ReadOnlySpan value, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-5.2 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | H | String Length (7+) | + // +---+---------------------------+ + // | String Data (Length octets) | + // +-------------------------------+ + + if (destination.Length != 0) + { + destination[0] = 0; // TODO: Use Huffman encoding + if (IntegerEncoder.Encode(value.Length, 7, destination, out int integerLength)) + { + Debug.Assert(integerLength >= 1); + + destination = destination.Slice(integerLength); + if (value.Length <= destination.Length) + { + // Note: No validation. Bytes should have already been validated. + value.CopyTo(destination); + + bytesWritten = integerLength + value.Length; + return true; + } + } + } + + bytesWritten = 0; + return false; + } + public static bool EncodeStringLiteral(string value, Span destination, out int bytesWritten) { // From https://tools.ietf.org/html/rfc7541#section-5.2 @@ -485,7 +428,7 @@ public static bool EncodeStringLiterals(ReadOnlySpan values, string sepa /// /// Encodes a "Literal Header Field without Indexing" to a new array, but only the index portion; - /// a subsequent call to must be used to encode the associated value. + /// a subsequent call to EncodeStringLiteral must be used to encode the associated value. /// public static byte[] EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(int index) { @@ -497,7 +440,7 @@ public static byte[] EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(int /// /// Encodes a "Literal Header Field without Indexing - New Name" to a new array, but only the name portion; - /// a subsequent call to must be used to encode the associated value. + /// a subsequent call to EncodeStringLiteral must be used to encode the associated value. /// public static byte[] EncodeLiteralHeaderFieldWithoutIndexingNewNameToAllocatedArray(string name) {