@@ -66,7 +66,7 @@ internal partial class HttpConnection : IDisposable
6666
6767 public HttpConnection (
6868 HttpConnectionPool pool ,
69- Stream stream ,
69+ Stream stream ,
7070 TransportContext transportContext )
7171 {
7272 Debug . Assert ( pool != null ) ;
@@ -413,7 +413,7 @@ public async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request,
413413
414414 // Parse the response status line.
415415 var response = new HttpResponseMessage ( ) { RequestMessage = request , Content = new HttpConnectionResponseContent ( ) } ;
416- ParseStatusLine ( await ReadNextLineAsync ( ) . ConfigureAwait ( false ) , response ) ;
416+ ParseStatusLine ( await ReadNextResponseHeaderLineAsync ( ) . ConfigureAwait ( false ) , response ) ;
417417
418418 // If we sent an Expect: 100-continue header, handle the response accordingly.
419419 if ( allowExpect100ToContinue != null )
@@ -441,12 +441,12 @@ public async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request,
441441 if ( response . StatusCode == HttpStatusCode . Continue )
442442 {
443443 // We got our continue header. Read the subsequent empty line and parse the additional status line.
444- if ( ! LineIsEmpty ( await ReadNextLineAsync ( ) . ConfigureAwait ( false ) ) )
444+ if ( ! LineIsEmpty ( await ReadNextResponseHeaderLineAsync ( ) . ConfigureAwait ( false ) ) )
445445 {
446446 ThrowInvalidHttpResponse ( ) ;
447447 }
448448
449- ParseStatusLine ( await ReadNextLineAsync ( ) . ConfigureAwait ( false ) , response ) ;
449+ ParseStatusLine ( await ReadNextResponseHeaderLineAsync ( ) . ConfigureAwait ( false ) , response ) ;
450450 }
451451 }
452452 }
@@ -463,7 +463,7 @@ public async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request,
463463 // Parse the response headers.
464464 while ( true )
465465 {
466- ArraySegment < byte > line = await ReadNextLineAsync ( ) . ConfigureAwait ( false ) ;
466+ ArraySegment < byte > line = await ReadNextResponseHeaderLineAsync ( foldedHeadersAllowed : true ) . ConfigureAwait ( false ) ;
467467 if ( LineIsEmpty ( line ) )
468468 {
469469 break ;
@@ -600,7 +600,7 @@ public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool doRe
600600 }
601601
602602 private HttpContentWriteStream CreateRequestContentStream ( HttpRequestMessage request )
603- {
603+ {
604604 bool requestTransferEncodingChunked = request . HasHeaders && request . Headers . TransferEncodingChunked == true ;
605605 HttpContentWriteStream requestContentStream = requestTransferEncodingChunked ? ( HttpContentWriteStream )
606606 new ChunkedEncodingWriteStream ( this ) :
@@ -1060,16 +1060,13 @@ private bool TryReadNextLine(out ReadOnlySpan<byte> line)
10601060 int bytesConsumed = length + 1 ;
10611061 _readOffset += bytesConsumed ;
10621062 _allowedReadLineBytes -= bytesConsumed ;
1063- if ( _allowedReadLineBytes < 0 )
1064- {
1065- ThrowInvalidHttpResponse ( ) ;
1066- }
1063+ ThrowIfExceededAllowedReadLineBytes ( ) ;
10671064
10681065 line = buffer . Slice ( 0 , length > 0 && buffer [ length - 1 ] == '\r ' ? length - 1 : length ) ;
10691066 return true ;
10701067 }
10711068
1072- private async ValueTask < ArraySegment < byte > > ReadNextLineAsync ( )
1069+ private async ValueTask < ArraySegment < byte > > ReadNextResponseHeaderLineAsync ( bool foldedHeadersAllowed = false )
10731070 {
10741071 int previouslyScannedBytes = 0 ;
10751072 while ( true )
@@ -1080,34 +1077,94 @@ private async ValueTask<ArraySegment<byte>> ReadNextLineAsync()
10801077 {
10811078 int startIndex = _readOffset ;
10821079 int length = lfIndex - startIndex ;
1083- if ( length > 0 && _readBuffer [ startIndex + length - 1 ] == '\r ' )
1080+ if ( lfIndex > 0 && _readBuffer [ lfIndex - 1 ] == '\r ' )
10841081 {
10851082 length -- ;
10861083 }
10871084
1088- // Advance read position past the LF
1089- _allowedReadLineBytes -= lfIndex + 1 - scanOffset ;
1090- if ( _allowedReadLineBytes < 0 )
1085+ // If this isn't the ending header, we need to account for the possibility
1086+ // of folded headers, which per RFC2616 are headers split across multiple
1087+ // lines, where the continuation line begins with a space or horizontal tab.
1088+ // The feature was deprecated in RFC 7230 3.2.4, but some servers still use it.
1089+ if ( foldedHeadersAllowed && length > 0 )
10911090 {
1092- ThrowInvalidHttpResponse ( ) ;
1091+ // If the newline is the last character we've buffered, we need at least
1092+ // one more character in order to see whether it's space/tab, in which
1093+ // case it's a folded header.
1094+ if ( lfIndex + 1 == _readLength )
1095+ {
1096+ // The LF is at the end of the buffer, so we need to read more
1097+ // to determine whether there's a continuation. We'll read
1098+ // and then loop back around again, but to avoid needing to
1099+ // rescan the whole header, reposition to one character before
1100+ // the newline so that we'll find it quickly.
1101+ int backPos = _readBuffer [ lfIndex - 1 ] == '\r ' ? lfIndex - 2 : lfIndex - 1 ;
1102+ Debug . Assert ( backPos >= 0 ) ;
1103+ previouslyScannedBytes = backPos - _readOffset ;
1104+ _allowedReadLineBytes -= backPos - scanOffset ;
1105+ ThrowIfExceededAllowedReadLineBytes ( ) ;
1106+ await FillAsync ( ) . ConfigureAwait ( false ) ;
1107+ continue ;
1108+ }
1109+
1110+ // We have at least one more character we can look at.
1111+ Debug . Assert ( lfIndex + 1 < _readLength ) ;
1112+ char nextChar = ( char ) _readBuffer [ lfIndex + 1 ] ;
1113+ if ( nextChar == ' ' || nextChar == '\t ' )
1114+ {
1115+ // The next header is a continuation.
1116+
1117+ // Folded headers are only allowed within header field values, not within header field names,
1118+ // so if we haven't seen a colon, this is invalid.
1119+ if ( Array . IndexOf ( _readBuffer , ( byte ) ':' , _readOffset , lfIndex - _readOffset ) == - 1 )
1120+ {
1121+ ThrowInvalidHttpResponse ( ) ;
1122+ }
1123+
1124+ // When we return the line, we need the interim newlines filtered out. According
1125+ // to RFC 7230 3.2.4, a valid approach to dealing with them is to "replace each
1126+ // received obs-fold with one or more SP octets prior to interpreting the field
1127+ // value or forwarding the message downstream", so that's what we do.
1128+ _readBuffer [ lfIndex ] = ( byte ) ' ' ;
1129+ if ( _readBuffer [ lfIndex - 1 ] == '\r ' )
1130+ {
1131+ _readBuffer [ lfIndex - 1 ] = ( byte ) ' ' ;
1132+ }
1133+
1134+ // Update how much we've read, and simply go back to search for the next newline.
1135+ previouslyScannedBytes = ( lfIndex + 1 - _readOffset ) ;
1136+ _allowedReadLineBytes -= ( lfIndex + 1 - scanOffset ) ;
1137+ ThrowIfExceededAllowedReadLineBytes ( ) ;
1138+ continue ;
1139+ }
1140+
1141+ // Not at the end of a header with a continuation.
10931142 }
1143+
1144+ // Advance read position past the LF
1145+ _allowedReadLineBytes -= lfIndex + 1 - scanOffset ;
1146+ ThrowIfExceededAllowedReadLineBytes ( ) ;
10941147 _readOffset = lfIndex + 1 ;
10951148
10961149 return new ArraySegment < byte > ( _readBuffer , startIndex , length ) ;
10971150 }
10981151
1099- // Couldn't find LF. Read more.
1100- // Note this may cause _readOffset to change.
1152+ // Couldn't find LF. Read more. Note this may cause _readOffset to change.
11011153 previouslyScannedBytes = _readLength - _readOffset ;
11021154 _allowedReadLineBytes -= _readLength - scanOffset ;
1103- if ( _allowedReadLineBytes < 0 )
1104- {
1105- ThrowInvalidHttpResponse ( ) ;
1106- }
1155+ ThrowIfExceededAllowedReadLineBytes ( ) ;
11071156 await FillAsync ( ) . ConfigureAwait ( false ) ;
11081157 }
11091158 }
11101159
1160+ private void ThrowIfExceededAllowedReadLineBytes ( )
1161+ {
1162+ if ( _allowedReadLineBytes < 0 )
1163+ {
1164+ ThrowInvalidHttpResponse ( ) ;
1165+ }
1166+ }
1167+
11111168 // Throws IOException on EOF. This is only called when we expect more data.
11121169 private async Task FillAsync ( )
11131170 {
0 commit comments