Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

Commit 2940895

Browse files
author
Cesar Blum Silveira
committed
Handle tokens in Transfer-Encoding header (#1181).
1 parent cc05e36 commit 2940895

File tree

12 files changed

+443
-18
lines changed

12 files changed

+443
-18
lines changed

src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ internal static BadHttpRequestException GetException(RequestRejectionReason reas
9999
case RequestRejectionReason.UnrecognizedHTTPVersion:
100100
ex = new BadHttpRequestException($"Unrecognized HTTP version: {value}", 505);
101101
break;
102+
case RequestRejectionReason.FinalTransferCodingNotChunked:
103+
ex = new BadHttpRequestException($"Final transfer coding is not \"chunked\": \"{value}\"", 400);
104+
break;
102105
default:
103106
ex = new BadHttpRequestException("Bad request.", 400);
104107
break;

src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,9 @@ private void CreateResponseHeader(
811811
{
812812
var responseHeaders = FrameResponseHeaders;
813813
var hasConnection = responseHeaders.HasConnection;
814-
var connectionOptions = hasConnection ? FrameHeaders.ParseConnection(responseHeaders.HeaderConnection) : ConnectionOptions.None;
814+
var connectionOptions = FrameHeaders.ParseConnection(responseHeaders.HeaderConnection);
815+
var hasTransferEncoding = responseHeaders.HasTransferEncoding;
816+
var transferCoding = FrameHeaders.GetFinalTransferCoding(responseHeaders.HeaderTransferEncoding);
815817

816818
var end = SocketOutput.ProducingStart();
817819

@@ -820,14 +822,24 @@ private void CreateResponseHeader(
820822
_keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive;
821823
}
822824

825+
// https://tools.ietf.org/html/rfc7230#section-3.3.1
826+
// If any transfer coding other than
827+
// chunked is applied to a response payload body, the sender MUST either
828+
// apply chunked as the final transfer coding or terminate the message
829+
// by closing the connection.
830+
if (hasTransferEncoding && transferCoding != TransferCoding.Chunked)
831+
{
832+
_keepAlive = false;
833+
}
834+
823835
// Set whether response can have body
824836
_canHaveBody = StatusCanHaveBody(StatusCode) && Method != "HEAD";
825837

826838
// Don't set the Content-Length or Transfer-Encoding headers
827839
// automatically for HEAD requests or 204, 205, 304 responses.
828840
if (_canHaveBody)
829841
{
830-
if (!responseHeaders.HasTransferEncoding && !responseHeaders.HasContentLength)
842+
if (!hasTransferEncoding && !responseHeaders.HasContentLength)
831843
{
832844
if (appCompleted && StatusCode != 101)
833845
{
@@ -856,12 +868,9 @@ private void CreateResponseHeader(
856868
}
857869
}
858870
}
859-
else
871+
else if (hasTransferEncoding)
860872
{
861-
if (responseHeaders.HasTransferEncoding)
862-
{
863-
RejectNonBodyTransferEncodingResponse(appCompleted);
864-
}
873+
RejectNonBodyTransferEncodingResponse(appCompleted);
865874
}
866875

867876
responseHeaders.SetReadOnly();

src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ public static unsafe ConnectionOptions ParseConnection(StringValues connection)
308308
ch++;
309309
}
310310

311-
if (ch == tokenEnd || *ch == ',')
311+
if (ch == tokenEnd)
312312
{
313313
connectionOptions |= ConnectionOptions.KeepAlive;
314314
}
@@ -329,7 +329,7 @@ public static unsafe ConnectionOptions ParseConnection(StringValues connection)
329329
ch++;
330330
}
331331

332-
if (ch == tokenEnd || *ch == ',')
332+
if (ch == tokenEnd)
333333
{
334334
connectionOptions |= ConnectionOptions.Upgrade;
335335
}
@@ -348,7 +348,7 @@ public static unsafe ConnectionOptions ParseConnection(StringValues connection)
348348
ch++;
349349
}
350350

351-
if (ch == tokenEnd || *ch == ',')
351+
if (ch == tokenEnd)
352352
{
353353
connectionOptions |= ConnectionOptions.Close;
354354
}
@@ -364,6 +364,68 @@ public static unsafe ConnectionOptions ParseConnection(StringValues connection)
364364
return connectionOptions;
365365
}
366366

367+
public static unsafe TransferCoding GetFinalTransferCoding(StringValues transferEncoding)
368+
{
369+
var transferEncodingOptions = TransferCoding.None;
370+
371+
foreach (var value in transferEncoding)
372+
{
373+
fixed (char* ptr = value)
374+
{
375+
var ch = ptr;
376+
var tokenEnd = ch;
377+
var end = ch + value.Length;
378+
379+
while (ch < end)
380+
{
381+
while (tokenEnd < end && *tokenEnd != ',')
382+
{
383+
tokenEnd++;
384+
}
385+
386+
while (ch < tokenEnd && *ch == ' ')
387+
{
388+
ch++;
389+
}
390+
391+
var tokenLength = tokenEnd - ch;
392+
393+
if (tokenLength >= 7 && (*ch | 0x20) == 'c')
394+
{
395+
if ((*++ch | 0x20) == 'h' &&
396+
(*++ch | 0x20) == 'u' &&
397+
(*++ch | 0x20) == 'n' &&
398+
(*++ch | 0x20) == 'k' &&
399+
(*++ch | 0x20) == 'e' &&
400+
(*++ch | 0x20) == 'd')
401+
{
402+
ch++;
403+
while (ch < tokenEnd && *ch == ' ')
404+
{
405+
ch++;
406+
}
407+
408+
if (ch == tokenEnd)
409+
{
410+
transferEncodingOptions = TransferCoding.Chunked;
411+
}
412+
}
413+
}
414+
415+
if (tokenLength > 0 && ch != tokenEnd)
416+
{
417+
transferEncodingOptions = TransferCoding.Other;
418+
}
419+
420+
tokenEnd++;
421+
ch = tokenEnd;
422+
}
423+
}
424+
}
425+
426+
return transferEncodingOptions;
427+
}
428+
367429
private static void ThrowInvalidContentLengthException(string value)
368430
{
369431
throw new InvalidOperationException($"Invalid Content-Length: \"{value}\". Value must be a positive integral number.");

src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,22 @@ public static MessageBody For(
247247
keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive;
248248
}
249249

250-
var transferEncoding = headers.HeaderTransferEncoding.ToString();
251-
if (transferEncoding.Length > 0)
250+
var transferEncoding = headers.HeaderTransferEncoding;
251+
if (transferEncoding.Count > 0)
252252
{
253+
var transferCoding = FrameHeaders.GetFinalTransferCoding(headers.HeaderTransferEncoding);
254+
255+
// https://tools.ietf.org/html/rfc7230#section-3.3.3
256+
// If a Transfer-Encoding header field
257+
// is present in a request and the chunked transfer coding is not
258+
// the final encoding, the message body length cannot be determined
259+
// reliably; the server MUST respond with the 400 (Bad Request)
260+
// status code and then close the connection.
261+
if (transferCoding != TransferCoding.Chunked)
262+
{
263+
context.RejectRequest(RequestRejectionReason.FinalTransferCodingNotChunked, transferEncoding.ToString());
264+
}
265+
253266
return new ForChunkedEncoding(keepAlive, headers, context);
254267
}
255268

src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ public enum RequestRejectionReason
2727
MissingCRInHeaderLine,
2828
TooManyHeaders,
2929
RequestTimeout,
30+
FinalTransferCodingNotChunked
3031
}
3132
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
7+
{
8+
[Flags]
9+
public enum TransferCoding
10+
{
11+
None,
12+
Chunked,
13+
Other
14+
}
15+
}

test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,140 @@ await connection.ReceiveEnd(
848848
}
849849
}
850850

