207 lines
8.3 KiB
C#
207 lines
8.3 KiB
C#
// 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.Server.Kestrel.Infrastructure;
|
||
using Xunit;
|
||
|
||
namespace Microsoft.AspNetCore.Server.KestrelTests
|
||
{
|
||
public class BadHttpRequestTests
|
||
{
|
||
// Don't send more data than necessary to fail, otherwise the test throws trying to
|
||
// send data after the server has already closed the connection. This would cause the
|
||
// test to fail on Windows, due to a winsock limitation: after the error when trying
|
||
// to write to the socket closed by the server, winsock disposes all resources used
|
||
// by that socket. The test then fails when we try to read the expected response
|
||
// from the server because, although it would have been buffered, it got discarded
|
||
// by winsock on the send() error.
|
||
// The solution for this is for the client to always try to receive before doing
|
||
// any sends, that way it can detect that the connection has been closed by the server
|
||
// and not try to send() on the closed connection, triggering the error that would cause
|
||
// any buffered received data to be lost.
|
||
// We do not deem necessary to mitigate this issue in TestConnection, since it would only
|
||
// be ensuring that we have a properly implemented HTTP client that can handle the
|
||
// winsock issue. There is nothing to be verified in Kestrel in this situation.
|
||
[Theory]
|
||
// Incomplete request lines
|
||
[InlineData("G")]
|
||
[InlineData("GE")]
|
||
[InlineData("GET")]
|
||
[InlineData("GET ")]
|
||
[InlineData("GET /")]
|
||
[InlineData("GET / ")]
|
||
[InlineData("GET / H")]
|
||
[InlineData("GET / HT")]
|
||
[InlineData("GET / HTT")]
|
||
[InlineData("GET / HTTP")]
|
||
[InlineData("GET / HTTP/")]
|
||
[InlineData("GET / HTTP/1")]
|
||
[InlineData("GET / HTTP/1.")]
|
||
[InlineData("GET / HTTP/1.1")]
|
||
[InlineData("GET / HTTP/1.1\r")]
|
||
[InlineData("GET / HTTP/1.0")]
|
||
[InlineData("GET / HTTP/1.0\r")]
|
||
// Missing method
|
||
[InlineData(" ")]
|
||
// Missing second space
|
||
[InlineData("/ ")] // This fails trying to read the '/' because that's invalid for an HTTP method
|
||
[InlineData("GET /\r\n")]
|
||
// Missing target
|
||
[InlineData("GET ")]
|
||
// Missing version
|
||
[InlineData("GET / \r")]
|
||
// Missing CR
|
||
[InlineData("GET / \n")]
|
||
// Unrecognized HTTP version
|
||
[InlineData("GET / http/1.0\r")]
|
||
[InlineData("GET / http/1.1\r")]
|
||
[InlineData("GET / HTTP/1.1 \r")]
|
||
[InlineData("GET / HTTP/1.1a\r")]
|
||
[InlineData("GET / HTTP/1.0\n\r")]
|
||
[InlineData("GET / HTTP/1.2\r")]
|
||
[InlineData("GET / HTTP/3.0\r")]
|
||
[InlineData("GET / H\r")]
|
||
[InlineData("GET / HTTP/1.\r")]
|
||
[InlineData("GET / hello\r")]
|
||
[InlineData("GET / 8charact\r")]
|
||
// Missing LF
|
||
[InlineData("GET / HTTP/1.0\rA")]
|
||
// 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 TestBadRequestLines(string request)
|
||
{
|
||
using (var server = new TestServer(context => TaskUtilities.CompletedTask))
|
||
{
|
||
using (var connection = server.CreateConnection())
|
||
{
|
||
await connection.SendAllEnd(request);
|
||
await ReceiveBadRequestResponse(connection);
|
||
}
|
||
}
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData(" ")]
|
||
[InlineData("GET ")]
|
||
[InlineData("GET / HTTP/1.2\r")]
|
||
[InlineData("GET / HTTP/1.0\rA")]
|
||
// Bad HTTP Methods (invalid according to RFC)
|
||
[InlineData("( ")]
|
||
[InlineData(") ")]
|
||
[InlineData("< ")]
|
||
[InlineData("> ")]
|
||
[InlineData("@ ")]
|
||
[InlineData(", ")]
|
||
[InlineData("; ")]
|
||
[InlineData(": ")]
|
||
[InlineData("\\ ")]
|
||
[InlineData("\" ")]
|
||
[InlineData("/ ")]
|
||
[InlineData("[ ")]
|
||
[InlineData("] ")]
|
||
[InlineData("? ")]
|
||
[InlineData("= ")]
|
||
[InlineData("{ ")]
|
||
[InlineData("} ")]
|
||
[InlineData("get@ ")]
|
||
[InlineData("post= ")]
|
||
public async Task ServerClosesConnectionAsSoonAsBadRequestLineIsDetected(string request)
|
||
{
|
||
using (var server = new TestServer(context => TaskUtilities.CompletedTask))
|
||
{
|
||
using (var connection = server.CreateConnection())
|
||
{
|
||
await connection.Send(request);
|
||
await ReceiveBadRequestResponse(connection);
|
||
}
|
||
}
|
||
}
|
||
|
||
[Theory]
|
||
// Missing final CRLF
|
||
[InlineData("Header-1: value1\r\nHeader-2: value2\r\n")]
|
||
// 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 => TaskUtilities.CompletedTask))
|
||
{
|
||
using (var connection = server.CreateConnection())
|
||
{
|
||
await connection.SendAllEnd($"GET / HTTP/1.1\r\n{rawHeaders}");
|
||
await ReceiveBadRequestResponse(connection);
|
||
}
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public async Task BadRequestWhenNameHeaderNamesContainsNonASCIICharacters()
|
||
{
|
||
using (var server = new TestServer(context => { return Task.FromResult(0); }))
|
||
{
|
||
using (var connection = server.CreateConnection())
|
||
{
|
||
await connection.SendEnd(
|
||
"GET / HTTP/1.1",
|
||
"H<><48>d<EFBFBD>r: value",
|
||
"",
|
||
"");
|
||
await ReceiveBadRequestResponse(connection);
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task ReceiveBadRequestResponse(TestConnection connection)
|
||
{
|
||
await connection.Receive(
|
||
"HTTP/1.1 400 Bad Request",
|
||
"");
|
||
await connection.Receive(
|
||
"Connection: close",
|
||
"");
|
||
await connection.ReceiveForcedEnd(
|
||
$"Date: {connection.Server.Context.DateHeaderValue}",
|
||
"Content-Length: 0",
|
||
"",
|
||
"");
|
||
}
|
||
}
|
||
} |