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()