Fast path for validating static table HTTP/2 headers (#24730)

This commit is contained in:
James Newton-King 2020-08-14 13:13:33 +12:00 committed by GitHub
parent 7f7528faae
commit ffc0bf914a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 614 additions and 144 deletions

View File

@ -6617,7 +6617,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
ref StringValues values = ref Unsafe.AsRef<StringValues>(null);
var flag = 0L;
// Does the name matched any "known" headers
// Does the name match any "known" headers
switch (name.Length)
{
case 2:
@ -7070,6 +7070,251 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public unsafe bool TryHPackAppend(int index, ReadOnlySpan<byte> value)
{
ref StringValues values = ref Unsafe.AsRef<StringValues>(null);
var nameStr = string.Empty;
var flag = 0L;
// Does the HPack static index match any "known" headers
switch (index)
{
case 1:
flag = 0x100000L;
values = ref _headers._Authority;
nameStr = HeaderNames.Authority;
break;
case 2:
case 3:
flag = 0x200000L;
values = ref _headers._Method;
nameStr = HeaderNames.Method;
break;
case 4:
case 5:
flag = 0x400000L;
values = ref _headers._Path;
nameStr = HeaderNames.Path;
break;
case 6:
case 7:
flag = 0x800000L;
values = ref _headers._Scheme;
nameStr = HeaderNames.Scheme;
break;
case 15:
flag = 0x2000000L;
values = ref _headers._AcceptCharset;
nameStr = HeaderNames.AcceptCharset;
break;
case 16:
flag = 0x4000000L;
values = ref _headers._AcceptEncoding;
nameStr = HeaderNames.AcceptEncoding;
break;
case 17:
flag = 0x8000000L;
values = ref _headers._AcceptLanguage;
nameStr = HeaderNames.AcceptLanguage;
break;
case 19:
flag = 0x1000000L;
values = ref _headers._Accept;
nameStr = HeaderNames.Accept;
break;
case 22:
flag = 0x800L;
values = ref _headers._Allow;
nameStr = HeaderNames.Allow;
break;
case 23:
flag = 0x10000000L;
values = ref _headers._Authorization;
nameStr = HeaderNames.Authorization;
break;
case 24:
flag = 0x1L;
values = ref _headers._CacheControl;
nameStr = HeaderNames.CacheControl;
break;
case 26:
flag = 0x2000L;
values = ref _headers._ContentEncoding;
nameStr = HeaderNames.ContentEncoding;
break;
case 27:
flag = 0x4000L;
values = ref _headers._ContentLanguage;
nameStr = HeaderNames.ContentLanguage;
break;
case 28:
if (ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultRequestHeaderEncodingSelector))
{
AppendContentLength(value);
}
else
{
AppendContentLengthCustomEncoding(value, EncodingSelector(HeaderNames.ContentLength));
}
return true;
case 29:
flag = 0x8000L;
values = ref _headers._ContentLocation;
nameStr = HeaderNames.ContentLocation;
break;
case 30:
flag = 0x20000L;
values = ref _headers._ContentRange;
nameStr = HeaderNames.ContentRange;
break;
case 31:
flag = 0x1000L;
values = ref _headers._ContentType;
nameStr = HeaderNames.ContentType;
break;
case 32:
flag = 0x20000000L;
values = ref _headers._Cookie;
nameStr = HeaderNames.Cookie;
break;
case 33:
flag = 0x4L;
values = ref _headers._Date;
nameStr = HeaderNames.Date;
break;
case 35:
flag = 0x40000000L;
values = ref _headers._Expect;
nameStr = HeaderNames.Expect;
break;
case 36:
flag = 0x40000L;
values = ref _headers._Expires;
nameStr = HeaderNames.Expires;
break;
case 37:
flag = 0x80000000L;
values = ref _headers._From;
nameStr = HeaderNames.From;
break;
case 38:
flag = 0x400000000L;
values = ref _headers._Host;
nameStr = HeaderNames.Host;
break;
case 39:
flag = 0x800000000L;
values = ref _headers._IfMatch;
nameStr = HeaderNames.IfMatch;
break;
case 40:
flag = 0x1000000000L;
values = ref _headers._IfModifiedSince;
nameStr = HeaderNames.IfModifiedSince;
break;
case 41:
flag = 0x2000000000L;
values = ref _headers._IfNoneMatch;
nameStr = HeaderNames.IfNoneMatch;
break;
case 42:
flag = 0x4000000000L;
values = ref _headers._IfRange;
nameStr = HeaderNames.IfRange;
break;
case 43:
flag = 0x8000000000L;
values = ref _headers._IfUnmodifiedSince;
nameStr = HeaderNames.IfUnmodifiedSince;
break;
case 44:
flag = 0x80000L;
values = ref _headers._LastModified;
nameStr = HeaderNames.LastModified;
break;
case 47:
flag = 0x10000000000L;
values = ref _headers._MaxForwards;
nameStr = HeaderNames.MaxForwards;
break;
case 49:
flag = 0x20000000000L;
values = ref _headers._ProxyAuthorization;
nameStr = HeaderNames.ProxyAuthorization;
break;
case 50:
flag = 0x80000000000L;
values = ref _headers._Range;
nameStr = HeaderNames.Range;
break;
case 51:
flag = 0x40000000000L;
values = ref _headers._Referer;
nameStr = HeaderNames.Referer;
break;
case 57:
flag = 0x80L;
values = ref _headers._TransferEncoding;
nameStr = HeaderNames.TransferEncoding;
break;
case 58:
flag = 0x400000000000L;
values = ref _headers._UserAgent;
nameStr = HeaderNames.UserAgent;
break;
case 60:
flag = 0x200L;
values = ref _headers._Via;
nameStr = HeaderNames.Via;
break;
}
if (flag != 0)
{
// Matched a known header
if ((_previousBits & flag) != 0)
{
// Had a previous string for this header, mark it as used so we don't clear it OnHeadersComplete or consider it if we get a second header
_previousBits ^= flag;
// We will only reuse this header if there was only one previous header
if (values.Count == 1)
{
var previousValue = values.ToString();
// Check lengths are the same, then if the bytes were converted to an ascii string if they would be the same.
// We do not consider Utf8 headers for reuse.
if (previousValue.Length == value.Length &&
StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, value))
{
// The previous string matches what the bytes would convert to, so we will just use that one.
_bits |= flag;
return true;
}
}
}
// We didn't have a previous matching header value, or have already added a header, so get the string for this value.
var valueStr = value.GetRequestHeaderString(nameStr, EncodingSelector);
if ((_bits & flag) == 0)
{
// We didn't already have a header set, so add a new one.
_bits |= flag;
values = new StringValues(valueStr);
}
else
{
// We already had a header set, so concatenate the new one.
values = AppendValue(values, valueStr);
}
return true;
}
else
{
return false;
}
}
private struct HeaderReferences
{
public StringValues _CacheControl;
@ -13716,4 +13961,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
}
}
}

