Skip to content
This repository has been archived by the owner on Dec 18, 2018. It is now read-only.

Commit

Permalink
Don't emit TE header or body for non-body responses
Browse files Browse the repository at this point in the history
  • Loading branch information
benaadams authored and Cesar Blum Silveira committed Oct 3, 2016
1 parent e8fa402 commit e7e6b89
Show file tree
Hide file tree
Showing 8 changed files with 493 additions and 72 deletions.
75 changes: 75 additions & 0 deletions src/Microsoft.AspNetCore.Server.Kestrel/BadHttpResponse.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
199 changes: 136 additions & 63 deletions src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public abstract partial class Frame : IFrameControl

private RequestProcessingStatus _requestProcessingStatus;
protected bool _keepAlive;
private bool _canHaveBody;
private bool _autoChunk;
protected Exception _applicationException;

Expand Down Expand Up @@ -171,7 +172,7 @@ public int StatusCode
{
if (HasResponseStarted)
{
ThrowResponseAlreadyStartedException(nameof(StatusCode));
BadHttpResponse.ThrowException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, ResponseRejectionParameter.StatusCode);
}

_statusCode = value;
Expand All @@ -189,7 +190,7 @@ public string ReasonPhrase
{
if (HasResponseStarted)
{
ThrowResponseAlreadyStartedException(nameof(ReasonPhrase));
BadHttpResponse.ThrowException(ResponseRejectionReasons.ValueCannotBeSetResponseStarted, ResponseRejectionParameter.ReasonPhrase);
}

_reasonPhrase = value;
Expand Down Expand Up @@ -425,7 +426,7 @@ public void OnStarting(Func<object, Task> callback, object state)
{
if (HasResponseStarted)
{
ThrowResponseAlreadyStartedException(nameof(OnStarting));
BadHttpResponse.ThrowException(ResponseRejectionReasons.OnStartingCannotBeSetResponseStarted, ResponseRejectionParameter.OnStarting);
}

if (_onStarting == null)
Expand Down Expand Up @@ -512,17 +513,24 @@ public void Write(ArraySegment<byte> 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);
}
}

Expand All @@ -533,36 +541,53 @@ public Task WriteAsync(ArraySegment<byte> data, CancellationToken cancellationTo
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;
}
}

public async Task WriteAsyncAwaited(ArraySegment<byte> data, CancellationToken cancellationToken)
{
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)
Expand Down Expand Up @@ -679,21 +704,7 @@ protected Task ProduceEnd()
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);
}
}

Expand Down Expand Up @@ -747,50 +758,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)
{
Expand Down Expand Up @@ -1255,12 +1277,67 @@ public bool StatusCanHaveBody(int statusCode)
statusCode != 304;
}

private void ThrowResponseAlreadyStartedException(string value)
private void RejectNonBodyTransferEncodingResponse(bool appCompleted)
{
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)
{
throw new InvalidOperationException(value + " cannot be set, response had already started.");
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.",
Expand Down Expand Up @@ -1288,11 +1365,7 @@ public void SetBadRequestState(RequestRejectionReason reason)

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;
Expand Down
Loading

0 comments on commit e7e6b89

Please sign in to comment.