Don't emit TE header or body for non-body responses

This commit is contained in:
Ben Adams 2016-07-09 08:48:36 +01:00 committed by Cesar Blum Silveira
parent e8fa40235b
commit e7e6b896ba
8 changed files with 493 additions and 72 deletions

View File

@ -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;
}
}
}

View File

@ -60,6 +60,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private RequestProcessingStatus _requestProcessingStatus;
protected bool _keepAlive;
private bool _canHaveBody;
private bool _autoChunk;
protected Exception _applicationException;
@ -171,7 +172,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
if (HasResponseStarted)
{
ThrowResponseAlreadyStartedException(nameof(StatusCode));
BadHttpResponse.ThrowException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, ResponseRejectionParameter.StatusCode);
}
_statusCode = value;
@ -189,7 +190,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
if (HasResponseStarted)
{
ThrowResponseAlreadyStartedException(nameof(ReasonPhrase));
BadHttpResponse.ThrowException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, ResponseRejectionParameter.ReasonPhrase);
}
_reasonPhrase = value;
@ -425,7 +426,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
if (HasResponseStarted)
{
ThrowResponseAlreadyStartedException(nameof(OnStarting));
BadHttpResponse.ThrowException(ResponseRejectionReasons.OnStartingCannotBeSetResponseStarted, ResponseRejectionParameter.OnStarting);
}
if (_onStarting == null)
@ -512,17 +513,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
ProduceStartAndFireOnStarting().GetAwaiter().GetResult();
if (_autoChunk)
if (_canHaveBody)
{
if (data.Count == 0)
if (_autoChunk)
{
return;
if (data.Count == 0)
{
return;
}
WriteChunked(data);
}
else
{
SocketOutput.Write(data);
}
WriteChunked(data);
}
else
{
SocketOutput.Write(data);
HandleNonBodyResponseWrite(data.Count);
}
}
@ -533,17 +541,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return WriteAsyncAwaited(data, cancellationToken);
}
if (_autoChunk)
if (_canHaveBody)
{
if (data.Count == 0)
if (_autoChunk)
{
return TaskCache.CompletedTask;
if (data.Count == 0)
{
return TaskCache.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 TaskCache.CompletedTask;
}
}
@ -551,18 +567,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
await ProduceStartAndFireOnStarting();
if (_autoChunk)
if (_canHaveBody)
{
if (data.Count == 0)
if (_autoChunk)
{
return;
if (data.Count == 0)
{
return;
}
await WriteChunkedAsync(data, cancellationToken);
}
else
{
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<byte> data)
@ -679,21 +704,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
if (!_requestRejected)
{
// 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);
}
}
@ -747,50 +758,61 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
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)
{
@ -1255,12 +1277,67 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
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)
{
// Setting status code will throw if response has already started
if (!HasResponseStarted)
{
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.",
@ -1288,11 +1365,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public void SetBadRequestState(BadHttpRequestException ex)
{
// Setting status code will throw if response has already started
if (!HasResponseStarted)
{
StatusCode = ex.StatusCode;
}
ErrorResetHeadersToDefaults(statusCode: ex.StatusCode);
_keepAlive = false;
_requestProcessingStopping = true;

View File

@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
if (_isReadOnly)
{
ThrowHeadersReadOnlyException();
BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted);
}
SetValueFast(key, value);
}
@ -48,11 +48,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
protected void ThrowHeadersReadOnlyException()
{
throw new InvalidOperationException("Headers are read-only, response has already started.");
}
protected void ThrowArgumentException()
{
throw new ArgumentException();
@ -144,7 +139,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
if (_isReadOnly)
{
ThrowHeadersReadOnlyException();
BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted);
}
AddValueFast(key, value);
}
@ -153,7 +148,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
if (_isReadOnly)
{
ThrowHeadersReadOnlyException();
BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted);
}
ClearFast();
}
@ -200,7 +195,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
if (_isReadOnly)
{
ThrowHeadersReadOnlyException();
BadHttpResponse.ThrowException(ResponseRejectionReasons.HeadersReadonlyResponseStarted);
}
return RemoveFast(key);
}

View File

@ -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
}
}

View File

@ -33,6 +33,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
void ConnectionDisconnectedWrite(string connectionId, int count, Exception ex);
void ConnectionHeadResponseBodyWrite(string connectionId, int count);
void ConnectionBadRequest(string connectionId, BadHttpRequestException ex);
void NotAllConnectionsClosedGracefully();

View File

