Send 'Connection: close' in all 400 responses to HTTP/1.1 requests (#840).
This commit is contained in:
parent
3e841ccba1
commit
8c8ee150f7
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
private static readonly byte[] _bytesConnectionClose = Encoding.ASCII.GetBytes("\r\nConnection: close");
|
private static readonly byte[] _bytesConnectionClose = Encoding.ASCII.GetBytes("\r\nConnection: close");
|
||||||
private static readonly byte[] _bytesConnectionKeepAlive = Encoding.ASCII.GetBytes("\r\nConnection: keep-alive");
|
private static readonly byte[] _bytesConnectionKeepAlive = Encoding.ASCII.GetBytes("\r\nConnection: keep-alive");
|
||||||
private static readonly byte[] _bytesTransferEncodingChunked = Encoding.ASCII.GetBytes("\r\nTransfer-Encoding: chunked");
|
private static readonly byte[] _bytesTransferEncodingChunked = Encoding.ASCII.GetBytes("\r\nTransfer-Encoding: chunked");
|
||||||
private static readonly byte[] _bytesHttpVersion1_1 = Encoding.ASCII.GetBytes("HTTP/1.1 ");
|
private static readonly byte[] _bytesHttpVersion11 = Encoding.ASCII.GetBytes("HTTP/1.1 ");
|
||||||
private static readonly byte[] _bytesContentLengthZero = Encoding.ASCII.GetBytes("\r\nContent-Length: 0");
|
private static readonly byte[] _bytesContentLengthZero = Encoding.ASCII.GetBytes("\r\nContent-Length: 0");
|
||||||
private static readonly byte[] _bytesSpace = Encoding.ASCII.GetBytes(" ");
|
private static readonly byte[] _bytesSpace = Encoding.ASCII.GetBytes(" ");
|
||||||
private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n");
|
private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n");
|
||||||
|
|
@ -87,29 +87,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_httpVersion == HttpVersionType.Http1_1)
|
if (_httpVersion == HttpVersionType.Http11)
|
||||||
{
|
{
|
||||||
return "HTTP/1.1";
|
return "HTTP/1.1";
|
||||||
}
|
}
|
||||||
if (_httpVersion == HttpVersionType.Http1_0)
|
if (_httpVersion == HttpVersionType.Http10)
|
||||||
{
|
{
|
||||||
return "HTTP/1.0";
|
return "HTTP/1.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (value == "HTTP/1.1")
|
if (value == "HTTP/1.1")
|
||||||
{
|
{
|
||||||
_httpVersion = HttpVersionType.Http1_1;
|
_httpVersion = HttpVersionType.Http11;
|
||||||
}
|
}
|
||||||
else if (value == "HTTP/1.0")
|
else if (value == "HTTP/1.0")
|
||||||
{
|
{
|
||||||
_httpVersion = HttpVersionType.Http1_0;
|
_httpVersion = HttpVersionType.Http10;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_httpVersion = HttpVersionType.Unknown;
|
_httpVersion = HttpVersionType.Unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -229,7 +230,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
PathBase = null;
|
PathBase = null;
|
||||||
Path = null;
|
Path = null;
|
||||||
QueryString = null;
|
QueryString = null;
|
||||||
_httpVersion = HttpVersionType.Unknown;
|
_httpVersion = HttpVersionType.Unset;
|
||||||
StatusCode = 200;
|
StatusCode = 200;
|
||||||
ReasonPhrase = null;
|
ReasonPhrase = null;
|
||||||
|
|
||||||
|
|
@ -512,7 +513,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
}
|
}
|
||||||
|
|
||||||
StringValues expect;
|
StringValues expect;
|
||||||
if (_httpVersion == HttpVersionType.Http1_1 &&
|
if (_httpVersion == HttpVersionType.Http11 &&
|
||||||
RequestHeaders.TryGetValue("Expect", out expect) &&
|
RequestHeaders.TryGetValue("Expect", out expect) &&
|
||||||
(expect.FirstOrDefault() ?? "").Equals("100-continue", StringComparison.OrdinalIgnoreCase))
|
(expect.FirstOrDefault() ?? "").Equals("100-continue", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|
@ -595,6 +596,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
{
|
{
|
||||||
// 400 Bad Request
|
// 400 Bad Request
|
||||||
StatusCode = 400;
|
StatusCode = 400;
|
||||||
|
_keepAlive = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -712,7 +714,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
//
|
//
|
||||||
// A server MUST NOT send a response containing Transfer-Encoding unless the corresponding
|
// A server MUST NOT send a response containing Transfer-Encoding unless the corresponding
|
||||||
// request indicates HTTP/1.1 (or later).
|
// request indicates HTTP/1.1 (or later).
|
||||||
if (_httpVersion == HttpVersionType.Http1_1)
|
if (_httpVersion == HttpVersionType.Http11)
|
||||||
{
|
{
|
||||||
_autoChunk = true;
|
_autoChunk = true;
|
||||||
responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked);
|
responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked);
|
||||||
|
|
@ -724,16 +726,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_keepAlive == false && responseHeaders.HasConnection == false && _httpVersion == HttpVersionType.Http1_1)
|
if (!_keepAlive && !responseHeaders.HasConnection && _httpVersion != HttpVersionType.Http10)
|
||||||
{
|
{
|
||||||
responseHeaders.SetRawConnection("close", _bytesConnectionClose);
|
responseHeaders.SetRawConnection("close", _bytesConnectionClose);
|
||||||
}
|
}
|
||||||
else if (_keepAlive && responseHeaders.HasConnection == false && _httpVersion == HttpVersionType.Http1_0)
|
else if (_keepAlive && !responseHeaders.HasConnection && _httpVersion == HttpVersionType.Http10)
|
||||||
{
|
{
|
||||||
responseHeaders.SetRawConnection("keep-alive", _bytesConnectionKeepAlive);
|
responseHeaders.SetRawConnection("keep-alive", _bytesConnectionKeepAlive);
|
||||||
}
|
}
|
||||||
|
|
||||||
end.CopyFrom(_bytesHttpVersion1_1);
|
end.CopyFrom(_bytesHttpVersion11);
|
||||||
end.CopyFrom(statusBytes);
|
end.CopyFrom(statusBytes);
|
||||||
responseHeaders.CopyTo(ref end);
|
responseHeaders.CopyTo(ref end);
|
||||||
end.CopyFrom(_bytesEndHeaders, 0, _bytesEndHeaders.Length);
|
end.CopyFrom(_bytesEndHeaders, 0, _bytesEndHeaders.Length);
|
||||||
|
|
@ -838,13 +840,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
}
|
}
|
||||||
else if (httpVersion != "HTTP/1.0" && httpVersion != "HTTP/1.1")
|
else if (httpVersion != "HTTP/1.0" && httpVersion != "HTTP/1.1")
|
||||||
{
|
{
|
||||||
RejectRequest("Malformed request.");
|
RejectRequest("Unrecognized HTTP version.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HttpVersion must be set here to send correct response when request is rejected
|
|
||||||
HttpVersion = httpVersion;
|
|
||||||
|
|
||||||
scan.Take();
|
scan.Take();
|
||||||
var next = scan.Take();
|
var next = scan.Take();
|
||||||
if (next == -1)
|
if (next == -1)
|
||||||
|
|
@ -879,6 +878,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
Method = method;
|
Method = method;
|
||||||
RequestUri = requestUrlPath;
|
RequestUri = requestUrlPath;
|
||||||
QueryString = queryString;
|
QueryString = queryString;
|
||||||
|
HttpVersion = httpVersion;
|
||||||
|
|
||||||
bool caseMatches;
|
bool caseMatches;
|
||||||
|
|
||||||
|
|
@ -1099,9 +1099,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||||
|
|
||||||
private enum HttpVersionType
|
private enum HttpVersionType
|
||||||
{
|
{
|
||||||
Unknown = -1,
|
Unset = -1,
|
||||||
Http1_0 = 0,
|
Http10 = 0,
|
||||||
Http1_1 = 1
|
Http11 = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
protected enum RequestLineStatus
|
protected enum RequestLineStatus
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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 System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -9,64 +8,107 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||||
{
|
{
|
||||||
public class BadHttpRequestTests
|
public class BadHttpRequestTests
|
||||||
{
|
{
|
||||||
[Theory(Skip = "This test fails intermittently and needs to be investigated.")]
|
// Don't send more data than necessary to fail, otherwise the test throws trying to
|
||||||
[InlineData("/ HTTP/1.1\r\n\r\n")]
|
// send data after the server has already closed the connection. This would cause the
|
||||||
[InlineData(" / HTTP/1.1\r\n\r\n")]
|
// test to fail on Windows, due to a winsock limitation: after the error when trying
|
||||||
[InlineData(" / HTTP/1.1\r\n\r\n")]
|
// to write to the socket closed by the server, winsock disposes all resources used
|
||||||
[InlineData("GET / HTTP/1.1\r\n\r\n")]
|
// by that socket. The test then fails when we try to read the expected response
|
||||||
[InlineData("GET / HTTP/1.1\r\n\r\n")]
|
// from the server because, although it would have been buffered, it got discarded
|
||||||
[InlineData("GET HTTP/1.1\r\n\r\n")]
|
// 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 / ")]
|
[InlineData("GET / ")]
|
||||||
[InlineData("GET / H")]
|
[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.")]
|
||||||
|
[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("/ HTTP/1.1\r\n\r\n")]
|
||||||
[InlineData("GET /\r\n")]
|
[InlineData("GET /\r\n")]
|
||||||
[InlineData("GET / \r\n")]
|
// Missing target
|
||||||
|
[InlineData("GET ")]
|
||||||
|
// Missing version
|
||||||
|
[InlineData("GET / \r")]
|
||||||
|
// Missing CR
|
||||||
[InlineData("GET / \n")]
|
[InlineData("GET / \n")]
|
||||||
[InlineData("GET / http/1.0\r\n\r\n")]
|
// Unrecognized HTTP version
|
||||||
[InlineData("GET / http/1.1\r\n\r\n")]
|
[InlineData("GET / http/1.0\r")]
|
||||||
[InlineData("GET / HTTP/1.1 \r\n\r\n")]
|
[InlineData("GET / http/1.1\r")]
|
||||||
[InlineData("GET / HTTP/1.1a\r\n\r\n")]
|
[InlineData("GET / HTTP/1.1 \r")]
|
||||||
[InlineData("GET / HTTP/1.0\n\r\n")]
|
[InlineData("GET / HTTP/1.1a\r")]
|
||||||
[InlineData("GET / HTTP/3.0\r\n\r\n")]
|
[InlineData("GET / HTTP/1.0\n\r")]
|
||||||
[InlineData("GET / H\r\n\r\n")]
|
[InlineData("GET / HTTP/1.2\r")]
|
||||||
[InlineData("GET / HTTP/1.\r\n\r\n")]
|
[InlineData("GET / HTTP/3.0\r")]
|
||||||
[InlineData("GET / hello\r\n\r\n")]
|
[InlineData("GET / H\r")]
|
||||||
[InlineData("GET / 8charact\r\n\r\n")]
|
[InlineData("GET / HTTP/1.\r")]
|
||||||
public async Task TestBadRequests(string request)
|
[InlineData("GET / hello\r")]
|
||||||
|
[InlineData("GET / 8charact\r")]
|
||||||
|
// Missing LF
|
||||||
|
[InlineData("GET / HTTP/1.0\rA")]
|
||||||
|
public async Task TestBadRequestLines(string request)
|
||||||
{
|
{
|
||||||
using (var server = new TestServer(context => { return Task.FromResult(0); }))
|
using (var server = new TestServer(context => { return Task.FromResult(0); }))
|
||||||
{
|
{
|
||||||
using (var connection = new TestConnection(server.Port))
|
using (var connection = new TestConnection(server.Port))
|
||||||
{
|
{
|
||||||
var receiveTask = Task.Run(async () =>
|
await connection.SendEnd(request);
|
||||||
{
|
await ReceiveBadRequestResponse(connection);
|
||||||
await connection.Receive(
|
|
||||||
"HTTP/1.1 400 Bad Request",
|
|
||||||
"");
|
|
||||||
await connection.ReceiveStartsWith("Date: ");
|
|
||||||
await connection.ReceiveForcedEnd(
|
|
||||||
"Content-Length: 0",
|
|
||||||
"Server: Kestrel",
|
|
||||||
"",
|
|
||||||
"");
|
|
||||||
});
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await connection.SendEnd(request).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// TestConnection.SendEnd will start throwing while sending characters
|
|
||||||
// in cases where the server rejects the request as soon as it
|
|
||||||
// determines the request line is malformed, even though there
|
|
||||||
// are more characters following.
|
|
||||||
}
|
|
||||||
|
|
||||||
await receiveTask;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(" ")]
|
||||||
|
[InlineData("GET ")]
|
||||||
|
[InlineData("GET / HTTP/1.2\r")]
|
||||||
|
[InlineData("GET / HTTP/1.0\rA")]
|
||||||
|
public async Task ServerClosesConnectionAsSoonAsBadRequestLineIsDetected(string request)
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(context => { return Task.FromResult(0); }))
|
||||||
|
{
|
||||||
|
using (var connection = new TestConnection(server.Port))
|
||||||
|
{
|
||||||
|
await connection.Send(request);
|
||||||
|
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.ReceiveStartsWith("Date: ");
|
||||||
|
await connection.ReceiveEnd(
|
||||||
|
"Content-Length: 0",
|
||||||
|
"Server: Kestrel",
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -371,6 +371,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||||
|
|
||||||
await connection.Receive(
|
await connection.Receive(
|
||||||
"HTTP/1.1 400 Bad Request",
|
"HTTP/1.1 400 Bad Request",
|
||||||
|
"Connection: close",
|
||||||
"");
|
"");
|
||||||
await connection.ReceiveStartsWith("Date:");
|
await connection.ReceiveStartsWith("Date:");
|
||||||
await connection.ReceiveForcedEnd(
|
await connection.ReceiveForcedEnd(
|
||||||
|
|
@ -415,6 +416,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||||
|
|
||||||
await connection.Receive(
|
await connection.Receive(
|
||||||
"HTTP/1.1 400 Bad Request",
|
"HTTP/1.1 400 Bad Request",
|
||||||
|
"Connection: close",
|
||||||
"");
|
"");
|
||||||
await connection.ReceiveStartsWith("Date:");
|
await connection.ReceiveStartsWith("Date:");
|
||||||
await connection.ReceiveForcedEnd(
|
await connection.ReceiveForcedEnd(
|
||||||
|
|
|
||||||
|
|
@ -724,21 +724,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||||
{
|
{
|
||||||
using (var server = new TestServer(AppChunked, testContext))
|
using (var server = new TestServer(AppChunked, testContext))
|
||||||
{
|
{
|
||||||
using (var connection = new TestConnection(server.Port))
|
|
||||||
{
|
|
||||||
await connection.SendEnd(
|
|
||||||
"GET /");
|
|
||||||
await connection.Receive(
|
|
||||||
"HTTP/1.1 400 Bad Request",
|
|
||||||
"");
|
|
||||||
await connection.ReceiveStartsWith("Date:");
|
|
||||||
await connection.ReceiveForcedEnd(
|
|
||||||
"Content-Length: 0",
|
|
||||||
"Server: Kestrel",
|
|
||||||
"",
|
|
||||||
"");
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var connection = new TestConnection(server.Port))
|
using (var connection = new TestConnection(server.Port))
|
||||||
{
|
{
|
||||||
await connection.SendEnd(
|
await connection.SendEnd(
|
||||||
|
|
@ -750,6 +735,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||||
"Content-Length: 0",
|
"Content-Length: 0",
|
||||||
"",
|
"",
|
||||||
"HTTP/1.1 400 Bad Request",
|
"HTTP/1.1 400 Bad Request",
|
||||||
|
"Connection: close",
|
||||||
"");
|
"");
|
||||||
await connection.ReceiveStartsWith("Date:");
|
await connection.ReceiveStartsWith("Date:");
|
||||||
await connection.ReceiveForcedEnd(
|
await connection.ReceiveForcedEnd(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue