Skip to content

Commit a81d189

Browse files
sebastienrosadityamandaleekagfoidlhalter73
authored
Support LF request line terminator in HttpParser (#43202)
Co-authored-by: Aditya Mandaleeka <[email protected]> Co-authored-by: Günther Foidl <[email protected]> Co-authored-by: Stephen Halter <[email protected]>
1 parent 76b6868 commit a81d189

File tree

8 files changed

+548
-163
lines changed

8 files changed

+548
-163
lines changed

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

Lines changed: 180 additions & 122 deletions
Large diffs are not rendered by default.

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

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

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core;
2525
/// </summary>
2626
public class KestrelServerOptions
2727
{
28+
internal const string DisableHttp1LineFeedTerminatorsSwitchKey = "Microsoft.AspNetCore.Server.Kestrel.DisableHttp1LineFeedTerminators";
29+
2830
// internal to fast-path header decoding when RequestHeaderEncodingSelector is unchanged.
2931
internal static readonly Func<string, Encoding?> DefaultHeaderEncodingSelector = _ => null;
3032

@@ -175,6 +177,24 @@ internal bool EnableWebTransportAndH3Datagrams
175177
set => _enableWebTransportAndH3Datagrams = value;
176178
}
177179

180+
/// <summary>
181+
/// Internal AppContext switch to toggle whether a request line can end with LF only instead of CR/LF.
182+
/// </summary>
183+
private bool? _disableHttp1LineFeedTerminators;
184+
internal bool DisableHttp1LineFeedTerminators
185+
{
186+
get
187+
{
188+
if (!_disableHttp1LineFeedTerminators.HasValue)
189+
{
190+
_disableHttp1LineFeedTerminators = AppContext.TryGetSwitch(DisableHttp1LineFeedTerminatorsSwitchKey, out var disabled) && disabled;
191+
}
192+
193+
return _disableHttp1LineFeedTerminators.Value;
194+
}
195+
set => _disableHttp1LineFeedTerminators = value;
196+
}
197+
178198
/// <summary>
179199
/// Specifies a configuration Action to run for each newly created endpoint. Calling this again will replace
180200
/// the prior action.

src/Servers/Kestrel/Core/test/HttpParserTests.cs

Lines changed: 261 additions & 30 deletions
Large diffs are not rendered by default.

src/Servers/Kestrel/shared/test/HttpParsingData.cs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,15 @@ public static IEnumerable<string> RequestLineInvalidData
205205
"CUSTOM / HTTP/1.1a\n",
206206
"CUSTOM / HTTP/1.1a\r\n",
207207
"CUSTOM / HTTP/1.1ab\r\n",
208+
"CUSTOM / H\n",
209+
"CUSTOM / HT\n",
210+
"CUSTOM / HTT\n",
211+
"CUSTOM / HTTP\n",
212+
"CUSTOM / HTTP/\n",
213+
"CUSTOM / HTTP/1\n",
214+
"CUSTOM / HTTP/1.\n",
208215
"CUSTOM / hello\r\n",
216+
"CUSTOM / hello\n",
209217
"CUSTOM ? HTTP/1.1\r\n",
210218
"CUSTOM /a?b=cHTTP/1.1\r\n",
211219
"CUSTOM /a%20bHTTP/1.1\r\n",
@@ -217,6 +225,21 @@ public static IEnumerable<string> RequestLineInvalidData
217225
}
218226
}
219227

228+
// This list is valid in quirk mode
229+
public static IEnumerable<string> RequestLineInvalidDataLineFeedTerminator
230+
{
231+
get
232+
{
233+
return new[]
234+
{
235+
"GET / HTTP/1.0\n",
236+
"GET / HTTP/1.1\n",
237+
"CUSTOM / HTTP/1.0\n",
238+
"CUSTOM / HTTP/1.1\n",
239+
};
240+
}
241+
}
242+
220243
// Bad HTTP Methods (invalid according to RFC)
221244
public static IEnumerable<string> MethodWithNonTokenCharData
222245
{
@@ -364,13 +387,19 @@ public static IEnumerable<string> TargetWithNullCharData
364387
"8charact",
365388
};
366389

367-
public static IEnumerable<object[]> RequestHeaderInvalidData => new[]
390+
public static IEnumerable<object[]> RequestHeaderInvalidDataLineFeedTerminator => new[]
368391
{
369392
// Missing CR
370393
new[] { "Header: value\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header: value\x0A") },
371394
new[] { "Header-1: value1\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1: value1\x0A") },
372395
new[] { "Header-1: value1\r\nHeader-2: value2\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2: value2\x0A") },
373396

397+
// Empty header name
398+
new[] { ":a\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@":a\x0A") },
399+
};
400+
401+
public static IEnumerable<object[]> RequestHeaderInvalidData => new[]
402+
{
374403
// Line folding
375404
new[] { "Header: line1\r\n line2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" line2\x0D\x0A") },
376405
new[] { "Header: line1\r\n\tline2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x09line2\x0D\x0A") },
@@ -404,7 +433,7 @@ public static IEnumerable<string> TargetWithNullCharData
404433
new[] { "Header-1 value1\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1 value1\x0D\x0A") },
405434
new[] { "Header-1 value1\r\nHeader-2: value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-1 value1\x0D\x0A") },
406435
new[] { "Header-1: value1\r\nHeader-2 value2\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2 value2\x0D\x0A") },
407-
new[] { "\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"\x0A") },
436+
new[] { "HeaderValue1\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"HeaderValue1\x0D\x0A") },
408437

409438
// Starting with whitespace
410439
new[] { " Header: value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@" Header: value\x0D\x0A") },
@@ -435,11 +464,13 @@ public static IEnumerable<string> TargetWithNullCharData
435464

436465
// Headers not ending in CRLF line
437466
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r\r", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF },
438-
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r ", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF },
467+
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r ", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF },
439468
new[] { "Header-1: value1\r\nHeader-2: value2\r\n\r \n", CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF },
469+
new[] { "Header-1: value1\r\nHeader-2\t: value2 \n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@"Header-2\x09: value2 \x0A") },
440470

441471
// Empty header name
442472
new[] { ": value\r\n\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@": value\x0D\x0A") },
473+
new[] { ":a\r\n", CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(@":a\x0D\x0A") },
443474
};
444475

445476
public static TheoryData<string, string> HostHeaderData

src/Servers/Kestrel/shared/test/TestServiceContext.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@ internal class TestServiceContext : ServiceContext
1717
{
1818
public TestServiceContext()
1919
{
20-
Initialize(NullLoggerFactory.Instance, CreateLoggingTrace(NullLoggerFactory.Instance));
20+
Initialize(NullLoggerFactory.Instance, CreateLoggingTrace(NullLoggerFactory.Instance), false);
2121
}
2222

23-
public TestServiceContext(ILoggerFactory loggerFactory)
23+
public TestServiceContext(ILoggerFactory loggerFactory, bool disableHttp1LineFeedTerminators = true)
2424
{
25-
Initialize(loggerFactory, CreateLoggingTrace(loggerFactory));
25+
Initialize(loggerFactory, CreateLoggingTrace(loggerFactory), disableHttp1LineFeedTerminators);
2626
}
2727

28-
public TestServiceContext(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace)
28+
public TestServiceContext(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, bool disableHttp1LineFeedTerminators = true)
2929
{
30-
Initialize(loggerFactory, kestrelTrace);
30+
Initialize(loggerFactory, kestrelTrace, disableHttp1LineFeedTerminators);
3131
}
3232

3333
private static KestrelTrace CreateLoggingTrace(ILoggerFactory loggerFactory)
@@ -49,7 +49,7 @@ public void InitializeHeartbeat()
4949
SystemClock = heartbeatManager;
5050
}
5151

52-
private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace)
52+
private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, bool disableHttp1LineFeedTerminators)
5353
{
5454
LoggerFactory = loggerFactory;
5555
Log = kestrelTrace;
@@ -58,7 +58,7 @@ private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace)
5858
SystemClock = MockSystemClock;
5959
DateHeaderValueManager = new DateHeaderValueManager();
6060
ConnectionManager = new ConnectionManager(Log, ResourceCounter.Unlimited);
61-
HttpParser = new HttpParser<Http1ParsingHandler>(Log.IsEnabled(LogLevel.Information));
61+
HttpParser = new HttpParser<Http1ParsingHandler>(Log.IsEnabled(LogLevel.Information), disableHttp1LineFeedTerminators);
6262
ServerOptions = new KestrelServerOptions
6363
{
6464
AddServerHeader = false

src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,5 +524,7 @@ public static TheoryData<string, string> InvalidRequestLineData
524524

525525
public static IEnumerable<object[]> InvalidRequestHeaderData => HttpParsingData.RequestHeaderInvalidData;
526526

527+
public static IEnumerable<object[]> InvalidRequestHeaderDataLineFeedTerminator => HttpParsingData.RequestHeaderInvalidDataLineFeedTerminator;
528+
527529
public static TheoryData<string, string> InvalidHostHeaderData => HttpParsingData.HostHeaderInvalidData;
528530
}

src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,6 +2284,49 @@ await connection.Receive(
22842284
}
22852285
}
22862286

2287+
[Fact]
2288+
public async Task SingleLineFeedIsSupportedAnywhere()
2289+
{
2290+
// Exercises all combinations of LF and CRLF as line separators.
2291+
// Uses a bit mask for all the possible combinations.
2292+
2293+
var lines = new[]
2294+
{
2295+
$"GET / HTTP/1.1",
2296+
"Content-Length: 0",
2297+
$"Host: localhost",
2298+
"",
2299+
};
2300+
2301+
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory, disableHttp1LineFeedTerminators: false)))
2302+
{
2303+
var mask = Math.Pow(2, lines.Length) - 1;
2304+
2305+
for (var m = 0; m <= mask; m++)
2306+
{
2307+
using (var client = server.CreateConnection())
2308+
{
2309+
var sb = new StringBuilder();
2310+
2311+
for (var pos = 0; pos < lines.Length; pos++)
2312+
{
2313+
sb.Append(lines[pos]);
2314+
var separator = (m & (1 << pos)) != 0 ? "\n" : "\r\n";
2315+
sb.Append(separator);
2316+
}
2317+
2318+
var text = sb.ToString();
2319+
var writer = new StreamWriter(client.Stream, Encoding.GetEncoding("iso-8859-1"));
2320+
await writer.WriteAsync(text).ConfigureAwait(false);
2321+
await writer.FlushAsync().ConfigureAwait(false);
2322+
await client.Stream.FlushAsync().ConfigureAwait(false);
2323+
2324+
await client.Receive("HTTP/1.1 200");
2325+
}
2326+
}
2327+
}
2328+
}
2329+
22872330
public static TheoryData<string, string> HostHeaderData => HttpParsingData.HostHeaderData;
22882331

22892332
private class IntAsClass

0 commit comments

Comments
 (0)