Skip to content

Commit 4ba9084

Browse files
committed
Add LF request line terminator option
1 parent 3418843 commit 4ba9084

File tree

9 files changed

+432
-155
lines changed

9 files changed

+432
-155
lines changed

src/Servers/Kestrel/Core/src/CoreStrings.resx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,4 +668,4 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
668668
<data name="Http3ControlStreamErrorInitializingOutbound" xml:space="preserve">
669669
<value>Error initializing outbound control stream.</value>
670670
</data>
671-
</root>
671+
</root>

src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs

Lines changed: 113 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
1616
public class HttpParser<TRequestHandler> : IHttpParser<TRequestHandler> where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler
1717
{
1818
private readonly bool _showErrorDetails;
19+
private readonly bool _enableLineFeedTerminator;
1920

20-
public HttpParser() : this(showErrorDetails: true)
21+
public HttpParser() : this(showErrorDetails: true, enableLineFeedTerminator: false)
2122
{
2223
}
2324

24-
public HttpParser(bool showErrorDetails)
25+
public HttpParser(bool showErrorDetails) : this(showErrorDetails, enableLineFeedTerminator: false)
26+
{
27+
}
28+
29+
internal HttpParser(bool showErrorDetails, bool enableLineFeedTerminator)
2530
{
2631
_showErrorDetails = showErrorDetails;
32+
_enableLineFeedTerminator = enableLineFeedTerminator;
2733
}
2834

2935
// 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
126132

127133
// Version + CR is 9 bytes which should take us to .Length
128134
// 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)
130136
{
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+
}
132144
}
133145

134146
// Version
@@ -152,188 +164,162 @@ public bool ParseHeaders(TRequestHandler handler, ref SequenceReader<byte> reade
152164
while (!reader.End)
153165
{
154166
var span = reader.UnreadSpan;
167+
168+
// Size of header in the current span, if known
169+
var length = -1;
170+
155171
while (span.Length > 0)
156172
{
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;
160175

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)
168178
{
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);
171180
}
172181

173-
if (ch1 == ByteCR)
182+
if (length != -1)
174183
{
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);
184186

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)
190189
{
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;
194191
}
195-
196-
Debug.Assert(readAhead == 0 || readAhead == 2);
197-
// Headers don't end in CRLF line.
198-
199-
KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestHeadersNoCRLF);
200192
}
201193

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)
206196
{
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+
}
223201

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;
241208

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]);
251214
}
215+
216+
// Read the header successfully, skip the reader forward past the headerSpan.
217+
span = span[lineLength..];
218+
reader.Advance(lineLength);
252219
}
253220

254221
// End found in current span
255222
if (length > 0)
256223
{
224+
length = -1;
257225
continue;
258226
}
259227

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);
265230

266-
length = ParseMultiSpanHeader(handler, ref reader);
267-
if (length < 0)
231+
// If there any remaining line?
232+
if (length == -1 && span.Length == 0)
268233
{
269-
// Not there
270234
return false;
271235
}
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;
277236
}
278237
}
279238

280239
return false;
281240
}
282241

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)
284245
{
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+
285289
var currentSlice = reader.UnreadSequence;
286290
var lineEndPosition = currentSlice.PositionOfAny(ByteCR, ByteLF);
287291

288292
if (lineEndPosition == null)
289293
{
290-
// Not there.
291-
return -1;
294+
return ReadOnlySpan<byte>.Empty;
292295
}
293296

294297
SequencePosition lineEnd;
295298
ReadOnlySpan<byte> headerSpan;
296299
if (currentSlice.Slice(reader.Position, lineEndPosition.Value).Length == currentSlice.Length - 1)
297300
{
298301
// 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)
300303

301304
// Advance 1 to include CR/LF in lineEnd
302305
lineEnd = currentSlice.GetPosition(1, lineEndPosition.Value);
303306
headerSpan = currentSlice.Slice(reader.Position, lineEnd).ToSpan();
304-
if (headerSpan[^1] != ByteCR)
307+
308+
if (headerSpan[^1] == ByteLF)
305309
{
306-
RejectRequestHeader(headerSpan);
310+
length = headerSpan.Length - 1;
311+
return headerSpan;
307312
}
308-
return -1;
313+
314+
return ReadOnlySpan<byte>.Empty;
309315
}
310316

311317
// Advance 2 to include CR{LF?} in lineEnd
312318
lineEnd = currentSlice.GetPosition(2, lineEndPosition.Value);
313319
headerSpan = currentSlice.Slice(reader.Position, lineEnd).ToSpan();
314320

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;
337323
}
338324

339325
private static bool TryTakeSingleHeader(TRequestHandler handler, ReadOnlySpan<byte> headerLine)

src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ private static ServiceContext CreateServiceContext(IOptions<KestrelServerOptions
130130
{
131131
Log = trace,
132132
Scheduler = PipeScheduler.ThreadPool,
133-
HttpParser = new HttpParser<Http1ParsingHandler>(trace.IsEnabled(LogLevel.Information)),
133+
HttpParser = new HttpParser<Http1ParsingHandler>(trace.IsEnabled(LogLevel.Information), serverOptions.EnableLineFeedTerminator),
134134
SystemClock = heartbeatManager,
135135
DateHeaderValueManager = dateHeaderValueManager,
136136
ConnectionManager = connectionManager,

src/Servers/Kestrel/Core/src/KestrelServerOptions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,21 @@ internal bool EnableInsecureAbsoluteFormHostOverride
176176
/// </summary>
177177
internal bool IsDevCertLoaded { get; set; }
178178

179+
private bool? _enableLineFeedTerminator;
180+
internal bool EnableLineFeedTerminator
181+
{
182+
get
183+
{
184+
if (!_enableLineFeedTerminator.HasValue)
185+
{
186+
_enableLineFeedTerminator = AppContext.TryGetSwitch("Microsoft.AspNetCore.Server.Kestrel.EnableLineFeedTerminator", out var enabled) && enabled;
187+
}
188+
189+
return _enableLineFeedTerminator.Value;
190+
}
191+
set => _enableLineFeedTerminator = value;
192+
}
193+
179194
/// <summary>
180195
/// Specifies a configuration Action to run for each newly created endpoint. Calling this again will replace
181196
/// the prior action.

0 commit comments

Comments
 (0)