View File

@ -513,27 +513,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public virtual void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
_requestHeadersParsed++;
if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
{
KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
}
IncrementRequestHeadersCount();
HttpRequestHeaders.Append(name, value);
}
public virtual void OnHeader(int index, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
IncrementRequestHeadersCount();
// This method should be overriden in specific implementations and the base should be
// called to validate the header count.
}
public void OnTrailer(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
// Trailers still count towards the limit.
IncrementRequestHeadersCount();
string key = name.GetHeaderName();
var valueStr = value.GetRequestHeaderString(key, HttpRequestHeaders.EncodingSelector);
RequestTrailers.Append(key, valueStr);
}
private void IncrementRequestHeadersCount()
{
_requestHeadersParsed++;
if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
{
KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
}
string key = name.GetHeaderName();
var valueStr = value.GetRequestHeaderString(key, HttpRequestHeaders.EncodingSelector);
RequestTrailers.Append(key, valueStr);
}
public void OnHeadersComplete()

View File

@ -1230,6 +1230,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// For now these either need to be connection errors or BadRequests. If we want to downgrade any of them to stream errors later then we need to
// rework the flow so that the remaining headers are drained and the decompression state is maintained.
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
OnHeaderCore(index: null, name, value);
}
public void OnStaticIndexedHeader(int index)
{
Debug.Assert(index <= H2StaticTable.Count);
ref readonly var entry = ref H2StaticTable.Get(index - 1);
OnHeaderCore(index, entry.Name, entry.Value);
}
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
Debug.Assert(index <= H2StaticTable.Count);
OnHeaderCore(index, H2StaticTable.Get(index - 1).Name, value);
}
// We can't throw a Http2StreamErrorException here, it interrupts the header decompression state and may corrupt subsequent header frames on other streams.
// For now these either need to be connection errors or BadRequests. If we want to downgrade any of them to stream errors later then we need to
// rework the flow so that the remaining headers are drained and the decompression state is maintained.
private void OnHeaderCore(int? index, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
// https://tools.ietf.org/html/rfc7540#section-6.5.2
// "The value is based on the uncompressed size of header fields, including the length of the name and value in octets plus an overhead of 32 octets for each header field.";
@ -1239,7 +1262,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
throw new Http2ConnectionErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http2ErrorCode.PROTOCOL_ERROR);
}
ValidateHeader(name, value);
if (index != null)
{
ValidateStaticHeader(index.Value, value);
}
else
{
ValidateHeader(name, value);
}
try
{
if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
@ -1250,7 +1281,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
// Throws BadRequest for header count limit breaches.
// Throws InvalidOperation for bad encoding.
_currentHeadersStream.OnHeader(name, value);
if (index != null)
{
_currentHeadersStream.OnHeader(index.Value, name, value);
}
else
{
_currentHeadersStream.OnHeader(name, value);
}
}
}
catch (Microsoft.AspNetCore.Http.BadHttpRequestException bre)
@ -1267,6 +1305,62 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
=> _currentHeadersStream.OnHeadersComplete();
private void ValidateHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
// Clients will normally send pseudo headers as an indexed header.
// Because pseudo headers can still be sent by name we need to check for them.
UpdateHeaderParsingState(value, GetPseudoHeaderField(name));
if (IsConnectionSpecificHeaderField(name, value))
{
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorConnectionSpecificHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
}
// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2
// A request or response containing uppercase header field names MUST be treated as malformed (Section 8.1.2.6).
for (var i = 0; i < name.Length; i++)
{
if (((uint)name[i] - 65) <= (90 - 65))
{
if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorTrailerNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
}
else
{
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorHeaderNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
}
}
}
}
private void ValidateStaticHeader(int index, ReadOnlySpan<byte> value)
{
var headerField = index switch
{
1 => PseudoHeaderFields.Authority,
2 => PseudoHeaderFields.Method,
3 => PseudoHeaderFields.Method,
4 => PseudoHeaderFields.Path,
5 => PseudoHeaderFields.Path,
6 => PseudoHeaderFields.Scheme,
7 => PseudoHeaderFields.Scheme,
8 => PseudoHeaderFields.Status,
9 => PseudoHeaderFields.Status,
10 => PseudoHeaderFields.Status,
11 => PseudoHeaderFields.Status,
12 => PseudoHeaderFields.Status,
13 => PseudoHeaderFields.Status,
14 => PseudoHeaderFields.Status,
_ => PseudoHeaderFields.None
};
UpdateHeaderParsingState(value, headerField);
// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2
// No need to validate if header name if it is specified using a static index.
}
private void UpdateHeaderParsingState(ReadOnlySpan<byte> value, PseudoHeaderFields headerField)
{
// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.1
/*
@ -1282,7 +1376,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
against several types of common attacks against HTTP; they are
deliberately strict because being permissive can expose
implementations to these vulnerabilities.*/
if (IsPseudoHeaderField(name, out var headerField))
if (headerField != PseudoHeaderFields.None)
{
if (_requestHeaderParsingState == RequestHeaderParsingState.Headers)
{
@ -1332,65 +1426,38 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
_requestHeaderParsingState = RequestHeaderParsingState.Headers;
}
if (IsConnectionSpecificHeaderField(name, value))
{
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorConnectionSpecificHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
}
// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2
// A request or response containing uppercase header field names MUST be treated as malformed (Section 8.1.2.6).
for (var i = 0; i < name.Length; i++)
{
if (name[i] >= 65 && name[i] <= 90)
{
if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorTrailerNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
}
else
{
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorHeaderNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
}
}
}
}
private bool IsPseudoHeaderField(ReadOnlySpan<byte> name, out PseudoHeaderFields headerField)
private PseudoHeaderFields GetPseudoHeaderField(ReadOnlySpan<byte> name)
{
headerField = PseudoHeaderFields.None;
if (name.IsEmpty || name[0] != (byte)':')
{
return false;
return PseudoHeaderFields.None;
}
if (name.SequenceEqual(PathBytes))
else if (name.SequenceEqual(PathBytes))
{
headerField = PseudoHeaderFields.Path;
return PseudoHeaderFields.Path;
}
else if (name.SequenceEqual(MethodBytes))
{
headerField = PseudoHeaderFields.Method;
return PseudoHeaderFields.Method;
}
else if (name.SequenceEqual(SchemeBytes))
{
headerField = PseudoHeaderFields.Scheme;
return PseudoHeaderFields.Scheme;
}
else if (name.SequenceEqual(StatusBytes))
{
headerField = PseudoHeaderFields.Status;
return PseudoHeaderFields.Status;
}
else if (name.SequenceEqual(AuthorityBytes))
{
headerField = PseudoHeaderFields.Authority;
return PseudoHeaderFields.Authority;
}
else
{
headerField = PseudoHeaderFields.Unknown;
return PseudoHeaderFields.Unknown;
}
return true;
}
private static bool IsConnectionSpecificHeaderField(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
@ -1468,16 +1535,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
}
public void OnStaticIndexedHeader(int index)
{
throw new NotImplementedException();
}
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
throw new NotImplementedException();
}
private class StreamCloseAwaitable : ICriticalNotifyCompletion
{
private static readonly Action _callbackCompleted = () => { };

View File

@ -6,6 +6,7 @@ using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
@ -618,5 +619,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
EndStreamReceived = 2,
Aborted = 4,
}
public override void OnHeader(int index, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
base.OnHeader(index, name, value);
// HPack append will return false if the index is not a known request header.
// For example, someone could send the index of "Server" (a response header) in the request.
// If that happens then fallback to using Append with the name bytes.
if (!HttpRequestHeaders.TryHPackAppend(index, value))
{
AppendHeader(name, value);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void AppendHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
HttpRequestHeaders.Append(name, value);
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Core components of ASP.NET Core Kestrel cross-platform web server.</Description>

View File

@ -50,10 +50,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
_connectionPair = DuplexPipe.CreateConnectionPair(options, options);
_httpRequestHeaders = new HttpRequestHeaders();
_httpRequestHeaders.Append(HeaderNames.Method, new StringValues("GET"));
_httpRequestHeaders.Append(HeaderNames.Path, new StringValues("/"));
_httpRequestHeaders.Append(HeaderNames.Scheme, new StringValues("http"));
_httpRequestHeaders.Append(HeaderNames.Authority, new StringValues("localhost:80"));
_httpRequestHeaders.HeaderMethod = new StringValues("GET");
_httpRequestHeaders.HeaderPath = new StringValues("/");
_httpRequestHeaders.HeaderScheme = new StringValues("http");
_httpRequestHeaders.HeaderAuthority = new StringValues("localhost:80");
_headersBuffer = new byte[1024 * 16];
_hpackEncoder = new HPackEncoder();

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http.HPack;
using System.Text;
using Microsoft.Net.Http.Headers;
@ -219,6 +220,74 @@ namespace CodeGenerator
break;")}
}}";
static string AppendHPackSwitch(IEnumerable<HPackGroup> values) =>
$@"switch (index)
{{{Each(values, header => $@"{Each(header.HPackStaticTableIndexes, index => $@"
case {index}:")}
{AppendHPackSwitchSection(header)}")}
}}";
static string AppendValue(bool returnTrue = false) =>
$@"// Matched a known header
if ((_previousBits & flag) != 0)
{{
// Had a previous string for this header, mark it as used so we don't clear it OnHeadersComplete or consider it if we get a second header
_previousBits ^= flag;
// We will only reuse this header if there was only one previous header
if (values.Count == 1)
{{
var previousValue = values.ToString();
// Check lengths are the same, then if the bytes were converted to an ascii string if they would be the same.
// We do not consider Utf8 headers for reuse.
if (previousValue.Length == value.Length &&
StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, value))
{{
// The previous string matches what the bytes would convert to, so we will just use that one.
_bits |= flag;
return{(returnTrue ? " true" : "")};
}}
}}
}}
// We didn't have a previous matching header value, or have already added a header, so get the string for this value.
var valueStr = value.GetRequestHeaderString(nameStr, EncodingSelector);
if ((_bits & flag) == 0)
{{
// We didn't already have a header set, so add a new one.
_bits |= flag;
values = new StringValues(valueStr);
}}
else
{{
// We already had a header set, so concatenate the new one.
values = AppendValue(values, valueStr);
}}";
static string AppendHPackSwitchSection(HPackGroup group)
{
var header = group.Header;
if (header.Identifier == "ContentLength")
{
return $@"if (ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultRequestHeaderEncodingSelector))
{{
AppendContentLength(value);
}}
else
{{
AppendContentLengthCustomEncoding(value, EncodingSelector(HeaderNames.ContentLength));
}}
return true;";
}
else
{
return $@"flag = {header.FlagBit()};
values = ref _headers._{header.Identifier};
nameStr = HeaderNames.{header.Identifier};
break;";
}
}
static string AppendSwitchSection(int length, IOrderedEnumerable<KnownHeader> values)
{
var useVarForFirstTerm = values.Count() > 1 && values.Select(h => h.FirstNameIgnoreCaseSegment()).Distinct().Count() == 1;
@ -1012,46 +1081,12 @@ $@" private void Clear(long bitsToClear)
ref StringValues values = ref Unsafe.AsRef<StringValues>(null);
var flag = 0L;
// Does the name matched any ""known"" headers
// Does the name match any ""known"" headers
{AppendSwitch(loop.Headers.GroupBy(x => x.Name.Length).OrderBy(x => x.Key))}
if (flag != 0)
{{
// Matched a known header
if ((_previousBits & flag) != 0)
{{
// Had a previous string for this header, mark it as used so we don't clear it OnHeadersComplete or consider it if we get a second header
_previousBits ^= flag;
// We will only reuse this header if there was only one previous header
if (values.Count == 1)
{{
var previousValue = values.ToString();
// Check lengths are the same, then if the bytes were converted to an ascii string if they would be the same.
// We do not consider Utf8 headers for reuse.
if (previousValue.Length == value.Length &&
StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, value))
{{
// The previous string matches what the bytes would convert to, so we will just use that one.
_bits |= flag;
return;
}}
}}
}}
// We didn't have a previous matching header value, or have already added a header, so get the string for this value.
var valueStr = value.GetRequestHeaderString(nameStr, EncodingSelector);
if ((_bits & flag) == 0)
{{
// We didn't already have a header set, so add a new one.
_bits |= flag;
values = new StringValues(valueStr);
}}
else
{{
// We already had a header set, so concatenate the new one.
values = AppendValue(values, valueStr);
}}
{AppendValue()}
}}
else
{{
@ -1062,6 +1097,27 @@ $@" private void Clear(long bitsToClear)
var valueStr = value.GetRequestHeaderString(nameStr, EncodingSelector);
AppendUnknownHeaders(nameStr, valueStr);
}}
}}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public unsafe bool TryHPackAppend(int index, ReadOnlySpan<byte> value)
{{
ref StringValues values = ref Unsafe.AsRef<StringValues>(null);
var nameStr = string.Empty;
var flag = 0L;
// Does the HPack static index match any ""known"" headers
{AppendHPackSwitch(GroupHPack(loop.Headers))}
if (flag != 0)
{{
{AppendValue(returnTrue: true)}
return true;
}}
else
{{
return false;
}}
}}" : "")}
private struct HeaderReferences
@ -1117,5 +1173,33 @@ $@" private void Clear(long bitsToClear)
}}
")}}}";
}
private class HPackGroup
{
public int[] HPackStaticTableIndexes { get; set; }
public KnownHeader Header { get; set; }
public string Name { get; set; }
}
private static IEnumerable<HPackGroup> GroupHPack(KnownHeader[] headers)
{
var staticHeaders = new (int Index, HeaderField HeaderField)[H2StaticTable.Count];
for (var i = 0; i < H2StaticTable.Count; i++)
{
staticHeaders[i] = (i + 1, H2StaticTable.Get(i));
}
var groupedHeaders = staticHeaders.GroupBy(h => Encoding.ASCII.GetString(h.HeaderField.Name)).Select(g =>
{
return new HPackGroup
{
Name = g.Key,
Header = headers.SingleOrDefault(knownHeader => string.Equals(knownHeader.Name, g.Key, StringComparison.OrdinalIgnoreCase)),
HPackStaticTableIndexes = g.Select(h => h.Index).ToArray()
};
}).Where(g => g.Header != null).ToList();
return groupedHeaders;
}
}
}

