Merge branch 'benaadams/chunked-request' into dev
This commit is contained in:
commit
c7f64d34bb
|
|
@ -0,0 +1,16 @@
|
|||
// 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.IO;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Exceptions
|
||||
{
|
||||
public sealed class BadHttpRequestException : IOException
|
||||
{
|
||||
internal BadHttpRequestException(string message)
|
||||
: base(message)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
|
@ -13,6 +12,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Exceptions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
private readonly object _onStartingSync = new Object();
|
||||
private readonly object _onCompletedSync = new Object();
|
||||
|
||||
protected bool _poolingPermitted = true;
|
||||
protected bool _corruptedRequest = false;
|
||||
private Headers _frameHeaders;
|
||||
private Streams _frameStreams;
|
||||
|
||||
|
|
@ -211,7 +211,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
|
||||
public void Reset()
|
||||
{
|
||||
ResetComponents(poolingPermitted: true);
|
||||
ResetComponents();
|
||||
|
||||
_onStarting = null;
|
||||
_onCompleted = null;
|
||||
|
|
@ -248,27 +248,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
_abortedCts = null;
|
||||
}
|
||||
|
||||
protected void ResetComponents(bool poolingPermitted)
|
||||
protected void ResetComponents()
|
||||
{
|
||||
if (_frameHeaders != null)
|
||||
var frameHeaders = Interlocked.Exchange(ref _frameHeaders, null);
|
||||
if (frameHeaders != null)
|
||||
{
|
||||
var frameHeaders = _frameHeaders;
|
||||
_frameHeaders = null;
|
||||
|
||||
RequestHeaders = null;
|
||||
ResponseHeaders = null;
|
||||
HttpComponentFactory.DisposeHeaders(frameHeaders, poolingPermitted);
|
||||
HttpComponentFactory.DisposeHeaders(frameHeaders);
|
||||
}
|
||||
|
||||
if (_frameStreams != null)
|
||||
var frameStreams = Interlocked.Exchange(ref _frameStreams, null);
|
||||
if (frameStreams != null)
|
||||
{
|
||||
var frameStreams = _frameStreams;
|
||||
_frameStreams = null;
|
||||
|
||||
RequestBody = null;
|
||||
ResponseBody = null;
|
||||
DuplexStream = null;
|
||||
HttpComponentFactory.DisposeStreams(frameStreams, poolingPermitted);
|
||||
HttpComponentFactory.DisposeStreams(frameStreams);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -568,8 +564,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
|
||||
protected Task ProduceEnd()
|
||||
{
|
||||
if (_applicationException != null)
|
||||
if (_corruptedRequest || _applicationException != null)
|
||||
{
|
||||
if (_corruptedRequest)
|
||||
{
|
||||
// 400 Bad Request
|
||||
StatusCode = 400;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 500 Internal Server Error
|
||||
StatusCode = 500;
|
||||
}
|
||||
|
||||
if (_responseStarted)
|
||||
{
|
||||
// We can no longer respond with a 500, so we simply close the connection.
|
||||
|
|
@ -578,7 +585,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
}
|
||||
else
|
||||
{
|
||||
StatusCode = 500;
|
||||
ReasonPhrase = null;
|
||||
|
||||
var responseHeaders = _frameHeaders.ResponseHeaders;
|
||||
|
|
@ -711,7 +717,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
{
|
||||
string method;
|
||||
var begin = scan;
|
||||
if (!begin.GetKnownMethod(ref scan,out method))
|
||||
if (!begin.GetKnownMethod(ref scan, out method))
|
||||
{
|
||||
if (scan.Seek(ref _vectorSpaces) == -1)
|
||||
{
|
||||
|
|
@ -834,7 +840,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
return true;
|
||||
}
|
||||
|
||||
public static bool TakeMessageHeaders(SocketInput input, FrameRequestHeaders requestHeaders)
|
||||
public bool TakeMessageHeaders(SocketInput input, FrameRequestHeaders requestHeaders)
|
||||
{
|
||||
var scan = input.ConsumingStart();
|
||||
var consumed = scan;
|
||||
|
|
@ -863,7 +869,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
consumed = scan;
|
||||
return true;
|
||||
}
|
||||
throw new InvalidDataException("Malformed request");
|
||||
|
||||
ReportCorruptedHttpRequest(new BadHttpRequestException("Headers corrupted, invalid header sequence."));
|
||||
// Headers corrupted, parsing headers is complete
|
||||
return true;
|
||||
}
|
||||
|
||||
while (
|
||||
|
|
@ -953,16 +962,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
statusCode != 304;
|
||||
}
|
||||
|
||||
public void ReportCorruptedHttpRequest(BadHttpRequestException ex)
|
||||
{
|
||||
_corruptedRequest = true;
|
||||
Log.ConnectionBadRequest(ConnectionId, ex);
|
||||
}
|
||||
|
||||
protected void ReportApplicationError(Exception ex)
|
||||
{
|
||||
if (_applicationException == null)
|
||||
{
|
||||
_applicationException = ex;
|
||||
}
|
||||
else if (_applicationException is AggregateException)
|
||||
{
|
||||
_applicationException = new AggregateException(_applicationException, ex).Flatten();
|
||||
}
|
||||
else
|
||||
{
|
||||
_applicationException = new AggregateException(_applicationException, ex);
|
||||
}
|
||||
|
||||
Log.ApplicationError(ex);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Exceptions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||
|
|
@ -64,55 +63,66 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
_abortedCts = null;
|
||||
_manuallySetRequestAbortToken = null;
|
||||
|
||||
var context = _application.CreateContext(this);
|
||||
try
|
||||
if (!_corruptedRequest)
|
||||
{
|
||||
await _application.ProcessRequestAsync(context).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ReportApplicationError(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Trigger OnStarting if it hasn't been called yet and the app hasn't
|
||||
// already failed. If an OnStarting callback throws we can go through
|
||||
// our normal error handling in ProduceEnd.
|
||||
// https://github.com/aspnet/KestrelHttpServer/issues/43
|
||||
if (!_responseStarted && _applicationException == null && _onStarting != null)
|
||||
var context = _application.CreateContext(this);
|
||||
try
|
||||
{
|
||||
await FireOnStarting();
|
||||
await _application.ProcessRequestAsync(context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
PauseStreams();
|
||||
|
||||
if (_onCompleted != null)
|
||||
catch (Exception ex)
|
||||
{
|
||||
await FireOnCompleted();
|
||||
ReportApplicationError(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Trigger OnStarting if it hasn't been called yet and the app hasn't
|
||||
// already failed. If an OnStarting callback throws we can go through
|
||||
// our normal error handling in ProduceEnd.
|
||||
// https://github.com/aspnet/KestrelHttpServer/issues/43
|
||||
if (!_responseStarted && _applicationException == null && _onStarting != null)
|
||||
{
|
||||
await FireOnStarting();
|
||||
}
|
||||
|
||||
_application.DisposeContext(context, _applicationException);
|
||||
PauseStreams();
|
||||
|
||||
if (_onCompleted != null)
|
||||
{
|
||||
await FireOnCompleted();
|
||||
}
|
||||
|
||||
_application.DisposeContext(context, _applicationException);
|
||||
}
|
||||
|
||||
// If _requestAbort is set, the connection has already been closed.
|
||||
if (Volatile.Read(ref _requestAborted) == 0)
|
||||
{
|
||||
ResumeStreams();
|
||||
|
||||
await ProduceEnd();
|
||||
|
||||
if (_keepAlive)
|
||||
if (_keepAlive && !_corruptedRequest)
|
||||
{
|
||||
// Finish reading the request body in case the app did not.
|
||||
await messageBody.Consume();
|
||||
try
|
||||
{
|
||||
// Finish reading the request body in case the app did not.
|
||||
await messageBody.Consume();
|
||||
}
|
||||
catch (BadHttpRequestException ex)
|
||||
{
|
||||
ReportCorruptedHttpRequest(ex);
|
||||
}
|
||||
}
|
||||
|
||||
await ProduceEnd();
|
||||
}
|
||||
|
||||
StopStreams();
|
||||
}
|
||||
|
||||
if (!_keepAlive)
|
||||
if (!_keepAlive || _corruptedRequest)
|
||||
{
|
||||
ResetComponents(poolingPermitted: true);
|
||||
// End the connection for non keep alive and Bad Requests
|
||||
// as data incoming may have been thrown off
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -122,15 +132,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Error occurred, do not return components to pool
|
||||
_poolingPermitted = false;
|
||||
Log.LogWarning(0, ex, "Connection processing ended abnormally");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
ResetComponents(poolingPermitted: _poolingPermitted);
|
||||
ResetComponents();
|
||||
_abortedCts = null;
|
||||
|
||||
// If _requestAborted is set, the connection has already been closed.
|
||||
|
|
|
|||
|
|
@ -3,18 +3,20 @@
|
|||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Exceptions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
||||
{
|
||||
public abstract class MessageBody
|
||||
{
|
||||
private readonly FrameContext _context;
|
||||
private readonly Frame _context;
|
||||
private int _send100Continue = 1;
|
||||
|
||||
protected MessageBody(FrameContext context)
|
||||
protected MessageBody(Frame context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
|
@ -99,7 +101,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
public static MessageBody For(
|
||||
string httpVersion,
|
||||
FrameRequestHeaders headers,
|
||||
FrameContext context)
|
||||
Frame context)
|
||||
{
|
||||
// see also http://tools.ietf.org/html/rfc2616#section-4.4
|
||||
|
||||
|
|
@ -114,13 +116,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
var transferEncoding = headers.HeaderTransferEncoding.ToString();
|
||||
if (transferEncoding.Length > 0)
|
||||
{
|
||||
return new ForChunkedEncoding(keepAlive, context);
|
||||
return new ForChunkedEncoding(keepAlive, headers, context);
|
||||
}
|
||||
|
||||
var contentLength = headers.HeaderContentLength.ToString();
|
||||
if (contentLength.Length > 0)
|
||||
var unparsedContentLength = headers.HeaderContentLength.ToString();
|
||||
if (unparsedContentLength.Length > 0)
|
||||
{
|
||||
return new ForContentLength(keepAlive, int.Parse(contentLength), context);
|
||||
int contentLength;
|
||||
if (!int.TryParse(unparsedContentLength, out contentLength) || contentLength < 0)
|
||||
{
|
||||
context.ReportCorruptedHttpRequest(new BadHttpRequestException("Invalid content length."));
|
||||
return new ForContentLength(keepAlive, 0, context);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ForContentLength(keepAlive, contentLength, context);
|
||||
}
|
||||
}
|
||||
|
||||
if (keepAlive)
|
||||
|
|
@ -131,9 +142,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
return new ForRemainingData(context);
|
||||
}
|
||||
|
||||
private int ThrowBadRequestException(string message)
|
||||
{
|
||||
// returns int so can be used as item non-void function
|
||||
var ex = new BadHttpRequestException(message);
|
||||
_context.ReportCorruptedHttpRequest(ex);
|
||||
|
||||
throw ex;
|
||||
}
|
||||
|
||||
private class ForRemainingData : MessageBody
|
||||
{
|
||||
public ForRemainingData(FrameContext context)
|
||||
public ForRemainingData(Frame context)
|
||||
: base(context)
|
||||
{
|
||||
}
|
||||
|
|
@ -149,7 +169,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
private readonly int _contentLength;
|
||||
private int _inputLength;
|
||||
|
||||
public ForContentLength(bool keepAlive, int contentLength, FrameContext context)
|
||||
public ForContentLength(bool keepAlive, int contentLength, Frame context)
|
||||
: base(context)
|
||||
{
|
||||
RequestKeepAlive = keepAlive;
|
||||
|
|
@ -176,7 +196,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
_inputLength -= actual;
|
||||
if (actual == 0)
|
||||
{
|
||||
throw new InvalidDataException("Unexpected end of request content");
|
||||
ThrowBadRequestException("Unexpected end of request content");
|
||||
}
|
||||
return actual;
|
||||
}
|
||||
|
|
@ -192,7 +212,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
_inputLength -= actual;
|
||||
if (actual == 0)
|
||||
{
|
||||
throw new InvalidDataException("Unexpected end of request content");
|
||||
ThrowBadRequestException("Unexpected end of request content");
|
||||
}
|
||||
|
||||
return actual;
|
||||
|
|
@ -204,188 +224,337 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
|
|||
/// </summary>
|
||||
private class ForChunkedEncoding : MessageBody
|
||||
{
|
||||
private Vector<byte> _vectorCRs = new Vector<byte>((byte)'\r');
|
||||
|
||||
private int _inputLength;
|
||||
private Mode _mode = Mode.Prefix;
|
||||
private FrameRequestHeaders _requestHeaders;
|
||||
|
||||
private Mode _mode = Mode.ChunkPrefix;
|
||||
|
||||
public ForChunkedEncoding(bool keepAlive, FrameContext context)
|
||||
public ForChunkedEncoding(bool keepAlive, FrameRequestHeaders headers, Frame context)
|
||||
: base(context)
|
||||
{
|
||||
RequestKeepAlive = keepAlive;
|
||||
_requestHeaders = headers;
|
||||
}
|
||||
|
||||
public override ValueTask<int> ReadAsyncImplementation(ArraySegment<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
return ReadAsyncAwaited(buffer, cancellationToken);
|
||||
return ReadStateMachineAsync(_context.SocketInput, buffer, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<int> ReadAsyncAwaited(ArraySegment<byte> buffer, CancellationToken cancellationToken)
|
||||
private async Task<int> ReadStateMachineAsync(SocketInput input, ArraySegment<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
var input = _context.SocketInput;
|
||||
|
||||
while (_mode != Mode.Complete)
|
||||
while (_mode < Mode.Trailer)
|
||||
{
|
||||
while (_mode == Mode.ChunkPrefix)
|
||||
while (_mode == Mode.Prefix)
|
||||
{
|
||||
var chunkSize = 0;
|
||||
if (!TakeChunkedLine(input, ref chunkSize))
|
||||
ParseChunkedPrefix(input);
|
||||
if (_mode != Mode.Prefix)
|
||||
{
|
||||
await input;
|
||||
break;
|
||||
}
|
||||
else if (chunkSize == 0)
|
||||
{
|
||||
_mode = Mode.Complete;
|
||||
}
|
||||
else
|
||||
{
|
||||
_mode = Mode.ChunkData;
|
||||
}
|
||||
_inputLength = chunkSize;
|
||||
|
||||
await GetDataAsync(input);
|
||||
}
|
||||
while (_mode == Mode.ChunkData)
|
||||
|
||||
while (_mode == Mode.Extension)
|
||||
{
|
||||
var limit = buffer.Array == null ? _inputLength : Math.Min(buffer.Count, _inputLength);
|
||||
if (limit != 0)
|
||||
ParseExtension(input);
|
||||
if (_mode != Mode.Extension)
|
||||
{
|
||||
await input;
|
||||
break;
|
||||
}
|
||||
|
||||
var begin = input.ConsumingStart();
|
||||
int actual;
|
||||
var end = begin.CopyTo(buffer.Array, buffer.Offset, limit, out actual);
|
||||
_inputLength -= actual;
|
||||
input.ConsumingComplete(end, end);
|
||||
await GetDataAsync(input);
|
||||
}
|
||||
|
||||
if (_inputLength == 0)
|
||||
{
|
||||
_mode = Mode.ChunkSuffix;
|
||||
}
|
||||
while (_mode == Mode.Data)
|
||||
{
|
||||
int actual = ReadChunkedData(input, buffer.Array, buffer.Offset, buffer.Count);
|
||||
if (actual != 0)
|
||||
{
|
||||
return actual;
|
||||
}
|
||||
else if (_mode != Mode.Data)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await GetDataAsync(input);
|
||||
}
|
||||
while (_mode == Mode.ChunkSuffix)
|
||||
|
||||
while (_mode == Mode.Suffix)
|
||||
{
|
||||
var scan = input.ConsumingStart();
|
||||
var consumed = scan;
|
||||
var ch1 = scan.Take();
|
||||
var ch2 = scan.Take();
|
||||
if (ch1 == -1 || ch2 == -1)
|
||||
ParseChunkedSuffix(input);
|
||||
if (_mode != Mode.Suffix)
|
||||
{
|
||||
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");
|
||||
break;
|
||||
}
|
||||
|
||||
await GetDataAsync(input);
|
||||
}
|
||||
}
|
||||
|
||||
// Chunks finished, parse trailers
|
||||
while (_mode == Mode.Trailer)
|
||||
{
|
||||
ParseChunkedTrailer(input);
|
||||
if (_mode != Mode.Trailer)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await GetDataAsync(input);
|
||||
}
|
||||
|
||||
if (_mode == Mode.TrailerHeaders)
|
||||
{
|
||||
while (!_context.TakeMessageHeaders(input, _requestHeaders))
|
||||
{
|
||||
await GetDataAsync(input);
|
||||
}
|
||||
|
||||
_mode = Mode.Complete;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool TakeChunkedLine(SocketInput baton, ref int chunkSizeOut)
|
||||
private void ParseChunkedPrefix(SocketInput input)
|
||||
{
|
||||
var scan = baton.ConsumingStart();
|
||||
var scan = input.ConsumingStart();
|
||||
var consumed = scan;
|
||||
try
|
||||
{
|
||||
var ch0 = scan.Take();
|
||||
var chunkSize = 0;
|
||||
var mode = 0;
|
||||
while (ch0 != -1)
|
||||
var ch1 = scan.Take();
|
||||
var ch2 = scan.Take();
|
||||
if (ch1 == -1 || ch2 == -1)
|
||||
{
|
||||
var ch1 = scan.Take();
|
||||
if (ch1 == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode == 0)
|
||||
{
|
||||
if (ch0 >= '0' && ch0 <= '9')
|
||||
{
|
||||
chunkSize = chunkSize * 0x10 + (ch0 - '0');
|
||||
}
|
||||
else if (ch0 >= 'A' && ch0 <= 'F')
|
||||
{
|
||||
chunkSize = chunkSize * 0x10 + (ch0 - ('A' - 10));
|
||||
}
|
||||
else if (ch0 >= 'a' && ch0 <= 'f')
|
||||
{
|
||||
chunkSize = chunkSize * 0x10 + (ch0 - ('a' - 10));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException("INVALID REQUEST FORMAT");
|
||||
}
|
||||
mode = 1;
|
||||
}
|
||||
else if (mode == 1)
|
||||
{
|
||||
if (ch0 >= '0' && ch0 <= '9')
|
||||
{
|
||||
chunkSize = chunkSize * 0x10 + (ch0 - '0');
|
||||
}
|
||||
else if (ch0 >= 'A' && ch0 <= 'F')
|
||||
{
|
||||
chunkSize = chunkSize * 0x10 + (ch0 - ('A' - 10));
|
||||
}
|
||||
else if (ch0 >= 'a' && ch0 <= 'f')
|
||||
{
|
||||
chunkSize = chunkSize * 0x10 + (ch0 - ('a' - 10));
|
||||
}
|
||||
else if (ch0 == ';')
|
||||
{
|
||||
mode = 2;
|
||||
}
|
||||
else if (ch0 == '\r' && ch1 == '\n')
|
||||
{
|
||||
consumed = scan;
|
||||
chunkSizeOut = chunkSize;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException("INVALID REQUEST FORMAT");
|
||||
}
|
||||
}
|
||||
else if (mode == 2)
|
||||
{
|
||||
if (ch0 == '\r' && ch1 == '\n')
|
||||
{
|
||||
consumed = scan;
|
||||
chunkSizeOut = chunkSize;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// chunk-extensions not currently parsed
|
||||
}
|
||||
}
|
||||
|
||||
ch0 = ch1;
|
||||
return;
|
||||
}
|
||||
return false;
|
||||
|
||||
var chunkSize = CalculateChunkSize(ch1, 0);
|
||||
ch1 = ch2;
|
||||
|
||||
do
|
||||
{
|
||||
if (ch1 == ';')
|
||||
{
|
||||
consumed = scan;
|
||||
|
||||
_inputLength = chunkSize;
|
||||
_mode = Mode.Extension;
|
||||
return;
|
||||
}
|
||||
|
||||
ch2 = scan.Take();
|
||||
if (ch2 == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ch1 == '\r' && ch2 == '\n')
|
||||
{
|
||||
consumed = scan;
|
||||
_inputLength = chunkSize;
|
||||
|
||||
if (chunkSize > 0)
|
||||
{
|
||||
_mode = Mode.Data;
|
||||
}
|
||||
else
|
||||
{
|
||||
_mode = Mode.Trailer;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
chunkSize = CalculateChunkSize(ch1, chunkSize);
|
||||
ch1 = ch2;
|
||||
} while (ch1 != -1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
baton.ConsumingComplete(consumed, scan);
|
||||
input.ConsumingComplete(consumed, scan);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseExtension(SocketInput input)
|
||||
{
|
||||
var scan = input.ConsumingStart();
|
||||
var consumed = scan;
|
||||
try
|
||||
{
|
||||
// Chunk-extensions not currently parsed
|
||||
// Just drain the data
|
||||
do
|
||||
{
|
||||
if (scan.Seek(ref _vectorCRs) == -1)
|
||||
{
|
||||
// End marker not found yet
|
||||
consumed = scan;
|
||||
return;
|
||||
};
|
||||
|
||||
var ch1 = scan.Take();
|
||||
var ch2 = scan.Take();
|
||||
|
||||
if (ch2 == '\n')
|
||||
{
|
||||
consumed = scan;
|
||||
if (_inputLength > 0)
|
||||
{
|
||||
_mode = Mode.Data;
|
||||
}
|
||||
else
|
||||
{
|
||||
_mode = Mode.Trailer;
|
||||
}
|
||||
}
|
||||
else if (ch2 == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
} while (_mode == Mode.Extension);
|
||||
}
|
||||
finally
|
||||
{
|
||||
input.ConsumingComplete(consumed, scan);
|
||||
}
|
||||
}
|
||||
|
||||
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.Suffix;
|
||||
}
|
||||
else if (actual == 0)
|
||||
{
|
||||
ThrowIfRequestIncomplete(input);
|
||||
}
|
||||
|
||||
return actual;
|
||||
}
|
||||
|
||||
private void ParseChunkedSuffix(SocketInput input)
|
||||
{
|
||||
var scan = input.ConsumingStart();
|
||||
var consumed = scan;
|
||||
try
|
||||
{
|
||||
var ch1 = scan.Take();
|
||||
var ch2 = scan.Take();
|
||||
if (ch1 == -1 || ch2 == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (ch1 == '\r' && ch2 == '\n')
|
||||
{
|
||||
consumed = scan;
|
||||
_mode = Mode.Prefix;
|
||||
}
|
||||
else
|
||||
{
|
||||
ThrowBadRequestException("Bad chunk suffix");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
input.ConsumingComplete(consumed, scan);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseChunkedTrailer(SocketInput input)
|
||||
{
|
||||
var scan = input.ConsumingStart();
|
||||
var consumed = scan;
|
||||
try
|
||||
{
|
||||
var ch1 = scan.Take();
|
||||
var ch2 = scan.Take();
|
||||
|
||||
if (ch1 == -1 || ch2 == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (ch1 == '\r' && ch2 == '\n')
|
||||
{
|
||||
consumed = scan;
|
||||
_mode = Mode.Complete;
|
||||
}
|
||||
else
|
||||
{
|
||||
_mode = Mode.TrailerHeaders;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
input.ConsumingComplete(consumed, scan);
|
||||
}
|
||||
}
|
||||
|
||||
private int CalculateChunkSize(int extraHexDigit, int currentParsedSize)
|
||||
{
|
||||
checked
|
||||
{
|
||||
if (extraHexDigit >= '0' && extraHexDigit <= '9')
|
||||
{
|
||||
return currentParsedSize * 0x10 + (extraHexDigit - '0');
|
||||
}
|
||||
else if (extraHexDigit >= 'A' && extraHexDigit <= 'F')
|
||||
{
|
||||
return currentParsedSize * 0x10 + (extraHexDigit - ('A' - 10));
|
||||
}
|
||||
else if (extraHexDigit >= 'a' && extraHexDigit <= 'f')
|
||||
{
|
||||
return currentParsedSize * 0x10 + (extraHexDigit - ('a' - 10));
|
||||
}
|
||||
else
|
||||
{
|
||||
return ThrowBadRequestException("Bad chunk size data");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SocketInput GetDataAsync(SocketInput input)
|
||||
{
|
||||
ThrowIfRequestIncomplete(input);
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
private void ThrowIfRequestIncomplete(SocketInput input)
|
||||
{
|
||||
if (input.RemoteIntakeFin)
|
||||
{
|
||||
ThrowBadRequestException("Chunked request incomplete");
|
||||
}
|
||||
}
|
||||
|
||||
private enum Mode
|
||||
{
|
||||
ChunkPrefix,
|
||||
ChunkData,
|
||||
ChunkSuffix,
|
||||
Complete,
|
||||
Prefix,
|
||||
Extension,
|
||||
Data,
|
||||
Suffix,
|
||||
Trailer,
|
||||
TrailerHeaders,
|
||||
Complete
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
|
|||
return streams;
|
||||
}
|
||||
|
||||
public void DisposeStreams(Streams streams, bool poolingPermitted)
|
||||
public void DisposeStreams(Streams streams)
|
||||
{
|
||||
if (poolingPermitted && _streamPool.Count < ServerInformation.PoolingParameters.MaxPooledStreams)
|
||||
if (_streamPool.Count < ServerInformation.PoolingParameters.MaxPooledStreams)
|
||||
{
|
||||
streams.Uninitialize();
|
||||
|
||||
|
|
@ -58,9 +58,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
|
|||
return headers;
|
||||
}
|
||||
|
||||
public void DisposeHeaders(Headers headers, bool poolingPermitted)
|
||||
public void DisposeHeaders(Headers headers)
|
||||
{
|
||||
if (poolingPermitted && _headerPool.Count < ServerInformation.PoolingParameters.MaxPooledHeaders)
|
||||
if (_headerPool.Count < ServerInformation.PoolingParameters.MaxPooledHeaders)
|
||||
{
|
||||
headers.Uninitialize();
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
|
|||
|
||||
Streams CreateStreams(FrameContext owner);
|
||||
|
||||
void DisposeStreams(Streams streams, bool poolingPermitted);
|
||||
void DisposeStreams(Streams streams);
|
||||
|
||||
Headers CreateHeaders(DateHeaderValueManager dateValueManager);
|
||||
|
||||
void DisposeHeaders(Headers headers, bool poolingPermitted);
|
||||
void DisposeHeaders(Headers headers);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Exceptions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
|
||||
{
|
||||
|
|
@ -33,6 +34,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
|
|||
|
||||
void ConnectionDisconnectedWrite(string connectionId, int count, Exception ex);
|
||||
|
||||
void ConnectionBadRequest(string connectionId, BadHttpRequestException ex);
|
||||
|
||||
void NotAllConnectionsClosedGracefully();
|
||||
|
||||
void ApplicationError(Exception ex);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Exceptions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel
|
||||
|
|
@ -24,6 +25,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
private static readonly Action<ILogger, string, Exception> _connectionError;
|
||||
private static readonly Action<ILogger, string, int, Exception> _connectionDisconnectedWrite;
|
||||
private static readonly Action<ILogger, Exception> _notAllConnectionsClosedGracefully;
|
||||
private static readonly Action<ILogger, string, string, Exception> _connectionBadRequest;
|
||||
|
||||
protected readonly ILogger _logger;
|
||||
|
||||
|
|
@ -45,6 +47,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
_connectionError = LoggerMessage.Define<string>(LogLevel.Information, 14, @"Connection id ""{ConnectionId}"" communication error");
|
||||
_connectionDisconnectedWrite = LoggerMessage.Define<string, int>(LogLevel.Debug, 15, @"Connection id ""{ConnectionId}"" write of ""{count}"" bytes to disconnected client.");
|
||||
_notAllConnectionsClosedGracefully = LoggerMessage.Define(LogLevel.Debug, 16, "Some connections failed to close gracefully during server shutdown.");
|
||||
_connectionBadRequest = LoggerMessage.Define<string, string>(LogLevel.Information, 17, @"Connection id ""{ConnectionId}"" bad request data: ""{message}""");
|
||||
}
|
||||
|
||||
public KestrelTrace(ILogger logger)
|
||||
|
|
@ -135,6 +138,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel
|
|||
_notAllConnectionsClosedGracefully(_logger, null);
|
||||
}
|
||||
|
||||
public void ConnectionBadRequest(string connectionId, BadHttpRequestException ex)
|
||||
{
|
||||
_connectionBadRequest(_logger, connectionId, ex.Message, ex);
|
||||
}
|
||||
|
||||
public virtual void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
_logger.Log(logLevel, eventId, state, exception, formatter);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,430 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||
{
|
||||
public class ChunkedRequestTests
|
||||
{
|
||||
public static TheoryData<ServiceContext> ConnectionFilterData
|
||||
{
|
||||
get
|
||||
{
|
||||
return new TheoryData<ServiceContext>
|
||||
{
|
||||
{
|
||||
new TestServiceContext()
|
||||
},
|
||||
{
|
||||
new TestServiceContext(new PassThroughConnectionFilter())
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async Task App(HttpContext httpContext)
|
||||
{
|
||||
var request = httpContext.Request;
|
||||
var response = httpContext.Response;
|
||||
response.Headers.Clear();
|
||||
while (true)
|
||||
{
|
||||
var buffer = new byte[8192];
|
||||
var count = await request.Body.ReadAsync(buffer, 0, buffer.Length);
|
||||
if (count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
await response.Body.WriteAsync(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AppChunked(HttpContext httpContext)
|
||||
{
|
||||
var request = httpContext.Request;
|
||||
var response = httpContext.Response;
|
||||
var data = new MemoryStream();
|
||||
await request.Body.CopyToAsync(data);
|
||||
var bytes = data.ToArray();
|
||||
|
||||
response.Headers.Clear();
|
||||
response.Headers["Content-Length"] = bytes.Length.ToString();
|
||||
await response.Body.WriteAsync(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task Http10TransferEncoding(ServiceContext testContext)
|
||||
{
|
||||
using (var server = new TestServer(App, testContext))
|
||||
{
|
||||
using (var connection = new TestConnection(server.Port))
|
||||
{
|
||||
await connection.SendEnd(
|
||||
"POST / HTTP/1.0",
|
||||
"Transfer-Encoding: chunked",
|
||||
"",
|
||||
"5", "Hello",
|
||||
"6", " World",
|
||||
"0",
|
||||
"");
|
||||
await connection.ReceiveEnd(
|
||||
"HTTP/1.0 200 OK",
|
||||
"",
|
||||
"Hello World");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task Http10KeepAliveTransferEncoding(ServiceContext testContext)
|
||||
{
|
||||
using (var server = new TestServer(AppChunked, testContext))
|
||||
{
|
||||
using (var connection = new TestConnection(server.Port))
|
||||
{
|
||||
await connection.SendEnd(
|
||||
"POST / HTTP/1.0",
|
||||
"Transfer-Encoding: chunked",
|
||||
"Connection: keep-alive",
|
||||
"",
|
||||
"5", "Hello",
|
||||
"6", " World",
|
||||
"0",
|
||||
"",
|
||||
"POST / HTTP/1.0",
|
||||
"",
|
||||
"Goodbye");
|
||||
await connection.Receive(
|
||||
"HTTP/1.0 200 OK",
|
||||
"Connection: keep-alive",
|
||||
"Content-Length: 11",
|
||||
"",
|
||||
"Hello World");
|
||||
await connection.ReceiveEnd(
|
||||
"HTTP/1.0 200 OK",
|
||||
"Content-Length: 7",
|
||||
"",
|
||||
"Goodbye");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task RequestBodyIsConsumedAutomaticallyIfAppDoesntConsumeItFully(ServiceContext testContext)
|
||||
{
|
||||
using (var server = new TestServer(async httpContext =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
var request = httpContext.Request;
|
||||
|
||||
Assert.Equal("POST", request.Method);
|
||||
|
||||
response.Headers.Clear();
|
||||
response.Headers["Content-Length"] = new[] { "11" };
|
||||
|
||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
||||
}, testContext))
|
||||
{
|
||||
using (var connection = new TestConnection(server.Port))
|
||||
{
|
||||
await connection.SendEnd(
|
||||
"POST / HTTP/1.1",
|
||||
"Content-Length: 5",
|
||||
"",
|
||||
"HelloPOST / HTTP/1.1",
|
||||
"Transfer-Encoding: chunked",
|
||||
"",
|
||||
"C", "HelloChunked",
|
||||
"0",
|
||||
"",
|
||||
"POST / HTTP/1.1",
|
||||
"Content-Length: 7",
|
||||
"",
|
||||
"Goodbye");
|
||||
await connection.ReceiveEnd(
|
||||
"HTTP/1.1 200 OK",
|
||||
"Content-Length: 11",
|
||||
"",
|
||||
"Hello WorldHTTP/1.1 200 OK",
|
||||
"Content-Length: 11",
|
||||
"",
|
||||
"Hello WorldHTTP/1.1 200 OK",
|
||||
"Content-Length: 11",
|
||||
"",
|
||||
"Hello World");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task TrailingHeadersAreParsed(ServiceContext testContext)
|
||||
{
|
||||
var requestCount = 10;
|
||||
var requestsReceived = 0;
|
||||
|
||||
using (var server = new TestServer(async httpContext =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
var request = httpContext.Request;
|
||||
|
||||
var buffer = new byte[200];
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"]));
|
||||
|
||||
while (await request.Body.ReadAsync(buffer, 0, buffer.Length) != 0)
|
||||
{
|
||||
;// read to end
|
||||
}
|
||||
|
||||
if (requestsReceived < requestCount)
|
||||
{
|
||||
Assert.Equal(new string('a', requestsReceived), request.Headers["X-Trailer-Header"].ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"]));
|
||||
}
|
||||
|
||||
requestsReceived++;
|
||||
|
||||
response.Headers.Clear();
|
||||
response.Headers["Content-Length"] = new[] { "11" };
|
||||
|
||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
||||
}, testContext))
|
||||
{
|
||||
var response = string.Join("\r\n", new string[] {
|
||||
"HTTP/1.1 200 OK",
|
||||
"Content-Length: 11",
|
||||
"",
|
||||
"Hello World"});
|
||||
|
||||
var expectedFullResponse = string.Join("", Enumerable.Repeat(response, requestCount + 1));
|
||||
|
||||
IEnumerable<string> sendSequence = new string[] {
|
||||
"POST / HTTP/1.1",
|
||||
"Transfer-Encoding: chunked",
|
||||
"",
|
||||
"C",
|
||||
"HelloChunked",
|
||||
"0",
|
||||
""};
|
||||
|
||||
for (var i = 1; i < requestCount; i++)
|
||||
{
|
||||
sendSequence = sendSequence.Concat(new string[] {
|
||||
"POST / HTTP/1.1",
|
||||
"Transfer-Encoding: chunked",
|
||||
"",
|
||||
"C",
|
||||
$"HelloChunk{i:00}",
|
||||
"0",
|
||||
string.Concat("X-Trailer-Header: ", new string('a', i)),
|
||||
"" });
|
||||
}
|
||||
|
||||
sendSequence = sendSequence.Concat(new string[] {
|
||||
"POST / HTTP/1.1",
|
||||
"Content-Length: 7",
|
||||
"",
|
||||
"Goodbye"
|
||||
});
|
||||
|
||||
var fullRequest = sendSequence.ToArray();
|
||||
|
||||
using (var connection = new TestConnection(server.Port))
|
||||
{
|
||||
await connection.SendEnd(fullRequest);
|
||||
|
||||
await connection.ReceiveEnd(expectedFullResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task ExtensionsAreIgnored(ServiceContext testContext)
|
||||
{
|
||||
var requestCount = 10;
|
||||
var requestsReceived = 0;
|
||||
|
||||
using (var server = new TestServer(async httpContext =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
var request = httpContext.Request;
|
||||
|
||||
var buffer = new byte[200];
|
||||
|
||||
Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"]));
|
||||
|
||||
while (await request.Body.ReadAsync(buffer, 0, buffer.Length) != 0)
|
||||
{
|
||||
;// read to end
|
||||
}
|
||||
|
||||
if (requestsReceived < requestCount)
|
||||
{
|
||||
Assert.Equal(new string('a', requestsReceived), request.Headers["X-Trailer-Header"].ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"]));
|
||||
}
|
||||
|
||||
requestsReceived++;
|
||||
|
||||
response.Headers.Clear();
|
||||
response.Headers["Content-Length"] = new[] { "11" };
|
||||
|
||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
||||
}, testContext))
|
||||
{
|
||||
var response = string.Join("\r\n", new string[] {
|
||||
"HTTP/1.1 200 OK",
|
||||
"Content-Length: 11",
|
||||
"",
|
||||
"Hello World"});
|
||||
|
||||
var expectedFullResponse = string.Join("", Enumerable.Repeat(response, requestCount + 1));
|
||||
|
||||
IEnumerable<string> sendSequence = new string[] {
|
||||
"POST / HTTP/1.1",
|
||||
"Transfer-Encoding: chunked",
|
||||
"",
|
||||
"C;hello there",
|
||||
"HelloChunked",
|
||||
"0;hello there",
|
||||
""};
|
||||
|
||||
for (var i = 1; i < requestCount; i++)
|
||||
{
|
||||
sendSequence = sendSequence.Concat(new string[] {
|
||||
"POST / HTTP/1.1",
|
||||
"Transfer-Encoding: chunked",
|
||||
"",
|
||||
"C;hello there",
|
||||
$"HelloChunk{i:00}",
|
||||
"0;hello there",
|
||||
string.Concat("X-Trailer-Header: ", new string('a', i)),
|
||||
"" });
|
||||
}
|
||||
|
||||
sendSequence = sendSequence.Concat(new string[] {
|
||||
"POST / HTTP/1.1",
|
||||
"Content-Length: 7",
|
||||
"",
|
||||
"Goodbye"
|
||||
});
|
||||
|
||||
var fullRequest = sendSequence.ToArray();
|
||||
|
||||
using (var connection = new TestConnection(server.Port))
|
||||
{
|
||||
await connection.SendEnd(fullRequest);
|
||||
|
||||
await connection.ReceiveEnd(expectedFullResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task InvalidLengthResultsIn400(ServiceContext testContext)
|
||||
{
|
||||
using (var server = new TestServer(async httpContext =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
var request = httpContext.Request;
|
||||
|
||||
var buffer = new byte[200];
|
||||
|
||||
while (await request.Body.ReadAsync(buffer, 0, buffer.Length) != 0)
|
||||
{
|
||||
;// read to end
|
||||
}
|
||||
|
||||
response.Headers.Clear();
|
||||
response.Headers["Content-Length"] = new[] { "11" };
|
||||
|
||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
||||
}, testContext))
|
||||
{
|
||||
using (var connection = new TestConnection(server.Port))
|
||||
{
|
||||
await connection.Send(
|
||||
"POST / HTTP/1.1",
|
||||
"Transfer-Encoding: chunked",
|
||||
"",
|
||||
"Cii");
|
||||
|
||||
await connection.Receive(
|
||||
"HTTP/1.1 400 Bad Request",
|
||||
"");
|
||||
await connection.ReceiveStartsWith("Date:");
|
||||
await connection.ReceiveEnd(
|
||||
"Content-Length: 0",
|
||||
"Server: Kestrel",
|
||||
"",
|
||||
"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task InvalidSizedDataResultsIn400(ServiceContext testContext)
|
||||
{
|
||||
using (var server = new TestServer(async httpContext =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
var request = httpContext.Request;
|
||||
|
||||
var buffer = new byte[200];
|
||||
|
||||
while (await request.Body.ReadAsync(buffer, 0, buffer.Length) != 0)
|
||||
{
|
||||
;// read to end
|
||||
}
|
||||
|
||||
response.Headers.Clear();
|
||||
response.Headers["Content-Length"] = new[] { "11" };
|
||||
|
||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
||||
}, testContext))
|
||||
{
|
||||
using (var connection = new TestConnection(server.Port))
|
||||
{
|
||||
await connection.Send(
|
||||
"POST / HTTP/1.1",
|
||||
"Transfer-Encoding: chunked",
|
||||
"",
|
||||
"C",
|
||||
"HelloChunkedIn");
|
||||
|
||||
await connection.Receive(
|
||||
"HTTP/1.1 400 Bad Request",
|
||||
"");
|
||||
await connection.ReceiveStartsWith("Date:");
|
||||
await connection.ReceiveEnd(
|
||||
"Content-Length: 0",
|
||||
"Server: Kestrel",
|
||||
"",
|
||||
"");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
|
@ -12,11 +11,6 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Filter;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Networking;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||
|
|
@ -282,27 +276,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task Http10TransferEncoding(ServiceContext testContext)
|
||||
{
|
||||
using (var server = new TestServer(App, testContext))
|
||||
{
|
||||
using (var connection = new TestConnection(server.Port))
|
||||
{
|
||||
await connection.Send(
|
||||
"POST / HTTP/1.0",
|
||||
"Transfer-Encoding: chunked",
|
||||
"",
|
||||
"5", "Hello", "6", " World", "0\r\n");
|
||||
await connection.ReceiveEnd(
|
||||
"HTTP/1.0 200 OK",
|
||||
"",
|
||||
"Hello World");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task Http10KeepAlive(ServiceContext testContext)
|
||||
|
|
@ -393,38 +366,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task Http10KeepAliveTransferEncoding(ServiceContext testContext)
|
||||
{
|
||||
using (var server = new TestServer(AppChunked, testContext))
|
||||
{
|
||||
using (var connection = new TestConnection(server.Port))
|
||||
{
|
||||
await connection.SendEnd(
|
||||
"POST / HTTP/1.0",
|
||||
"Transfer-Encoding: chunked",
|
||||
"Connection: keep-alive",
|
||||
"",
|
||||
"5", "Hello", "6", " World", "0",
|
||||
"POST / HTTP/1.0",
|
||||
"",
|
||||
"Goodbye");
|
||||
await connection.Receive(
|
||||
"HTTP/1.0 200 OK",
|
||||
"Connection: keep-alive",
|
||||
"Content-Length: 11",
|
||||
"",
|
||||
"Hello World");
|
||||
await connection.ReceiveEnd(
|
||||
"HTTP/1.0 200 OK",
|
||||
"Content-Length: 7",
|
||||
"",
|
||||
"Goodbye");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task Expect100ContinueForBody(ServiceContext testContext)
|
||||
|
|
@ -943,52 +884,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task RequestBodyIsConsumedAutomaticallyIfAppDoesntConsumeItFully(ServiceContext testContext)
|
||||
{
|
||||
using (var server = new TestServer(async httpContext =>
|
||||
{
|
||||
var response = httpContext.Response;
|
||||
var request = httpContext.Request;
|
||||
|
||||
Assert.Equal("POST", request.Method);
|
||||
|
||||
response.Headers.Clear();
|
||||
response.Headers["Content-Length"] = new[] { "11" };
|
||||
|
||||
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
||||
}, testContext))
|
||||
{
|
||||
using (var connection = new TestConnection(server.Port))
|
||||
{
|
||||
await connection.SendEnd(
|
||||
"POST / HTTP/1.1",
|
||||
"Content-Length: 5",
|
||||
"",
|
||||
"HelloPOST / HTTP/1.1",
|
||||
"Transfer-Encoding: chunked",
|
||||
"",
|
||||
"C", "HelloChunked", "0",
|
||||
"POST / HTTP/1.1",
|
||||
"Content-Length: 7",
|
||||
"",
|
||||
"Goodbye");
|
||||
await connection.ReceiveEnd(
|
||||
"HTTP/1.1 200 OK",
|
||||
"Content-Length: 11",
|
||||
"",
|
||||
"Hello WorldHTTP/1.1 200 OK",
|
||||
"Content-Length: 11",
|
||||
"",
|
||||
"Hello WorldHTTP/1.1 200 OK",
|
||||
"Content-Length: 11",
|
||||
"",
|
||||
"Hello World");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ConnectionFilterData))]
|
||||
public async Task RequestsCanBeAbortedMidRead(ServiceContext testContext)
|
||||
|
|
@ -1123,38 +1018,5 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
Assert.True(registrationWh.Wait(1000));
|
||||
}
|
||||
}
|
||||
|
||||
private class TestApplicationErrorLogger : ILogger
|
||||
{
|
||||
public int ApplicationErrorsLogged { get; set; }
|
||||
|
||||
public IDisposable BeginScopeImpl(object state)
|
||||
{
|
||||
return new Disposable(() => { });
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
// Application errors are logged using 13 as the eventId.
|
||||
if (eventId.Id == 13)
|
||||
{
|
||||
ApplicationErrorsLogged++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PassThroughConnectionFilter : IConnectionFilter
|
||||
{
|
||||
public Task OnConnectionAsync(ConnectionFilterContext context)
|
||||
{
|
||||
context.Connection = new LoggingStream(context.Connection, new TestApplicationErrorLogger());
|
||||
return TaskUtilities.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,18 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
using (var pool = new MemoryPool2())
|
||||
using (var socketInput = new SocketInput(pool, ltp))
|
||||
{
|
||||
var connectionContext = new ConnectionContext()
|
||||
{
|
||||
DateHeaderValueManager = new DateHeaderValueManager(),
|
||||
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
|
||||
};
|
||||
var frame = new Frame<object>(application: null, context: connectionContext);
|
||||
var headerCollection = new FrameRequestHeaders();
|
||||
|
||||
var headerArray = Encoding.ASCII.GetBytes(rawHeaders);
|
||||
socketInput.IncomingData(headerArray, 0, headerArray.Length);
|
||||
|
||||
var success = Frame.TakeMessageHeaders(socketInput, headerCollection);
|
||||
var success = frame.TakeMessageHeaders(socketInput, headerCollection);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.Equal(numHeaders, headerCollection.Count());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
// 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.Filter;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||
{
|
||||
|
||||
public class PassThroughConnectionFilter : IConnectionFilter
|
||||
{
|
||||
public Task OnConnectionAsync(ConnectionFilterContext context)
|
||||
{
|
||||
context.Connection = new LoggingStream(context.Connection, new TestApplicationErrorLogger());
|
||||
return TaskUtilities.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// 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;
|
||||
using Microsoft.AspNetCore.Server.Kestrel;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.KestrelTests
|
||||
{
|
||||
public class TestApplicationErrorLogger : ILogger
|
||||
{
|
||||
// Application errors are logged using 13 as the eventId.
|
||||
private const int ApplicationErrorEventId = 13;
|
||||
|
||||
public int ApplicationErrorsLogged { get; set; }
|
||||
|
||||
public IDisposable BeginScopeImpl(object state)
|
||||
{
|
||||
return new Disposable(() => { });
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
if (eventId.Id == ApplicationErrorEventId)
|
||||
{
|
||||
ApplicationErrorsLogged++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,17 +18,20 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
|
|||
{
|
||||
var trace = new KestrelTrace(new TestKestrelTrace());
|
||||
var ltp = new LoggingThreadPool(trace);
|
||||
FrameContext = new FrameContext
|
||||
var context = new FrameContext()
|
||||
{
|
||||
DateHeaderValueManager = new DateHeaderValueManager(),
|
||||
ServerAddress = ServerAddress.FromUrl("http://localhost:5000"),
|
||||
ConnectionControl = this,
|
||||
FrameControl = this
|
||||
};
|
||||
FrameContext = new Frame<object>(null, context);
|
||||
|
||||
_memoryPool = new MemoryPool2();
|
||||
FrameContext.SocketInput = new SocketInput(_memoryPool, ltp);
|
||||
}
|
||||
|
||||
public FrameContext FrameContext { get; set; }
|
||||
public Frame FrameContext { get; set; }
|
||||
|
||||
public void Add(string text, bool fin = false)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue