diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpResponse.cs b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpResponse.cs new file mode 100644 index 000000000..2e953b46c --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpResponse.cs @@ -0,0 +1,75 @@ +// 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 System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; + +namespace Microsoft.AspNetCore.Server.Kestrel +{ + public static class BadHttpResponse + { + internal static void ThrowException(ResponseRejectionReasons reason) + { + throw GetException(reason); + } + + internal static void ThrowException(ResponseRejectionReasons reason, int value) + { + throw GetException(reason, value.ToString()); + } + + internal static void ThrowException(ResponseRejectionReasons reason, ResponseRejectionParameter parameter) + { + throw GetException(reason, parameter.ToString()); + } + + internal static InvalidOperationException GetException(ResponseRejectionReasons reason, int value) + { + return GetException(reason, value.ToString()); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static InvalidOperationException GetException(ResponseRejectionReasons reason) + { + InvalidOperationException ex; + switch (reason) + { + case ResponseRejectionReasons.HeadersReadonlyResponseStarted: + ex = new InvalidOperationException("Headers are read-only, response has already started."); + break; + case ResponseRejectionReasons.OnStartingCannotBeSetResponseStarted: + ex = new InvalidOperationException("OnStarting cannot be set, response has already started."); + break; + default: + ex = new InvalidOperationException("Bad response."); + break; + } + + return ex; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static InvalidOperationException GetException(ResponseRejectionReasons reason, string value) + { + InvalidOperationException ex; + switch (reason) + { + case ResponseRejectionReasons.ValueCannotBeSetResponseStarted: + ex = new InvalidOperationException(value + " cannot be set, response had already started."); + break; + case ResponseRejectionReasons.TransferEncodingSetOnNonBodyResponse: + ex = new InvalidOperationException($"Transfer-Encoding set on a {value} non-body request."); + break; + case ResponseRejectionReasons.WriteToNonBodyResponse: + ex = new InvalidOperationException($"Write to non-body {value} response."); + break; + default: + ex = new InvalidOperationException("Bad response."); + break; + } + + return ex; + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index ea969d122..ad2204a62 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -58,6 +58,7 @@ public abstract partial class Frame : ConnectionContext, IFrameControl private RequestProcessingStatus _requestProcessingStatus; protected bool _keepAlive; + private bool _canHaveBody; private bool _autoChunk; protected Exception _applicationException; @@ -135,7 +136,7 @@ public int StatusCode { if (HasResponseStarted) { - ThrowResponseAlreadyStartedException(nameof(StatusCode)); + BadHttpResponse.ThrowException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, ResponseRejectionParameter.StatusCode); } _statusCode = value; @@ -153,7 +154,7 @@ public string ReasonPhrase { if (HasResponseStarted) { - ThrowResponseAlreadyStartedException(nameof(ReasonPhrase)); + BadHttpResponse.ThrowException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, ResponseRejectionParameter.ReasonPhrase); } _reasonPhrase = value; @@ -388,7 +389,7 @@ public void OnStarting(Func callback, object state) { if (HasResponseStarted) { - ThrowResponseAlreadyStartedException(nameof(OnStarting)); + BadHttpResponse.ThrowException(ResponseRejectionReasons.OnStartingCannotBeSetResponseStarted, ResponseRejectionParameter.OnStarting); } if (_onStarting == null) @@ -475,17 +476,24 @@ public void Write(ArraySegment data) { ProduceStartAndFireOnStarting().GetAwaiter().GetResult(); - if (_autoChunk) + if (_canHaveBody) { - if (data.Count == 0) + if (_autoChunk) + { + if (data.Count == 0) + { + return; + } + WriteChunked(data); + } + else { - return; + SocketOutput.Write(data); } - WriteChunked(data); } else { - SocketOutput.Write(data); + HandleNonBodyResponseWrite(data.Count); } } @@ -496,17 +504,25 @@ public Task WriteAsync(ArraySegment data, CancellationToken cancellationTo return WriteAsyncAwaited(data, cancellationToken); } - if (_autoChunk) + if (_canHaveBody) { - if (data.Count == 0) + if (_autoChunk) { - return TaskUtilities.CompletedTask; + if (data.Count == 0) + { + return TaskUtilities.CompletedTask; + } + return WriteChunkedAsync(data, cancellationToken); + } + else + { + return SocketOutput.WriteAsync(data, cancellationToken: cancellationToken); } - return WriteChunkedAsync(data, cancellationToken); } else { - return SocketOutput.WriteAsync(data, cancellationToken: cancellationToken); + HandleNonBodyResponseWrite(data.Count); + return TaskUtilities.CompletedTask; } } @@ -514,18 +530,27 @@ public async Task WriteAsyncAwaited(ArraySegment data, CancellationToken c { await ProduceStartAndFireOnStarting(); - if (_autoChunk) + if (_canHaveBody) { - if (data.Count == 0) + if (_autoChunk) + { + if (data.Count == 0) + { + return; + } + await WriteChunkedAsync(data, cancellationToken); + } + else { - return; + await SocketOutput.WriteAsync(data, cancellationToken: cancellationToken); } - await WriteChunkedAsync(data, cancellationToken); } else { - await SocketOutput.WriteAsync(data, cancellationToken: cancellationToken); + HandleNonBodyResponseWrite(data.Count); + return; } + } private void WriteChunked(ArraySegment data) @@ -640,28 +665,14 @@ protected Task ProduceEnd() if (_requestRejected) { - // 400 Bad Request - StatusCode = 400; _keepAlive = false; + // 400 Bad Request + ErrorResetHeadersToDefaults(statusCode: 400); } else { // 500 Internal Server Error - StatusCode = 500; - } - - ReasonPhrase = null; - - var responseHeaders = FrameResponseHeaders; - responseHeaders.Reset(); - var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues(); - - responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes); - responseHeaders.SetRawContentLength("0", _bytesContentLengthZero); - - if (ServerOptions.AddServerHeader) - { - responseHeaders.SetRawServer(Constants.ServerName, _bytesServer); + ErrorResetHeadersToDefaults(statusCode: 500); } } @@ -715,50 +726,61 @@ private void CreateResponseHeader( bool appCompleted) { var responseHeaders = FrameResponseHeaders; - responseHeaders.SetReadOnly(); var hasConnection = responseHeaders.HasConnection; + // Set whether response can have body + _canHaveBody = StatusCanHaveBody(StatusCode) && Method != "HEAD"; + var end = SocketOutput.ProducingStart(); if (_keepAlive && hasConnection) { var connectionValue = responseHeaders.HeaderConnection.ToString(); _keepAlive = connectionValue.Equals("keep-alive", StringComparison.OrdinalIgnoreCase); } - - if (!responseHeaders.HasTransferEncoding && !responseHeaders.HasContentLength) + + if (_canHaveBody) { - if (appCompleted) + if (!responseHeaders.HasTransferEncoding && !responseHeaders.HasContentLength) { - // Don't set the Content-Length or Transfer-Encoding headers - // automatically for HEAD requests or 101, 204, 205, 304 responses. - if (Method != "HEAD" && StatusCanHaveBody(StatusCode)) + if (appCompleted) { // Since the app has completed and we are only now generating // the headers we can safely set the Content-Length to 0. responseHeaders.SetRawContentLength("0", _bytesContentLengthZero); } - } - else if(_keepAlive) - { - // Note for future reference: never change this to set _autoChunk to true on HTTP/1.0 - // connections, even if we were to infer the client supports it because an HTTP/1.0 request - // was received that used chunked encoding. Sending a chunked response to an HTTP/1.0 - // client would break compliance with RFC 7230 (section 3.3.1): - // - // A server MUST NOT send a response containing Transfer-Encoding unless the corresponding - // request indicates HTTP/1.1 (or later). - if (_httpVersion == Http.HttpVersion.Http11) - { - _autoChunk = true; - responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked); - } else { - _keepAlive = false; + // Note for future reference: never change this to set _autoChunk to true on HTTP/1.0 + // connections, even if we were to infer the client supports it because an HTTP/1.0 request + // was received that used chunked encoding. Sending a chunked response to an HTTP/1.0 + // client would break compliance with RFC 7230 (section 3.3.1): + // + // A server MUST NOT send a response containing Transfer-Encoding unless the corresponding + // request indicates HTTP/1.1 (or later). + if (_httpVersion == Http.HttpVersion.Http11) + { + _autoChunk = true; + responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked); + } + else + { + _keepAlive = false; + } } } } + else + { + // Don't set the Content-Length or Transfer-Encoding headers + // automatically for HEAD requests or 101, 204, 205, 304 responses. + if (responseHeaders.HasTransferEncoding) + { + RejectNonBodyTransferEncodingResponse(appCompleted); + } + } + + responseHeaders.SetReadOnly(); if (!_keepAlive && !hasConnection) { @@ -1215,12 +1237,63 @@ public bool StatusCanHaveBody(int statusCode) statusCode != 304; } - private void ThrowResponseAlreadyStartedException(string value) + private void RejectNonBodyTransferEncodingResponse(bool appCompleted) { - throw new InvalidOperationException(value + " cannot be set, response had already started."); + var ex = BadHttpResponse.GetException(ResponseRejectionReasons.TransferEncodingSetOnNonBodyResponse, StatusCode); + if (!appCompleted) + { + // Back out of header creation surface exeception in user code + _requestProcessingStatus = RequestProcessingStatus.RequestStarted; + throw ex; + } + else + { + ReportApplicationError(ex); + // 500 Internal Server Error + ErrorResetHeadersToDefaults(statusCode: 500); + } + } + + private void ErrorResetHeadersToDefaults(int statusCode) + { + StatusCode = statusCode; + ReasonPhrase = null; + + var responseHeaders = FrameResponseHeaders; + responseHeaders.Reset(); + var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues(); + + responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes); + responseHeaders.SetRawContentLength("0", _bytesContentLengthZero); + + if (ServerOptions.AddServerHeader) + { + responseHeaders.SetRawServer(Constants.ServerName, _bytesServer); + } + } + + public void HandleNonBodyResponseWrite(int count) + { + if (Method == "HEAD") + { + // Don't write to body for HEAD requests. + Log.ConnectionHeadResponseBodyWrite(ConnectionId, count); + } + else + { + // Throw Exception for 101, 204, 205, 304 responses. + BadHttpResponse.ThrowException(ResponseRejectionReasons.WriteToNonBodyResponse, StatusCode); + } } private void ThrowResponseAbortedException() + { + throw new ObjectDisposedException( + "The response has been aborted due to an unhandled application exception.", + _applicationException); + } + + public void RejectRequest(string message) { throw new ObjectDisposedException( "The response has been aborted due to an unhandled application exception.", diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs index ac283a006..d91aec8d6 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs @@ -29,7 +29,7 @@ StringValues IHeaderDictionary.this[string key] { if (_isReadOnly) { - ThrowHeadersReadOnlyException(); + BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted); } SetValueFast(key, value); } @@ -48,11 +48,6 @@ StringValues IDictionary.this[string key] } } - protected void ThrowHeadersReadOnlyException() - { - throw new InvalidOperationException("Headers are read-only, response has already started."); - } - protected void ThrowArgumentException() { throw new ArgumentException(); @@ -144,7 +139,7 @@ void IDictionary.Add(string key, StringValues value) { if (_isReadOnly) { - ThrowHeadersReadOnlyException(); + BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted); } AddValueFast(key, value); } @@ -153,7 +148,7 @@ void ICollection>.Clear() { if (_isReadOnly) { - ThrowHeadersReadOnlyException(); + BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted); } ClearFast(); } @@ -200,7 +195,7 @@ bool IDictionary.Remove(string key) { if (_isReadOnly) { - ThrowHeadersReadOnlyException(); + BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted); } return RemoveFast(key); } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ResponseRejectionReasons.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ResponseRejectionReasons.cs new file mode 100644 index 000000000..4ccc69902 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/ResponseRejectionReasons.cs @@ -0,0 +1,22 @@ +// 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. + +namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http +{ + public enum ResponseRejectionReasons + { + HeadersReadonlyResponseStarted, + ValueCannotBeSetResponseStarted, + TransferEncodingSetOnNonBodyResponse, + WriteToNonBodyResponse, + OnStartingCannotBeSetResponseStarted + } + + public enum ResponseRejectionParameter + { + StatusCode, + ReasonPhrase, + OnStarting + } + +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/IKestrelTrace.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/IKestrelTrace.cs index e18a99ce2..900adc876 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/IKestrelTrace.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/IKestrelTrace.cs @@ -33,6 +33,8 @@ public interface IKestrelTrace : ILogger void ConnectionDisconnectedWrite(string connectionId, int count, Exception ex); + void ConnectionHeadResponseBodyWrite(string connectionId, int count); + void ConnectionBadRequest(string connectionId, BadHttpRequestException ex); void NotAllConnectionsClosedGracefully(); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelTrace.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelTrace.cs index 0215b4537..5d3ecff7b 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelTrace.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelTrace.cs @@ -24,6 +24,7 @@ public class KestrelTrace : IKestrelTrace private static readonly Action _applicationError; private static readonly Action _connectionError; private static readonly Action _connectionDisconnectedWrite; + private static readonly Action _connectionHeadResponseBodyWrite; private static readonly Action _notAllConnectionsClosedGracefully; private static readonly Action _connectionBadRequest; @@ -48,6 +49,7 @@ static KestrelTrace() _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}"""); + _connectionHeadResponseBodyWrite = LoggerMessage.Define(LogLevel.Debug, 18, @"Connection id ""{ConnectionId}"" write of ""{count}"" body bytes to non-body HEAD response."); } public KestrelTrace(ILogger logger) @@ -133,6 +135,11 @@ public virtual void ConnectionDisconnectedWrite(string connectionId, int count, _connectionDisconnectedWrite(_logger, connectionId, count, ex); } + public virtual void ConnectionHeadResponseBodyWrite(string connectionId, int count) + { + _connectionHeadResponseBodyWrite(_logger, connectionId, count, null); + } + public virtual void NotAllConnectionsClosedGracefully() { _notAllConnectionsClosedGracefully(_logger, null); diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs index ecd438a5c..a10178061 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Text; +using System.Threading; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel.Internal; @@ -712,5 +713,182 @@ public void InitializeStreamsResetsStreams() Assert.Same(originalResponseBody, frame.ResponseBody); Assert.Same(originalDuplexStream, frame.DuplexStream); } + + [Fact] + public void FlushSetsTransferEncodingSetForUnknownLengthBodyResponse() + { + // Arrange + var connectionContext = new ConnectionContext() + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), + SocketOutput = new MockSocketOuptut() + }; + var frame = new TestFrameProtectedMembers(application: null, context: connectionContext); + frame.InitializeHeaders(); + frame.KeepAlive = true; + frame.HttpVersion = "HTTP/1.1"; + + // Act + frame.Flush(); + + // Assert + Assert.True(frame.HasResponseStarted); + Assert.True(frame.ResponseHeaders.ContainsKey("Transfer-Encoding")); + } + + [Fact] + public void FlushDoesNotSetTransferEncodingSetForNoBodyResponse() + { + // Arrange + var connectionContext = new ConnectionContext() + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), + SocketOutput = new MockSocketOuptut() + }; + var frame = new TestFrameProtectedMembers(application: null, context: connectionContext); + frame.InitializeHeaders(); + frame.KeepAlive = true; + frame.HttpVersion = "HTTP/1.1"; + ((IHttpResponseFeature)frame).StatusCode = 304; + + // Act + frame.Flush(); + + // Assert + Assert.True(frame.HasResponseStarted); + Assert.False(frame.ResponseHeaders.ContainsKey("Transfer-Encoding")); + } + + [Fact] + public void FlushDoesNotSetTransferEncodingSetForHeadResponse() + { + // Arrange + var connectionContext = new ConnectionContext() + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), + SocketOutput = new MockSocketOuptut() + }; + var frame = new TestFrameProtectedMembers(application: null, context: connectionContext); + frame.InitializeHeaders(); + frame.KeepAlive = true; + frame.HttpVersion = "HTTP/1.1"; + ((IHttpRequestFeature)frame).Method = "HEAD"; + + // Act + frame.Flush(); + + // Assert + Assert.True(frame.HasResponseStarted); + Assert.False(frame.ResponseHeaders.ContainsKey("Transfer-Encoding")); + } + + [Fact] + public void WriteThrowsForNoBodyResponse() + { + // Arrange + var connectionContext = new ConnectionContext() + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), + SocketOutput = new MockSocketOuptut() + }; + var frame = new TestFrameProtectedMembers(application: null, context: connectionContext); + frame.InitializeHeaders(); + frame.KeepAlive = true; + frame.HttpVersion = "HTTP/1.1"; + ((IHttpResponseFeature)frame).StatusCode = 304; + + // Assert + frame.Flush(); // Does not throw + + Assert.Throws(() => frame.Write(new ArraySegment(new byte[1]))); + Assert.ThrowsAsync(() => frame.WriteAsync(new ArraySegment(new byte[1]), default(CancellationToken))); + + frame.Flush(); // Does not throw + } + + [Fact] + public void WriteDoesNotThrowForHeadResponse() + { + // Arrange + var connectionContext = new ConnectionContext() + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), + SocketOutput = new MockSocketOuptut(), + Log = new TestKestrelTrace() + }; + var frame = new TestFrameProtectedMembers(application: null, context: connectionContext); + frame.InitializeHeaders(); + frame.KeepAlive = true; + frame.HttpVersion = "HTTP/1.1"; + ((IHttpRequestFeature)frame).Method = "HEAD"; + + // Assert + frame.Flush(); // Does not throw + + frame.Write(new ArraySegment(new byte[1])); + + frame.Flush(); // Does not throw + } + + + [Fact] + public void ManuallySettingTransferEncodingThrowsForHeadResponse() + { + // Arrange + var connectionContext = new ConnectionContext() + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), + SocketOutput = new MockSocketOuptut(), + Log = new TestKestrelTrace() + }; + var frame = new TestFrameProtectedMembers(application: null, context: connectionContext); + frame.InitializeHeaders(); + frame.KeepAlive = true; + frame.HttpVersion = "HTTP/1.1"; + ((IHttpRequestFeature)frame).Method = "HEAD"; + + //Act + frame.ResponseHeaders.Add("Transfer-Encoding", "chunked"); + + // Assert + Assert.Throws(() => frame.Flush()); + } + + [Fact] + public void ManuallySettingTransferEncodingThrowsForNoBodyResponse() + { + // Arrange + var connectionContext = new ConnectionContext() + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), + SocketOutput = new MockSocketOuptut(), + Log = new TestKestrelTrace() + }; + var frame = new TestFrameProtectedMembers(application: null, context: connectionContext); + frame.InitializeHeaders(); + frame.KeepAlive = true; + frame.HttpVersion = "HTTP/1.1"; + ((IHttpResponseFeature)frame).StatusCode = 304; + + //Act + frame.ResponseHeaders.Add("Transfer-Encoding", "chunked"); + + // Assert + Assert.Throws(() => frame.Flush()); + } } } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestFrameProtectedMembers.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestFrameProtectedMembers.cs new file mode 100644 index 000000000..01d6418ce --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestFrameProtectedMembers.cs @@ -0,0 +1,23 @@ +// 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 Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; + +namespace Microsoft.AspNetCore.Server.KestrelTests +{ + public class TestFrameProtectedMembers : Frame + { + public TestFrameProtectedMembers(IHttpApplication application, ConnectionContext context) + : base(application, context) + { + } + + public bool KeepAlive + { + get { return _keepAlive; } + set { _keepAlive = value; } + } + } +}