Skip to content

Commit c84d95d

Browse files
authored
Respect SETTINGS_MAX_HEADER_LIST_SIZE on HTTP/2 and HTTP/3 (#79281)
1 parent 89b2740 commit c84d95d

File tree

9 files changed

+273
-36
lines changed

9 files changed

+273
-36
lines changed

src/libraries/Common/tests/System/Net/Http/Http3LoopbackConnection.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
namespace System.Net.Test.Common
1616
{
17-
internal sealed class Http3LoopbackConnection : GenericLoopbackConnection
17+
public sealed class Http3LoopbackConnection : GenericLoopbackConnection
1818
{
1919
public const long H3_NO_ERROR = 0x100;
2020
public const long H3_GENERAL_PROTOCOL_ERROR = 0x101;
@@ -188,11 +188,11 @@ public async Task<Http3LoopbackStream> AcceptRequestStreamAsync()
188188
return (controlStream, requestStream);
189189
}
190190

191-
public async Task EstablishControlStreamAsync()
191+
public async Task EstablishControlStreamAsync(SettingsEntry[] settingsEntries)
192192
{
193193
_outboundControlStream = await OpenUnidirectionalStreamAsync();
194194
await _outboundControlStream.SendUnidirectionalStreamTypeAsync(Http3LoopbackStream.ControlStream);
195-
await _outboundControlStream.SendSettingsFrameAsync();
195+
await _outboundControlStream.SendSettingsFrameAsync(settingsEntries);
196196
}
197197

198198
public override async Task<byte[]> ReadRequestBodyAsync()

src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,12 @@ public override void Dispose()
6666
_cert.Dispose();
6767
}
6868

69-
private async Task<Http3LoopbackConnection> EstablishHttp3ConnectionAsync()
69+
private async Task<Http3LoopbackConnection> EstablishHttp3ConnectionAsync(params SettingsEntry[] settingsEntries)
7070
{
7171
QuicConnection con = await _listener.AcceptConnectionAsync().ConfigureAwait(false);
7272
Http3LoopbackConnection connection = new Http3LoopbackConnection(con);
7373

74-
await connection.EstablishControlStreamAsync();
74+
await connection.EstablishControlStreamAsync(settingsEntries);
7575
return connection;
7676
}
7777

@@ -80,6 +80,11 @@ public override async Task<GenericLoopbackConnection> EstablishGenericConnection
8080
return await EstablishHttp3ConnectionAsync();
8181
}
8282

