From 568aaff9c4eb661c83d8215d5f0e44b04956fd5e Mon Sep 17 00:00:00 2001 From: Cesar Blum Silveira Date: Tue, 28 Feb 2017 09:35:10 -0800 Subject: [PATCH] Improve HTTP parsing tests (#1393). - Add several more test cases - Share data between functional and unit tests --- KestrelHttpServer.sln | 63 ++-- .../Internal/Http/Frame.cs | 8 +- .../BadHttpRequestTests.cs | 136 ++++++++ .../BadHttpRequestTests.cs | 231 -------------- .../FrameTests.cs | 290 ++++++----------- test/shared/HttpParsingData.cs | 301 ++++++++++++++++++ 6 files changed, 579 insertions(+), 450 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/BadHttpRequestTests.cs delete mode 100644 test/Microsoft.AspNetCore.Server.KestrelTests/BadHttpRequestTests.cs create mode 100644 test/shared/HttpParsingData.cs diff --git a/KestrelHttpServer.sln b/KestrelHttpServer.sln index 24da20c0e0..10d207a841 100644 --- a/KestrelHttpServer.sln +++ b/KestrelHttpServer.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26206.0 +VisualStudioVersion = 15.0.26223.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7972A5D6-3385-4127-9277-428506DD44FF}" ProjectSection(SolutionItems) = preProject @@ -21,6 +21,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{0EF2AC ProjectSection(SolutionItems) = preProject test\shared\DummyApplication.cs = test\shared\DummyApplication.cs test\shared\HttpClientSlim.cs = test\shared\HttpClientSlim.cs + test\shared\HttpParsingData.cs = test\shared\HttpParsingData.cs test\shared\KestrelTestLoggerFactory.cs = test\shared\KestrelTestLoggerFactory.cs test\shared\LifetimeNotImplemented.cs = test\shared\LifetimeNotImplemented.cs test\shared\MockConnection.cs = test\shared\MockConnection.cs @@ -83,38 +84,38 @@ Global {F510611A-3BEE-4B88-A613-5F4A74ED82A1}.Release|x86.Build.0 = Release|Any CPU {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Debug|x64.ActiveCfg = Debug|x64 - {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Debug|x64.Build.0 = Debug|x64 - {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Debug|x86.ActiveCfg = Debug|x86 - {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Debug|x86.Build.0 = Debug|x86 + {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Debug|x64.ActiveCfg = Debug|Any CPU + {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Debug|x64.Build.0 = Debug|Any CPU + {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Debug|x86.ActiveCfg = Debug|Any CPU + {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Debug|x86.Build.0 = Debug|Any CPU {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Release|Any CPU.ActiveCfg = Release|Any CPU {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Release|Any CPU.Build.0 = Release|Any CPU - {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Release|x64.ActiveCfg = Release|x64 - {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Release|x64.Build.0 = Release|x64 + {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Release|x64.ActiveCfg = Release|Any CPU + {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Release|x64.Build.0 = Release|Any CPU {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Release|x86.ActiveCfg = Release|Any CPU {37F3BFB2-6454-49E5-9D7F-581BF755CCFE}.Release|x86.Build.0 = Release|Any CPU {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Debug|x64.ActiveCfg = Debug|x64 - {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Debug|x64.Build.0 = Debug|x64 - {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Debug|x86.ActiveCfg = Debug|x86 - {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Debug|x86.Build.0 = Debug|x86 + {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Debug|x64.ActiveCfg = Debug|Any CPU + {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Debug|x64.Build.0 = Debug|Any CPU + {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Debug|x86.ActiveCfg = Debug|Any CPU + {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Debug|x86.Build.0 = Debug|Any CPU {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Release|Any CPU.Build.0 = Release|Any CPU - {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Release|x64.ActiveCfg = Release|x64 - {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Release|x64.Build.0 = Release|x64 + {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Release|x64.ActiveCfg = Release|Any CPU + {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Release|x64.Build.0 = Release|Any CPU {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Release|x86.ActiveCfg = Release|Any CPU {2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}.Release|x86.Build.0 = Release|Any CPU {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Debug|x64.ActiveCfg = Debug|x64 - {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Debug|x64.Build.0 = Debug|x64 - {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Debug|x86.ActiveCfg = Debug|x86 - {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Debug|x86.Build.0 = Debug|x86 + {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Debug|x64.ActiveCfg = Debug|Any CPU + {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Debug|x64.Build.0 = Debug|Any CPU + {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Debug|x86.ActiveCfg = Debug|Any CPU + {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Debug|x86.Build.0 = Debug|Any CPU {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Release|Any CPU.ActiveCfg = Release|Any CPU {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Release|Any CPU.Build.0 = Release|Any CPU - {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Release|x64.ActiveCfg = Release|x64 - {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Release|x64.Build.0 = Release|x64 + {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Release|x64.ActiveCfg = Release|Any CPU + {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Release|x64.Build.0 = Release|Any CPU {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Release|x86.ActiveCfg = Release|Any CPU {B35D4D31-E74C-4646-8A11-7A7A40F0021E}.Release|x86.Build.0 = Release|Any CPU {BD2D4D29-1BD9-40D0-BB31-337D5416B63C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -143,26 +144,26 @@ Global {5F64B3C3-0C2E-431A-B820-A81BBFC863DA}.Release|x86.Build.0 = Release|Any CPU {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Debug|x64.ActiveCfg = Debug|x64 - {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Debug|x64.Build.0 = Debug|x64 - {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Debug|x86.ActiveCfg = Debug|x86 - {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Debug|x86.Build.0 = Debug|x86 + {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Debug|x64.ActiveCfg = Debug|Any CPU + {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Debug|x64.Build.0 = Debug|Any CPU + {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Debug|x86.ActiveCfg = Debug|Any CPU + {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Debug|x86.Build.0 = Debug|Any CPU {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Release|Any CPU.ActiveCfg = Release|Any CPU {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Release|Any CPU.Build.0 = Release|Any CPU - {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Release|x64.ActiveCfg = Release|x64 - {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Release|x64.Build.0 = Release|x64 + {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Release|x64.ActiveCfg = Release|Any CPU + {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Release|x64.Build.0 = Release|Any CPU {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Release|x86.ActiveCfg = Release|Any CPU {9559A5F1-080C-4909-B6CF-7E4B3DC55748}.Release|x86.Build.0 = Release|Any CPU {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Debug|x64.ActiveCfg = Debug|x64 - {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Debug|x64.Build.0 = Debug|x64 - {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Debug|x86.ActiveCfg = Debug|x86 - {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Debug|x86.Build.0 = Debug|x86 + {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Debug|x64.ActiveCfg = Debug|Any CPU + {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Debug|x64.Build.0 = Debug|Any CPU + {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Debug|x86.ActiveCfg = Debug|Any CPU + {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Debug|x86.Build.0 = Debug|Any CPU {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Release|Any CPU.Build.0 = Release|Any CPU - {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Release|x64.ActiveCfg = Release|x64 - {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Release|x64.Build.0 = Release|x64 + {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Release|x64.ActiveCfg = Release|Any CPU + {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Release|x64.Build.0 = Release|Any CPU {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Release|x86.ActiveCfg = Release|Any CPU {EBFE9719-A44B-4978-A71F-D5C254E7F35A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index 046c29c0e6..f3d6787b55 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -1129,6 +1129,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http } else if (ch == BytePercentage) { + if (pathStart == -1) + { + // Empty path is illegal + RejectRequestLine(start, end); + } + needDecode = true; } @@ -1268,7 +1274,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { const int MaxRequestLineError = 32; RejectRequest(RequestRejectionReason.InvalidRequestLine, - Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxRequestLineError) : string.Empty); + Log.IsEnabled(LogLevel.Information) ? start.GetAsciiStringEscaped(end, MaxRequestLineError) : string.Empty); } private static bool IsValidTokenChar(char c) diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/BadHttpRequestTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/BadHttpRequestTests.cs new file mode 100644 index 0000000000..0394b7fcd3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/BadHttpRequestTests.cs @@ -0,0 +1,136 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Internal; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class BadHttpRequestTests + { + [Theory] + [MemberData(nameof(InvalidRequestLineData))] + public async Task TestInvalidRequestLines(string request) + { + using (var server = new TestServer(context => TaskCache.CompletedTask)) + { + using (var connection = server.CreateConnection()) + { + await connection.SendAll(request); + await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); + } + } + } + + [Theory] + [MemberData(nameof(UnrecognizedHttpVersionData))] + public async Task TestInvalidRequestLinesWithUnrecognizedVersion(string httpVersion) + { + using (var server = new TestServer(context => TaskCache.CompletedTask)) + { + using (var connection = server.CreateConnection()) + { + await connection.SendAll($"GET / {httpVersion}\r\n"); + await ReceiveBadRequestResponse(connection, "505 HTTP Version Not Supported", server.Context.DateHeaderValue); + } + } + } + + [Theory] + [MemberData(nameof(InvalidRequestHeaderData))] + public async Task TestInvalidHeaders(string rawHeaders) + { + using (var server = new TestServer(context => TaskCache.CompletedTask)) + { + using (var connection = server.CreateConnection()) + { + await connection.SendAll($"GET / HTTP/1.1\r\n{rawHeaders}"); + await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); + } + } + } + + [Fact] + public async Task BadRequestWhenHeaderNameContainsNonASCIICharacters() + { + using (var server = new TestServer(context => { return Task.FromResult(0); })) + { + using (var connection = server.CreateConnection()) + { + await connection.SendAll( + "GET / HTTP/1.1", + "H\u00eb\u00e4d\u00ebr: value", + "", + ""); + await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); + } + } + } + + [Theory] + [InlineData("POST")] + [InlineData("PUT")] + public async Task BadRequestIfMethodRequiresLengthButNoContentLengthOrTransferEncodingInRequest(string method) + { + using (var server = new TestServer(context => { return Task.FromResult(0); })) + { + using (var connection = server.CreateConnection()) + { + await connection.Send($"{method} / HTTP/1.1\r\n\r\n"); + await ReceiveBadRequestResponse(connection, "411 Length Required", server.Context.DateHeaderValue); + } + } + } + + [Theory] + [InlineData("POST")] + [InlineData("PUT")] + public async Task BadRequestIfMethodRequiresLengthButNoContentLengthInHttp10Request(string method) + { + using (var server = new TestServer(context => { return Task.FromResult(0); })) + { + using (var connection = server.CreateConnection()) + { + await connection.Send($"{method} / HTTP/1.0\r\n\r\n"); + await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); + } + } + } + + [Theory] + [InlineData("NaN")] + [InlineData("-1")] + public async Task BadRequestIfContentLengthInvalid(string contentLength) + { + using (var server = new TestServer(context => { return Task.FromResult(0); })) + { + using (var connection = server.CreateConnection()) + { + await connection.SendAll($"GET / HTTP/1.1\r\nContent-Length: {contentLength}\r\n\r\n"); + await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); + } + } + } + + private async Task ReceiveBadRequestResponse(TestConnection connection, string expectedResponseStatusCode, string expectedDateHeaderValue) + { + await connection.ReceiveForcedEnd( + $"HTTP/1.1 {expectedResponseStatusCode}", + "Connection: close", + $"Date: {expectedDateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + + public static IEnumerable InvalidRequestLineData => HttpParsingData.InvalidRequestLineData.Select(data => new[] { data[0] }); + + public static TheoryData UnrecognizedHttpVersionData => HttpParsingData.UnrecognizedHttpVersionData; + + public static IEnumerable InvalidRequestHeaderData => HttpParsingData.InvalidRequestHeaderData.Select(data => new[] { data[0] }); + } +} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/BadHttpRequestTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/BadHttpRequestTests.cs deleted file mode 100644 index cf3ebc03de..0000000000 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/BadHttpRequestTests.cs +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Internal; -using Xunit; - -namespace Microsoft.AspNetCore.Server.KestrelTests -{ - public class BadHttpRequestTests - { - // All test cases for this theory must end in '\n', otherwise the server will spin forever - [Theory] - // Incomplete request lines - [InlineData("G\r\n")] - [InlineData("GE\r\n")] - [InlineData("GET\r\n")] - [InlineData("GET \r\n")] - [InlineData("GET /\r\n")] - [InlineData("GET / \r\n")] - // Missing method - [InlineData(" \r\n")] - // Missing second space - [InlineData("/ \r\n")] // This fails trying to read the '/' because that's invalid for an HTTP method - [InlineData("GET /\r\n")] - // Missing target - [InlineData("GET \r\n")] - // Missing version - [InlineData("GET / \r\n")] - // Missing CR - [InlineData("GET / \n")] - // Missing LF after CR - [InlineData("GET / HTTP/1.0\rA\n")] - // Bad HTTP Methods (invalid according to RFC) - [InlineData("( / HTTP/1.0\r\n")] - [InlineData(") / HTTP/1.0\r\n")] - [InlineData("< / HTTP/1.0\r\n")] - [InlineData("> / HTTP/1.0\r\n")] - [InlineData("@ / HTTP/1.0\r\n")] - [InlineData(", / HTTP/1.0\r\n")] - [InlineData("; / HTTP/1.0\r\n")] - [InlineData(": / HTTP/1.0\r\n")] - [InlineData("\\ / HTTP/1.0\r\n")] - [InlineData("\" / HTTP/1.0\r\n")] - [InlineData("/ / HTTP/1.0\r\n")] - [InlineData("[ / HTTP/1.0\r\n")] - [InlineData("] / HTTP/1.0\r\n")] - [InlineData("? / HTTP/1.0\r\n")] - [InlineData("= / HTTP/1.0\r\n")] - [InlineData("{ / HTTP/1.0\r\n")] - [InlineData("} / HTTP/1.0\r\n")] - [InlineData("get@ / HTTP/1.0\r\n")] - [InlineData("post= / HTTP/1.0\r\n")] - public async Task TestInvalidRequestLines(string request) - { - using (var server = new TestServer(context => TaskCache.CompletedTask)) - { - using (var connection = server.CreateConnection()) - { - await connection.SendAll(request); - await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); - } - } - } - - [Theory] - [InlineData("GET / H\r\n")] - [InlineData("GET / HT\r\n")] - [InlineData("GET / HTT\r\n")] - [InlineData("GET / HTTP\r\n")] - [InlineData("GET / HTTP/\r\n")] - [InlineData("GET / HTTP/1\r\n")] - [InlineData("GET / HTTP/1.\r\n")] - [InlineData("GET / http/1.0\r\n")] - [InlineData("GET / http/1.1\r\n")] - [InlineData("GET / HTTP/1.1 \r\n")] - [InlineData("GET / HTTP/1.1a\r\n")] - [InlineData("GET / HTTP/1.2\r\n")] - [InlineData("GET / HTTP/3.0\r\n")] - [InlineData("GET / H\r\n")] - [InlineData("GET / HTTP/1.\r\n")] - [InlineData("GET / hello\r\n")] - [InlineData("GET / 8charact\r\n")] - public async Task TestInvalidRequestLinesWithUnsupportedVersion(string request) - { - using (var server = new TestServer(context => TaskCache.CompletedTask)) - { - using (var connection = server.CreateConnection()) - { - await connection.SendAll(request); - await ReceiveBadRequestResponse(connection, "505 HTTP Version Not Supported", server.Context.DateHeaderValue); - } - } - } - - [Theory] - // Leading whitespace - [InlineData(" Header-1: value1\r\nHeader-2: value2\r\n\r\n")] - [InlineData("\tHeader-1: value1\r\nHeader-2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\n Header-2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\n\tHeader-2: value2\r\n\r\n")] - // Missing LF - [InlineData("Header-1: value1\rHeader-2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\nHeader-2: value2\r\r\n")] - // Line folding - [InlineData("Header-1: multi\r\n line\r\nHeader-2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\nHeader-2: multi\r\n line\r\n\r\n")] - // Missing ':' - [InlineData("Header-1 value1\r\nHeader-2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\nHeader-2 value2\r\n\r\n")] - // Whitespace in header name - [InlineData("Header 1: value1\r\nHeader-2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\nHeader 2: value2\r\n\r\n")] - [InlineData("Header-1 : value1\r\nHeader-2: value2\r\n\r\n")] - [InlineData("Header-1\t: value1\r\nHeader-2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\nHeader-2 : value2\r\n\r\n")] - [InlineData("Header-1: value1\r\nHeader-2\t: value2\r\n\r\n")] - public async Task TestInvalidHeaders(string rawHeaders) - { - using (var server = new TestServer(context => TaskCache.CompletedTask)) - { - using (var connection = server.CreateConnection()) - { - await connection.SendAll($"GET / HTTP/1.1\r\n{rawHeaders}"); - await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); - } - } - } - - [Fact] - public async Task BadRequestWhenNameHeaderNamesContainsNonASCIICharacters() - { - using (var server = new TestServer(context => { return Task.FromResult(0); })) - { - using (var connection = server.CreateConnection()) - { - await connection.SendAll( - "GET / HTTP/1.1", - "H\u00eb\u00e4d\u00ebr: value", - "", - ""); - await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); - } - } - } - - [Theory] - [InlineData("\0")] - [InlineData("%00")] - [InlineData("/\0")] - [InlineData("/%00")] - [InlineData("/\0\0")] - [InlineData("/%00%00")] - [InlineData("/%C8\0")] - [InlineData("/%E8%00%84")] - [InlineData("/%E8%85%00")] - [InlineData("/%F3%00%82%86")] - [InlineData("/%F3%85%00%82")] - [InlineData("/%F3%85%82%00")] - [InlineData("/%E8%85%00")] - [InlineData("/%E8%01%00")] - public async Task BadRequestIfPathContainsNullCharacters(string path) - { - using (var server = new TestServer(context => { return Task.FromResult(0); })) - { - using (var connection = server.CreateConnection()) - { - await connection.SendAll($"GET {path} HTTP/1.1\r\n"); - await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); - } - } - } - - [Theory] - [InlineData("POST")] - [InlineData("PUT")] - public async Task BadRequestIfMethodRequiresLengthButNoContentLengthOrTransferEncodingInRequest(string method) - { - using (var server = new TestServer(context => { return Task.FromResult(0); })) - { - using (var connection = server.CreateConnection()) - { - await connection.Send($"{method} / HTTP/1.1\r\n\r\n"); - await ReceiveBadRequestResponse(connection, "411 Length Required", server.Context.DateHeaderValue); - } - } - } - - [Theory] - [InlineData("POST")] - [InlineData("PUT")] - public async Task BadRequestIfMethodRequiresLengthButNoContentLengthInHttp10Request(string method) - { - using (var server = new TestServer(context => { return Task.FromResult(0); })) - { - using (var connection = server.CreateConnection()) - { - await connection.Send($"{method} / HTTP/1.0\r\n\r\n"); - await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); - } - } - } - - [Theory] - [InlineData("NaN")] - [InlineData("-1")] - public async Task BadRequestIfContentLengthInvalid(string contentLength) - { - using (var server = new TestServer(context => { return Task.FromResult(0); })) - { - using (var connection = server.CreateConnection()) - { - await connection.SendAll($"GET / HTTP/1.1\r\nContent-Length: {contentLength}\r\n\r\n"); - await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); - } - } - } - - private async Task ReceiveBadRequestResponse(TestConnection connection, string expectedResponseStatusCode, string expectedDateHeaderValue) - { - await connection.ReceiveForcedEnd( - $"HTTP/1.1 {expectedResponseStatusCode}", - "Connection: close", - $"Date: {expectedDateHeaderValue}", - "Content-Length: 0", - "", - ""); - } - } -} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs index d8f7dc701c..9e5fe001ef 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.IO; using System.IO.Pipelines; using System.Net; @@ -27,10 +28,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests private readonly TestFrame _frame; private readonly ServiceContext _serviceContext; private readonly ConnectionContext _connectionContext; - private PipeFactory _pipelineFactory; - - ReadCursor consumed; - ReadCursor examined; + private readonly PipeFactory _pipelineFactory; + private ReadCursor _consumed; + private ReadCursor _examined; private class TestFrame : Frame { @@ -88,13 +88,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes("Header:value\r\n\r\n")); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders) _frame.RequestHeaders, out consumed, out examined); - _socketInput.Reader.Advance(consumed, examined); + var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined); + _socketInput.Reader.Advance(_consumed, _examined); Assert.True(success); Assert.Equal(1, _frame.RequestHeaders.Count); Assert.Equal("value", _frame.RequestHeaders["Header"]); - Assert.Equal(readableBuffer.End, consumed); + Assert.Equal(readableBuffer.End, _consumed); } [Theory] @@ -113,13 +113,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders)); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined); - _socketInput.Reader.Advance(consumed, examined); + var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined); + _socketInput.Reader.Advance(_consumed, _examined); Assert.True(success); Assert.Equal(1, _frame.RequestHeaders.Count); Assert.Equal("value", _frame.RequestHeaders["Header"]); - Assert.Equal(readableBuffer.End, consumed); + Assert.Equal(readableBuffer.End, _consumed); } [Theory] @@ -137,13 +137,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders)); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined); - _socketInput.Reader.Advance(consumed, examined); + var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined); + _socketInput.Reader.Advance(_consumed, _examined); Assert.True(success); Assert.Equal(1, _frame.RequestHeaders.Count); Assert.Equal("value", _frame.RequestHeaders["Header"]); - Assert.Equal(readableBuffer.End, consumed); + Assert.Equal(readableBuffer.End, _consumed); } [Theory] @@ -160,32 +160,26 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders)); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined); - _socketInput.Reader.Advance(consumed, examined); + var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined); + _socketInput.Reader.Advance(_consumed, _examined); Assert.True(success); Assert.Equal(1, _frame.RequestHeaders.Count); Assert.Equal(expectedValue, _frame.RequestHeaders["Header"]); - Assert.Equal(readableBuffer.End, consumed); + Assert.Equal(readableBuffer.End, _consumed); } [Theory] - [InlineData("Header: line1\r\n line2\r\n\r\n")] - [InlineData("Header: line1\r\n\tline2\r\n\r\n")] - [InlineData("Header: line1\r\n line2\r\n\r\n")] - [InlineData("Header: line1\r\n \tline2\r\n\r\n")] - [InlineData("Header: line1\r\n\t line2\r\n\r\n")] - [InlineData("Header: line1\r\n\t\tline2\r\n\r\n")] - [InlineData("Header: line1\r\n \t\t line2\r\n\r\n")] - [InlineData("Header: line1\r\n \t \t line2\r\n\r\n")] - public async Task TakeMessageHeadersThrowsOnHeaderValueWithLineFolding(string rawHeaders) + [MemberData(nameof(InvalidRequestHeaderData))] + public async Task TakeMessageHeadersThrowsOnInvalidRequestHeaders(string rawHeaders, string expectedExceptionMessage) { await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders)); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - _socketInput.Reader.Advance(consumed, examined); - var exception = Assert.Throws(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined)); - Assert.Equal("Header value line folding not supported.", exception.Message); + var exception = Assert.Throws(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined)); + _socketInput.Reader.Advance(_consumed, _examined); + + Assert.Equal(expectedExceptionMessage, exception.Message); Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); } @@ -195,107 +189,19 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes("Header-1: value1\r\n")); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - Assert.False(_frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined)); - _socketInput.Reader.Advance(consumed, examined); + Assert.False(_frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined)); + _socketInput.Reader.Advance(_consumed, _examined); await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(" ")); readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - var exception = Assert.Throws(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined)); - _socketInput.Reader.Advance(consumed, examined); + var exception = Assert.Throws(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined)); + _socketInput.Reader.Advance(_consumed, _examined); Assert.Equal("Header value line folding not supported.", exception.Message); Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); } - [Theory] - [InlineData("Header-1: value1\r\r\n")] - [InlineData("Header-1: val\rue1\r\n")] - [InlineData("Header-1: value1\rHeader-2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\nHeader-2: value2\r\r\n")] - [InlineData("Header-1: value1\r\nHeader-2: v\ralue2\r\n")] - public async Task TakeMessageHeadersThrowsOnHeaderValueContainingCR(string rawHeaders) - { - await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders)); - var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - - var exception = Assert.Throws(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined)); - _socketInput.Reader.Advance(consumed, examined); - - Assert.Equal("Header value must not contain CR characters.", exception.Message); - Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); - } - - [Theory] - [InlineData("Header-1 value1\r\n\r\n")] - [InlineData("Header-1 value1\r\nHeader-2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\nHeader-2 value2\r\n\r\n")] - public async Task TakeMessageHeadersThrowsOnHeaderLineMissingColon(string rawHeaders) - { - await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders)); - var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - - var exception = Assert.Throws(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined)); - _socketInput.Reader.Advance(consumed, examined); - - Assert.Equal("No ':' character found in header line.", exception.Message); - Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); - } - - [Theory] - [InlineData(" Header: value\r\n\r\n")] - [InlineData("\tHeader: value\r\n\r\n")] - [InlineData(" Header-1: value1\r\nHeader-2: value2\r\n\r\n")] - [InlineData("\tHeader-1: value1\r\nHeader-2: value2\r\n\r\n")] - public async Task TakeMessageHeadersThrowsOnHeaderLineStartingWithWhitespace(string rawHeaders) - { - await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders)); - var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - - var exception = Assert.Throws(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined)); - _socketInput.Reader.Advance(consumed, examined); - - Assert.Equal("Header line must not start with whitespace.", exception.Message); - Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); - } - - [Theory] - [InlineData("Header : value\r\n\r\n")] - [InlineData("Header\t: value\r\n\r\n")] - [InlineData("Header 1: value1\r\nHeader-2: value2\r\n\r\n")] - [InlineData("Header 1 : value1\r\nHeader-2: value2\r\n\r\n")] - [InlineData("Header 1\t: value1\r\nHeader-2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\nHeader 2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\nHeader-2 : value2\r\n\r\n")] - [InlineData("Header-1: value1\r\nHeader-2\t: value2\r\n\r\n")] - public async Task TakeMessageHeadersThrowsOnWhitespaceInHeaderName(string rawHeaders) - { - await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders)); - var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - - var exception = Assert.Throws(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined)); - _socketInput.Reader.Advance(consumed, examined); - - Assert.Equal("Whitespace is not allowed in header name.", exception.Message); - Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); - } - - [Theory] - [InlineData("Header-1: value1\r\nHeader-2: value2\r\n\r\r")] - [InlineData("Header-1: value1\r\nHeader-2: value2\r\n\r ")] - [InlineData("Header-1: value1\r\nHeader-2: value2\r\n\r \n")] - public async Task TakeMessageHeadersThrowsOnHeadersNotEndingInCRLFLine(string rawHeaders) - { - await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders)); - var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - - var exception = Assert.Throws(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined)); - _socketInput.Reader.Advance(consumed, examined); - - Assert.Equal("Headers corrupted, invalid header sequence.", exception.Message); - Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); - } - [Fact] public async Task TakeMessageHeadersThrowsWhenHeadersExceedTotalSizeLimit() { @@ -306,8 +212,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine}\r\n")); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - var exception = Assert.Throws(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined)); - _socketInput.Reader.Advance(consumed, examined); + var exception = Assert.Throws(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined)); + _socketInput.Reader.Advance(_consumed, _examined); Assert.Equal("Request headers too long.", exception.Message); Assert.Equal(StatusCodes.Status431RequestHeaderFieldsTooLarge, exception.StatusCode); @@ -322,8 +228,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes($"{headerLines}\r\n")); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - var exception = Assert.Throws(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined)); - _socketInput.Reader.Advance(consumed, examined); + var exception = Assert.Throws(() => _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined)); + _socketInput.Reader.Advance(_consumed, _examined); Assert.Equal("Request contains too many headers.", exception.Message); Assert.Equal(StatusCodes.Status431RequestHeaderFieldsTooLarge, exception.StatusCode); @@ -341,12 +247,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeaders)); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined); - _socketInput.Reader.Advance(consumed, examined); + var success = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined); + _socketInput.Reader.Advance(_consumed, _examined); Assert.True(success); Assert.Equal(numHeaders, _frame.RequestHeaders.Count); - Assert.Equal(readableBuffer.End, consumed); + Assert.Equal(readableBuffer.End, _consumed); } [Fact] @@ -375,8 +281,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine1}\r\n")); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - var takeMessageHeaders = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined); - _socketInput.Reader.Advance(consumed, examined); + var takeMessageHeaders = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined); + _socketInput.Reader.Advance(_consumed, _examined); Assert.True(takeMessageHeaders); Assert.Equal(1, _frame.RequestHeaders.Count); @@ -387,8 +293,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes($"{headerLine2}\r\n")); readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - takeMessageHeaders = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined); - _socketInput.Reader.Advance(consumed, examined); + takeMessageHeaders = _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined); + _socketInput.Reader.Advance(_consumed, _examined); Assert.True(takeMessageHeaders); Assert.Equal(1, _frame.RequestHeaders.Count); @@ -478,6 +384,29 @@ namespace Microsoft.AspNetCore.Server.KestrelTests Assert.Same(originalDuplexStream, _frame.DuplexStream); } + [Theory] + [MemberData(nameof(ValidRequestLineData))] + public async Task TakeStartLineSetsFrameProperties( + string requestLine, + string expectedMethod, + string expectedPath, + string expectedQueryString, + string expectedHttpVersion) + { + var requestLineBytes = Encoding.ASCII.GetBytes(requestLine); + await _socketInput.Writer.WriteAsync(requestLineBytes); + var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; + + var returnValue = _frame.TakeStartLine(readableBuffer, out _consumed, out _examined); + _socketInput.Reader.Advance(_consumed, _examined); + + Assert.True(returnValue); + Assert.Equal(expectedMethod, _frame.Method); + Assert.Equal(expectedPath, _frame.Path); + Assert.Equal(expectedQueryString, _frame.QueryString); + Assert.Equal(expectedHttpVersion, _frame.HttpVersion); + } + [Fact] public async Task TakeStartLineCallsConsumingCompleteWithFurthestExamined() { @@ -485,21 +414,21 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(requestLineBytes); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - _frame.TakeStartLine(readableBuffer, out consumed, out examined); - _socketInput.Reader.Advance(consumed, examined); + _frame.TakeStartLine(readableBuffer, out _consumed, out _examined); + _socketInput.Reader.Advance(_consumed, _examined); - Assert.Equal(readableBuffer.Start, consumed); - Assert.Equal(readableBuffer.End, examined); + Assert.Equal(readableBuffer.Start, _consumed); + Assert.Equal(readableBuffer.End, _examined); requestLineBytes = Encoding.ASCII.GetBytes("HTTP/1.1\r\n"); await _socketInput.Writer.WriteAsync(requestLineBytes); readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - _frame.TakeStartLine(readableBuffer, out consumed, out examined); - _socketInput.Reader.Advance(consumed, examined); + _frame.TakeStartLine(readableBuffer, out _consumed, out _examined); + _socketInput.Reader.Advance(_consumed, _examined); - Assert.Equal(readableBuffer.End, consumed); - Assert.Equal(readableBuffer.End, examined); + Assert.Equal(readableBuffer.End, _consumed); + Assert.Equal(readableBuffer.End, _examined); } [Theory] @@ -524,8 +453,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(requestLineBytes); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - var returnValue = _frame.TakeStartLine(readableBuffer, out consumed, out examined); - _socketInput.Reader.Advance(consumed, examined); + var returnValue = _frame.TakeStartLine(readableBuffer, out _consumed, out _examined); + _socketInput.Reader.Advance(_consumed, _examined); Assert.False(returnValue); } @@ -538,8 +467,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes("G")); - _frame.TakeStartLine((await _socketInput.Reader.ReadAsync()).Buffer, out consumed, out examined); - _socketInput.Reader.Advance(consumed, examined); + _frame.TakeStartLine((await _socketInput.Reader.ReadAsync()).Buffer, out _consumed, out _examined); + _socketInput.Reader.Advance(_consumed, _examined); var expectedRequestHeadersTimeout = (long)_serviceContext.ServerOptions.Limits.RequestHeadersTimeout.TotalMilliseconds; connectionControl.Verify(cc => cc.ResetTimeout(expectedRequestHeadersTimeout, TimeoutAction.SendTimeoutResponse)); @@ -554,79 +483,58 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _socketInput.Writer.WriteAsync(requestLineBytes); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - var exception = Assert.Throws(() => _frame.TakeStartLine(readableBuffer, out consumed, out examined)); - _socketInput.Reader.Advance(consumed, examined); + var exception = Assert.Throws(() =>_frame.TakeStartLine(readableBuffer, out _consumed, out _examined)); + _socketInput.Reader.Advance(_consumed, _examined); Assert.Equal("Request line too long.", exception.Message); Assert.Equal(StatusCodes.Status414UriTooLong, exception.StatusCode); } [Theory] - [InlineData("GET/HTTP/1.1\r\n", "Invalid request line: GET/HTTP/1.1<0x0D><0x0A>")] - [InlineData(" / HTTP/1.1\r\n", "Invalid request line: / HTTP/1.1<0x0D><0x0A>")] - [InlineData("GET? / HTTP/1.1\r\n", "Invalid request line: GET? / HTTP/1.1<0x0D><0x0A>")] - [InlineData("GET /HTTP/1.1\r\n", "Invalid request line: GET /HTTP/1.1<0x0D><0x0A>")] - [InlineData("GET /a?b=cHTTP/1.1\r\n", "Invalid request line: GET /a?b=cHTTP/1.1<0x0D><0x0A>")] - [InlineData("GET /a%20bHTTP/1.1\r\n", "Invalid request line: GET /a%20bHTTP/1.1<0x0D><0x0A>")] - [InlineData("GET /a%20b?c=dHTTP/1.1\r\n", "Invalid request line: GET /a%20b?c=dHTTP/1.1<0x0D><0x0A>")] - [InlineData("GET HTTP/1.1\r\n", "Invalid request line: GET HTTP/1.1<0x0D><0x0A>")] - [InlineData("GET / HTTP/1.1\n", "Invalid request line: GET / HTTP/1.1<0x0A>")] - [InlineData("GET / \r\n", "Invalid request line: GET / <0x0D><0x0A>")] - [InlineData("GET ? HTTP/1.1\r\n", "Invalid request line: GET ? HTTP/1.1<0x0D><0x0A>")] - [InlineData("GET / HTTP/1.1\ra\n", "Invalid request line: GET / HTTP/1.1<0x0D>a<0x0A>")] - public async Task TakeStartLineThrowsWhenInvalid(string requestLine, string expectedExceptionMessage) + [MemberData(nameof(InvalidRequestLineData))] + public async Task TakeStartLineThrowsOnInvalidRequestLine(string requestLine, Type expectedExceptionType, string expectedExceptionMessage) { await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(requestLine)); - var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - var exception = Assert.Throws(() => _frame.TakeStartLine(readableBuffer, out consumed, out examined)); - _socketInput.Reader.Advance(consumed, examined); + var exception = Assert.Throws(expectedExceptionType, () => + _frame.TakeStartLine(readableBuffer, out _consumed, out _examined)); + _socketInput.Reader.Advance(_consumed, _examined); Assert.Equal(expectedExceptionMessage, exception.Message); - Assert.Equal(StatusCodes.Status400BadRequest, exception.StatusCode); + + if (expectedExceptionType == typeof(BadHttpRequestException)) + { + Assert.Equal(StatusCodes.Status400BadRequest, (exception as BadHttpRequestException).StatusCode); + } } - [Fact] - public async Task TakeStartLineThrowsOnUnsupportedHttpVersion() + [Theory] + [MemberData(nameof(UnrecognizedHttpVersionData))] + public async Task TakeStartLineThrowsOnUnrecognizedHttpVersion(string httpVersion) { - await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes("GET / HTTP/1.2\r\n")); + await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes($"GET / {httpVersion}\r\n")); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - var exception = Assert.Throws(() => _frame.TakeStartLine(readableBuffer, out consumed, out examined)); - _socketInput.Reader.Advance(consumed, examined); + var exception = Assert.Throws(() => _frame.TakeStartLine(readableBuffer, out _consumed, out _examined)); + _socketInput.Reader.Advance(_consumed, _examined); - Assert.Equal("Unrecognized HTTP version: HTTP/1.2", exception.Message); - Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, exception.StatusCode); - } - - [Fact] - public async Task TakeStartLineThrowsOnUnsupportedHttpVersionLongerThanEightCharacters() - { - var requestLineBytes = Encoding.ASCII.GetBytes("GET / HTTP/1.1ab\r\n"); - await _socketInput.Writer.WriteAsync(requestLineBytes); - - var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - - var exception = Assert.Throws(() => _frame.TakeStartLine(readableBuffer, out consumed, out examined)); - _socketInput.Reader.Advance(consumed, examined); - - Assert.Equal("Unrecognized HTTP version: HTTP/1.1ab", exception.Message); + Assert.Equal($"Unrecognized HTTP version: {httpVersion}", exception.Message); Assert.Equal(StatusCodes.Status505HttpVersionNotsupported, exception.StatusCode); } [Fact] public async Task TakeMessageHeadersCallsConsumingCompleteWithFurthestExamined() { - foreach (var rawHeader in new [] { "Header: " , "value\r\n" , "\r\n"}) + foreach (var rawHeader in new[] { "Header: ", "value\r\n", "\r\n" }) { await _socketInput.Writer.WriteAsync(Encoding.ASCII.GetBytes(rawHeader)); var readableBuffer = (await _socketInput.Reader.ReadAsync()).Buffer; - _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out consumed, out examined); - _socketInput.Reader.Advance(consumed, examined); - Assert.Equal(readableBuffer.End, examined); + _frame.TakeMessageHeaders(readableBuffer, (FrameRequestHeaders)_frame.RequestHeaders, out _consumed, out _examined); + _socketInput.Reader.Advance(_consumed, _examined); + Assert.Equal(readableBuffer.End, _examined); } } @@ -831,5 +739,13 @@ namespace Microsoft.AspNetCore.Server.KestrelTests await _frame.ProduceEndAsync(); Assert.NotSame(original, _frame.RequestAborted.WaitHandle); } + + public static IEnumerable ValidRequestLineData => HttpParsingData.ValidRequestLineData; + + public static IEnumerable InvalidRequestLineData => HttpParsingData.InvalidRequestLineData; + + public static TheoryData UnrecognizedHttpVersionData => HttpParsingData.UnrecognizedHttpVersionData; + + public static IEnumerable InvalidRequestHeaderData => HttpParsingData.InvalidRequestHeaderData; } } diff --git a/test/shared/HttpParsingData.cs b/test/shared/HttpParsingData.cs new file mode 100644 index 0000000000..b38afb96c5 --- /dev/null +++ b/test/shared/HttpParsingData.cs @@ -0,0 +1,301 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Server.Kestrel; +using Xunit; + +namespace Microsoft.AspNetCore.Testing +{ + public class HttpParsingData + { + public static IEnumerable ValidRequestLineData + { + get + { + var methods = new[] + { + "GET", + "CUSTOM", + }; + var targets = new[] + { + Tuple.Create("/", "/"), + Tuple.Create("/abc", "/abc"), + Tuple.Create("/abc/de/f", "/abc/de/f"), + Tuple.Create("/%20", "/ "), + Tuple.Create("/a%20", "/a "), + Tuple.Create("/%20a", "/ a"), + Tuple.Create("/a/b%20c", "/a/b c"), + Tuple.Create("/%C3%A5", "/\u00E5"), + Tuple.Create("/a%C3%A5a", "/a\u00E5a"), + Tuple.Create("/%C3%A5/bc", "/\u00E5/bc"), + Tuple.Create("/%25", "/%"), + Tuple.Create("/%2F", "/%2F"), + }; + var queryStrings = new[] + { + "", + "?", + "?arg1=val1", + "?arg1=a%20b", + "?%A", + "?%20=space", + "?%C3%A5=val", + "?path=/home", + "?path=/%C3%A5/", + "?question=what?", + "?%00", + "?arg=%00" + }; + var httpVersions = new[] + { + "HTTP/1.0", + "HTTP/1.1" + }; + + return from method in methods + from target in targets + from queryString in queryStrings + from httpVersion in httpVersions + select new[] + { + $"{method} {target.Item1}{queryString} {httpVersion}\r\n", + method, + $"{target.Item2}", + queryString, + httpVersion + }; + } + } + + // All these test cases must end in '\n', otherwise the server will spin forever + public static IEnumerable InvalidRequestLineData + { + get + { + var invalidRequestLines = new[] + { + "G\r\n", + "GE\r\n", + "GET\r\n", + "GET \r\n", + "GET /\r\n", + "GET / \r\n", + "GET/HTTP/1.1\r\n", + "GET /HTTP/1.1\r\n", + " \r\n", + " \r\n", + "/ HTTP/1.1\r\n", + " / HTTP/1.1\r\n", + "/ \r\n", + "GET \r\n", + "GET HTTP/1.0\r\n", + "GET HTTP/1.1\r\n", + "GET / \n", + "GET / HTTP/1.0\n", + "GET / HTTP/1.1\n", + "GET / HTTP/1.0\rA\n", + "GET / HTTP/1.1\ra\n", + "GET? / HTTP/1.1\r\n", + "GET ? HTTP/1.1\r\n", + "GET /a?b=cHTTP/1.1\r\n", + "GET /a%20bHTTP/1.1\r\n", + "GET /a%20b?c=dHTTP/1.1\r\n", + "GET %2F HTTP/1.1\r\n", + "GET %00 HTTP/1.1\r\n", + "CUSTOM \r\n", + "CUSTOM /\r\n", + "CUSTOM / \r\n", + "CUSTOM /HTTP/1.1\r\n", + "CUSTOM \r\n", + "CUSTOM HTTP/1.0\r\n", + "CUSTOM HTTP/1.1\r\n", + "CUSTOM / \n", + "CUSTOM / HTTP/1.0\n", + "CUSTOM / HTTP/1.1\n", + "CUSTOM / HTTP/1.0\rA\n", + "CUSTOM / HTTP/1.1\ra\n", + "CUSTOM ? HTTP/1.1\r\n", + "CUSTOM /a?b=cHTTP/1.1\r\n", + "CUSTOM /a%20bHTTP/1.1\r\n", + "CUSTOM /a%20b?c=dHTTP/1.1\r\n", + "CUSTOM %2F HTTP/1.1\r\n", + "CUSTOM %00 HTTP/1.1\r\n", + // Bad HTTP Methods (invalid according to RFC) + "( / HTTP/1.0\r\n", + ") / HTTP/1.0\r\n", + "< / HTTP/1.0\r\n", + "> / HTTP/1.0\r\n", + "@ / HTTP/1.0\r\n", + ", / HTTP/1.0\r\n", + "; / HTTP/1.0\r\n", + ": / HTTP/1.0\r\n", + "\\ / HTTP/1.0\r\n", + "\" / HTTP/1.0\r\n", + "/ / HTTP/1.0\r\n", + "[ / HTTP/1.0\r\n", + "] / HTTP/1.0\r\n", + "? / HTTP/1.0\r\n", + "= / HTTP/1.0\r\n", + "{ / HTTP/1.0\r\n", + "} / HTTP/1.0\r\n", + "get@ / HTTP/1.0\r\n", + "post= / HTTP/1.0\r\n", + }; + + var encodedNullCharInTargetRequestLines = new[] + { + "GET /%00 HTTP/1.1\r\n", + "GET /%00%00 HTTP/1.1\r\n", + "GET /%E8%00%84 HTTP/1.1\r\n", + "GET /%E8%85%00 HTTP/1.1\r\n", + "GET /%F3%00%82%86 HTTP/1.1\r\n", + "GET /%F3%85%00%82 HTTP/1.1\r\n", + "GET /%F3%85%82%00 HTTP/1.1\r\n", + "GET /%E8%85%00 HTTP/1.1\r\n", + "GET /%E8%01%00 HTTP/1.1\r\n", + }; + + var nullCharInTargetRequestLines = new[] + { + "GET \0 HTTP/1.1\r\n", + "GET /\0 HTTP/1.1\r\n", + "GET /\0\0 HTTP/1.1\r\n", + "GET /%C8\0 HTTP/1.1\r\n", + }; + + return invalidRequestLines.Select(requestLine => new object[] + { + requestLine, + typeof(BadHttpRequestException), + $"Invalid request line: {requestLine.Replace("\r", "<0x0D>").Replace("\n", "<0x0A>")}" + }) + .Concat(encodedNullCharInTargetRequestLines.Select(requestLine => new object[] + { + requestLine, + typeof(InvalidOperationException), + $"The path contains null characters." + })) + .Concat(nullCharInTargetRequestLines.Select(requestLine => new object[] + { + requestLine, + typeof(InvalidOperationException), + new InvalidOperationException().Message + })); + } + } + + public static TheoryData UnrecognizedHttpVersionData + { + get + { + return new TheoryData + { + "H", + "HT", + "HTT", + "HTTP", + "HTTP/", + "HTTP/1", + "HTTP/1.", + "http/1.0", + "http/1.1", + "HTTP/1.1 ", + "HTTP/1.0a", + "HTTP/1.0ab", + "HTTP/1.1a", + "HTTP/1.1ab", + "HTTP/1.2", + "HTTP/3.0", + "hello", + "8charact", + }; + } + } + + public static IEnumerable InvalidRequestHeaderData + { + get + { + // Line folding + var headersWithLineFolding = new[] + { + "Header: line1\r\n line2\r\n\r\n", + "Header: line1\r\n\tline2\r\n\r\n", + "Header: line1\r\n line2\r\n\r\n", + "Header: line1\r\n \tline2\r\n\r\n", + "Header: line1\r\n\t line2\r\n\r\n", + "Header: line1\r\n\t\tline2\r\n\r\n", + "Header: line1\r\n \t\t line2\r\n\r\n", + "Header: line1\r\n \t \t line2\r\n\r\n", + "Header-1: multi\r\n line\r\nHeader-2: value2\r\n\r\n", + "Header-1: value1\r\nHeader-2: multi\r\n line\r\n\r\n", + "Header-1: value1\r\n Header-2: value2\r\n\r\n", + "Header-1: value1\r\n\tHeader-2: value2\r\n\r\n", + }; + + // CR in value + var headersWithCRInValue = new[] + { + "Header-1: value1\r\r\n", + "Header-1: val\rue1\r\n", + "Header-1: value1\rHeader-2: value2\r\n\r\n", + "Header-1: value1\r\nHeader-2: value2\r\r\n", + "Header-1: value1\r\nHeader-2: v\ralue2\r\n", + }; + + // Missing colon + var headersWithMissingColon = new[] + { + "Header-1 value1\r\n\r\n", + "Header-1 value1\r\nHeader-2: value2\r\n\r\n", + "Header-1: value1\r\nHeader-2 value2\r\n\r\n", + }; + + // Starting with whitespace + var headersStartingWithWhitespace = new[] + { + " Header: value\r\n\r\n", + "\tHeader: value\r\n\r\n", + " Header-1: value1\r\nHeader-2: value2\r\n\r\n", + "\tHeader-1: value1\r\nHeader-2: value2\r\n\r\n", + }; + + // Whitespace in header name + var headersWithWithspaceInName = new[] + { + "Header : value\r\n\r\n", + "Header\t: value\r\n\r\n", + "Header 1: value1\r\nHeader-2: value2\r\n\r\n", + "Header 1 : value1\r\nHeader-2: value2\r\n\r\n", + "Header 1\t: value1\r\nHeader-2: value2\r\n\r\n", + "Header-1: value1\r\nHeader 2: value2\r\n\r\n", + "Header-1: value1\r\nHeader-2 : value2\r\n\r\n", + "Header-1: value1\r\nHeader-2\t: value2\r\n\r\n", + }; + + // Headers not ending in CRLF line + var headersNotEndingInCrLfLine = new[] + { + "Header-1: value1\r\nHeader-2: value2\r\n\r\r", + "Header-1: value1\r\nHeader-2: value2\r\n\r ", + "Header-1: value1\r\nHeader-2: value2\r\n\r \n", + }; + + return new[] + { + Tuple.Create(headersWithLineFolding,"Header value line folding not supported."), + Tuple.Create(headersWithCRInValue,"Header value must not contain CR characters."), + Tuple.Create(headersWithMissingColon,"No ':' character found in header line."), + Tuple.Create(headersStartingWithWhitespace, "Header line must not start with whitespace."), + Tuple.Create(headersWithWithspaceInName,"Whitespace is not allowed in header name."), + Tuple.Create(headersNotEndingInCrLfLine, "Headers corrupted, invalid header sequence.") + } + .SelectMany(t => t.Item1.Select(headers => new[] { headers, t.Item2 })); + } + } + } +}