diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Exceptions/BadHttpRequestException.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Exceptions/BadHttpRequestException.cs new file mode 100644 index 0000000000..19c004857e --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Exceptions/BadHttpRequestException.cs @@ -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) + { + + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Http/Frame.cs index 2c83fc7c84..9d14928e16 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Http/Frame.cs @@ -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); } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Http/FrameOfT.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Http/FrameOfT.cs index 77e7c4047b..053688b587 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Http/FrameOfT.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Http/FrameOfT.cs @@ -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. diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Http/MessageBody.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Http/MessageBody.cs index 2731ec42a6..381b084048 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Http/MessageBody.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Http/MessageBody.cs @@ -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 /// private class ForChunkedEncoding : MessageBody { + private Vector _vectorCRs = new Vector((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 ReadAsyncImplementation(ArraySegment buffer, CancellationToken cancellationToken) { - return ReadAsyncAwaited(buffer, cancellationToken); + return ReadStateMachineAsync(_context.SocketInput, buffer, cancellationToken); } - private async Task ReadAsyncAwaited(ArraySegment buffer, CancellationToken cancellationToken) + private async Task ReadStateMachineAsync(SocketInput input, ArraySegment 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 }; } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/HttpComponentFactory.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/HttpComponentFactory.cs index f58102c1bb..5b346d73fd 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/HttpComponentFactory.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/HttpComponentFactory.cs @@ -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(); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/IHttpComponentFactory.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/IHttpComponentFactory.cs index 620c663fbb..c64f039680 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/IHttpComponentFactory.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/IHttpComponentFactory.cs @@ -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); } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/IKestrelTrace.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/IKestrelTrace.cs index b004cb9eee..b5460ecb4c 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/IKestrelTrace.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/IKestrelTrace.cs @@ -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); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/KestrelTrace.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/KestrelTrace.cs index 3c92c771ef..f3f12cbd1f 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/KestrelTrace.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Infrastructure/KestrelTrace.cs @@ -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 _connectionError; private static readonly Action _connectionDisconnectedWrite; private static readonly Action _notAllConnectionsClosedGracefully; + private static readonly Action _connectionBadRequest; protected readonly ILogger _logger; @@ -45,6 +47,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel _connectionError = LoggerMessage.Define(LogLevel.Information, 14, @"Connection id ""{ConnectionId}"" communication error"); _connectionDisconnectedWrite = LoggerMessage.Define(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(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(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { _logger.Log(logLevel, eventId, state, exception, formatter); diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs new file mode 100644 index 0000000000..f741539480 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs @@ -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 ConnectionFilterData + { + get + { + return new TheoryData + { + { + 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 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 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", + "", + ""); + } + } + } + } +} + diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/EngineTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/EngineTests.cs index 51b2cae261..2a4e788c58 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/EngineTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/EngineTests.cs @@ -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(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func 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; - } - } } } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs index 70c4454b53..78d46f5d6c 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs @@ -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(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()); diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/PassThroughConnectionFilter.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/PassThroughConnectionFilter.cs new file mode 100644 index 0000000000..2342536d3c --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/PassThroughConnectionFilter.cs @@ -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; + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/TestApplicationErrorLogger.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/TestApplicationErrorLogger.cs new file mode 100644 index 0000000000..f6e7f04169 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/TestApplicationErrorLogger.cs @@ -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(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (eventId.Id == ApplicationErrorEventId) + { + ApplicationErrorsLogged++; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs index ba52b21d43..3214c94ed8 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs @@ -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(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) {