View File

@ -2095,12 +2095,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public void OnStaticIndexedHeader(int index)
{
throw new NotImplementedException();
ref readonly var entry = ref H2StaticTable.Get(index - 1);
OnHeader(entry.Name, entry.Value);
}
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
throw new NotImplementedException();
OnHeader(H2StaticTable.Get(index - 1).Name, value);
}
}

View File

@ -6,6 +6,7 @@ using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.IO.Pipelines;
@ -410,6 +411,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_decodedHeaders[nameStr] = value.GetRequestHeaderString(nameStr, _serviceContext.ServerOptions.RequestHeaderEncodingSelector);
}
public void OnStaticIndexedHeader(int index)
{
Debug.Assert(index <= H2StaticTable.Count);
ref readonly var entry = ref H2StaticTable.Get(index - 1);
((IHttpHeadersHandler)this).OnHeader(entry.Name, entry.Value);
}
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
Debug.Assert(index <= H2StaticTable.Count);
((IHttpHeadersHandler)this).OnHeader(H2StaticTable.Get(index - 1).Name, value);
}
void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }
protected void CreateConnection()
@ -1271,16 +1287,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
return bufferSize ?? 0;
}
public void OnStaticIndexedHeader(int index)
{
throw new NotImplementedException();
}
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
throw new NotImplementedException();
}
internal class Http2FrameWithPayload : Http2Frame
{
public Http2FrameWithPayload() : base()

View File

@ -9,6 +9,8 @@
<ItemGroup>
<Compile Include="$(KestrelSharedSourceRoot)KnownHeaders.cs" />
<Compile Include="$(SharedSourceRoot)runtime\Http2\Hpack\H2StaticTable.cs" Link="Shared\runtime\Http2\%(Filename)%(Extension)" />
<Compile Include="$(SharedSourceRoot)runtime\Http2\Hpack\HeaderField.cs" Link="Shared\runtime\Http2\%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>

View File

@ -75,12 +75,11 @@ namespace CodeGenerator
if (!string.Equals(content, existingContent))
{
File.WriteAllText(path, content);
Console.WriteLine($"{path} updated.");
}
var existingHttp2Connection = File.Exists(path) ? File.ReadAllText(path) : "";
if (!string.Equals(content, existingHttp2Connection))
else
{
File.WriteAllText(path, content);
Console.WriteLine($"{path} already up to date.");
}
}
}

View File

@ -1022,12 +1022,13 @@ namespace Microsoft.AspNetCore.Http2Cat
public void OnStaticIndexedHeader(int index)
{
throw new NotImplementedException();
ref readonly var entry = ref H2StaticTable.Get(index - 1);
((IHttpHeadersHandler)this).OnHeader(entry.Name, entry.Value);
}
public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)
{
throw new NotImplementedException();
((IHttpHeadersHandler)this).OnHeader(H2StaticTable.Get(index - 1).Name, value);
}
internal class Http2FrameWithPayload : Http2Frame

View File

@ -92,6 +92,7 @@ namespace System.Net.Http.HPack
private State _state = State.Ready;
private byte[]? _headerName;
private int _headerStaticIndex;
private int _stringIndex;
private int _stringLength;
private int _headerNameLength;
@ -492,23 +493,36 @@ namespace System.Net.Http.HPack
private void ProcessHeaderValue(ReadOnlySpan<byte> data, IHttpHeadersHandler handler)
{
ReadOnlySpan<byte> headerNameSpan = _headerNameRange == null
? new Span<byte>(_headerName, 0, _headerNameLength)
: data.Slice(_headerNameRange.GetValueOrDefault().start, _headerNameRange.GetValueOrDefault().length);
ReadOnlySpan<byte> headerValueSpan = _headerValueRange == null
? new Span<byte>(_headerValueOctets, 0, _headerValueLength)
? _headerValueOctets.AsSpan(0, _headerValueLength)
: data.Slice(_headerValueRange.GetValueOrDefault().start, _headerValueRange.GetValueOrDefault().length);
handler.OnHeader(headerNameSpan, headerValueSpan);
if (_headerStaticIndex > 0)
{
handler.OnStaticIndexedHeader(_headerStaticIndex, headerValueSpan);
if (_index)
{
_dynamicTable.Insert(H2StaticTable.Get(_headerStaticIndex - 1).Name, headerValueSpan);
}
}
else
{
ReadOnlySpan<byte> headerNameSpan = _headerNameRange == null
? _headerName.AsSpan(0, _headerNameLength)
: data.Slice(_headerNameRange.GetValueOrDefault().start, _headerNameRange.GetValueOrDefault().length);
handler.OnHeader(headerNameSpan, headerValueSpan);
if (_index)
{
_dynamicTable.Insert(headerNameSpan, headerValueSpan);
}
}
_headerStaticIndex = 0;
_headerNameRange = null;
_headerValueRange = null;
if (_index)
{
_dynamicTable.Insert(headerNameSpan, headerValueSpan);
}
}
public void CompleteDecode()
@ -522,15 +536,30 @@ namespace System.Net.Http.HPack
private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler)
{
ref readonly HeaderField header = ref GetHeader(index);
handler.OnHeader(header.Name, header.Value);
if (index <= H2StaticTable.Count)
{
handler.OnStaticIndexedHeader(index);
}
else
{
ref readonly HeaderField header = ref GetDynamicHeader(index);
handler.OnHeader(header.Name, header.Value);
}
_state = State.Ready;
}
private void OnIndexedHeaderName(int index)
{
_headerName = GetHeader(index).Name;
_headerNameLength = _headerName.Length;
if (index <= H2StaticTable.Count)
{
_headerStaticIndex = index;
}
else
{
_headerName = GetDynamicHeader(index).Name;
_headerNameLength = _headerName.Length;
}
_state = State.HeaderValueLength;
}
@ -615,13 +644,11 @@ namespace System.Net.Http.HPack
return (b & HuffmanMask) != 0;
}
private ref readonly HeaderField GetHeader(int index)
private ref readonly HeaderField GetDynamicHeader(int index)
{
try
{
return ref index <= H2StaticTable.Count
? ref H2StaticTable.Get(index - 1)
: ref _dynamicTable[index - H2StaticTable.Count - 1];
return ref _dynamicTable[index - H2StaticTable.Count - 1];
}
catch (IndexOutOfRangeException)
{

View File

@ -104,10 +104,27 @@ namespace System.Net.Http.Unit.Tests.HPack
}
[Fact]
public void DecodesIndexedHeaderField_StaticTable()
public void DecodesIndexedHeaderField_StaticTableWithValue()
{
_decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: _handler);
Assert.Equal("GET", _handler.DecodedHeaders[":method"]);
Assert.Equal(":method", _handler.DecodedStaticHeaders[H2StaticTable.MethodGet].Key);
Assert.Equal("GET", _handler.DecodedStaticHeaders[H2StaticTable.MethodGet].Value);
}
[Fact]
public void DecodesIndexedHeaderField_StaticTableWithoutValue()
{
byte[] encoded = _literalHeaderFieldWithIndexingIndexedName
.Concat(_headerValue)
.ToArray();
_decoder.Decode(encoded, endHeaders: true, handler: _handler);
Assert.Equal(_headerValueString, _handler.DecodedHeaders[_userAgentString]);
Assert.Equal(_userAgentString, _handler.DecodedStaticHeaders[H2StaticTable.UserAgent].Key);
Assert.Equal(_headerValueString, _handler.DecodedStaticHeaders[H2StaticTable.UserAgent].Value);
}
[Fact]
@ -707,6 +724,7 @@ namespace System.Net.Http.Unit.Tests.HPack
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)
{
@ -718,14 +736,16 @@ namespace System.Net.Http.Unit.Tests.HPack
void IHttpHeadersHandler.OnStaticIndexedHeader(int index)
{
// Not yet implemented for HPACK.
throw new NotImplementedException();
ref readonly HeaderField entry = ref H2StaticTable.Get(index - 1);
((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)
{
// Not yet implemented for HPACK.
throw new NotImplementedException();
byte[] name = H2StaticTable.Get(index - 1).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) { }