Skip to content

Commit 2bddabf

Browse files
committed
Use SETTINGS_HEADER_TABLE_SIZE
1 parent 166ea04 commit 2bddabf

File tree

8 files changed

+153
-24
lines changed

8 files changed

+153
-24
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ 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>
4444
/// Value must be greater than 0, defaults to 4096
4545
/// </para>

src/Servers/Kestrel/Core/src/Internal/Http2/HPack/Http2HPackEncoder.cs

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ internal class Http2HPackEncoder
2424
private uint _headerTableSize;
2525
private uint _maxHeaderTableSize;
2626
private uint _maxHeaderListSize;
27+
private bool _pendingTableSizeUpdate;
2728

2829
public bool IgnoreMaxHeaderListSize => _maxHeaderListSize == Http2PeerSettings.DefaultMaxHeaderListSize;
2930

30-
public Http2HPackEncoder(IHeaderSensitivityDetector sensitivityDetector = null)
31+
public Http2HPackEncoder(uint maxHeaderTableSize = Http2PeerSettings.DefaultHeaderTableSize, IHeaderSensitivityDetector sensitivityDetector = null)
3132
{
32-
_maxHeaderTableSize = Http2PeerSettings.DefaultHeaderTableSize;
33+
_maxHeaderTableSize = maxHeaderTableSize;
3334
_maxHeaderListSize = Http2PeerSettings.DefaultMaxHeaderListSize;
3435
Head = new HPackHeaderEntry();
3536
Head.Initialize(-1, string.Empty, string.Empty, int.MaxValue, null);
@@ -39,11 +40,12 @@ public Http2HPackEncoder(IHeaderSensitivityDetector sensitivityDetector = null)
3940
_sensitivityDetector = sensitivityDetector;
4041
}
4142

