Handle tokens in Transfer-Encoding header (#1181).

This commit is contained in:
Cesar Blum Silveira 2016-10-27 15:54:23 -07:00
parent cc05e36dc6
commit 29408956f9
12 changed files with 443 additions and 18 deletions

View File

@ -99,6 +99,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel
case RequestRejectionReason.UnrecognizedHTTPVersion:
ex = new BadHttpRequestException($"Unrecognized HTTP version: {value}", 505);
break;
case RequestRejectionReason.FinalTransferCodingNotChunked:
ex = new BadHttpRequestException($"Final transfer coding is not \"chunked\": \"{value}\"", 400);
break;
default:
ex = new BadHttpRequestException("Bad request.", 400);
break;

View File

@ -811,7 +811,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
var responseHeaders = FrameResponseHeaders;
var hasConnection = responseHeaders.HasConnection;
var connectionOptions = hasConnection ? FrameHeaders.ParseConnection(responseHeaders.HeaderConnection) : ConnectionOptions.None;
var connectionOptions = FrameHeaders.ParseConnection(responseHeaders.HeaderConnection);
var hasTransferEncoding = responseHeaders.HasTransferEncoding;
var transferCoding = FrameHeaders.GetFinalTransferCoding(responseHeaders.HeaderTransferEncoding);
var end = SocketOutput.ProducingStart();
@ -820,6 +822,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
_keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive;
}
// https://tools.ietf.org/html/rfc7230#section-3.3.1
// If any transfer coding other than
// chunked is applied to a response payload body, the sender MUST either
// apply chunked as the final transfer coding or terminate the message
// by closing the connection.
if (hasTransferEncoding && transferCoding != TransferCoding.Chunked)
{
_keepAlive = false;
}
// Set whether response can have body
_canHaveBody = StatusCanHaveBody(StatusCode) && Method != "HEAD";
@ -827,7 +839,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
// automatically for HEAD requests or 204, 205, 304 responses.
if (_canHaveBody)
{
if (!responseHeaders.HasTransferEncoding && !responseHeaders.HasContentLength)
if (!hasTransferEncoding && !responseHeaders.HasContentLength)
{
if (appCompleted && StatusCode != 101)
{
@ -856,12 +868,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
}
else
else if (hasTransferEncoding)
{
if (responseHeaders.HasTransferEncoding)
{
RejectNonBodyTransferEncodingResponse(appCompleted);
}
RejectNonBodyTransferEncodingResponse(appCompleted);
}
responseHeaders.SetReadOnly();

View File

@ -308,7 +308,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
ch++;
}
if (ch == tokenEnd || *ch == ',')
if (ch == tokenEnd)
{
connectionOptions |= ConnectionOptions.KeepAlive;
}
@ -329,7 +329,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
ch++;
}
if (ch == tokenEnd || *ch == ',')
if (ch == tokenEnd)
{
connectionOptions |= ConnectionOptions.Upgrade;
}
@ -348,7 +348,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
ch++;
}
if (ch == tokenEnd || *ch == ',')
if (ch == tokenEnd)
{
connectionOptions |= ConnectionOptions.Close;
}
@ -364,6 +364,68 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return connectionOptions;
}
public static unsafe TransferCoding GetFinalTransferCoding(StringValues transferEncoding)
{
var transferEncodingOptions = TransferCoding.None;
foreach (var value in transferEncoding)
{
fixed (char* ptr = value)
{
var ch = ptr;
var tokenEnd = ch;
var end = ch + value.Length;
while (ch < end)
{
while (tokenEnd < end && *tokenEnd != ',')
{
tokenEnd++;
}
while (ch < tokenEnd && *ch == ' ')
{
ch++;
}
var tokenLength = tokenEnd - ch;
if (tokenLength >= 7 && (*ch | 0x20) == 'c')
{
if ((*++ch | 0x20) == 'h' &&
(*++ch | 0x20) == 'u' &&
(*++ch | 0x20) == 'n' &&
(*++ch | 0x20) == 'k' &&
(*++ch | 0x20) == 'e' &&
(*++ch | 0x20) == 'd')
{
ch++;
while (ch < tokenEnd && *ch == ' ')
{
ch++;
}
if (ch == tokenEnd)
{
transferEncodingOptions = TransferCoding.Chunked;
}
}
}
if (tokenLength > 0 && ch != tokenEnd)
{
transferEncodingOptions = TransferCoding.Other;
}
tokenEnd++;
ch = tokenEnd;
}
}
}
return transferEncodingOptions;
}
private static void ThrowInvalidContentLengthException(string value)
{
throw new InvalidOperationException($"Invalid Content-Length: \"{value}\". Value must be a positive integral number.");

View File

@ -247,9 +247,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive;
}
var transferEncoding = headers.HeaderTransferEncoding.ToString();
if (transferEncoding.Length > 0)
var transferEncoding = headers.HeaderTransferEncoding;
if (transferEncoding.Count > 0)
{
var transferCoding = FrameHeaders.GetFinalTransferCoding(headers.HeaderTransferEncoding);
// https://tools.ietf.org/html/rfc7230#section-3.3.3
// If a Transfer-Encoding header field
// is present in a request and the chunked transfer coding is not
// the final encoding, the message body length cannot be determined
// reliably; the server MUST respond with the 400 (Bad Request)
// status code and then close the connection.
if (transferCoding != TransferCoding.Chunked)
{
context.RejectRequest(RequestRejectionReason.FinalTransferCodingNotChunked, transferEncoding.ToString());
}
return new ForChunkedEncoding(keepAlive, headers, context);
}

View File

@ -27,5 +27,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
MissingCRInHeaderLine,
TooManyHeaders,
RequestTimeout,
FinalTransferCodingNotChunked
}
}

View File

@ -0,0 +1,15 @@
// 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;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
[Flags]
public enum TransferCoding
{
None,
Chunked,
Other
}
}

View File

@ -848,6 +848,140 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
}
}
[Theory]
[InlineData("gzip")]
[InlineData("chunked, gzip")]
[InlineData("gzip")]
[InlineData("chunked, gzip")]
public async Task ConnectionClosedWhenChunkedIsNotFinalTransferCoding(string responseTransferEncoding)
{
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.Headers["Transfer-Encoding"] = responseTransferEncoding;
await httpContext.Response.WriteAsync("hello, world");
}, new TestServiceContext()))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
"Connection: close",
$"Date: {server.Context.DateHeaderValue}",
$"Transfer-Encoding: {responseTransferEncoding}",
"",
"hello, world");
}
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.0",
"Connection: keep-alive",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
"Connection: close",
$"Date: {server.Context.DateHeaderValue}",
$"Transfer-Encoding: {responseTransferEncoding}",
"",
"hello, world");
}
}
}
[Theory]
[InlineData("gzip")]
[InlineData("chunked, gzip")]
[InlineData("gzip")]
[InlineData("chunked, gzip")]
public async Task ConnectionClosedWhenChunkedIsNotFinalTransferCodingEvenIfConnectionKeepAliveSetInResponse(string responseTransferEncoding)
{
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.Headers["Connection"] = "keep-alive";
httpContext.Response.Headers["Transfer-Encoding"] = responseTransferEncoding;
await httpContext.Response.WriteAsync("hello, world");
}, new TestServiceContext()))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
"Connection: keep-alive",
$"Date: {server.Context.DateHeaderValue}",
$"Transfer-Encoding: {responseTransferEncoding}",
"",
"hello, world");
}
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.0",
"Connection: keep-alive",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
"Connection: keep-alive",
$"Date: {server.Context.DateHeaderValue}",
$"Transfer-Encoding: {responseTransferEncoding}",
"",
"hello, world");
}
}
}
[Theory]
[InlineData("chunked")]
[InlineData("gzip, chunked")]
public async Task ConnectionKeptAliveWhenChunkedIsFinalTransferCoding(string responseTransferEncoding)
{
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.Headers["Transfer-Encoding"] = responseTransferEncoding;
// App would have to chunk manually, but here we don't care
await httpContext.Response.WriteAsync("hello, world");
}, new TestServiceContext()))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
$"Transfer-Encoding: {responseTransferEncoding}",
"",
"hello, world");
// Make sure connection was kept open
await connection.SendEnd(
"GET / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
$"Transfer-Encoding: {responseTransferEncoding}",
"",
"hello, world");
}
}
}
public static TheoryData<string, StringValues, string> NullHeaderData
{
get

View File

@ -8,6 +8,7 @@ using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests
@ -512,6 +513,106 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
}
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ChunkedNotFinalTransferCodingResultsIn400(TestServiceContext testContext)
{
using (var server = new TestServer(httpContext =>
{
return TaskCache.CompletedTask;
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.SendAll(
"POST / HTTP/1.1",
"Transfer-Encoding: not-chunked",
"",
"C",
"hello, world",
"0",
"",
"");
await connection.ReceiveForcedEnd(
"HTTP/1.1 400 Bad Request",
"Connection: close",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
// Content-Length should not affect this
using (var connection = server.CreateConnection())
{
await connection.SendAll(
"POST / HTTP/1.1",
"Transfer-Encoding: not-chunked",
"Content-Length: 22",
"",
"C",
"hello, world",
"0",
"",
"");
await connection.ReceiveForcedEnd(
"HTTP/1.1 400 Bad Request",
"Connection: close",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
using (var connection = server.CreateConnection())
{
await connection.SendAll(
"POST / HTTP/1.1",
"Transfer-Encoding: chunked, not-chunked",
"",
"C",
"hello, world",
"0",
"",
"");
await connection.ReceiveForcedEnd(
"HTTP/1.1 400 Bad Request",
"Connection: close",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
// Content-Length should not affect this
using (var connection = server.CreateConnection())
{
await connection.SendAll(
"POST / HTTP/1.1",
"Transfer-Encoding: chunked, not-chunked",
"Content-Length: 22",
"",
"C",
"hello, world",
"0",
"",
"");
await connection.ReceiveForcedEnd(
"HTTP/1.1 400 Bad Request",
"Connection: close",
$"Date: {testContext.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
}
}
}
}

View File

@ -10,6 +10,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
public class FrameHeadersTests
{
[Theory]
[InlineData("", ConnectionOptions.None)]
[InlineData(",", ConnectionOptions.None)]
[InlineData(" ,", ConnectionOptions.None)]
[InlineData(" , ", ConnectionOptions.None)]
[InlineData(",,", ConnectionOptions.None)]
[InlineData(" ,,", ConnectionOptions.None)]
[InlineData(",, ", ConnectionOptions.None)]
[InlineData(" , ,", ConnectionOptions.None)]
[InlineData(" , ,", ConnectionOptions.None)]
[InlineData(" , , ", ConnectionOptions.None)]
[InlineData("keep-alive", ConnectionOptions.KeepAlive)]
[InlineData("keep-alive, upgrade", ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade)]
[InlineData("keep-alive,upgrade", ConnectionOptions.KeepAlive | ConnectionOptions.Upgrade)]
@ -123,10 +133,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[InlineData("up2rade ,", ConnectionOptions.None)]
[InlineData("c2ose ,", ConnectionOptions.None)]
[InlineData("cl2se ,", ConnectionOptions.None)]
public void TestParseConnection(string connection, ConnectionOptions expectedConnectionOptionss)
public void TestParseConnection(string connection, ConnectionOptions expectedConnectionOptions)
{
var connectionOptions = FrameHeaders.ParseConnection(connection);
Assert.Equal(expectedConnectionOptionss, connectionOptions);
Assert.Equal(expectedConnectionOptions, connectionOptions);
}
[Theory]
@ -145,11 +155,73 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[InlineData("", "close", ConnectionOptions.Close)]
[InlineData("close", "upgrade", ConnectionOptions.Close | ConnectionOptions.Upgrade)]
[InlineData("upgrade", "close", ConnectionOptions.Close | ConnectionOptions.Upgrade)]
public void TestParseConnectionMultipleValues(string value1, string value2, ConnectionOptions expectedConnectionOptionss)
public void TestParseConnectionMultipleValues(string value1, string value2, ConnectionOptions expectedConnectionOptions)
{
var connection = new StringValues(new[] { value1, value2 });
var connectionOptions = FrameHeaders.ParseConnection(connection);
Assert.Equal(expectedConnectionOptionss, connectionOptions);
Assert.Equal(expectedConnectionOptions, connectionOptions);
}
[Theory]
[InlineData("", TransferCoding.None)]
[InlineData(",,", TransferCoding.None)]
[InlineData(" ,,", TransferCoding.None)]
[InlineData(",, ", TransferCoding.None)]
[InlineData(" , ,", TransferCoding.None)]
[InlineData(" , ,", TransferCoding.None)]
[InlineData(" , , ", TransferCoding.None)]
[InlineData("chunked,", TransferCoding.Chunked)]
[InlineData("chunked,,", TransferCoding.Chunked)]
[InlineData(",chunked", TransferCoding.Chunked)]
[InlineData(",,chunked", TransferCoding.Chunked)]
[InlineData("chunked, ", TransferCoding.Chunked)]
[InlineData("chunked, ,", TransferCoding.Chunked)]
[InlineData("chunked, , ", TransferCoding.Chunked)]
[InlineData("chunked ,", TransferCoding.Chunked)]
[InlineData(",chunked", TransferCoding.Chunked)]
[InlineData(", chunked", TransferCoding.Chunked)]
[InlineData(",,chunked", TransferCoding.Chunked)]
[InlineData(", ,chunked", TransferCoding.Chunked)]
[InlineData(",, chunked", TransferCoding.Chunked)]
[InlineData(", , chunked", TransferCoding.Chunked)]
[InlineData("chunked, gzip", TransferCoding.Other)]
[InlineData("chunked,compress", TransferCoding.Other)]
[InlineData("deflate, chunked", TransferCoding.Chunked)]
[InlineData("gzip,chunked", TransferCoding.Chunked)]
[InlineData("compress,,chunked", TransferCoding.Chunked)]
[InlineData("chunkedchunked", TransferCoding.Other)]
[InlineData("chunked2", TransferCoding.Other)]
[InlineData("chunked 2", TransferCoding.Other)]
[InlineData("2chunked", TransferCoding.Other)]
[InlineData("c2unked", TransferCoding.Other)]
[InlineData("ch2nked", TransferCoding.Other)]
[InlineData("chunked 2, gzip", TransferCoding.Other)]
[InlineData("chunked2, gzip", TransferCoding.Other)]
[InlineData("gzip, chunked 2", TransferCoding.Other)]
[InlineData("gzip, chunked2", TransferCoding.Other)]
public void TestParseTransferEncoding(string transferEncoding, TransferCoding expectedTransferEncodingOptions)
{
var transferEncodingOptions = FrameHeaders.GetFinalTransferCoding(transferEncoding);
Assert.Equal(expectedTransferEncodingOptions, transferEncodingOptions);
}
[Theory]
[InlineData("chunked", "gzip", TransferCoding.Other)]
[InlineData("compress", "chunked", TransferCoding.Chunked)]
[InlineData("chunked", "", TransferCoding.Chunked)]
[InlineData("", "chunked", TransferCoding.Chunked)]
[InlineData("chunked, deflate", "", TransferCoding.Other)]
[InlineData("gzip, chunked", "", TransferCoding.Chunked)]
[InlineData("", "chunked, compress", TransferCoding.Other)]
[InlineData("", "compress, chunked", TransferCoding.Chunked)]
[InlineData("", "", TransferCoding.None)]
[InlineData("deflate", "", TransferCoding.Other)]
[InlineData("", "gzip", TransferCoding.Other)]
public void TestParseTransferEncodingMultipleValues(string value1, string value2, TransferCoding expectedTransferEncodingOptions)
{
var transferEncoding = new StringValues(new[] { value1, value2 });
var transferEncodingOptions = FrameHeaders.GetFinalTransferCoding(transferEncoding);
Assert.Equal(expectedTransferEncodingOptions, transferEncodingOptions);
}
}
}

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers;
using Microsoft.Extensions.Internal;
@ -189,6 +190,19 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
}
[Fact]
public void ForThrowsWhenFinalTransferCodingIsNotChunked()
{
using (var input = new TestInput())
{
var ex = Assert.Throws<BadHttpRequestException>(() =>
MessageBody.For(HttpVersion.Http10, new FrameRequestHeaders { HeaderTransferEncoding = "chunked, not-chunked" }, input.FrameContext));
Assert.Equal(400, ex.StatusCode);
Assert.Equal("Final transfer coding is not \"chunked\": \"chunked, not-chunked\"", ex.Message);
}
}
public static IEnumerable<object[]> StreamData => new[]
{
new object[] { new ThrowOnWriteSynchronousStream() },

View File

@ -38,6 +38,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
var context = new Frame<object>(null, connectionContext);
FrameContext = context;
FrameContext.FrameControl = this;
FrameContext.ConnectionContext.ListenerContext.ServiceContext.Log = trace;
_memoryPool = new MemoryPool();
FrameContext.SocketInput = new SocketInput(_memoryPool, ltp);

View File

@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Testing
{
await Receive(lines);
var ch = new char[128];
var count = await _reader.ReadAsync(ch, 0, 128);
var count = await _reader.ReadAsync(ch, 0, 128).TimeoutAfter(TimeSpan.FromMinutes(1));
var text = new string(ch, 0, count);
Assert.Equal("", text);
}
@ -127,7 +127,7 @@ namespace Microsoft.AspNetCore.Testing
try
{
var ch = new char[128];
var count = await _reader.ReadAsync(ch, 0, 128);
var count = await _reader.ReadAsync(ch, 0, 128).TimeoutAfter(TimeSpan.FromMinutes(1));
var text = new string(ch, 0, count);
Assert.Equal("", text);
}