@@ -16,14 +16,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
16
16
public class HttpParser < TRequestHandler > : IHttpParser < TRequestHandler > where TRequestHandler : IHttpHeadersHandler , IHttpRequestLineHandler
17
17
{
18
18
private readonly bool _showErrorDetails ;
19
+ private readonly bool _enableLineFeedTerminator ;
19
20
20
- public HttpParser ( ) : this ( showErrorDetails : true )
21
+ public HttpParser ( ) : this ( showErrorDetails : true , enableLineFeedTerminator : false )
21
22
{
22
23
}
23
24
24
- public HttpParser ( bool showErrorDetails )
25
+ public HttpParser ( bool showErrorDetails ) : this ( showErrorDetails , enableLineFeedTerminator : false )
26
+ {
27
+ }
28
+
29
+ internal HttpParser ( bool showErrorDetails , bool enableLineFeedTerminator )
25
30
{
26
31
_showErrorDetails = showErrorDetails ;
32
+ _enableLineFeedTerminator = enableLineFeedTerminator ;
27
33
}
28
34
29
35
// byte types don't have a data type annotation so we pre-cast them; to avoid in-place casts
@@ -126,9 +132,15 @@ private void ParseRequestLine(TRequestHandler handler, ReadOnlySpan<byte> reques
126
132
127
133
// Version + CR is 9 bytes which should take us to .Length
128
134
// LF should have been dropped prior to method call
129
- if ( ( uint ) offset + 9 != ( uint ) requestLine . Length || requestLine [ offset + sizeof ( ulong ) ] != ByteCR )
135
+ if ( ( uint ) offset + 9 != ( uint ) requestLine . Length || requestLine [ offset + 8 ] != ByteCR )
130
136
{
131
- RejectRequestLine ( requestLine ) ;
137
+ // LF should have been dropped prior to method call
138
+ // If _enableLineFeedTerminator and offset + 8 is .Length,
139
+ // then requestLine is valid since it mean LF was the next char
140
+ if ( ! _enableLineFeedTerminator || ( uint ) offset + 8 != ( uint ) requestLine . Length )
141
+ {
142
+ RejectRequestLine ( requestLine ) ;
143
+ }
132
144
}
133
145
134
146
// Version
@@ -152,188 +164,162 @@ public bool ParseHeaders(TRequestHandler handler, ref SequenceReader<byte> reade
152
164
while ( ! reader . End )
153
165
{
154
166
var span = reader . UnreadSpan ;
167
+
168
+ // Size of header in the current span, if known
169
+ var length = - 1 ;
170
+
155
171
while ( span . Length > 0 )
156
172
{
157
- var ch1 = ( byte ) 0 ;
158
- var ch2 = ( byte ) 0 ;
159
- var readAhead = 0 ;
173
+ // The size of the EOL terminator. Always -1 (no valid EOL), 1 (LF) or 2 (CRLF)
174
+ var eolSize = - 1 ;
160
175
161
- // Fast path, we're still looking at the same span
162
- if ( span . Length >= 2 )
163
- {
164
- ch1 = span [ 0 ] ;
165
- ch2 = span [ 1 ] ;
166
- }
167
- else if ( reader . TryRead ( out ch1 ) ) // Possibly split across spans
176
+ // length can be set when the span is returned by ParseMultiSpanHeader
177
+ if ( length == - 1 )
168
178
{
169
- // Note if we read ahead by 1 or 2 bytes
170
- readAhead = ( reader . TryRead ( out ch2 ) ) ? 2 : 1 ;
179
+ length = span . IndexOfAny ( ByteCR , ByteLF ) ;
171
180
}
172
181
173
- if ( ch1 == ByteCR )
182
+ if ( length != - 1 )
174
183
{
175
- // Check for final CRLF.
176
- if ( ch2 == ByteLF )
177
- {
178
- // If we got 2 bytes from the span directly so skip ahead 2 so that
179
- // the reader's state matches what we expect
180
- if ( readAhead == 0 )
181
- {
182
- reader . Advance ( 2 ) ;
183
- }
184
+ // Validate the EOL terminator
185
+ eolSize = ParseHeaderLineEnd ( span , length ) ;
184
186
185
- // Double CRLF found, so end of headers.
186
- handler . OnHeadersComplete ( endStream : false ) ;
187
- return true ;
188
- }
189
- else if ( readAhead == 1 )
187
+ // Not valid
188
+ if ( eolSize == - 1 )
190
189
{
191
- // Didn't read 2 bytes, reset the reader so we don't consume anything
192
- reader . Rewind ( 1 ) ;
193
- return false ;
190
+ length = - 1 ;
194
191
}
195
-
196
- Debug . Assert ( readAhead == 0 || readAhead == 2 ) ;
197
- // Headers don't end in CRLF line.
198
-
199
- KestrelBadHttpRequestException . Throw ( RequestRejectionReason . InvalidRequestHeadersNoCRLF ) ;
200
192
}
201
193
202
- var length = 0 ;
203
- // We only need to look for the end if we didn't read ahead; otherwise there isn't enough in
204
- // in the span to contain a header.
205
- if ( readAhead == 0 )
194
+ // Empty header (EOL only)?
195
+ if ( length == 0 )
206
196
{
207
- length = span . IndexOfAny ( ByteCR , ByteLF ) ;
208
- // If not found length with be -1; casting to uint will turn it to uint.MaxValue
209
- // which will be larger than any possible span.Length. This also serves to eliminate
210
- // the bounds check for the next lookup of span[length]
211
- if ( ( uint ) length < ( uint ) span . Length )
212
- {
213
- // Early memory read to hide latency
214
- var expectedCR = span [ length ] ;
215
- // Correctly has a CR, move to next
216
- length ++ ;
217
-
218
- if ( expectedCR != ByteCR )
219
- {
220
- // Sequence needs to be CRLF not LF first.
221
- RejectRequestHeader ( span [ ..length ] ) ;
222
- }
197
+ handler . OnHeadersComplete ( endStream : false ) ;
198
+ reader . Advance ( eolSize ) ;
199
+ return true ;
200
+ }
223
201
224
- if ( ( uint ) length < ( uint ) span . Length )
225
- {
226
- // Early memory read to hide latency
227
- var expectedLF = span [ length ] ;
228
- // Correctly has a LF, move to next
229
- length ++ ;
230
-
231
- if ( expectedLF != ByteLF ||
232
- length < 5 ||
233
- // Exclude the CRLF from the headerLine and parse the header name:value pair
234
- ! TryTakeSingleHeader ( handler , span [ ..( length - 2 ) ] ) )
235
- {
236
- // Sequence needs to be CRLF and not contain an inner CR not part of terminator.
237
- // Less than min possible headerSpan of 5 bytes a:b\r\n
238
- // Not parsable as a valid name:value header pair.
239
- RejectRequestHeader ( span [ ..length ] ) ;
240
- }
202
+ // If not found length will be -1; casting to uint will turn it to uint.MaxValue
203
+ // which will be larger than any possible span.Length. This also serves to eliminate
204
+ // the bounds check for the next lookup of span[length]
205
+ if ( ( uint ) length < ( uint ) span . Length )
206
+ {
207
+ var lineLength = length + eolSize ;
241
208
242
- // Read the header successfully, skip the reader forward past the headerSpan.
243
- span = span . Slice ( length ) ;
244
- reader . Advance ( length ) ;
245
- }
246
- else
247
- {
248
- // No enough data, set length to 0.
249
- length = 0 ;
250
- }
209
+ if ( length != 0 && ! TryTakeSingleHeader ( handler , span [ ..length ] ) )
210
+ {
211
+ // Sequence needs to be CRLF and not contain an inner CR not part of terminator.
212
+ // Not parsable as a valid name:value header pair.
213
+ RejectRequestHeader ( span [ ..lineLength ] ) ;
251
214
}
215
+
216
+ // Read the header successfully, skip the reader forward past the headerSpan.
217
+ span = span [ lineLength ..] ;
218
+ reader . Advance ( lineLength ) ;
252
219
}
253
220
254
221
// End found in current span
255
222
if ( length > 0 )
256
223
{
224
+ length = - 1 ;
257
225
continue ;
258
226
}
259
227
260
- // We moved the reader to look ahead 2 bytes so rewind the reader
261
- if ( readAhead > 0 )
262
- {
263
- reader . Rewind ( readAhead ) ;
264
- }
228
+ // Load next header line to parse as a span
229
+ span = ParseMultiSpanHeader ( ref reader , out length ) ;
265
230
266
- length = ParseMultiSpanHeader ( handler , ref reader ) ;
267
- if ( length < 0 )
231
+ // If there any remaining line?
232
+ if ( length == - 1 && span . Length == 0 )
268
233
{
269
- // Not there
270
234
return false ;
271
235
}
272
-
273
- reader . Advance ( length ) ;
274
- // As we crossed spans set the current span to default
275
- // so we move to the next span on the next iteration
276
- span = default ;
277
236
}
278
237
}
279
238
280
239
return false ;
281
240
}
282
241
283
- private int ParseMultiSpanHeader ( TRequestHandler handler , ref SequenceReader < byte > reader )
242
+ // Returns the length of the line terminator (CRLF = 2, LF = 1)
243
+ // If no valid EOL is detected then -1
244
+ private int ParseHeaderLineEnd ( ReadOnlySpan < byte > headerSpan , int headerLineLength )
284
245
{
246
+ // This method needs to be called with a positive value representing the index of either CR or LF
247
+ Debug . Assert ( headerLineLength >= 0 ) ;
248
+
249
+ if ( headerSpan [ headerLineLength ] == ByteCR )
250
+ {
251
+ // No more chars after CR? Don't consume an incomplete header
252
+ if ( headerSpan . Length == headerLineLength + 1 )
253
+ {
254
+ return - 1 ;
255
+ }
256
+
257
+ // CR must be followed by LF in all cases
258
+ if ( headerSpan [ headerLineLength + 1 ] != ByteLF )
259
+ {
260
+ if ( headerLineLength == 0 )
261
+ {
262
+ KestrelBadHttpRequestException . Throw ( RequestRejectionReason . InvalidRequestHeadersNoCRLF ) ;
263
+ }
264
+ else
265
+ {
266
+ RejectRequestHeader ( headerSpan [ ..( headerLineLength + 2 ) ] ) ;
267
+ }
268
+ }
269
+
270
+ return 2 ;
271
+ }
272
+
273
+ if ( _enableLineFeedTerminator )
274
+ {
275
+ return 1 ;
276
+ }
277
+
278
+ // LF but not allowed
279
+ RejectRequestHeader ( headerSpan [ ..( headerLineLength + 1 ) ] ) ;
280
+
281
+ return 0 ;
282
+ }
283
+
284
+ // Returns a span from the remaining sequence until the next valid EOL
285
+ private ReadOnlySpan < byte > ParseMultiSpanHeader ( ref SequenceReader < byte > reader , out int length )
286
+ {
287
+ length = - 1 ;
288
+
285
289
var currentSlice = reader . UnreadSequence ;
286
290
var lineEndPosition = currentSlice . PositionOfAny ( ByteCR , ByteLF ) ;
287
291
288
292
if ( lineEndPosition == null )
289
293
{
290
- // Not there.
291
- return - 1 ;
294
+ return ReadOnlySpan < byte > . Empty ;
292
295
}
293
296
294
297
SequencePosition lineEnd ;
295
298
ReadOnlySpan < byte > headerSpan ;
296
299
if ( currentSlice . Slice ( reader . Position , lineEndPosition . Value ) . Length == currentSlice . Length - 1 )
297
300
{
298
301
// No enough data, so CRLF can't currently be there.
299
- // However, we need to check the found char is CR and not LF
302
+ // However, we need to check the found char is CR and not LF (unless quirk mode)
300
303
301
304
// Advance 1 to include CR/LF in lineEnd
302
305
lineEnd = currentSlice . GetPosition ( 1 , lineEndPosition . Value ) ;
303
306
headerSpan = currentSlice . Slice ( reader . Position , lineEnd ) . ToSpan ( ) ;
304
- if ( headerSpan [ ^ 1 ] != ByteCR )
307
+
308
+ if ( headerSpan [ ^ 1 ] == ByteLF )
305
309
{
306
- RejectRequestHeader ( headerSpan ) ;
310
+ length = headerSpan . Length - 1 ;
311
+ return headerSpan ;
307
312
}
308
- return - 1 ;
313
+
314
+ return ReadOnlySpan < byte > . Empty ;
309
315
}
310
316
311
317
// Advance 2 to include CR{LF?} in lineEnd
312
318
lineEnd = currentSlice . GetPosition ( 2 , lineEndPosition . Value ) ;
313
319
headerSpan = currentSlice . Slice ( reader . Position , lineEnd ) . ToSpan ( ) ;
314
320
315
- if ( headerSpan . Length < 5 )
316
- {
317
- // Less than min possible headerSpan is 5 bytes a:b\r\n
318
- RejectRequestHeader ( headerSpan ) ;
319
- }
320
-
321
- if ( headerSpan [ ^ 2 ] != ByteCR )
322
- {
323
- // Sequence needs to be CRLF not LF first.
324
- RejectRequestHeader ( headerSpan [ ..^ 1 ] ) ;
325
- }
326
-
327
- if ( headerSpan [ ^ 1 ] != ByteLF ||
328
- // Exclude the CRLF from the headerLine and parse the header name:value pair
329
- ! TryTakeSingleHeader ( handler , headerSpan [ ..^ 2 ] ) )
330
- {
331
- // Sequence needs to be CRLF and not contain an inner CR not part of terminator.
332
- // Not parsable as a valid name:value header pair.
333
- RejectRequestHeader ( headerSpan ) ;
334
- }
335
-
336
- return headerSpan . Length ;
321
+ length = headerSpan . Length - 2 ;
322
+ return headerSpan ;
337
323
}
338
324
339
325
private static bool TryTakeSingleHeader ( TRequestHandler handler , ReadOnlySpan < byte > headerLine )
0 commit comments