@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
private static readonly Action<ILogger, string, Exception> _applicationError;
private static readonly Action<ILogger, string, Exception> _connectionError;
private static readonly Action<ILogger, string, int, Exception> _connectionDisconnectedWrite;
private static readonly Action<ILogger, string, int, Exception> _connectionHeadResponseBodyWrite;
private static readonly Action<ILogger, Exception> _notAllConnectionsClosedGracefully;
private static readonly Action<ILogger, string, string, Exception> _connectionBadRequest;
@ -48,6 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
_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}""");
_connectionHeadResponseBodyWrite = LoggerMessage.Define<string, int>(LogLevel.Debug, 18, @"Connection id ""{ConnectionId}"" write of ""{count}"" body bytes to non-body HEAD response.");
}
public KestrelTrace(ILogger logger)
@ -133,6 +135,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
_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);

View File

@ -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;
@ -1260,5 +1261,228 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
requestProcessingTask.Wait();
}
}
[Fact]
public void FlushSetsTransferEncodingSetForUnknownLengthBodyResponse()
{
// Arrange
var serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
Log = new TestKestrelTrace()
};
var listenerContext = new ListenerContext(serviceContext)
{
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
};
var connectionContext = new ConnectionContext(listenerContext)
{
SocketOutput = new MockSocketOuptut(),
};
var frame = new TestFrameProtectedMembers<object>(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 serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
Log = new TestKestrelTrace()
};
var listenerContext = new ListenerContext(serviceContext)
{
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
};
var connectionContext = new ConnectionContext(listenerContext)
{
SocketOutput = new MockSocketOuptut(),
};
var frame = new TestFrameProtectedMembers<object>(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 serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
Log = new TestKestrelTrace()
};
var listenerContext = new ListenerContext(serviceContext)
{
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
};
var connectionContext = new ConnectionContext(listenerContext)
{
SocketOutput = new MockSocketOuptut(),
};
var frame = new TestFrameProtectedMembers<object>(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 serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
Log = new TestKestrelTrace()
};
var listenerContext = new ListenerContext(serviceContext)
{
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
};
var connectionContext = new ConnectionContext(listenerContext)
{
SocketOutput = new MockSocketOuptut(),
};
var frame = new TestFrameProtectedMembers<object>(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<InvalidOperationException>(() => frame.Write(new ArraySegment<byte>(new byte[1])));
Assert.ThrowsAsync<InvalidOperationException>(() => frame.WriteAsync(new ArraySegment<byte>(new byte[1]), default(CancellationToken)));
frame.Flush(); // Does not throw
}
[Fact]
public void WriteDoesNotThrowForHeadResponse()
{
// Arrange
var serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
Log = new TestKestrelTrace()
};
var listenerContext = new ListenerContext(serviceContext)
{
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
};
var connectionContext = new ConnectionContext(listenerContext)
{
SocketOutput = new MockSocketOuptut(),
};
var frame = new TestFrameProtectedMembers<object>(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<byte>(new byte[1]));
frame.Flush(); // Does not throw
}
[Fact]
public void ManuallySettingTransferEncodingThrowsForHeadResponse()
{
// Arrange
var serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
Log = new TestKestrelTrace()
};
var listenerContext = new ListenerContext(serviceContext)
{
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
};
var connectionContext = new ConnectionContext(listenerContext)
{
SocketOutput = new MockSocketOuptut(),
};
var frame = new TestFrameProtectedMembers<object>(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<InvalidOperationException>(() => frame.Flush());
}
[Fact]
public void ManuallySettingTransferEncodingThrowsForNoBodyResponse()
{
// Arrange
var serviceContext = new ServiceContext
{
DateHeaderValueManager = new DateHeaderValueManager(),
ServerOptions = new KestrelServerOptions(),
Log = new TestKestrelTrace()
};
var listenerContext = new ListenerContext(serviceContext)
{
ServerAddress = ServerAddress.FromUrl("http://localhost:5000")
};
var connectionContext = new ConnectionContext(listenerContext)
{
SocketOutput = new MockSocketOuptut(),
};
var frame = new TestFrameProtectedMembers<object>(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<InvalidOperationException>(() => frame.Flush());
}
}
}

View File

@ -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<TContext> : Frame<TContext>
{
public TestFrameProtectedMembers(IHttpApplication<TContext> application, ConnectionContext context)
: base(application, context)
{
}
public bool KeepAlive
{
get { return _keepAlive; }
set { _keepAlive = value; }
}
}
}