42-
public void SetMaxHeaderTableSize(uint maxHeaderTableSize)
43+
public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize)
4344
{
4445
if (_maxHeaderTableSize != maxHeaderTableSize)
4546
{
4647
_maxHeaderTableSize = maxHeaderTableSize;
48+
_pendingTableSizeUpdate = true;
4749

4850
// Check capacity and remove entries that exceed the new capacity
4951
EnsureCapacity(0);
@@ -60,21 +62,33 @@ public void SetMaxHeaderListSize(uint maxHeaderListSize)
6062
/// </summary>
6163
public bool BeginEncodeHeaders(int statusCode, Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
6264
{
63-
if (!EncodeStatusHeader(statusCode, buffer, out var statusCodeLength))
65+
length = 0;
66+
67+
if (_pendingTableSizeUpdate)
68+
{
69+
if (!HPackEncoder.EncodeDynamicTableSizeUpdate((int)_maxHeaderTableSize, buffer, out var sizeUpdateLength))
70+
{
71+
throw new HPackEncodingException(SR.net_http_hpack_encode_failure);
72+
}
73+
length += sizeUpdateLength;
74+
_pendingTableSizeUpdate = false;
75+
}
76+
77+
if (!EncodeStatusHeader(statusCode, buffer.Slice(length), out var statusCodeLength))
6478
{
6579
throw new HPackEncodingException(SR.net_http_hpack_encode_failure);
6680
}
81+
length += statusCodeLength;
6782

6883
if (!headersEnumerator.MoveNext())
6984
{
70-
length = statusCodeLength;
7185
return true;
7286
}
7387

7488
// We're ok with not throwing if no headers were encoded because we've already encoded the status.
7589
// There is a small chance that the header will encode if there is no other content in the next HEADERS frame.
76-
var done = EncodeHeadersCore(headersEnumerator, buffer.Slice(statusCodeLength), throwIfNoneEncoded: false, out var headersLength);
77-
length = statusCodeLength + headersLength;
90+
var done = EncodeHeadersCore(headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: false, out var headersLength);
91+
length += headersLength;
7892
return done;
7993
}
8094

@@ -83,13 +97,26 @@ public bool BeginEncodeHeaders(int statusCode, Http2HeadersEnumerator headersEnu
8397
/// </summary>
8498
public bool BeginEncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span<byte> buffer, out int length)
8599
{
100+
length = 0;
101+
102+
if (_pendingTableSizeUpdate)
103+
{
104+
if (!HPackEncoder.EncodeDynamicTableSizeUpdate(1, buffer, out var sizeUpdateLength))
105+
{
106+
throw new HPackEncodingException(SR.net_http_hpack_encode_failure);
107+
}
108+
length += sizeUpdateLength;
109+
_pendingTableSizeUpdate = false;
110+
}
111+
86112
if (!headersEnumerator.MoveNext())
87113
{
88-
length = 0;
89114
return true;
90115
}
91116

92-
return EncodeHeadersCore(headersEnumerator, buffer, throwIfNoneEncoded: true, out length);
117+
var done = EncodeHeadersCore(headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: true, out var headersLength);
118+
length += headersLength;
119+
return done;
93120
}
94121

95122
/// <summary>
@@ -115,7 +142,7 @@ private bool EncodeStatusHeader(int statusCode, Span<byte> buffer, out int lengt
115142
return HPackEncoder.EncodeIndexedHeaderField(H2StaticTable.StatusIndex[statusCode], buffer, out length);
116143
default:
117144
const string name = ":status";
118-
var value = StatusCodes.ToStatusBytes(statusCode);
145+
var value = StatusCodes.ToStatusString(statusCode);
119146
return EncodeHeader(buffer, H2StaticTable.Status200, name, value, out length);
120147
}
121148
}

src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StatusCodes.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

@@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
99
{
1010
internal static class StatusCodes
1111
{
12-
public static string ToStatusBytes(int statusCode)
12+
public static string ToStatusString(int statusCode)
1313
{
1414
switch (statusCode)
1515
{

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,9 @@ private Task ProcessSettingsFrameAsync(in ReadOnlySequence<byte> payload)
717717

718718
_clientSettings.Update(Http2FrameReader.ReadSettings(payload));
719719

720+
// Maximum encoder size is limited by the configured max on the server
721+
_frameWriter.UpdateMaxHeaderTableSize(Math.Min(_clientSettings.HeaderTableSize, (uint)Limits.Http2.HeaderTableSize));
722+
720723
// Ack before we update the windows, they could send data immediately.
721724
var ackTask = _frameWriter.WriteSettingsAckAsync();
722725

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ public Http2FrameWriter(
7474
_hpackEncoder = new Http2HPackEncoder();
7575
}
7676

77+
public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize)
78+
{
79+
lock (_writeLock)
80+
{
81+
_hpackEncoder.UpdateMaxHeaderTableSize(maxHeaderTableSize);
82+
}
83+
}
84+
7785
public void UpdateMaxFrameSize(uint maxFrameSize)
7886
{
7987
lock (_writeLock)

src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit()
7575

7676
var enumerator = new Http2HeadersEnumerator();
7777

78-
var http2HPackEncoder = new Http2HPackEncoder();
79-
http2HPackEncoder.SetMaxHeaderTableSize(256);
78+
var http2HPackEncoder = new Http2HPackEncoder(256);
8079

8180
// First response
8281
enumerator.Initialize(headers);
@@ -194,7 +193,7 @@ public void BeginEncodeHeaders_SensitiveValue_NeverIndexAndNoHeaderEntry()
194193
var enumerator = new Http2HeadersEnumerator();
195194
enumerator.Initialize(headers);
196195

197-
var http2HPackEncoder = new Http2HPackEncoder(new TestSensitivityDetector());
196+
var http2HPackEncoder = new Http2HPackEncoder(Http2PeerSettings.DefaultHeaderTableSize, new TestSensitivityDetector());
198197
Assert.True(http2HPackEncoder.BeginEncodeHeaders(200, enumerator, buffer, out var length));
199198

200199
var result = buffer.Slice(0, length).ToArray();
@@ -235,8 +234,8 @@ public void BeginEncodeHeaders_HeaderExceedHeaderTableSize_NoIndexAndNoHeaderEnt
235234
},
236235
new byte[]
237236
{
238-
// 0 12 c u s t o m
239-
0x00, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
237+
// 12 c u s t o m
238+
0x40, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
240239
// h e a d e r 11 C
241240
0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x0b, 0x43,
242241
// u s t o m V a l
@@ -253,8 +252,8 @@ public void BeginEncodeHeaders_HeaderExceedHeaderTableSize_NoIndexAndNoHeaderEnt
253252
},
254253
new byte[]
255254
{
256-
// 0 27 c u s t o m
257-
0x00, 0x1b, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
255+
// 27 c u s t o m
256+
0x40, 0x1b, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
258257
// h e a d e r ! #
259258
0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x21, 0x23,
260259
// $ % & ' * + - .
@@ -277,16 +276,16 @@ public void BeginEncodeHeaders_HeaderExceedHeaderTableSize_NoIndexAndNoHeaderEnt
277276
},
278277
new byte[]
279278
{
280-
0x88, 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d,
279+
0x88, 0x40, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d,
281280
0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20,
282281
0x4a, 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37,
283282
0x20, 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33,
284-
0x30, 0x20, 0x47, 0x4d, 0x54, 0x00, 0x0c, 0x63,
283+
0x30, 0x20, 0x47, 0x4d, 0x54, 0x40, 0x0c, 0x63,
285284
0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x74,
286285
0x79, 0x70, 0x65, 0x18, 0x74, 0x65, 0x78, 0x74,
287286
0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3b, 0x20, 0x63,
288287
0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x75,
289-
0x74, 0x66, 0x2d, 0x38, 0x00, 0x06, 0x73, 0x65,
288+
0x74, 0x66, 0x2d, 0x38, 0x40, 0x06, 0x73, 0x65,
290289
0x72, 0x76, 0x65, 0x72, 0x07, 0x4b, 0x65, 0x73,
291290
0x74, 0x72, 0x65, 0x6c
292291
},
@@ -398,6 +397,30 @@ public void EncodesHeadersInMultiplePayloadsWhenSpaceNotAvailable(bool exactSize
398397
Assert.Equal(expectedServerHeaderPayload, payload.Slice(offset, length).ToArray());
399398
}
400399

400+
[Fact]
401+
public void BeginEncodeHeaders_MaxHeaderTableSizeUpdated_SizeUpdateInHeaders()
402+
{
403+
Span<byte> buffer = new byte[1024 * 16];
404+
405+
var hpackEncoder = new Http2HPackEncoder();
406+
hpackEncoder.UpdateMaxHeaderTableSize(100);
407+
408+
var enumerator = new Http2HeadersEnumerator();
409+
410+
// First request
411+
enumerator.Initialize(new Dictionary<string, StringValues>());
412+
Assert.True(hpackEncoder.BeginEncodeHeaders(enumerator, buffer, out var length));
413+
414+
Assert.Equal(1, length);
415+
Assert.Equal(0x21, buffer[0]);
416+
417+
// Second request
418+
enumerator.Initialize(new Dictionary<string, StringValues>());
419+
Assert.True(hpackEncoder.BeginEncodeHeaders(enumerator, buffer, out length));
420+
421+
Assert.Equal(0, length);
422+
}
423+
401424
private static Http2HeadersEnumerator GetHeadersEnumerator(IEnumerable<KeyValuePair<string, string>> headers)
402425
{
403426
var groupedHeaders = headers

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3177,7 +3177,56 @@ await ExpectAsync(Http2FrameType.SETTINGS,
31773177
withFlags: (byte)Http2SettingsFrameFlags.ACK,
31783178
withStreamId: 0);
31793179

3180-
await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false);
3180+
// Start request
3181+
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
3182+
3183+
var headerFrame = await ExpectAsync(Http2FrameType.HEADERS,
3184+
withLength: 35,
3185+
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
3186+
withStreamId: 1);
3187+
3188+
// Headers start with :status = 200
3189+
Assert.Equal(0x88, headerFrame.Payload.Span[0]);
3190+
3191+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
3192+
}
3193+
3194+
[Fact]
3195+
public async Task SETTINGS_Received_WithLargeHeaderTableSizeLimit_ChangesHeaderTableSize()
3196+
{
3197+
_serviceContext.ServerOptions.Limits.Http2.HeaderTableSize = 40000;
3198+
3199+
await InitializeConnectionAsync(_noopApplication, expectedSettingsCount: 4);
3200+
3201+
// Update client settings
3202+
_clientSettings.HeaderTableSize = 65536; // Chrome's default, larger than the 4kb spec default
3203+
await SendSettingsAsync();
3204+
3205+
// ACK
3206+
await ExpectAsync(Http2FrameType.SETTINGS,
3207+
withLength: 0,
3208+
withFlags: (byte)Http2SettingsFrameFlags.ACK,
3209+
withStreamId: 0);
3210+
3211+
// Start request
3212+
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
3213+
3214+
var headerFrame = await ExpectAsync(Http2FrameType.HEADERS,
3215+
withLength: 39,
3216+
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
3217+
withStreamId: 1);
3218+
3219+
const byte DynamicTableSizeUpdateMask = 0xe0;
3220+
3221+
var integerDecoder = new IntegerDecoder();
3222+
Assert.False(integerDecoder.BeginTryDecode((byte)(headerFrame.Payload.Span[0] & ~DynamicTableSizeUpdateMask), prefixLength: 5, out _));
3223+
Assert.False(integerDecoder.TryDecode(headerFrame.Payload.Span[1], out _));
3224+
Assert.False(integerDecoder.TryDecode(headerFrame.Payload.Span[2], out _));
3225+
Assert.True(integerDecoder.TryDecode(headerFrame.Payload.Span[3], out var result));
3226+
3227+
Assert.Equal(40000, result);
3228+
3229+
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
31813230
}
31823231

31833232
[Fact]

src/Shared/runtime/Http2/Hpack/HPackEncoder.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,25 @@ public static bool EncodeStringLiteral(string value, Span<byte> destination, out
483483
return false;
484484
}
485485

486+
public static bool EncodeDynamicTableSizeUpdate(int value, Span<byte> destination, out int bytesWritten)
487+
{
488+
// From https://tools.ietf.org/html/rfc7541#section-6.3
489+
// ----------------------------------------------------
490+
// 0 1 2 3 4 5 6 7
491+
// +---+---+---+---+---+---+---+---+
492+
// | 0 | 0 | 1 | Max size (5+) |
493+
// +---+---------------------------+
494+
495+
if (destination.Length != 0)
496+
{
497+
destination[0] = 0x20;
498+
return IntegerEncoder.Encode(value, 5, destination, out bytesWritten);
499+
}
500+
501+
bytesWritten = 0;
502+
return false;
503+
}
504+
486505
public static bool EncodeStringLiterals(ReadOnlySpan<string> values, string? separator, Span<byte> destination, out int bytesWritten)
487506
{
488507
bytesWritten = 0;

0 commit comments

Comments
 (0)