diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Http/MessageBody.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Http/MessageBody.cs index 2731ec42a6..9bb6f0db79 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Http/MessageBody.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Http/MessageBody.cs @@ -222,75 +222,155 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http { var input = _context.SocketInput; - while (_mode != Mode.Complete) + while (_mode != Mode.Trailer && _mode != Mode.Complete) { while (_mode == Mode.ChunkPrefix) { - var chunkSize = 0; - if (!TakeChunkedLine(input, ref chunkSize)) - { - await input; - } - else if (chunkSize == 0) - { - _mode = Mode.Complete; - } - else - { - _mode = Mode.ChunkData; - } - _inputLength = chunkSize; + ReadChunkedPrefix(input); + await input; } + while (_mode == Mode.ChunkData) { - var limit = buffer.Array == null ? _inputLength : Math.Min(buffer.Count, _inputLength); - if (limit != 0) - { - await input; - } - - var begin = input.ConsumingStart(); - int actual; - var end = begin.CopyTo(buffer.Array, buffer.Offset, limit, out actual); - _inputLength -= actual; - input.ConsumingComplete(end, end); - - if (_inputLength == 0) - { - _mode = Mode.ChunkSuffix; - } + int actual = ReadChunkedData(input, buffer.Array, buffer.Offset, buffer.Count); if (actual != 0) { return actual; } + + await input; } + while (_mode == Mode.ChunkSuffix) { - var scan = input.ConsumingStart(); - var consumed = scan; - var ch1 = scan.Take(); - var ch2 = scan.Take(); - if (ch1 == -1 || ch2 == -1) - { - input.ConsumingComplete(consumed, scan); - await input; - } - else if (ch1 == '\r' && ch2 == '\n') - { - input.ConsumingComplete(scan, scan); - _mode = Mode.ChunkPrefix; - } - else - { - throw new NotImplementedException("INVALID REQUEST FORMAT"); - } + ReadChunkedSuffix(input); + await input; } } + while (_mode == Mode.Trailer) + { + ReadChunkedTrailer(input); + await input; + } + return 0; } - private static bool TakeChunkedLine(SocketInput baton, ref int chunkSizeOut) + private void ReadChunkedPrefix(SocketInput input) + { + int chunkSize; + if (TakeChunkedLine(input, out chunkSize)) + { + if (chunkSize == 0) + { + _mode = Mode.Trailer; + } + else + { + _mode = Mode.ChunkData; + } + _inputLength = chunkSize; + } + else if (input.RemoteIntakeFin) + { + ThrowChunkedRequestIncomplete(); + } + } + + private int ReadChunkedData(SocketInput input, byte[] buffer, int offset, int count) + { + var scan = input.ConsumingStart(); + int actual; + try + { + var limit = buffer == null ? _inputLength : Math.Min(count, _inputLength); + scan = scan.CopyTo(buffer, offset, limit, out actual); + _inputLength -= actual; + } + finally + { + input.ConsumingComplete(scan, scan); + } + + if (_inputLength == 0) + { + _mode = Mode.ChunkSuffix; + } + else if (actual == 0 && input.RemoteIntakeFin) + { + ThrowChunkedRequestIncomplete(); + } + + return actual; + } + + private void ReadChunkedSuffix(SocketInput input) + { + var scan = input.ConsumingStart(); + var consumed = scan; + try + { + var ch1 = scan.Take(); + var ch2 = scan.Take(); + + if (ch1 == '\r' && ch2 == '\n') + { + consumed = scan; + _mode = Mode.ChunkPrefix; + } + else if (ch1 == -1 || ch2 == -1) + { + if (input.RemoteIntakeFin) + { + ThrowChunkedRequestIncomplete(); + } + } + else + { + ThrowInvalidFormat(); + } + } + finally + { + input.ConsumingComplete(consumed, scan); + } + } + + private void ReadChunkedTrailer(SocketInput input) + { + var scan = input.ConsumingStart(); + var consumed = scan; + try + { + var ch1 = scan.Take(); + var ch2 = scan.Take(); + + if (ch1 == '\r' && ch2 == '\n') + { + consumed = scan; + _mode = Mode.Complete; + } + else if (ch1 == -1 || ch2 == -1) + { + if (input.RemoteIntakeFin) + { + ThrowChunkedRequestIncomplete(); + } + } + else + { + // Post request headers + ThrowTrailingHeadersNotSupported(); + } + } + finally + { + input.ConsumingComplete(consumed, scan); + } + } + + private static bool TakeChunkedLine(SocketInput baton, out int chunkSizeOut) { var scan = baton.ConsumingStart(); var consumed = scan; @@ -298,16 +378,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http { var ch0 = scan.Take(); var chunkSize = 0; - var mode = 0; + var mode = Mode.ChunkPrefix; while (ch0 != -1) { var ch1 = scan.Take(); if (ch1 == -1) { + chunkSizeOut = 0; return false; } - if (mode == 0) + if (mode == Mode.ChunkPrefix) { if (ch0 >= '0' && ch0 <= '9') { @@ -323,11 +404,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http } else { - throw new NotImplementedException("INVALID REQUEST FORMAT"); + ThrowInvalidFormat(); } - mode = 1; + mode = Mode.ChunkData; } - else if (mode == 1) + else if (mode == Mode.ChunkData) { if (ch0 >= '0' && ch0 <= '9') { @@ -343,7 +424,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http } else if (ch0 == ';') { - mode = 2; + mode = Mode.ChunkSuffix; } else if (ch0 == '\r' && ch1 == '\n') { @@ -353,10 +434,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http } else { - throw new NotImplementedException("INVALID REQUEST FORMAT"); + ThrowInvalidFormat(); } } - else if (mode == 2) + else if (mode == Mode.ChunkSuffix) { if (ch0 == '\r' && ch1 == '\n') { @@ -367,11 +448,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http else { // chunk-extensions not currently parsed + ThrowChunkedExtensionsNotSupported(); } } ch0 = ch1; } + chunkSizeOut = 0; return false; } finally @@ -380,12 +463,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http } } + private static void ThrowInvalidFormat() + { + throw new InvalidOperationException("Bad Request"); + } + + private static void ThrowChunkedRequestIncomplete() + { + throw new InvalidOperationException("Chunked request incomplete"); + } + + private static void ThrowChunkedExtensionsNotSupported() + { + throw new NotImplementedException("Chunked-extensions not supported"); + } + + private static void ThrowTrailingHeadersNotSupported() + { + throw new NotImplementedException("Trailing headers not supported"); + } + private enum Mode { ChunkPrefix, ChunkData, ChunkSuffix, - Complete, + Trailer, + Complete }; } } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/EngineTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/EngineTests.cs index 51b2cae261..c91e4488e0 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/EngineTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/EngineTests.cs @@ -290,11 +290,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { using (var connection = new TestConnection(server.Port)) { - await connection.Send( + await connection.SendEnd( "POST / HTTP/1.0", "Transfer-Encoding: chunked", "", - "5", "Hello", "6", " World", "0\r\n"); + "5", "Hello", + "6", " World", + "0", + ""); await connection.ReceiveEnd( "HTTP/1.0 200 OK", "", @@ -406,7 +409,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests "Transfer-Encoding: chunked", "Connection: keep-alive", "", - "5", "Hello", "6", " World", "0", + "5", "Hello", + "6", " World", + "0", + "", "POST / HTTP/1.0", "", "Goodbye"); @@ -969,7 +975,9 @@ namespace Microsoft.AspNetCore.Server.KestrelTests "HelloPOST / HTTP/1.1", "Transfer-Encoding: chunked", "", - "C", "HelloChunked", "0", + "C", "HelloChunked", + "0", + "", "POST / HTTP/1.1", "Content-Length: 7", "",