851+
[Theory]
852+
[InlineData("gzip")]
853+
[InlineData("chunked, gzip")]
854+
[InlineData("gzip")]
855+
[InlineData("chunked, gzip")]
856+
public async Task ConnectionClosedWhenChunkedIsNotFinalTransferCoding(string responseTransferEncoding)
857+
{
858+
using (var server = new TestServer(async httpContext =>
859+
{
860+
httpContext.Response.Headers["Transfer-Encoding"] = responseTransferEncoding;
861+
await httpContext.Response.WriteAsync("hello, world");
862+
}, new TestServiceContext()))
863+
{
864+
using (var connection = server.CreateConnection())
865+
{
866+
await connection.Send(
867+
"GET / HTTP/1.1",
868+
"",
869+
"");
870+
await connection.ReceiveEnd(
871+
"HTTP/1.1 200 OK",
872+
"Connection: close",
873+
$"Date: {server.Context.DateHeaderValue}",
874+
$"Transfer-Encoding: {responseTransferEncoding}",
875+
"",
876+
"hello, world");
877+
}
878+
879+
using (var connection = server.CreateConnection())
880+
{
881+
await connection.Send(
882+
"GET / HTTP/1.0",
883+
"Connection: keep-alive",
884+
"",
885+
"");
886+
await connection.ReceiveEnd(
887+
"HTTP/1.1 200 OK",
888+
"Connection: close",
889+
$"Date: {server.Context.DateHeaderValue}",
890+
$"Transfer-Encoding: {responseTransferEncoding}",
891+
"",
892+
"hello, world");
893+
}
894+
}
895+
}
896+
897+
[Theory]
898+
[InlineData("gzip")]
899+
[InlineData("chunked, gzip")]
900+
[InlineData("gzip")]
901+
[InlineData("chunked, gzip")]
902+
public async Task ConnectionClosedWhenChunkedIsNotFinalTransferCodingEvenIfConnectionKeepAliveSetInResponse(string responseTransferEncoding)
903+
{
904+
using (var server = new TestServer(async httpContext =>
905+
{
906+
httpContext.Response.Headers["Connection"] = "keep-alive";
907+
httpContext.Response.Headers["Transfer-Encoding"] = responseTransferEncoding;
908+
await httpContext.Response.WriteAsync("hello, world");
909+
}, new TestServiceContext()))
910+
{
911+
using (var connection = server.CreateConnection())
912+
{
913+
await connection.Send(
914+
"GET / HTTP/1.1",
915+
"",
916+
"");
917+
await connection.ReceiveEnd(
918+
"HTTP/1.1 200 OK",
919+
"Connection: keep-alive",
920+
$"Date: {server.Context.DateHeaderValue}",
921+
$"Transfer-Encoding: {responseTransferEncoding}",
922+
"",
923+
"hello, world");
924+
}
925+
926+
using (var connection = server.CreateConnection())
927+
{
928+
await connection.Send(
929+
"GET / HTTP/1.0",
930+
"Connection: keep-alive",
931+
"",
932+
"");
933+
await connection.ReceiveEnd(
934+
"HTTP/1.1 200 OK",
935+
"Connection: keep-alive",
936+
$"Date: {server.Context.DateHeaderValue}",
937+
$"Transfer-Encoding: {responseTransferEncoding}",
938+
"",
939+
"hello, world");
940+
}
941+
}
942+
}
943+
944+
[Theory]
945+
[InlineData("chunked")]
946+
[InlineData("gzip, chunked")]
947+
public async Task ConnectionKeptAliveWhenChunkedIsFinalTransferCoding(string responseTransferEncoding)
948+
{
949+
using (var server = new TestServer(async httpContext =>
950+
{
951+
httpContext.Response.Headers["Transfer-Encoding"] = responseTransferEncoding;
952+
953+
// App would have to chunk manually, but here we don't care
954+
await httpContext.Response.WriteAsync("hello, world");
955+
}, new TestServiceContext()))
956+
{
957+
using (var connection = server.CreateConnection())
958+
{
959+
await connection.Send(
960+
"GET / HTTP/1.1",
961+
"",
962+
"");
963+
await connection.Receive(
964+
"HTTP/1.1 200 OK",
965+
$"Date: {server.Context.DateHeaderValue}",
966+
$"Transfer-Encoding: {responseTransferEncoding}",
967+
"",
968+
"hello, world");
969+
970+
// Make sure connection was kept open
971+
await connection.SendEnd(
972+
"GET / HTTP/1.1",
973+
"",
974+
"");
975+
await connection.ReceiveEnd(
976+
"HTTP/1.1 200 OK",
977+
$"Date: {server.Context.DateHeaderValue}",
978+
$"Transfer-Encoding: {responseTransferEncoding}",
979+
"",
980+
"hello, world");
981+
}
982+
}
983+
}
984+
851985
public static TheoryData<string, StringValues, string> NullHeaderData
852986
{
853987
get

0 commit comments

Comments
 (0)