Skip to content

Commit 0e4bcf6

Browse files
authored
Add HPack dynamic compression (#20058)
1 parent c378f3f commit 0e4bcf6

29 files changed

+2044
-514
lines changed

src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ public partial class KestrelServerOptions
127127
{
128128
public KestrelServerOptions() { }
129129
public bool AddServerHeader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
130+
public bool AllowResponseHeaderCompression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
130131
public bool AllowSynchronousIO { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
131132
public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
132133
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }

src/Servers/Kestrel/Core/src/CoreStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,4 +599,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
599599
<data name="TransportNotFound" xml:space="preserve">
600600
<value>Unable to resolve service for type 'Microsoft.AspNetCore.Connections.IConnectionListenerFactory' while attempting to activate 'Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer'.</value>
601601
</data>
602+
<data name="GreaterThanOrEqualToZeroRequired" xml:space="preserve">
603+
<value>A value greater than or equal to zero is required.</value>
604+
</data>
602605
</root>

src/Servers/Kestrel/Core/src/Http2Limits.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,19 @@ public int MaxStreamsPerConnection
3939
}
4040

4141
/// <summary>
42-
/// Limits the size of the header compression table, in octets, the HPACK decoder on the server can use.
42+
/// Limits the size of the header compression tables, in octets, the HPACK encoder and decoder on the server can use.
4343
/// <para>
44-
/// Value must be greater than 0, defaults to 4096
44+
/// Value must be greater than or equal to 0, defaults to 4096
4545
/// </para>
4646
/// </summary>
4747
public int HeaderTableSize
4848
{
4949
get => _headerTableSize;
5050
set
5151
{
52-
if (value <= 0)
52+
if (value < 0)
5353
{
54-
throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanZeroRequired);
54+
throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanOrEqualToZeroRequired);
5555
}
5656

5757
_headerTableSize = value;

src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs

Lines changed: 90 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System;
55
using System.Net.Http;
66
using System.Net.Http.HPack;
7-
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
87

98
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
109
{
@@ -13,57 +12,105 @@ internal static class HPackHeaderWriter
1312
/// <summary>
1413
/// Begin encoding headers in the first HEADERS frame.
1514
/// </summary>
16-
public static bool BeginEncodeHeaders(int statusCode, Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
15+
public static bool BeginEncodeHeaders(int statusCode, HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
1716
{
18-
if (!HPackEncoder.EncodeStatusHeader(statusCode, buffer, out var statusCodeLength))
17+
length = 0;
18+
19+
if (!hpackEncoder.EnsureDynamicTableSizeUpdate(buffer, out var sizeUpdateLength))
20+
{
21+
throw new HPackEncodingException(SR.net_http_hpack_encode_failure);
22+
}
23+
length += sizeUpdateLength;
24+
25+
if (!EncodeStatusHeader(statusCode, hpackEncoder, buffer.Slice(length), out var statusCodeLength))
1926
{
2027
throw new HPackEncodingException(SR.net_http_hpack_encode_failure);
2128
}
29+
length += statusCodeLength;
2230

2331
if (!headersEnumerator.MoveNext())
2432
{
25-
length = statusCodeLength;
2633
return true;
2734
}
2835

2936
// We're ok with not throwing if no headers were encoded because we've already encoded the status.
3037
// There is a small chance that the header will encode if there is no other content in the next HEADERS frame.
31-
var done = EncodeHeaders(headersEnumerator, buffer.Slice(statusCodeLength), throwIfNoneEncoded: false, out var headersLength);
32-
length = statusCodeLength + headersLength;
33-
38+
var done = EncodeHeadersCore(hpackEncoder, headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: false, out var headersLength);
39+
length += headersLength;
3440
return done;
3541
}
3642

3743
/// <summary>
3844
/// Begin encoding headers in the first HEADERS frame.
3945
/// </summary>
40-
public static bool BeginEncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
46+
public static bool BeginEncodeHeaders(HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
4147
{
48+
length = 0;
49+
50+
if (!hpackEncoder.EnsureDynamicTableSizeUpdate(buffer, out var sizeUpdateLength))
51+
{
52+
throw new HPackEncodingException(SR.net_http_hpack_encode_failure);
53+
}
54+
length += sizeUpdateLength;
55+
4256
if (!headersEnumerator.MoveNext())
4357
{
44-
length = 0;
4558
return true;
4659
}
4760

48-
return EncodeHeaders(headersEnumerator, buffer, throwIfNoneEncoded: true, out length);
61+
var done = EncodeHeadersCore(hpackEncoder, headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: true, out var headersLength);
62+
length += headersLength;
63+
return done;
4964
}
5065

5166
/// <summary>
5267
/// Continue encoding headers in the next HEADERS frame. The enumerator should already have a current value.
5368
/// </summary>
54-
public static bool ContinueEncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
69+
public static bool ContinueEncodeHeaders(HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
5570
{
56-
return EncodeHeaders(headersEnumerator, buffer, throwIfNoneEncoded: true, out length);
71+
return EncodeHeadersCore(hpackEncoder, headersEnumerator, buffer, throwIfNoneEncoded: true, out length);
72+
}
73+
74+
private static bool EncodeStatusHeader(int statusCode, HPackEncoder hpackEncoder, Span<byte> buffer, out int length)
75+
{
76+
switch (statusCode)
77+
{
78+
case 200:
79+
case 204:
80+
case 206:
81+
case 304:
82+
case 400:
83+
case 404:
84+
case 500:
85+
// Status codes which exist in the HTTP/2 StaticTable.
86+
return HPackEncoder.EncodeIndexedHeaderField(H2StaticTable.StatusIndex[statusCode], buffer, out length);
87+
default:
88+
const string name = ":status";
89+
var value = StatusCodes.ToStatusString(statusCode);
90+
return hpackEncoder.EncodeHeader(buffer, H2StaticTable.Status200, HeaderEncodingHint.Index, name, value, out length);
91+
}
5792
}
5893

59-
private static bool EncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, bool throwIfNoneEncoded, out int length)
94+
private static bool EncodeHeadersCore(HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, bool throwIfNoneEncoded, out int length)
6095
{
6196
var currentLength = 0;
6297
do
6398
{
64-
if (!EncodeHeader(headersEnumerator.KnownHeaderType, headersEnumerator.Current.Key, headersEnumerator.Current.Value, buffer.Slice(currentLength), out int headerLength))
99+
var staticTableId = headersEnumerator.HPackStaticTableId;
100+
var name = headersEnumerator.Current.Key;
101+
var value = headersEnumerator.Current.Value;
102+
103+
var hint = ResolveHeaderEncodingHint(staticTableId, name);
104+
105+
if (!hpackEncoder.EncodeHeader(
106+
buffer.Slice(currentLength),
107+
staticTableId,
108+
hint,
109+
name,
110+
value,
111+
out var headerLength))
65112
{
66-
// The the header wasn't written and no headers have been written then the header is too large.
113+
// If the header wasn't written, and no headers have been written, then the header is too large.
67114
// Throw an error to avoid an infinite loop of attempting to write large header.
68115
if (currentLength == 0 && throwIfNoneEncoded)
69116
{
@@ -79,79 +126,48 @@ private static bool EncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span
79126
while (headersEnumerator.MoveNext());
80127

81128
length = currentLength;
82-
83129
return true;
84130
}
85131

86-
private static bool EncodeHeader(KnownHeaderType knownHeaderType, string name, string value, Span<byte> buffer, out int length)
132+
private static HeaderEncodingHint ResolveHeaderEncodingHint(int staticTableId, string name)
87133
{
88-
var hPackStaticTableId = GetResponseHeaderStaticTableId(knownHeaderType);
89-
90-
if (hPackStaticTableId == -1)
134+
HeaderEncodingHint hint;
135+
if (IsSensitive(staticTableId, name))
91136
{
92-
return HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out length);
137+
hint = HeaderEncodingHint.NeverIndex;
138+
}
139+
else if (IsNotDynamicallyIndexed(staticTableId))
140+
{
141+
hint = HeaderEncodingHint.IgnoreIndex;
93142
}
94143
else
95144
{
96-
return HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexing(hPackStaticTableId, value, buffer, out length);
145+
hint = HeaderEncodingHint.Index;
97146
}
147+
148+
return hint;
98149
}
99150

100-
private static int GetResponseHeaderStaticTableId(KnownHeaderType responseHeaderType)
151+
private static bool IsSensitive(int staticTableIndex, string name)
101152
{
102-
switch (responseHeaderType)
153+
// Set-Cookie could contain sensitive data.
154+
if (staticTableIndex == H2StaticTable.SetCookie)
103155
{
104-
case KnownHeaderType.CacheControl:
105-
return H2StaticTable.CacheControl;
106-
case KnownHeaderType.Date:
107-
return H2StaticTable.Date;
108-
case KnownHeaderType.TransferEncoding:
109-
return H2StaticTable.TransferEncoding;
110-
case KnownHeaderType.Via:
111-
return H2StaticTable.Via;
112-
case KnownHeaderType.Allow:
113-
return H2StaticTable.Allow;
114-
case KnownHeaderType.ContentType:
115-
return H2StaticTable.ContentType;
116-
case KnownHeaderType.ContentEncoding:
117-
return H2StaticTable.ContentEncoding;
118-
case KnownHeaderType.ContentLanguage:
119-
return H2StaticTable.ContentLanguage;
120-
case KnownHeaderType.ContentLocation:
121-
return H2StaticTable.ContentLocation;
122-
case KnownHeaderType.ContentRange:
123-
return H2StaticTable.ContentRange;
124-
case KnownHeaderType.Expires:
125-
return H2StaticTable.Expires;
126-
case KnownHeaderType.LastModified:
127-
return H2StaticTable.LastModified;
128-
case KnownHeaderType.AcceptRanges:
129-
return H2StaticTable.AcceptRanges;
130-
case KnownHeaderType.Age:
131-
return H2StaticTable.Age;
132-
case KnownHeaderType.ETag:
133-
return H2StaticTable.ETag;
134-
case KnownHeaderType.Location:
135-
return H2StaticTable.Location;
136-
case KnownHeaderType.ProxyAuthenticate:
137-
return H2StaticTable.ProxyAuthenticate;
138-
case KnownHeaderType.RetryAfter:
139-
return H2StaticTable.RetryAfter;
140-
case KnownHeaderType.Server:
141-
return H2StaticTable.Server;
142-
case KnownHeaderType.SetCookie:
143-
return H2StaticTable.SetCookie;
144-
case KnownHeaderType.Vary:
145-
return H2StaticTable.Vary;
146-
case KnownHeaderType.WWWAuthenticate:
147-
return H2StaticTable.WwwAuthenticate;
148-
case KnownHeaderType.AccessControlAllowOrigin:
149-
return H2StaticTable.AccessControlAllowOrigin;
150-
case KnownHeaderType.ContentLength:
151-
return H2StaticTable.ContentLength;
152-
default:
153-
return -1;
156+
return true;
157+
}
158+
if (string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase))
159+
{
160+
return true;
154161
}
162+
163+
return false;
164+
}
165+
166+
private static bool IsNotDynamicallyIndexed(int staticTableIndex)
167+
{
168+
// Content-Length is added to static content. Content length is different for each
169+
// file, and is unlikely to be reused because of browser caching.
170+
return staticTableIndex == H2StaticTable.ContentLength;
155171
}
156172
}
157173
}

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public Http2Connection(HttpConnectionContext context)
8787
httpLimits.MinResponseDataRate,
8888
context.ConnectionId,
8989
context.MemoryPool,
90-
context.ServiceContext.Log);
90+
context.ServiceContext);
9191

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

746+
// Maximum HPack encoder size is limited by Http2Limits.HeaderTableSize, configured max the server.
747+
//
748+
// Note that the client HPack decoder doesn't care about the ACK so we don't need to lock sending the
749+
// ACK and updating the table size on the server together.
750+
// The client will wait until a size agreed upon by it (sent in SETTINGS_HEADER_TABLE_SIZE) and the
751+
// server (sent as a dynamic table size update in the next HEADERS frame) is received before applying
752+
// the new size.
753+
_frameWriter.UpdateMaxHeaderTableSize(Math.Min(_clientSettings.HeaderTableSize, (uint)Limits.Http2.HeaderTableSize));
754+
746755
return ackTask.AsTask();
747756
}
748757
catch (Http2SettingsParameterOutOfRangeException ex)

src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ internal class Http2FrameWriter
3838
private readonly ITimeoutControl _timeoutControl;
3939
private readonly MinDataRate _minResponseDataRate;
4040
private readonly TimingPipeFlusher _flusher;
41+
private readonly HPackEncoder _hpackEncoder;
4142

4243
private uint _maxFrameSize = Http2PeerSettings.MinAllowedMaxFrameSize;
4344
private byte[] _headerEncodingBuffer;
@@ -55,20 +56,30 @@ public Http2FrameWriter(
5556
MinDataRate minResponseDataRate,
5657
string connectionId,
5758
MemoryPool<byte> memoryPool,
58-
IKestrelTrace log)
59+
ServiceContext serviceContext)
5960
{
6061
// Allow appending more data to the PipeWriter when a flush is pending.
6162
_outputWriter = new ConcurrentPipeWriter(outputPipeWriter, memoryPool, _writeLock);
6263
_connectionContext = connectionContext;
6364
_http2Connection = http2Connection;
6465
_connectionOutputFlowControl = connectionOutputFlowControl;
6566
_connectionId = connectionId;
66-
_log = log;
67+
_log = serviceContext.Log;
6768
_timeoutControl = timeoutControl;
6869
_minResponseDataRate = minResponseDataRate;
69-
_flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, log);
70+
_flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, serviceContext.Log);
7071
_outgoingFrame = new Http2Frame();
7172
_headerEncodingBuffer = new byte[_maxFrameSize];
73+
74+
_hpackEncoder = new HPackEncoder(serviceContext.ServerOptions.AllowResponseHeaderCompression);
75+
}
76+
77+
public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize)
78+
{
79+
lock (_writeLock)
80+
{
81+
_hpackEncoder.UpdateMaxHeaderTableSize(maxHeaderTableSize);
82+
}
7283
}
7384

