HTTP2: Optimize header processing (#24945)
This commit is contained in:
parent
bbb851e3eb
commit
58a75925f7
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 _);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue