Skip to content

Add HPack dynamic compression #20058

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Apr 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ public partial class KestrelServerOptions
{
public KestrelServerOptions() { }
public bool AddServerHeader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public bool AllowResponseHeaderCompression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public bool AllowSynchronousIO { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
Expand Down
3 changes: 3 additions & 0 deletions src/Servers/Kestrel/Core/src/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -599,4 +599,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="TransportNotFound" xml:space="preserve">
<value>Unable to resolve service for type 'Microsoft.AspNetCore.Connections.IConnectionListenerFactory' while attempting to activate 'Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer'.</value>
</data>
<data name="GreaterThanOrEqualToZeroRequired" xml:space="preserve">
<value>A value greater than or equal to zero is required.</value>
</data>
</root>
8 changes: 4 additions & 4 deletions src/Servers/Kestrel/Core/src/Http2Limits.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,19 @@ public int MaxStreamsPerConnection
}

/// <summary>
/// Limits the size of the header compression table, in octets, the HPACK decoder on the server can use.
/// Limits the size of the header compression tables, in octets, the HPACK encoder and decoder on the server can use.
/// <para>
/// Value must be greater than 0, defaults to 4096
/// Value must be greater than or equal to 0, defaults to 4096
/// </para>
/// </summary>
public int HeaderTableSize
{
get => _headerTableSize;
set
{
if (value <= 0)
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanZeroRequired);
throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanOrEqualToZeroRequired);
}

_headerTableSize = value;
Expand Down
164 changes: 90 additions & 74 deletions src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System;
using System.Net.Http;
using System.Net.Http.HPack;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
Expand All @@ -13,57 +12,105 @@ internal static class HPackHeaderWriter
/// <summary>
/// Begin encoding headers in the first HEADERS frame.
/// </summary>
public static bool BeginEncodeHeaders(int statusCode, Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
public static bool BeginEncodeHeaders(int statusCode, HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
{
if (!HPackEncoder.EncodeStatusHeader(statusCode, buffer, out var statusCodeLength))
length = 0;

if (!hpackEncoder.EnsureDynamicTableSizeUpdate(buffer, out var sizeUpdateLength))
{
throw new HPackEncodingException(SR.net_http_hpack_encode_failure);
}
length += sizeUpdateLength;

if (!EncodeStatusHeader(statusCode, hpackEncoder, buffer.Slice(length), out var statusCodeLength))
{
throw new HPackEncodingException(SR.net_http_hpack_encode_failure);
}
length += statusCodeLength;

if (!headersEnumerator.MoveNext())
{
length = statusCodeLength;
return true;
}

// We're ok with not throwing if no headers were encoded because we've already encoded the status.
// There is a small chance that the header will encode if there is no other content in the next HEADERS frame.
var done = EncodeHeaders(headersEnumerator, buffer.Slice(statusCodeLength), throwIfNoneEncoded: false, out var headersLength);
length = statusCodeLength + headersLength;

var done = EncodeHeadersCore(hpackEncoder, headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: false, out var headersLength);
length += headersLength;
return done;
}

/// <summary>
/// Begin encoding headers in the first HEADERS frame.
/// </summary>
public static bool BeginEncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
public static bool BeginEncodeHeaders(HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
{
length = 0;

if (!hpackEncoder.EnsureDynamicTableSizeUpdate(buffer, out var sizeUpdateLength))
{
throw new HPackEncodingException(SR.net_http_hpack_encode_failure);
}
length += sizeUpdateLength;

if (!headersEnumerator.MoveNext())
{
length = 0;
return true;
}

return EncodeHeaders(headersEnumerator, buffer, throwIfNoneEncoded: true, out length);
var done = EncodeHeadersCore(hpackEncoder, headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: true, out var headersLength);
length += headersLength;
return done;
}

/// <summary>
/// Continue encoding headers in the next HEADERS frame. The enumerator should already have a current value.
/// </summary>
public static bool ContinueEncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
public static bool ContinueEncodeHeaders(HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
{
return EncodeHeaders(headersEnumerator, buffer, throwIfNoneEncoded: true, out length);
return EncodeHeadersCore(hpackEncoder, headersEnumerator, buffer, throwIfNoneEncoded: true, out length);
}

private static bool EncodeStatusHeader(int statusCode, HPackEncoder hpackEncoder, Span<byte> buffer, out int length)
{
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 HPackEncoder.EncodeIndexedHeaderField(H2StaticTable.StatusIndex[statusCode], buffer, out length);
default:
const string name = ":status";
var value = StatusCodes.ToStatusString(statusCode);
return hpackEncoder.EncodeHeader(buffer, H2StaticTable.Status200, HeaderEncodingHint.Index, name, value, out length);
}
}

private static bool EncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, bool throwIfNoneEncoded, out int length)
private static bool EncodeHeadersCore(HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, bool throwIfNoneEncoded, out int length)
{
var currentLength = 0;
do
{
if (!EncodeHeader(headersEnumerator.KnownHeaderType, headersEnumerator.Current.Key, headersEnumerator.Current.Value, buffer.Slice(currentLength), out int headerLength))
var staticTableId = headersEnumerator.HPackStaticTableId;
var name = headersEnumerator.Current.Key;
var value = headersEnumerator.Current.Value;

var hint = ResolveHeaderEncodingHint(staticTableId, name);

if (!hpackEncoder.EncodeHeader(
buffer.Slice(currentLength),
staticTableId,
hint,
name,
value,
out var headerLength))
{
// The the header wasn't written and no headers have been written then the header is too large.
// If the header wasn't written, and no headers have been written, then the header is too large.
// Throw an error to avoid an infinite loop of attempting to write large header.
if (currentLength == 0 && throwIfNoneEncoded)
{
Expand All @@ -79,79 +126,48 @@ private static bool EncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span
while (headersEnumerator.MoveNext());

length = currentLength;

return true;
}

private static bool EncodeHeader(KnownHeaderType knownHeaderType, string name, string value, Span<byte> buffer, out int length)
private static HeaderEncodingHint ResolveHeaderEncodingHint(int staticTableId, string name)
{
var hPackStaticTableId = GetResponseHeaderStaticTableId(knownHeaderType);

if (hPackStaticTableId == -1)
HeaderEncodingHint hint;
if (IsSensitive(staticTableId, name))
{
return HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out length);
hint = HeaderEncodingHint.NeverIndex;
}
else if (IsNotDynamicallyIndexed(staticTableId))
{
hint = HeaderEncodingHint.IgnoreIndex;
}
else
{
return HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexing(hPackStaticTableId, value, buffer, out length);
hint = HeaderEncodingHint.Index;
}

return hint;
}

private static int GetResponseHeaderStaticTableId(KnownHeaderType responseHeaderType)
private static bool IsSensitive(int staticTableIndex, string name)
{
switch (responseHeaderType)
// Set-Cookie could contain sensitive data.
if (staticTableIndex == H2StaticTable.SetCookie)
{
case KnownHeaderType.CacheControl:
return H2StaticTable.CacheControl;
case KnownHeaderType.Date:
return H2StaticTable.Date;
case KnownHeaderType.TransferEncoding:
return H2StaticTable.TransferEncoding;
case KnownHeaderType.Via:
return H2StaticTable.Via;
case KnownHeaderType.Allow:
return H2StaticTable.Allow;
case KnownHeaderType.ContentType:
return H2StaticTable.ContentType;
case KnownHeaderType.ContentEncoding:
return H2StaticTable.ContentEncoding;
case KnownHeaderType.ContentLanguage:
return H2StaticTable.ContentLanguage;
case KnownHeaderType.ContentLocation:
return H2StaticTable.ContentLocation;
case KnownHeaderType.ContentRange:
return H2StaticTable.ContentRange;
case KnownHeaderType.Expires:
return H2StaticTable.Expires;
case KnownHeaderType.LastModified:
return H2StaticTable.LastModified;
case KnownHeaderType.AcceptRanges:
return H2StaticTable.AcceptRanges;
case KnownHeaderType.Age:
return H2StaticTable.Age;
case KnownHeaderType.ETag:
return H2StaticTable.ETag;
case KnownHeaderType.Location:
return H2StaticTable.Location;
case KnownHeaderType.ProxyAuthenticate:
return H2StaticTable.ProxyAuthenticate;
case KnownHeaderType.RetryAfter:
return H2StaticTable.RetryAfter;
case KnownHeaderType.Server:
return H2StaticTable.Server;
case KnownHeaderType.SetCookie:
return H2StaticTable.SetCookie;
case KnownHeaderType.Vary:
return H2StaticTable.Vary;
case KnownHeaderType.WWWAuthenticate:
return H2StaticTable.WwwAuthenticate;
case KnownHeaderType.AccessControlAllowOrigin:
return H2StaticTable.AccessControlAllowOrigin;
case KnownHeaderType.ContentLength:
return H2StaticTable.ContentLength;
default:
return -1;
return true;
}
if (string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase))
{
return true;
}

return false;
}

private static bool IsNotDynamicallyIndexed(int staticTableIndex)
{
// Content-Length is added to static content. Content length is different for each
// file, and is unlikely to be reused because of browser caching.
return staticTableIndex == H2StaticTable.ContentLength;
}
}
}
11 changes: 10 additions & 1 deletion src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public Http2Connection(HttpConnectionContext context)
httpLimits.MinResponseDataRate,
context.ConnectionId,
context.MemoryPool,
context.ServiceContext.Log);
context.ServiceContext);

var inputOptions = new PipeOptions(pool: context.MemoryPool,
readerScheduler: context.ServiceContext.Scheduler,
Expand Down Expand Up @@ -743,6 +743,15 @@ private Task ProcessSettingsFrameAsync(in ReadOnlySequence<byte> payload)
}
}

// Maximum HPack encoder size is limited by Http2Limits.HeaderTableSize, configured max the server.
//
// Note that the client HPack decoder doesn't care about the ACK so we don't need to lock sending the
// ACK and updating the table size on the server together.
// The client will wait until a size agreed upon by it (sent in SETTINGS_HEADER_TABLE_SIZE) and the
// server (sent as a dynamic table size update in the next HEADERS frame) is received before applying
// the new size.
_frameWriter.UpdateMaxHeaderTableSize(Math.Min(_clientSettings.HeaderTableSize, (uint)Limits.Http2.HeaderTableSize));

return ackTask.AsTask();
}
catch (Http2SettingsParameterOutOfRangeException ex)
Expand Down
23 changes: 17 additions & 6 deletions src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ internal class Http2FrameWriter
private readonly ITimeoutControl _timeoutControl;
private readonly MinDataRate _minResponseDataRate;
private readonly TimingPipeFlusher _flusher;
private readonly HPackEncoder _hpackEncoder;

private uint _maxFrameSize = Http2PeerSettings.MinAllowedMaxFrameSize;
private byte[] _headerEncodingBuffer;
Expand All @@ -55,20 +56,30 @@ public Http2FrameWriter(
MinDataRate minResponseDataRate,
string connectionId,
MemoryPool<byte> memoryPool,
IKestrelTrace log)
ServiceContext serviceContext)
{
// Allow appending more data to the PipeWriter when a flush is pending.
_outputWriter = new ConcurrentPipeWriter(outputPipeWriter, memoryPool, _writeLock);
_connectionContext = connectionContext;
_http2Connection = http2Connection;
_connectionOutputFlowControl = connectionOutputFlowControl;
_connectionId = connectionId;
_log = log;
_log = serviceContext.Log;
_timeoutControl = timeoutControl;
_minResponseDataRate = minResponseDataRate;
_flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, log);
_flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, serviceContext.Log);
_outgoingFrame = new Http2Frame();
_headerEncodingBuffer = new byte[_maxFrameSize];

_hpackEncoder = new HPackEncoder(serviceContext.ServerOptions.AllowResponseHeaderCompression);
}

public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize)
{
lock (_writeLock)
{
_hpackEncoder.UpdateMaxHeaderTableSize(maxHeaderTableSize);
}
}

public void UpdateMaxFrameSize(uint maxFrameSize)
Expand Down Expand Up @@ -175,7 +186,7 @@ public void WriteResponseHeaders(int streamId, int statusCode, Http2HeadersFrame
_headersEnumerator.Initialize(headers);
_outgoingFrame.PrepareHeaders(headerFrameFlags, streamId);
var buffer = _headerEncodingBuffer.AsSpan();
var done = HPackHeaderWriter.BeginEncodeHeaders(statusCode, _headersEnumerator, buffer, out var payloadLength);
var done = HPackHeaderWriter.BeginEncodeHeaders(statusCode, _hpackEncoder, _headersEnumerator, buffer, out var payloadLength);
FinishWritingHeaders(streamId, payloadLength, done);
}
catch (HPackEncodingException hex)
Expand All @@ -201,7 +212,7 @@ public ValueTask<FlushResult> WriteResponseTrailers(int streamId, HttpResponseTr
_headersEnumerator.Initialize(headers);
_outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.END_STREAM, streamId);
var buffer = _headerEncodingBuffer.AsSpan();
var done = HPackHeaderWriter.BeginEncodeHeaders(_headersEnumerator, buffer, out var payloadLength);
var done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out var payloadLength);
FinishWritingHeaders(streamId, payloadLength, done);
}
catch (HPackEncodingException hex)
Expand Down Expand Up @@ -230,7 +241,7 @@ private void FinishWritingHeaders(int streamId, int payloadLength, bool done)
{
_outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId);

done = HPackHeaderWriter.ContinueEncodeHeaders(_headersEnumerator, buffer, out payloadLength);
done = HPackHeaderWriter.ContinueEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out payloadLength);
_outgoingFrame.PayloadLength = payloadLength;

if (done)
Expand Down
Loading