7485
public void UpdateMaxFrameSize(uint maxFrameSize)
@@ -175,7 +186,7 @@ public void WriteResponseHeaders(int streamId, int statusCode, Http2HeadersFrame
175186
_headersEnumerator.Initialize(headers);
176187
_outgoingFrame.PrepareHeaders(headerFrameFlags, streamId);
177188
var buffer = _headerEncodingBuffer.AsSpan();
178-
var done = HPackHeaderWriter.BeginEncodeHeaders(statusCode, _headersEnumerator, buffer, out var payloadLength);
189+
var done = HPackHeaderWriter.BeginEncodeHeaders(statusCode, _hpackEncoder, _headersEnumerator, buffer, out var payloadLength);
179190
FinishWritingHeaders(streamId, payloadLength, done);
180191
}
181192
catch (HPackEncodingException hex)
@@ -201,7 +212,7 @@ public ValueTask<FlushResult> WriteResponseTrailers(int streamId, HttpResponseTr
201212
_headersEnumerator.Initialize(headers);
202213
_outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.END_STREAM, streamId);
203214
var buffer = _headerEncodingBuffer.AsSpan();
204-
var done = HPackHeaderWriter.BeginEncodeHeaders(_headersEnumerator, buffer, out var payloadLength);
215+
var done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out var payloadLength);
205216
FinishWritingHeaders(streamId, payloadLength, done);
206217
}
207218
catch (HPackEncodingException hex)
@@ -230,7 +241,7 @@ private void FinishWritingHeaders(int streamId, int payloadLength, bool done)
230241
{
231242
_outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId);
232243

233-
done = HPackHeaderWriter.ContinueEncodeHeaders(_headersEnumerator, buffer, out payloadLength);
244+
done = HPackHeaderWriter.ContinueEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out payloadLength);
234245
_outgoingFrame.PayloadLength = payloadLength;
235246

236247
if (done)

0 commit comments

Comments
 (0)