diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx
index f3d2f81720bb10..33bedff53e884f 100644
--- a/src/libraries/System.Net.Http/src/Resources/Strings.resx
+++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx
@@ -354,6 +354,9 @@
Received status phrase could not be decoded with iso-8859-1: '{0}'.
+
+ The response contained more than one status code.
+
Received an invalid folded header.
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
index d1932068ab5098..36ed2d946bc1d3 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
@@ -438,29 +438,126 @@ public void OnWindowUpdate(int amount)
}
}
+ private const int FirstHPackRequestPseudoHeaderId = 1;
+ private const int LastHPackRequestPseudoHeaderId = 7;
+ private const int FirstHPackStatusPseudoHeaderId = 8;
+ private const int LastHPackStatusPseudoHeaderId = 14;
+ private const int FirstHPackNormalHeaderId = 15;
+ private const int LastHPackNormalHeaderId = 61;
+
+ private static readonly int[] s_hpackStaticStatusCodeTable = new int[LastHPackStatusPseudoHeaderId - FirstHPackStatusPseudoHeaderId + 1] { 200, 204, 206, 304, 400, 404, 500 };
+
+ private static readonly (HeaderDescriptor descriptor, byte[] value)[] s_hpackStaticHeaderTable = new (HeaderDescriptor, byte[])[LastHPackNormalHeaderId - FirstHPackNormalHeaderId + 1]
+ {
+ (KnownHeaders.AcceptCharset.Descriptor, Array.Empty()),
+ (KnownHeaders.AcceptEncoding.Descriptor, Encoding.ASCII.GetBytes("gzip, deflate")),
+ (KnownHeaders.AcceptLanguage.Descriptor, Array.Empty()),
+ (KnownHeaders.AcceptRanges.Descriptor, Array.Empty()),
+ (KnownHeaders.Accept.Descriptor, Array.Empty()),
+ (KnownHeaders.AccessControlAllowOrigin.Descriptor, Array.Empty()),
+ (KnownHeaders.Age.Descriptor, Array.Empty()),
+ (KnownHeaders.Allow.Descriptor, Array.Empty()),
+ (KnownHeaders.Authorization.Descriptor, Array.Empty()),
+ (KnownHeaders.CacheControl.Descriptor, Array.Empty()),
+ (KnownHeaders.ContentDisposition.Descriptor, Array.Empty()),
+ (KnownHeaders.ContentEncoding.Descriptor, Array.Empty()),
+ (KnownHeaders.ContentLanguage.Descriptor, Array.Empty()),
+ (KnownHeaders.ContentLength.Descriptor, Array.Empty()),
+ (KnownHeaders.ContentLocation.Descriptor, Array.Empty()),
+ (KnownHeaders.ContentRange.Descriptor, Array.Empty()),
+ (KnownHeaders.ContentType.Descriptor, Array.Empty()),
+ (KnownHeaders.Cookie.Descriptor, Array.Empty()),
+ (KnownHeaders.Date.Descriptor, Array.Empty()),
+ (KnownHeaders.ETag.Descriptor, Array.Empty()),
+ (KnownHeaders.Expect.Descriptor, Array.Empty()),
+ (KnownHeaders.Expires.Descriptor, Array.Empty()),
+ (KnownHeaders.From.Descriptor, Array.Empty()),
+ (KnownHeaders.Host.Descriptor, Array.Empty()),
+ (KnownHeaders.IfMatch.Descriptor, Array.Empty()),
+ (KnownHeaders.IfModifiedSince.Descriptor, Array.Empty()),
+ (KnownHeaders.IfNoneMatch.Descriptor, Array.Empty()),
+ (KnownHeaders.IfRange.Descriptor, Array.Empty()),
+ (KnownHeaders.IfUnmodifiedSince.Descriptor, Array.Empty()),
+ (KnownHeaders.LastModified.Descriptor, Array.Empty()),
+ (KnownHeaders.Link.Descriptor, Array.Empty()),
+ (KnownHeaders.Location.Descriptor, Array.Empty()),
+ (KnownHeaders.MaxForwards.Descriptor, Array.Empty()),
+ (KnownHeaders.ProxyAuthenticate.Descriptor, Array.Empty()),
+ (KnownHeaders.ProxyAuthorization.Descriptor, Array.Empty()),
+ (KnownHeaders.Range.Descriptor, Array.Empty()),
+ (KnownHeaders.Referer.Descriptor, Array.Empty()),
+ (KnownHeaders.Refresh.Descriptor, Array.Empty()),
+ (KnownHeaders.RetryAfter.Descriptor, Array.Empty()),
+ (KnownHeaders.Server.Descriptor, Array.Empty()),
+ (KnownHeaders.SetCookie.Descriptor, Array.Empty()),
+ (KnownHeaders.StrictTransportSecurity.Descriptor, Array.Empty()),
+ (KnownHeaders.TransferEncoding.Descriptor, Array.Empty()),
+ (KnownHeaders.UserAgent.Descriptor, Array.Empty()),
+ (KnownHeaders.Vary.Descriptor, Array.Empty()),
+ (KnownHeaders.Via.Descriptor, Array.Empty()),
+ (KnownHeaders.WWWAuthenticate.Descriptor, Array.Empty()),
+ };
+
void IHttpHeadersHandler.OnStaticIndexedHeader(int index)
{
- // TODO: https://github.com/dotnet/runtime/issues/1505
- ref readonly HeaderField entry = ref H2StaticTable.Get(index - 1);
- OnHeader(entry.Name, entry.Value);
+ Debug.Assert(index >= FirstHPackRequestPseudoHeaderId && index <= LastHPackNormalHeaderId);
+
+ if (index <= LastHPackRequestPseudoHeaderId)
+ {
+ if (NetEventSource.Log.IsEnabled()) Trace($"Invalid request pseudo-header ID {index}.");
+ throw new HttpRequestException(SR.net_http_invalid_response);
+ }
+ else if (index <= LastHPackStatusPseudoHeaderId)
+ {
+ int statusCode = s_hpackStaticStatusCodeTable[index - FirstHPackStatusPseudoHeaderId];
+
+ OnStatus(statusCode);
+ }
+ else
+ {
+ (HeaderDescriptor descriptor, byte[] value) = s_hpackStaticHeaderTable[index - FirstHPackNormalHeaderId];
+
+ OnHeader(descriptor, value);
+ }
}
void IHttpHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan value)
{
- // TODO: https://github.com/dotnet/runtime/issues/1505
- OnHeader(H2StaticTable.Get(index - 1).Name, value);
+ Debug.Assert(index >= FirstHPackRequestPseudoHeaderId && index <= LastHPackNormalHeaderId);
+
+ if (index <= LastHPackRequestPseudoHeaderId)
+ {
+ if (NetEventSource.Log.IsEnabled()) Trace($"Invalid request pseudo-header ID {index}.");
+ throw new HttpRequestException(SR.net_http_invalid_response);
+ }
+ else if (index <= LastHPackStatusPseudoHeaderId)
+ {
+ int statusCode = ParseStatusCode(value);
+
+ OnStatus(statusCode);
+ }
+ else
+ {
+ (HeaderDescriptor descriptor, _) = s_hpackStaticHeaderTable[index - FirstHPackNormalHeaderId];
+
+ OnHeader(descriptor, value);
+ }
}
- public void OnHeader(ReadOnlySpan name, ReadOnlySpan value)
+ private void AdjustHeaderBudget(int amount)
{
- if (NetEventSource.Log.IsEnabled()) Trace($"{Encoding.ASCII.GetString(name)}: {Encoding.ASCII.GetString(value)}");
- Debug.Assert(name.Length > 0);
-
- _headerBudgetRemaining -= name.Length + value.Length;
+ _headerBudgetRemaining -= amount;
if (_headerBudgetRemaining < 0)
{
throw new HttpRequestException(SR.Format(SR.net_http_response_headers_exceeded_length, _connection._pool.Settings._maxResponseHeadersLength * 1024L));
}
+ }
+
+ private void OnStatus(int statusCode)
+ {
+ if (NetEventSource.Log.IsEnabled()) Trace($"Status code is {statusCode}");
+
+ AdjustHeaderBudget(10); // for ":status" plus 3-digit status code
Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
@@ -471,103 +568,132 @@ public void OnHeader(ReadOnlySpan name, ReadOnlySpan value)
return;
}
- if (name[0] == (byte)':')
+ if (_responseProtocolState == ResponseProtocolState.ExpectingHeaders)
{
- if (_responseProtocolState != ResponseProtocolState.ExpectingHeaders && _responseProtocolState != ResponseProtocolState.ExpectingStatus)
- {
- // Pseudo-headers are allowed only in header block
- if (NetEventSource.Log.IsEnabled()) Trace($"Pseudo-header received in {_responseProtocolState} state.");
- throw new HttpRequestException(SR.net_http_invalid_response_pseudo_header_in_trailer);
- }
-
- if (name.SequenceEqual(StatusHeaderName))
- {
- if (_responseProtocolState != ResponseProtocolState.ExpectingStatus)
- {
- if (NetEventSource.Log.IsEnabled()) Trace("Received extra status header.");
- throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_status_code, "duplicate status"));
- }
-
- int statusValue = ParseStatusCode(value);
- Debug.Assert(_response != null);
- _response.StatusCode = (HttpStatusCode)statusValue;
+ if (NetEventSource.Log.IsEnabled()) Trace("Received extra status header.");
+ throw new HttpRequestException(SR.net_http_invalid_response_multiple_status_codes);
+ }
- if (statusValue < 200)
- {
- // We do not process headers from 1xx responses.
- _responseProtocolState = ResponseProtocolState.ExpectingIgnoredHeaders;
+ if (_responseProtocolState != ResponseProtocolState.ExpectingStatus)
+ {
+ // Pseudo-headers are allowed only in header block
+ if (NetEventSource.Log.IsEnabled()) Trace($"Status pseudo-header received in {_responseProtocolState} state.");
+ throw new HttpRequestException(SR.net_http_invalid_response_pseudo_header_in_trailer);
+ }
- if (_response.StatusCode == HttpStatusCode.Continue && _expect100ContinueWaiter != null)
- {
- if (NetEventSource.Log.IsEnabled()) Trace("Received 100-Continue status.");
- _expect100ContinueWaiter.TrySetResult(true);
- }
- }
- else
- {
- _responseProtocolState = ResponseProtocolState.ExpectingHeaders;
+ Debug.Assert(_response != null);
+ _response.StatusCode = (HttpStatusCode)statusCode;
- // If we are waiting for a 100-continue response, signal the waiter now.
- if (_expect100ContinueWaiter != null)
- {
- // If the final status code is >= 300, skip sending the body.
- bool shouldSendBody = (statusValue < 300);
+ if (statusCode < 200)
+ {
+ // We do not process headers from 1xx responses.
+ _responseProtocolState = ResponseProtocolState.ExpectingIgnoredHeaders;
- if (NetEventSource.Log.IsEnabled()) Trace($"Expecting 100 Continue but received final status {statusValue}.");
- _expect100ContinueWaiter.TrySetResult(shouldSendBody);
- }
- }
- }
- else
+ if (_response.StatusCode == HttpStatusCode.Continue && _expect100ContinueWaiter != null)
{
- if (NetEventSource.Log.IsEnabled()) Trace($"Invalid response pseudo-header '{Encoding.ASCII.GetString(name)}'.");
- throw new HttpRequestException(SR.net_http_invalid_response);
+ if (NetEventSource.Log.IsEnabled()) Trace("Received 100-Continue status.");
+ _expect100ContinueWaiter.TrySetResult(true);
}
}
else
{
- if (_responseProtocolState == ResponseProtocolState.ExpectingIgnoredHeaders)
- {
- // for 1xx response we ignore all headers.
- return;
- }
+ _responseProtocolState = ResponseProtocolState.ExpectingHeaders;
- if (_responseProtocolState != ResponseProtocolState.ExpectingHeaders && _responseProtocolState != ResponseProtocolState.ExpectingTrailingHeaders)
+ // If we are waiting for a 100-continue response, signal the waiter now.
+ if (_expect100ContinueWaiter != null)
{
- if (NetEventSource.Log.IsEnabled()) Trace("Received header before status.");
- throw new HttpRequestException(SR.net_http_invalid_response);
- }
+ // If the final status code is >= 300, skip sending the body.
+ bool shouldSendBody = (statusCode < 300);
- if (!HeaderDescriptor.TryGet(name, out HeaderDescriptor descriptor))
- {
- // Invalid header name
- throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_header_name, Encoding.ASCII.GetString(name)));
+ if (NetEventSource.Log.IsEnabled()) Trace($"Expecting 100 Continue but received final status {statusCode}.");
+ _expect100ContinueWaiter.TrySetResult(shouldSendBody);
}
+ }
+ }
+ }
- Encoding? valueEncoding = _connection._pool.Settings._responseHeaderEncodingSelector?.Invoke(descriptor.Name, _request);
+ private void OnHeader(HeaderDescriptor descriptor, ReadOnlySpan value)
+ {
+ if (NetEventSource.Log.IsEnabled()) Trace($"{descriptor.Name}: {Encoding.ASCII.GetString(value)}");
- // Note we ignore the return value from TryAddWithoutValidation;
- // if the header can't be added, we silently drop it.
- if (_responseProtocolState == ResponseProtocolState.ExpectingTrailingHeaders)
- {
- Debug.Assert(_trailers != null);
- string headerValue = descriptor.GetHeaderValue(value, valueEncoding);
- _trailers.TryAddWithoutValidation((descriptor.HeaderType & HttpHeaderType.Request) == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor, headerValue);
- }
- else if ((descriptor.HeaderType & HttpHeaderType.Content) == HttpHeaderType.Content)
- {
- Debug.Assert(_response != null && _response.Content != null);
- string headerValue = descriptor.GetHeaderValue(value, valueEncoding);
- _response.Content.Headers.TryAddWithoutValidation(descriptor, headerValue);
- }
- else
- {
- Debug.Assert(_response != null);
- string headerValue = _connection.GetResponseHeaderValueWithCaching(descriptor, value, valueEncoding);
- _response.Headers.TryAddWithoutValidation((descriptor.HeaderType & HttpHeaderType.Request) == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor, headerValue);
- }
+ AdjustHeaderBudget(descriptor.Name.Length + value.Length);
+
+ Debug.Assert(!Monitor.IsEntered(SyncObject));
+ lock (SyncObject)
+ {
+ if (_responseProtocolState == ResponseProtocolState.Aborted)
+ {
+ // We could have aborted while processing the header block.
+ return;
+ }
+
+ if (_responseProtocolState == ResponseProtocolState.ExpectingIgnoredHeaders)
+ {
+ // for 1xx response we ignore all headers.
+ return;
+ }
+
+ if (_responseProtocolState != ResponseProtocolState.ExpectingHeaders && _responseProtocolState != ResponseProtocolState.ExpectingTrailingHeaders)
+ {
+ if (NetEventSource.Log.IsEnabled()) Trace("Received header before status.");
+ throw new HttpRequestException(SR.net_http_invalid_response);
+ }
+
+ Encoding? valueEncoding = _connection._pool.Settings._responseHeaderEncodingSelector?.Invoke(descriptor.Name, _request);
+
+ // Note we ignore the return value from TryAddWithoutValidation;
+ // if the header can't be added, we silently drop it.
+ if (_responseProtocolState == ResponseProtocolState.ExpectingTrailingHeaders)
+ {
+ Debug.Assert(_trailers != null);
+ string headerValue = descriptor.GetHeaderValue(value, valueEncoding);
+ _trailers.TryAddWithoutValidation((descriptor.HeaderType & HttpHeaderType.Request) == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor, headerValue);
+ }
+ else if ((descriptor.HeaderType & HttpHeaderType.Content) == HttpHeaderType.Content)
+ {
+ Debug.Assert(_response != null && _response.Content != null);
+ string headerValue = descriptor.GetHeaderValue(value, valueEncoding);
+ _response.Content.Headers.TryAddWithoutValidation(descriptor, headerValue);
+ }
+ else
+ {
+ Debug.Assert(_response != null);
+ string headerValue = _connection.GetResponseHeaderValueWithCaching(descriptor, value, valueEncoding);
+ _response.Headers.TryAddWithoutValidation((descriptor.HeaderType & HttpHeaderType.Request) == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor, headerValue);
+ }
+ }
+ }
+
+ public void OnHeader(ReadOnlySpan name, ReadOnlySpan value)
+ {
+ Debug.Assert(name.Length > 0);
+
+ if (name[0] == (byte)':')
+ {
+ // Pseudo-header
+ if (name.SequenceEqual(StatusHeaderName))
+ {
+ int statusCode = ParseStatusCode(value);
+
+ OnStatus(statusCode);
+ }
+ else
+ {
+ if (NetEventSource.Log.IsEnabled()) Trace($"Invalid response pseudo-header '{Encoding.ASCII.GetString(name)}'.");
+ throw new HttpRequestException(SR.net_http_invalid_response);
}
}
+ else
+ {
+ // Regular header
+ if (!HeaderDescriptor.TryGet(name, out HeaderDescriptor descriptor))
+ {
+ // Invalid header name
+ throw new HttpRequestException(SR.Format(SR.net_http_invalid_response_header_name, Encoding.ASCII.GetString(name)));
+ }
+
+ OnHeader(descriptor, value);
+ }
}
public void OnHeadersStart()