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
5+ using System . Buffers . Text ;
56using System . Diagnostics ;
67using System . IO ;
78using System . Threading ;
@@ -13,81 +14,83 @@ internal sealed partial class HttpConnection
1314 {
1415 private sealed class ChunkedEncodingReadStream : HttpContentReadStream
1516 {
17+ /// <summary>How long a chunk indicator is allowed to be.</summary>
18+ /// <remarks>
19+ /// While most chunks indicators will contain no more than ulong.MaxValue.ToString("X").Length characters,
20+ /// "chunk extensions" are allowed. We place a limit on how long a line can be to avoid OOM issues if an
21+ /// infinite chunk length is sent. This value is arbitrary and can be changed as needed.
22+ /// </remarks>
23+ private const int MaxChunkBytesAllowed = 16 * 1024 ;
24+ /// <summary>How long a trailing header can be. This value is arbitrary and can be changed as needed.</summary>
25+ private const int MaxTrailingHeaderLength = 16 * 1024 ;
26+ /// <summary>The number of bytes remaining in the chunk.</summary>
1627 private ulong _chunkBytesRemaining ;
1728
18- public ChunkedEncodingReadStream ( HttpConnection connection )
19- : base ( connection )
29+ public ChunkedEncodingReadStream ( HttpConnection connection ) : base ( connection )
2030 {
21- _chunkBytesRemaining = 0 ;
2231 }
2332
24- private async Task < bool > TryGetNextChunk ( CancellationToken cancellationToken )
33+ private async Task < bool > TryGetNextChunkAsync ( CancellationToken cancellationToken )
2534 {
2635 Debug . Assert ( _chunkBytesRemaining == 0 ) ;
2736
28- // Start of chunk, read chunk size.
29- ulong chunkSize = ParseHexSize ( await _connection . ReadNextLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ) ;
30- _chunkBytesRemaining = chunkSize ;
31-
32- if ( chunkSize > 0 )
33- {
34- return true ;
35- }
36-
37- // We received a chunk size of 0, which indicates end of response body.
38- // Read and discard any trailing headers, until we see an empty line.
39- while ( ! LineIsEmpty ( await _connection . ReadNextLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ) )
40- ;
37+ // Read the start of the chunk line.
38+ _connection . _allowedReadLineBytes = MaxChunkBytesAllowed ;
39+ ArraySegment < byte > line = await _connection . ReadNextLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
4140
42- _connection . ReturnConnectionToPool ( ) ;
43- _connection = null ;
44- return false ;
45- }
46-
47- private ulong ParseHexSize ( ArraySegment < byte > line )
48- {
49- if ( line . Count == 0 )
41+ // Parse the hex value.
42+ if ( ! Utf8Parser . TryParse ( line . AsReadOnlySpan ( ) , out ulong chunkSize , out int bytesConsumed , 'X' ) )
5043 {
5144 throw new IOException ( SR . net_http_invalid_response ) ;
5245 }
53-
54- ulong size = 0 ;
55- try
46+ else if ( bytesConsumed != line . Count )
5647 {
57- for ( int i = 0 ; i < line . Count ; i ++ )
48+ // There was data after the chunk size, presumably a "chunk extension".
49+ // Allow tabs and spaces and then stop validating once we get to an extension.
50+ int offset = line . Offset + bytesConsumed , end = line . Count - bytesConsumed + line . Offset ;
51+ for ( int i = offset ; i < end ; i ++ )
5852 {
59- char c = ( char ) line [ i ] ;
60- if ( ( uint ) ( c - '0' ) <= '9' - '0' )
61- {
62- size = checked ( size * 16 + ( ( ulong ) c - '0' ) ) ;
63- }
64- else if ( ( uint ) ( c - 'a' ) <= ( 'f' - 'a' ) )
53+ char c = ( char ) line . Array [ i ] ;
54+ if ( c == ';' )
6555 {
66- size = checked ( size * 16 + ( ( ulong ) c - 'a' + 10 ) ) ;
56+ break ;
6757 }
68- else if ( ( uint ) ( c - 'A' ) <= ( 'F' - 'A' ) )
69- {
70- size = checked ( size * 16 + ( ( ulong ) c - 'A' + 10 ) ) ;
71- }
72- else
58+ else if ( c != ' ' && c != '\t ' ) // not called out in the RFC, but WinHTTP allows it
7359 {
7460 throw new IOException ( SR . net_http_invalid_response ) ;
7561 }
7662 }
7763 }
78- catch ( OverflowException e )
64+
65+ _chunkBytesRemaining = chunkSize ;
66+ if ( chunkSize > 0 )
7967 {
80- throw new IOException ( SR . net_http_invalid_response , e ) ;
68+ return true ;
8169 }
82- return size ;
70+
71+ // We received a chunk size of 0, which indicates end of response body.
72+ // Read and discard any trailing headers, until we see an empty line.
73+ while ( true )
74+ {
75+ _connection . _allowedReadLineBytes = MaxTrailingHeaderLength ;
76+ if ( LineIsEmpty ( await _connection . ReadNextLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ) )
77+ {
78+ break ;
79+ }
80+ }
81+
82+ _connection . ReturnConnectionToPool ( ) ;
83+ _connection = null ;
84+ return false ;
8385 }
8486
85- private async Task ConsumeChunkBytes ( ulong bytesConsumed , CancellationToken cancellationToken )
87+ private async Task ConsumeChunkBytesAsync ( ulong bytesConsumed , CancellationToken cancellationToken )
8688 {
8789 Debug . Assert ( bytesConsumed <= _chunkBytesRemaining ) ;
8890 _chunkBytesRemaining -= bytesConsumed ;
8991 if ( _chunkBytesRemaining == 0 )
9092 {
93+ _connection . _allowedReadLineBytes = 2 ; // \r\n
9194 if ( ! LineIsEmpty ( await _connection . ReadNextLineAsync ( cancellationToken ) . ConfigureAwait ( false ) ) )
9295 {
9396 ThrowInvalidHttpResponse ( ) ;
@@ -111,7 +114,7 @@ public override async ValueTask<int> ReadAsync(Memory<byte> destination, Cancell
111114
112115 if ( _chunkBytesRemaining == 0 )
113116 {
114- if ( ! await TryGetNextChunk ( cancellationToken ) . ConfigureAwait ( false ) )
117+ if ( ! await TryGetNextChunkAsync ( cancellationToken ) . ConfigureAwait ( false ) )
115118 {
116119 // End of response body
117120 return 0 ;
@@ -131,7 +134,7 @@ public override async ValueTask<int> ReadAsync(Memory<byte> destination, Cancell
131134 throw new IOException ( SR . net_http_invalid_response ) ;
132135 }
133136
134- await ConsumeChunkBytes ( ( ulong ) bytesRead , cancellationToken ) . ConfigureAwait ( false ) ;
137+ await ConsumeChunkBytesAsync ( ( ulong ) bytesRead , cancellationToken ) . ConfigureAwait ( false ) ;
135138
136139 return bytesRead ;
137140 }
@@ -152,13 +155,13 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance
152155 if ( _chunkBytesRemaining > 0 )
153156 {
154157 await _connection . CopyToAsync ( destination , _chunkBytesRemaining , cancellationToken ) . ConfigureAwait ( false ) ;
155- await ConsumeChunkBytes ( _chunkBytesRemaining , cancellationToken ) . ConfigureAwait ( false ) ;
158+ await ConsumeChunkBytesAsync ( _chunkBytesRemaining , cancellationToken ) . ConfigureAwait ( false ) ;
156159 }
157160
158- while ( await TryGetNextChunk ( cancellationToken ) . ConfigureAwait ( false ) )
161+ while ( await TryGetNextChunkAsync ( cancellationToken ) . ConfigureAwait ( false ) )
159162 {
160163 await _connection . CopyToAsync ( destination , _chunkBytesRemaining , cancellationToken ) . ConfigureAwait ( false ) ;
161- await ConsumeChunkBytes ( _chunkBytesRemaining , cancellationToken ) . ConfigureAwait ( false ) ;
164+ await ConsumeChunkBytesAsync ( _chunkBytesRemaining , cancellationToken ) . ConfigureAwait ( false ) ;
162165 }
163166 }
164167 }
0 commit comments