From 49ff98f8cbcf511c343a7076ecdaece2b5306462 Mon Sep 17 00:00:00 2001 From: Cesar Blum Silveira Date: Wed, 21 Sep 2016 11:08:47 -0700 Subject: [PATCH] More specific response status codes for errors (#653). - 414 when request line exceeds size limit - 431 when request headers exceed size or count limits - 505 when request line contains unsupported HTTP version --- .../BadHttpRequestException.cs | 68 +++++----- .../Internal/Http/Frame.cs | 11 +- .../Internal/Http/ReasonPhrases.cs | 3 + .../MaxRequestLineSizeTests.cs | 61 +++++---- .../RequestHeaderLimitsTests.cs | 104 +++++++------- .../BadHttpRequestTests.cs | 61 +++++---- .../ChunkedRequestTests.cs | 4 +- .../FrameRequestHeadersTests.cs | 6 +- .../FrameTests.cs | 128 ++++++++++++++++-- 9 files changed, 294 insertions(+), 152 deletions(-) diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs index 64365e486e..196a6e941f 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs @@ -8,100 +8,102 @@ namespace Microsoft.AspNetCore.Server.Kestrel { public sealed class BadHttpRequestException : IOException { - private BadHttpRequestException(string message) + private BadHttpRequestException(string message, int statusCode) : base(message) { - + StatusCode = statusCode; } + internal int StatusCode { get; } + internal static BadHttpRequestException GetException(RequestRejectionReason reason) { BadHttpRequestException ex; switch (reason) { case RequestRejectionReason.MissingMethod: - ex = new BadHttpRequestException("Missing method."); + ex = new BadHttpRequestException("Missing method.", 400); break; case RequestRejectionReason.InvalidMethod: - ex = new BadHttpRequestException("Invalid method."); + ex = new BadHttpRequestException("Invalid method.", 400); break; case RequestRejectionReason.MissingRequestTarget: - ex = new BadHttpRequestException("Missing request target."); + ex = new BadHttpRequestException("Missing request target.", 400); break; case RequestRejectionReason.MissingHTTPVersion: - ex = new BadHttpRequestException("Missing HTTP version."); + ex = new BadHttpRequestException("Missing HTTP version.", 400); break; case RequestRejectionReason.UnrecognizedHTTPVersion: - ex = new BadHttpRequestException("Unrecognized HTTP version."); + ex = new BadHttpRequestException("Unrecognized HTTP version.", 505); break; case RequestRejectionReason.MissingLFInRequestLine: - ex = new BadHttpRequestException("Missing LF in request line."); + ex = new BadHttpRequestException("Missing LF in request line.", 400); break; case RequestRejectionReason.HeadersCorruptedInvalidHeaderSequence: - ex = new BadHttpRequestException("Headers corrupted, invalid header sequence."); + ex = new BadHttpRequestException("Headers corrupted, invalid header sequence.", 400); break; case RequestRejectionReason.HeaderLineMustNotStartWithWhitespace: - ex = new BadHttpRequestException("Header line must not start with whitespace."); + ex = new BadHttpRequestException("Header line must not start with whitespace.", 400); break; case RequestRejectionReason.NoColonCharacterFoundInHeaderLine: - ex = new BadHttpRequestException("No ':' character found in header line."); + ex = new BadHttpRequestException("No ':' character found in header line.", 400); break; case RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName: - ex = new BadHttpRequestException("Whitespace is not allowed in header name."); + ex = new BadHttpRequestException("Whitespace is not allowed in header name.", 400); break; case RequestRejectionReason.HeaderValueMustNotContainCR: - ex = new BadHttpRequestException("Header value must not contain CR characters."); + ex = new BadHttpRequestException("Header value must not contain CR characters.", 400); break; case RequestRejectionReason.HeaderValueLineFoldingNotSupported: - ex = new BadHttpRequestException("Header value line folding not supported."); + ex = new BadHttpRequestException("Header value line folding not supported.", 400); break; case RequestRejectionReason.MalformedRequestInvalidHeaders: - ex = new BadHttpRequestException("Malformed request: invalid headers."); + ex = new BadHttpRequestException("Malformed request: invalid headers.", 400); break; case RequestRejectionReason.UnexpectedEndOfRequestContent: - ex = new BadHttpRequestException("Unexpected end of request content."); + ex = new BadHttpRequestException("Unexpected end of request content.", 400); break; case RequestRejectionReason.BadChunkSuffix: - ex = new BadHttpRequestException("Bad chunk suffix."); + ex = new BadHttpRequestException("Bad chunk suffix.", 400); break; case RequestRejectionReason.BadChunkSizeData: - ex = new BadHttpRequestException("Bad chunk size data."); + ex = new BadHttpRequestException("Bad chunk size data.", 400); break; case RequestRejectionReason.ChunkedRequestIncomplete: - ex = new BadHttpRequestException("Chunked request incomplete."); + ex = new BadHttpRequestException("Chunked request incomplete.", 400); break; case RequestRejectionReason.PathContainsNullCharacters: - ex = new BadHttpRequestException("The path contains null characters."); + ex = new BadHttpRequestException("The path contains null characters.", 400); break; case RequestRejectionReason.InvalidCharactersInHeaderName: - ex = new BadHttpRequestException("Invalid characters in header name."); + ex = new BadHttpRequestException("Invalid characters in header name.", 400); break; case RequestRejectionReason.NonAsciiOrNullCharactersInInputString: - ex = new BadHttpRequestException("The input string contains non-ASCII or null characters."); + ex = new BadHttpRequestException("The input string contains non-ASCII or null characters.", 400); break; case RequestRejectionReason.RequestLineTooLong: - ex = new BadHttpRequestException("Request line too long."); + ex = new BadHttpRequestException("Request line too long.", 414); break; case RequestRejectionReason.MissingSpaceAfterMethod: - ex = new BadHttpRequestException("No space character found after method in request line."); + ex = new BadHttpRequestException("No space character found after method in request line.", 400); break; case RequestRejectionReason.MissingSpaceAfterTarget: - ex = new BadHttpRequestException("No space character found after target in request line."); + ex = new BadHttpRequestException("No space character found after target in request line.", 400); break; case RequestRejectionReason.MissingCrAfterVersion: - ex = new BadHttpRequestException("Missing CR in request line."); + ex = new BadHttpRequestException("Missing CR in request line.", 400); break; case RequestRejectionReason.HeadersExceedMaxTotalSize: - ex = new BadHttpRequestException("Request headers too long."); + ex = new BadHttpRequestException("Request headers too long.", 431); break; case RequestRejectionReason.MissingCRInHeaderLine: - ex = new BadHttpRequestException("No CR character found in header line."); + ex = new BadHttpRequestException("No CR character found in header line.", 400); break; case RequestRejectionReason.TooManyHeaders: - ex = new BadHttpRequestException("Request contains too many headers."); + ex = new BadHttpRequestException("Request contains too many headers.", 431); break; default: - ex = new BadHttpRequestException("Bad request."); + ex = new BadHttpRequestException("Bad request.", 400); break; } return ex; @@ -113,13 +115,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel switch (reason) { case RequestRejectionReason.MalformedRequestLineStatus: - ex = new BadHttpRequestException($"Invalid request line: {value}"); + ex = new BadHttpRequestException($"Invalid request line: {value}", 400); break; case RequestRejectionReason.InvalidContentLength: - ex = new BadHttpRequestException($"Invalid content length: {value}"); + ex = new BadHttpRequestException($"Invalid content length: {value}", 400); break; default: - ex = new BadHttpRequestException("Bad request."); + ex = new BadHttpRequestException("Bad request.", 400); break; } return ex; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index 2e17305777..af4d8882ef 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -640,13 +640,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http return TaskCache.CompletedTask; } - if (_requestRejected) - { - // 400 Bad Request - StatusCode = 400; - _keepAlive = false; - } - else + // If the request was rejected, StatusCode has already been set by SetBadRequestState + if (!_requestRejected) { // 500 Internal Server Error StatusCode = 500; @@ -1249,6 +1244,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http public void SetBadRequestState(BadHttpRequestException ex) { + StatusCode = ex.StatusCode; + _keepAlive = false; _requestProcessingStopping = true; _requestRejected = true; Log.ConnectionBadRequest(ConnectionId, ex); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ReasonPhrases.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ReasonPhrases.cs index cc01b728f4..c7d52fc91c 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ReasonPhrases.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ReasonPhrases.cs @@ -52,6 +52,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private static readonly byte[] _bytesStatus423 = Encoding.ASCII.GetBytes("423 Locked"); private static readonly byte[] _bytesStatus424 = Encoding.ASCII.GetBytes("424 Failed Dependency"); private static readonly byte[] _bytesStatus426 = Encoding.ASCII.GetBytes("426 Upgrade Required"); + private static readonly byte[] _bytesStatus431 = Encoding.ASCII.GetBytes("431 Request Header Fields Too Large"); private static readonly byte[] _bytesStatus451 = Encoding.ASCII.GetBytes("451 Unavailable For Legal Reasons"); private static readonly byte[] _bytesStatus500 = Encoding.ASCII.GetBytes("500 Internal Server Error"); private static readonly byte[] _bytesStatus501 = Encoding.ASCII.GetBytes("501 Not Implemented"); @@ -157,6 +158,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http return _bytesStatus424; case 426: return _bytesStatus426; + case 431: + return _bytesStatus431; case 451: return _bytesStatus451; case 500: diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestLineSizeTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestLineSizeTests.cs index 1e582f0bce..35b66ed5b5 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestLineSizeTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestLineSizeTests.cs @@ -1,10 +1,7 @@ // 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.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Testing; using Xunit; @@ -30,17 +27,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { var maxRequestLineSize = limit; - using (var host = BuildWebHost(options => + using (var server = CreateServer(limit)) { - options.Limits.MaxRequestLineSize = maxRequestLineSize; - })) - { - host.Start(); - - using (var connection = new TestConnection(host.GetPort())) + using (var connection = new TestConnection(server.Port)) { await connection.SendEnd($"{requestLine}\r\n"); - await connection.Receive($"HTTP/1.1 200 OK\r\n"); + await connection.ReceiveEnd( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Transfer-Encoding: chunked", + "", + "c", + "hello, world", + "0", + "", + ""); } } } @@ -52,33 +53,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [InlineData("DELETE /a%20b%20c/d%20e?f=ghi HTTP/1.1\r\n")] public async Task ServerRejectsRequestLineExceedingLimit(string requestLine) { - using (var host = BuildWebHost(options => + using (var server = CreateServer(requestLine.Length - 1)) { - options.Limits.MaxRequestLineSize = requestLine.Length - 1; // stop short of the '\n' - })) - { - host.Start(); - - using (var connection = new TestConnection(host.GetPort())) + using (var connection = new TestConnection(server.Port)) { await connection.SendAllTryEnd($"{requestLine}\r\n"); - await connection.Receive($"HTTP/1.1 400 Bad Request\r\n"); + await connection.Receive( + "HTTP/1.1 414 URI Too Long", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); } } } - private IWebHost BuildWebHost(Action options) + private TestServer CreateServer(int maxRequestLineSize) { - var host = new WebHostBuilder() - .UseKestrel(options) - .UseUrls("http://127.0.0.1:0/") - .Configure(app => app.Run(async context => + return new TestServer(async httpContext => await httpContext.Response.WriteAsync("hello, world"), new TestServiceContext + { + ServerOptions = new KestrelServerOptions { - await context.Response.WriteAsync("hello, world"); - })) - .Build(); - - return host; + AddServerHeader = false, + Limits = + { + MaxRequestLineSize = maxRequestLineSize + } + } + }); } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestHeaderLimitsTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestHeaderLimitsTests.cs index 2c7eec616e..712407f76c 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestHeaderLimitsTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestHeaderLimitsTests.cs @@ -1,12 +1,8 @@ // 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.Linq; -using System.Net.Http; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Testing; using Xunit; @@ -28,17 +24,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { var headers = MakeHeaders(headerCount); - using (var host = BuildWebHost(options => + using (var server = CreateServer(maxRequestHeadersTotalSize: headers.Length + extraLimit)) { - options.Limits.MaxRequestHeadersTotalSize = headers.Length + extraLimit; - })) - { - host.Start(); - - using (var connection = new TestConnection(host.GetPort())) + using (var connection = new TestConnection(server.Port)) { await connection.SendEnd($"GET / HTTP/1.1\r\n{headers}\r\n"); - await connection.Receive($"HTTP/1.1 200 OK\r\n"); + await connection.ReceiveEnd( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Transfer-Encoding: chunked", + "", + "c", + "hello, world", + "0", + "", + ""); } } } @@ -56,17 +56,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { var headers = MakeHeaders(headerCount); - using (var host = BuildWebHost(options => + using (var server = CreateServer(maxRequestHeaderCount: maxHeaderCount)) { - options.Limits.MaxRequestHeaderCount = maxHeaderCount; - })) - { - host.Start(); - - using (var connection = new TestConnection(host.GetPort())) + using (var connection = new TestConnection(server.Port)) { await connection.SendEnd($"GET / HTTP/1.1\r\n{headers}\r\n"); - await connection.Receive($"HTTP/1.1 200 OK\r\n"); + await connection.ReceiveEnd( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Transfer-Encoding: chunked", + "", + "c", + "hello, world", + "0", + "", + ""); } } } @@ -78,17 +82,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { var headers = MakeHeaders(headerCount); - using (var host = BuildWebHost(options => + using (var server = CreateServer(maxRequestHeadersTotalSize: headers.Length - 1)) { - options.Limits.MaxRequestHeadersTotalSize = headers.Length - 1; - })) - { - host.Start(); - - using (var connection = new TestConnection(host.GetPort())) + using (var connection = new TestConnection(server.Port)) { await connection.SendAllTryEnd($"GET / HTTP/1.1\r\n{headers}\r\n"); - await connection.Receive($"HTTP/1.1 400 Bad Request\r\n"); + await connection.ReceiveForcedEnd( + "HTTP/1.1 431 Request Header Fields Too Large", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); } } } @@ -101,17 +106,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { var headers = MakeHeaders(headerCount); - using (var host = BuildWebHost(options => + using (var server = CreateServer(maxRequestHeaderCount: maxHeaderCount)) { - options.Limits.MaxRequestHeaderCount = maxHeaderCount; - })) - { - host.Start(); - - using (var connection = new TestConnection(host.GetPort())) + using (var connection = new TestConnection(server.Port)) { await connection.SendAllTryEnd($"GET / HTTP/1.1\r\n{headers}\r\n"); - await connection.Receive($"HTTP/1.1 400 Bad Request\r\n"); + await connection.ReceiveForcedEnd( + "HTTP/1.1 431 Request Header Fields Too Large", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); } } } @@ -123,18 +129,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests .Select(i => $"Header-{i}: value{i}\r\n")); } - private static IWebHost BuildWebHost(Action options) + private TestServer CreateServer(int? maxRequestHeaderCount = null, int? maxRequestHeadersTotalSize = null) { - var host = new WebHostBuilder() - .UseKestrel(options) - .UseUrls("http://127.0.0.1:0/") - .Configure(app => app.Run(async context => - { - await context.Response.WriteAsync("hello, world"); - })) - .Build(); + var options = new KestrelServerOptions { AddServerHeader = false }; - return host; + if (maxRequestHeaderCount.HasValue) + { + options.Limits.MaxRequestHeaderCount = maxRequestHeaderCount.Value; + } + + if (maxRequestHeadersTotalSize.HasValue) + { + options.Limits.MaxRequestHeadersTotalSize = maxRequestHeadersTotalSize.Value; + } + + return new TestServer(async httpContext => await httpContext.Response.WriteAsync("hello, world"), new TestServiceContext + { + ServerOptions = options + }); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/BadHttpRequestTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/BadHttpRequestTests.cs index 74cbb05230..a4e0f11169 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/BadHttpRequestTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/BadHttpRequestTests.cs @@ -19,13 +19,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [InlineData("GET \r\n")] [InlineData("GET /\r\n")] [InlineData("GET / \r\n")] - [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")] // Missing method [InlineData(" \r\n")] // Missing second space @@ -37,18 +30,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [InlineData("GET / \r\n")] // Missing CR [InlineData("GET / \n")] - // Unrecognized HTTP version - [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.0\n\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")] // Missing LF after CR [InlineData("GET / HTTP/1.0\rA\n")] // Bad HTTP Methods (invalid according to RFC) @@ -78,7 +59,37 @@ namespace Microsoft.AspNetCore.Server.KestrelTests using (var connection = server.CreateConnection()) { await connection.SendAllTryEnd(request); - await ReceiveBadRequestResponse(connection, server.Context.DateHeaderValue); + 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.SendAllTryEnd(request); + await ReceiveBadRequestResponse(connection, "505 HTTP Version Not Supported", server.Context.DateHeaderValue); } } } @@ -114,7 +125,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests using (var connection = server.CreateConnection()) { await connection.SendAllTryEnd($"GET / HTTP/1.1\r\n{rawHeaders}"); - await ReceiveBadRequestResponse(connection, server.Context.DateHeaderValue); + await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); } } } @@ -131,7 +142,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests "H\u00eb\u00e4d\u00ebr: value", "", ""); - await ReceiveBadRequestResponse(connection, server.Context.DateHeaderValue); + await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); } } } @@ -158,15 +169,15 @@ namespace Microsoft.AspNetCore.Server.KestrelTests using (var connection = server.CreateConnection()) { await connection.SendAllTryEnd($"GET {path} HTTP/1.1\r\n"); - await ReceiveBadRequestResponse(connection, server.Context.DateHeaderValue); + await ReceiveBadRequestResponse(connection, "400 Bad Request", server.Context.DateHeaderValue); } } } - private async Task ReceiveBadRequestResponse(TestConnection connection, string expectedDateHeaderValue) + private async Task ReceiveBadRequestResponse(TestConnection connection, string expectedResponseStatusCode, string expectedDateHeaderValue) { await connection.ReceiveForcedEnd( - "HTTP/1.1 400 Bad Request", + $"HTTP/1.1 {expectedResponseStatusCode}", "Connection: close", $"Date: {expectedDateHeaderValue}", "Content-Length: 0", diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs index 6ef64cf30e..9214b5b1f7 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs @@ -291,7 +291,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests "", ""); await connection.ReceiveForcedEnd( - "HTTP/1.1 400 Bad Request", + "HTTP/1.1 431 Request Header Fields Too Large", "Connection: close", $"Date: {testContext.DateHeaderValue}", "Content-Length: 0", @@ -331,7 +331,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests "", ""); await connection.ReceiveForcedEnd( - "HTTP/1.1 400 Bad Request", + "HTTP/1.1 431 Request Header Fields Too Large", "Connection: close", $"Date: {testContext.DateHeaderValue}", "Content-Length: 0", diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameRequestHeadersTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameRequestHeadersTests.cs index 6f07dd90ac..44e421191d 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameRequestHeadersTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameRequestHeadersTests.cs @@ -1,3 +1,6 @@ +// 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.Text; @@ -243,8 +246,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests const string key = "\u00141ód\017c"; var encoding = Encoding.GetEncoding("iso-8859-1"); - Assert.Throws( + var exception = Assert.Throws( () => headers.Append(encoding.GetBytes(key), 0, encoding.GetByteCount(key), key)); + Assert.Equal(400, exception.StatusCode); } } } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs index 66c0941c2b..09bffeee85 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs @@ -187,7 +187,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [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 void ThrowsOnHeaderValueWithLineFolding(string rawHeaders) + public void TakeMessageHeadersThrowsOnHeaderValueWithLineFolding(string rawHeaders) { var trace = new KestrelTrace(new TestKestrelTrace()); var ltp = new LoggingThreadPool(trace); @@ -210,11 +210,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); Assert.Equal("Header value line folding not supported.", exception.Message); + Assert.Equal(400, exception.StatusCode); } } [Fact] - public void ThrowsOnHeaderValueWithLineFolding_CharacterNotAvailableOnFirstAttempt() + public void TakeMessageHeadersThrowsOnHeaderValueWithLineFolding_CharacterNotAvailableOnFirstAttempt() { var trace = new KestrelTrace(new TestKestrelTrace()); var ltp = new LoggingThreadPool(trace); @@ -241,6 +242,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); Assert.Equal("Header value line folding not supported.", exception.Message); + Assert.Equal(400, exception.StatusCode); } } @@ -250,7 +252,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [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 void ThrowsOnHeaderValueContainingCR(string rawHeaders) + public void TakeMessageHeadersThrowsOnHeaderValueContainingCR(string rawHeaders) { var trace = new KestrelTrace(new TestKestrelTrace()); var ltp = new LoggingThreadPool(trace); @@ -273,6 +275,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); Assert.Equal("Header value must not contain CR characters.", exception.Message); + Assert.Equal(400, exception.StatusCode); } } @@ -280,7 +283,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [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 void ThrowsOnHeaderLineMissingColon(string rawHeaders) + public void TakeMessageHeadersThrowsOnHeaderLineMissingColon(string rawHeaders) { var trace = new KestrelTrace(new TestKestrelTrace()); var ltp = new LoggingThreadPool(trace); @@ -303,6 +306,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); Assert.Equal("No ':' character found in header line.", exception.Message); + Assert.Equal(400, exception.StatusCode); } } @@ -311,7 +315,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [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 void ThrowsOnHeaderLineStartingWithWhitespace(string rawHeaders) + public void TakeMessageHeadersThrowsOnHeaderLineStartingWithWhitespace(string rawHeaders) { var trace = new KestrelTrace(new TestKestrelTrace()); var ltp = new LoggingThreadPool(trace); @@ -334,6 +338,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); Assert.Equal("Header line must not start with whitespace.", exception.Message); + Assert.Equal(400, exception.StatusCode); } } @@ -346,7 +351,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [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 void ThrowsOnWhitespaceInHeaderName(string rawHeaders) + public void TakeMessageHeadersThrowsOnWhitespaceInHeaderName(string rawHeaders) { var trace = new KestrelTrace(new TestKestrelTrace()); var ltp = new LoggingThreadPool(trace); @@ -369,6 +374,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); Assert.Equal("Whitespace is not allowed in header name.", exception.Message); + Assert.Equal(400, exception.StatusCode); } } @@ -376,7 +382,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [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 void ThrowsOnHeadersNotEndingInCRLFLine(string rawHeaders) + public void TakeMessageHeadersThrowsOnHeadersNotEndingInCRLFLine(string rawHeaders) { var trace = new KestrelTrace(new TestKestrelTrace()); var ltp = new LoggingThreadPool(trace); @@ -399,11 +405,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); Assert.Equal("Headers corrupted, invalid header sequence.", exception.Message); + Assert.Equal(400, exception.StatusCode); } } [Fact] - public void ThrowsWhenHeadersExceedTotalSizeLimit() + public void TakeMessageHeadersThrowsWhenHeadersExceedTotalSizeLimit() { var trace = new KestrelTrace(new TestKestrelTrace()); var ltp = new LoggingThreadPool(trace); @@ -432,11 +439,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); Assert.Equal("Request headers too long.", exception.Message); + Assert.Equal(431, exception.StatusCode); } } [Fact] - public void ThrowsWhenHeadersExceedCountLimit() + public void TakeMessageHeadersThrowsWhenHeadersExceedCountLimit() { var trace = new KestrelTrace(new TestKestrelTrace()); var ltp = new LoggingThreadPool(trace); @@ -465,6 +473,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); Assert.Equal("Request contains too many headers.", exception.Message); + Assert.Equal(431, exception.StatusCode); } } @@ -842,6 +851,107 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } } + [Fact] + public void TakeStartLineThrowsWhenTooLong() + { + var trace = new KestrelTrace(new TestKestrelTrace()); + var ltp = new LoggingThreadPool(trace); + using (var pool = new MemoryPool()) + using (var socketInput = new SocketInput(pool, ltp)) + { + var connectionContext = new ConnectionContext() + { + ConnectionControl = Mock.Of(), + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions() + { + Limits = + { + MaxRequestLineSize = "GET / HTTP/1.1\r\n".Length + } + }, + Log = trace + }; + var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); + + var requestLineBytes = Encoding.ASCII.GetBytes("GET /a HTTP/1.1\r\n"); + socketInput.IncomingData(requestLineBytes, 0, requestLineBytes.Length); + + var exception = Assert.Throws(() => frame.TakeStartLine(socketInput)); + Assert.Equal("Request line too long.", exception.Message); + Assert.Equal(414, exception.StatusCode); + } + } + + [Theory] + [InlineData("GET/HTTP/1.1\r\n", "No space character found after method in request line.")] + [InlineData(" / HTTP/1.1\r\n", "Missing method.")] + [InlineData("GET? / HTTP/1.1\r\n", "Invalid method.")] + [InlineData("GET /HTTP/1.1\r\n", "No space character found after target in request line.")] + [InlineData("GET /a?b=cHTTP/1.1\r\n", "No space character found after target in request line.")] + [InlineData("GET /a%20bHTTP/1.1\r\n", "No space character found after target in request line.")] + [InlineData("GET /a%20b?c=dHTTP/1.1\r\n", "No space character found after target in request line.")] + [InlineData("GET HTTP/1.1\r\n", "Missing request target.")] + [InlineData("GET / HTTP/1.1\n", "Missing CR in request line.")] + [InlineData("GET / \r\n", "Missing HTTP version.")] + [InlineData("GET / HTTP/1.1\ra\n", "Missing LF in request line.")] + public void TakeStartLineThrowsWhenInvalid(string requestLine, string expectedExceptionMessage) + { + var trace = new KestrelTrace(new TestKestrelTrace()); + var ltp = new LoggingThreadPool(trace); + using (var pool = new MemoryPool()) + using (var socketInput = new SocketInput(pool, ltp)) + { + var connectionContext = new ConnectionContext() + { + ConnectionControl = Mock.Of(), + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), + Log = trace + }; + var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); + + var requestLineBytes = Encoding.ASCII.GetBytes(requestLine); + socketInput.IncomingData(requestLineBytes, 0, requestLineBytes.Length); + + var exception = Assert.Throws(() => frame.TakeStartLine(socketInput)); + Assert.Equal(expectedExceptionMessage, exception.Message); + Assert.Equal(400, exception.StatusCode); + } + } + + [Fact] + public void TakeStartLineThrowsOnUnsupportedHttpVersion() + { + var trace = new KestrelTrace(new TestKestrelTrace()); + var ltp = new LoggingThreadPool(trace); + using (var pool = new MemoryPool()) + using (var socketInput = new SocketInput(pool, ltp)) + { + var connectionContext = new ConnectionContext() + { + ConnectionControl = Mock.Of(), + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), + Log = trace + }; + var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); + + var requestLineBytes = Encoding.ASCII.GetBytes("GET / HTTP/1.2\r\n"); + socketInput.IncomingData(requestLineBytes, 0, requestLineBytes.Length); + + var exception = Assert.Throws(() => frame.TakeStartLine(socketInput)); + Assert.Equal("Unrecognized HTTP version.", exception.Message); + Assert.Equal(505, exception.StatusCode); + } + } + [Fact] public void TakeMessageHeadersCallsConsumingCompleteWithFurthestExamined() {