HTTP2: Optimize header processing (#24945)

This commit is contained in:
James Newton-King 2020-08-18 14:31:57 +12:00 committed by GitHub
parent bbb851e3eb
commit 58a75925f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 120 additions and 21 deletions

View File

@ -34,6 +34,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly byte[] _bytesConnectionKeepAlive = Encoding.ASCII.GetBytes("\r\nConnection: keep-alive");
private static readonly byte[] _bytesTransferEncodingChunked = Encoding.ASCII.GetBytes("\r\nTransfer-Encoding: chunked");
private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: " + Constants.ServerName);
internal const string SchemeHttp = "http";
internal const string SchemeHttps = "https";
protected BodyControl _bodyControl;
private Stack<KeyValuePair<Func<object, Task>, object>> _onStarting;
@ -385,7 +387,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
if (_scheme == null)
{
var tlsFeature = ConnectionFeatures?[typeof(ITlsConnectionFeature)];
_scheme = tlsFeature != null ? "https" : "http";
_scheme = tlsFeature != null ? SchemeHttps : SchemeHttp;
}
Scheme = _scheme;
@ -518,7 +520,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
HttpRequestHeaders.Append(name, value);
}
public virtual void OnHeader(int index, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
public virtual void OnHeader(int index, bool indexOnly, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
IncrementRequestHeadersCount();

View File

@ -151,13 +151,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
private static bool IsSensitive(int staticTableIndex, string name)
{
// Set-Cookie could contain sensitive data.
if (staticTableIndex == H2StaticTable.SetCookie)
switch (staticTableIndex)
{
return true;
}
if (string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase))
{
return true;
case H2StaticTable.SetCookie:
case H2StaticTable.ContentDisposition:
return true;
case -1:
// Content-Disposition currently isn't a known header so a
// static index probably won't be specified.
return string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase);
}
return false;

View File

@ -1226,12 +1226,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
}
// 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.
public void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
OnHeaderCore(index: null, name, value);
OnHeaderCore(index: null, indexedValue: false, name, value);
}
public void OnStaticIndexedHeader(int index)
@ -1239,20 +1236,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
Debug.Assert(index <= H2StaticTable.Count);
ref readonly var entry = ref H2StaticTable.Get(index - 1);
OnHeaderCore(index, entry.Name, entry.Value);
OnHeaderCore(index, indexedValue: true, 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);
OnHeaderCore(index, indexedValue: false, 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)
private void OnHeaderCore(int? index, bool indexedValue, 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.";
@ -1283,7 +1280,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// Throws InvalidOperation for bad encoding.
if (index != null)
{
_currentHeadersStream.OnHeader(index.Value, name, value);
_currentHeadersStream.OnHeader(index.Value, indexedValue, name, value);
}
else
{

View File

@ -6,6 +6,7 @@ using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.IO.Pipelines;
using System.Net.Http.HPack;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@ -15,6 +16,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using HttpMethods = Microsoft.AspNetCore.Http.HttpMethods;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
@ -205,7 +207,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_httpVersion = Http.HttpVersion.Http2;
if (!TryValidateMethod())
// Method could already have been set from :method static table index
if (Method == HttpMethod.None && !TryValidateMethod())
{
return false;
}
@ -237,7 +240,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// - That said, we shouldn't allow arbitrary values or use them to populate Request.Scheme, right?
// - For now we'll restrict it to http/s and require it match the transport.
// - We'll need to find some concrete scenarios to warrant unblocking this.
if (!string.Equals(HttpRequestHeaders.HeaderScheme, Scheme, StringComparison.OrdinalIgnoreCase))
var headerScheme = HttpRequestHeaders.HeaderScheme.ToString();
if (!ReferenceEquals(headerScheme, Scheme) &&
!string.Equals(headerScheme, Scheme, StringComparison.OrdinalIgnoreCase))
{
ResetAndAbort(new ConnectionAbortedException(
CoreStrings.FormatHttp2StreamErrorSchemeMismatch(HttpRequestHeaders.HeaderScheme, Scheme)), Http2ErrorCode.PROTOCOL_ERROR);
@ -620,9 +625,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
Aborted = 4,
}
public override void OnHeader(int index, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
public override void OnHeader(int index, bool indexedValue, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
base.OnHeader(index, name, value);
base.OnHeader(index, indexedValue, name, value);
if (indexedValue)
{
// Special case setting headers when the value is indexed for performance.
switch (index)
{
case H2StaticTable.MethodGet:
HttpRequestHeaders.HeaderMethod = HttpMethods.Get;
Method = HttpMethod.Get;
_methodText = HttpMethods.Get;
return;
case H2StaticTable.MethodPost:
HttpRequestHeaders.HeaderMethod = HttpMethods.Post;
Method = HttpMethod.Post;
_methodText = HttpMethods.Post;
return;
case H2StaticTable.SchemeHttp:
HttpRequestHeaders.HeaderScheme = SchemeHttp;
return;
case H2StaticTable.SchemeHttps:
HttpRequestHeaders.HeaderScheme = SchemeHttps;
return;
}
}
// 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.

View File

@ -8,6 +8,7 @@ using System.Net.Sockets;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
namespace Microsoft.AspNetCore.Server.Kestrel.Core
{
@ -84,7 +85,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
{
get
{
return IsTls ? "https" : "http";
return IsTls ? HttpProtocol.SchemeHttps : HttpProtocol.SchemeHttp;
}
}

View File

@ -0,0 +1,68 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Net.Http.HPack;
using System.Text;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
public class HPackHeaderWriterBenchmark
{
private Http2HeadersEnumerator _http2HeadersEnumerator;
private HPackEncoder _hpackEncoder;
private HttpResponseHeaders _knownResponseHeaders;
private HttpResponseHeaders _unknownResponseHeaders;
private byte[] _buffer;
[GlobalSetup]
public void GlobalSetup()
{
_http2HeadersEnumerator = new Http2HeadersEnumerator();
_hpackEncoder = new HPackEncoder();
_buffer = new byte[1024 * 1024];
_knownResponseHeaders = new HttpResponseHeaders
{
HeaderServer = "Kestrel",
HeaderContentType = "application/json",
HeaderDate = "Date!",
HeaderContentLength = "0",
HeaderAcceptRanges = "Ranges!",
HeaderTransferEncoding = "Encoding!",
HeaderVia = "Via!",
HeaderVary = "Vary!",
HeaderWWWAuthenticate = "Authenticate!",
HeaderLastModified = "Modified!",
HeaderExpires = "Expires!",
HeaderAge = "Age!"
};
_unknownResponseHeaders = new HttpResponseHeaders();
for (var i = 0; i < 10; i++)
{
_unknownResponseHeaders.Append("Unknown" + i, "Value" + i);
}
}
[Benchmark]
public void BeginEncodeHeaders_KnownHeaders()
{
_http2HeadersEnumerator.Initialize(_knownResponseHeaders);
HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _http2HeadersEnumerator, _buffer, out _);
}
[Benchmark]
public void BeginEncodeHeaders_UnknownHeaders()
{
_http2HeadersEnumerator.Initialize(_unknownResponseHeaders);
HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _http2HeadersEnumerator, _buffer, out _);
}
}
}