83+
public Task<Http3LoopbackConnection> EstablishConnectionAsync(params SettingsEntry[] settingsEntries)
84+
{
85+
return EstablishHttp3ConnectionAsync(settingsEntries);
86+
}
87+
8388
public override async Task AcceptConnectionAsync(Func<GenericLoopbackConnection, Task> funcAsync)
8489
{
8590
await using Http3LoopbackConnection con = await EstablishHttp3ConnectionAsync().ConfigureAwait(false);

src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414

1515
namespace System.Net.Test.Common
1616
{
17-
18-
internal sealed class Http3LoopbackStream : IAsyncDisposable
17+
public sealed class Http3LoopbackStream : IAsyncDisposable
1918
{
2019
private const int MaximumVarIntBytes = 8;
2120
private const long VarIntMax = (1L << 62) - 1;
@@ -58,18 +57,16 @@ public async Task SendUnidirectionalStreamTypeAsync(long streamType)
5857
await _stream.WriteAsync(buffer.AsMemory(0, bytesWritten)).ConfigureAwait(false);
5958
}
6059

61-
public async Task SendSettingsFrameAsync(ICollection<(long settingId, long settingValue)> settings = null)
60+
public async Task SendSettingsFrameAsync(SettingsEntry[] settingsEntries)
6261
{
63-
settings ??= Array.Empty<(long settingId, long settingValue)>();
64-
65-
var buffer = new byte[settings.Count * MaximumVarIntBytes * 2];
62+
var buffer = new byte[settingsEntries.Length * MaximumVarIntBytes * 2];
6663

6764
int bytesWritten = 0;
6865

69-
foreach ((long settingId, long settingValue) in settings)
66+
foreach (SettingsEntry setting in settingsEntries)
7067
{
71-
bytesWritten += EncodeHttpInteger(settingId, buffer.AsSpan(bytesWritten));
72-
bytesWritten += EncodeHttpInteger(settingValue, buffer.AsSpan(bytesWritten));
68+
bytesWritten += EncodeHttpInteger((int)setting.SettingId, buffer.AsSpan(bytesWritten));
69+
bytesWritten += EncodeHttpInteger(setting.Value, buffer.AsSpan(bytesWritten));
7370
}
7471

7572
await SendFrameAsync(SettingsFrame, buffer.AsMemory(0, bytesWritten)).ConfigureAwait(false);

src/libraries/System.Net.Http/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,9 @@
357357
<data name="net_http_buffer_insufficient_length" xml:space="preserve">
358358
<value>The buffer was not long enough.</value>
359359
</data>
360+
<data name="net_http_request_headers_exceeded_length" xml:space="preserve">
361+
<value>The HTTP request headers length exceeded the server limit of {0} bytes.</value>
362+
</data>
360363
<data name="net_http_response_headers_exceeded_length" xml:space="preserve">
361364
<value>The HTTP response headers length exceeded the set limit of {0} bytes.</value>
362365
</data>

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ internal sealed partial class Http2Connection : HttpConnectionBase
5656
private readonly Channel<WriteQueueEntry> _writeChannel;
5757
private bool _lastPendingWriterShouldFlush;
5858

59+
// Server-advertised SETTINGS_MAX_HEADER_LIST_SIZE
60+
// https://www.rfc-editor.org/rfc/rfc9113.html#section-6.5.2-2.12.1
61+
private uint _maxHeaderListSize = uint.MaxValue; // Defaults to infinite
62+
5963
// This flag indicates that the connection is shutting down and cannot accept new requests, because of one of the following conditions:
6064
// (1) We received a GOAWAY frame from the server
6165
// (2) We have exhaustead StreamIds (i.e. _nextStream == MaxStreamId)
@@ -162,6 +166,14 @@ public Http2Connection(HttpConnectionPool pool, Stream stream)
162166
_nextPingRequestTimestamp = Environment.TickCount64 + _keepAlivePingDelay;
163167
_keepAlivePingPolicy = _pool.Settings._keepAlivePingPolicy;
164168

169+
uint maxHeaderListSize = _pool._lastSeenHttp2MaxHeaderListSize;
170+
if (maxHeaderListSize > 0)
171+
{
172+
// Previous connections to the same host advertised a limit.
173+
// Use this as an initial value before we receive the SETTINGS frame.
174+
_maxHeaderListSize = maxHeaderListSize;
175+
}
176+
165177
if (HttpTelemetry.Log.IsEnabled())
166178
{
167179
HttpTelemetry.Log.Http20ConnectionEstablished();
@@ -822,6 +834,8 @@ private void ProcessSettingsFrame(FrameHeader frameHeader, bool initialFrame = f
822834
uint settingValue = BinaryPrimitives.ReadUInt32BigEndian(settings);
823835
settings = settings.Slice(4);
824836

837+
if (NetEventSource.Log.IsEnabled()) Trace($"Applying setting {(SettingId)settingId}={settingValue}");
838+
825839
switch ((SettingId)settingId)
826840
{
827841
case SettingId.MaxConcurrentStreams:
@@ -861,6 +875,11 @@ private void ProcessSettingsFrame(FrameHeader frameHeader, bool initialFrame = f
861875
}
862876
break;
863877

878+
case SettingId.MaxHeaderListSize:
879+
_maxHeaderListSize = settingValue;
880+
_pool._lastSeenHttp2MaxHeaderListSize = _maxHeaderListSize;
881+
break;
882+
864883
default:
865884
// All others are ignored because we don't care about them.
866885
// Note, per RFC, unknown settings IDs should be ignored.
@@ -1379,14 +1398,18 @@ private void WriteBytes(ReadOnlySpan<byte> bytes, ref ArrayBuffer headerBuffer)
13791398
headerBuffer.Commit(bytes.Length);
13801399
}
13811400

1382-
private void WriteHeaderCollection(HttpRequestMessage request, HttpHeaders headers, ref ArrayBuffer headerBuffer)
1401+
private int WriteHeaderCollection(HttpRequestMessage request, HttpHeaders headers, ref ArrayBuffer headerBuffer)
13831402
{
13841403
if (NetEventSource.Log.IsEnabled()) Trace("");
13851404

13861405
HeaderEncodingSelector<HttpRequestMessage>? encodingSelector = _pool.Settings._requestHeaderEncodingSelector;
13871406

13881407
ref string[]? tmpHeaderValuesArray = ref t_headerValues;
1389-
foreach (HeaderEntry header in headers.GetEntries())
1408+
1409+
ReadOnlySpan<HeaderEntry> entries = headers.GetEntries();
1410+
int headerListSize = entries.Length * HeaderField.RfcOverhead;
1411+
1412+
foreach (HeaderEntry header in entries)
13901413
{
13911414
int headerValuesCount = HttpHeaders.GetStoreValuesIntoStringArray(header.Key, header.Value, ref tmpHeaderValuesArray);
13921415
Debug.Assert(headerValuesCount > 0, "No values for header??");
@@ -1402,6 +1425,10 @@ private void WriteHeaderCollection(HttpRequestMessage request, HttpHeaders heade
14021425
// The Connection, Upgrade and ProxyConnection headers are also not supported in HTTP2.
14031426
if (knownHeader != KnownHeaders.Host && knownHeader != KnownHeaders.Connection && knownHeader != KnownHeaders.Upgrade && knownHeader != KnownHeaders.ProxyConnection)
14041427
{
1428+
// The length of the encoded name may be shorter than the actual name.
1429+
// Ensure that headerListSize is always >= of the actual size.
1430+
headerListSize += knownHeader.Name.Length;
1431+
14051432
if (knownHeader == KnownHeaders.TE)
14061433
{
14071434
// HTTP/2 allows only 'trailers' TE header. rfc7540 8.1.2.2
@@ -1442,6 +1469,8 @@ private void WriteHeaderCollection(HttpRequestMessage request, HttpHeaders heade
14421469
WriteLiteralHeader(header.Key.Name, headerValues, valueEncoding, ref headerBuffer);
14431470
}
14441471
}
1472+
1473+
return headerListSize;
14451474
}
14461475

14471476
private void WriteHeaders(HttpRequestMessage request, ref ArrayBuffer headerBuffer)
@@ -1472,9 +1501,9 @@ private void WriteHeaders(HttpRequestMessage request, ref ArrayBuffer headerBuff
14721501

14731502
WriteIndexedHeader(_pool.IsSecure ? H2StaticTable.SchemeHttps : H2StaticTable.SchemeHttp, ref headerBuffer);
14741503

1475-
if (request.HasHeaders && request.Headers.Host != null)
1504+
if (request.HasHeaders && request.Headers.Host is string host)
14761505
{
1477-
WriteIndexedHeader(H2StaticTable.Authority, request.Headers.Host, ref headerBuffer);
1506+
WriteIndexedHeader(H2StaticTable.Authority, host, ref headerBuffer);
14781507
}
14791508
else
14801509
{
@@ -1492,16 +1521,19 @@ private void WriteHeaders(HttpRequestMessage request, ref ArrayBuffer headerBuff
14921521
WriteIndexedHeader(H2StaticTable.PathSlash, pathAndQuery, ref headerBuffer);
14931522
}
14941523

1524+
int headerListSize = 3 * HeaderField.RfcOverhead; // Method, Authority, Path
1525+
14951526
if (request.HasHeaders)
14961527
{
14971528
if (request.Headers.Protocol != null)
14981529
{
14991530
WriteBytes(ProtocolLiteralHeaderBytes, ref headerBuffer);
15001531
Encoding? protocolEncoding = _pool.Settings._requestHeaderEncodingSelector?.Invoke(":protocol", request);
15011532
WriteLiteralHeaderValue(request.Headers.Protocol, protocolEncoding, ref headerBuffer);
1533+
headerListSize += HeaderField.RfcOverhead;
15021534
}
15031535

1504-
WriteHeaderCollection(request, request.Headers, ref headerBuffer);
1536+
headerListSize += WriteHeaderCollection(request, request.Headers, ref headerBuffer);
15051537
}
15061538

15071539
// Determine cookies to send.
@@ -1511,9 +1543,9 @@ private void WriteHeaders(HttpRequestMessage request, ref ArrayBuffer headerBuff
15111543
if (cookiesFromContainer != string.Empty)
15121544
{
15131545
WriteBytes(KnownHeaders.Cookie.Http2EncodedName, ref headerBuffer);
1514-
15151546
Encoding? cookieEncoding = _pool.Settings._requestHeaderEncodingSelector?.Invoke(KnownHeaders.Cookie.Name, request);
15161547
WriteLiteralHeaderValue(cookiesFromContainer, cookieEncoding, ref headerBuffer);
1548+
headerListSize += HttpKnownHeaderNames.Cookie.Length + HeaderField.RfcOverhead;
15171549
}
15181550
}
15191551

@@ -1525,11 +1557,24 @@ private void WriteHeaders(HttpRequestMessage request, ref ArrayBuffer headerBuff
15251557
{
15261558
WriteBytes(KnownHeaders.ContentLength.Http2EncodedName, ref headerBuffer);
15271559
WriteLiteralHeaderValue("0", valueEncoding: null, ref headerBuffer);
1560+
headerListSize += HttpKnownHeaderNames.ContentLength.Length + HeaderField.RfcOverhead;
15281561
}
15291562
}
15301563
else
15311564
{
1532-
WriteHeaderCollection(request, request.Content.Headers, ref headerBuffer);
1565+
headerListSize += WriteHeaderCollection(request, request.Content.Headers, ref headerBuffer);
1566+
}
1567+
1568+
// The headerListSize is an approximation of the total header length.
1569+
// This is acceptable as long as the value is always >= the actual length.
1570+
// We must avoid ever sending more than the server allowed.
1571+
// This approach must be revisted if we ever support the dynamic table or compression when sending requests.
1572+
headerListSize += headerBuffer.ActiveLength;
1573+
1574+
uint maxHeaderListSize = _maxHeaderListSize;
1575+
if ((uint)headerListSize > maxHeaderListSize)
1576+
{
1577+
throw new HttpRequestException(SR.Format(SR.net_http_request_headers_exceeded_length, maxHeaderListSize));
15331578
}
15341579
}
15351580

@@ -1602,10 +1647,10 @@ private async ValueTask<Http2Stream> SendHeadersAsync(HttpRequestMessage request
16021647
// streams are created and started in order.
16031648
await PerformWriteAsync(totalSize, (thisRef: this, http2Stream, headerBytes, endStream: (request.Content == null && !request.IsExtendedConnectRequest), mustFlush), static (s, writeBuffer) =>
16041649
{
1605-
if (NetEventSource.Log.IsEnabled()) s.thisRef.Trace(s.http2Stream.StreamId, $"Started writing. Total header bytes={s.headerBytes.Length}");
1606-
16071650
s.thisRef.AddStream(s.http2Stream);
16081651

1652+
if (NetEventSource.Log.IsEnabled()) s.thisRef.Trace(s.http2Stream.StreamId, $"Started writing. Total header bytes={s.headerBytes.Length}");
1653+
16091654
Span<byte> span = writeBuffer.Span;
16101655

16111656
// Copy the HEADERS frame.

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ internal sealed class Http3Connection : HttpConnectionBase
3636
// Our control stream.
3737
private QuicStream? _clientControl;
3838

39-
// Current SETTINGS from the server.
40-
private int _maximumHeadersLength = int.MaxValue; // TODO: this is not yet observed by Http3Stream when buffering headers.
39+
// Server-advertised SETTINGS_MAX_FIELD_SECTION_SIZE
40+
// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4.1-2.2.1
41+
private uint _maxHeaderListSize = uint.MaxValue; // Defaults to infinite
4142

4243
// Once the server's streams are received, these are set to 1. Further receipt of these streams results in a connection error.
4344
private int _haveServerControlStream;
@@ -53,7 +54,7 @@ internal sealed class Http3Connection : HttpConnectionBase
5354

5455
public HttpAuthority Authority => _authority;
5556
public HttpConnectionPool Pool => _pool;
56-
public int MaximumRequestHeadersLength => _maximumHeadersLength;
57+
public uint MaxHeaderListSize => _maxHeaderListSize;
5758
public byte[]? AltUsedEncodedHeaderBytes => _altUsedEncodedHeader;
5859
public Exception? AbortException => Volatile.Read(ref _abortException);
5960
private object SyncObj => _activeRequests;
@@ -84,6 +85,13 @@ public Http3Connection(HttpConnectionPool pool, HttpAuthority? origin, HttpAutho
8485
_altUsedEncodedHeader = QPack.QPackEncoder.EncodeLiteralHeaderFieldWithoutNameReferenceToArray(KnownHeaders.AltUsed.Name, altUsedValue);
8586
}
8687

88+
uint maxHeaderListSize = _pool._lastSeenHttp3MaxHeaderListSize;
89+
if (maxHeaderListSize > 0)
90+
{
91+
// Previous connections to the same host advertised a limit.
92+
// Use this as an initial value before we receive the SETTINGS frame.
93+
_maxHeaderListSize = maxHeaderListSize;
94+
}
8795

8896
if (HttpTelemetry.Log.IsEnabled())
8997
{
@@ -725,10 +733,13 @@ async ValueTask ProcessSettingsFrameAsync(long settingsPayloadLength)
725733

726734
buffer.Discard(bytesRead);
727735

736+
if (NetEventSource.Log.IsEnabled()) Trace($"Applying setting {(Http3SettingType)settingId}={settingValue}");
737+
728738
switch ((Http3SettingType)settingId)
729739
{
730740
case Http3SettingType.MaxHeaderListSize:
731-
_maximumHeadersLength = (int)Math.Min(settingValue, int.MaxValue);
741+
_maxHeaderListSize = (uint)Math.Min((ulong)settingValue, uint.MaxValue);
742+
_pool._lastSeenHttp3MaxHeaderListSize = _maxHeaderListSize;
732743
break;
733744
case Http3SettingType.ReservedHttp2EnablePush:
734745
case Http3SettingType.ReservedHttp2MaxConcurrentStreams:

0 commit comments

Comments
 (0)