Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 183 additions & 33 deletions src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;

Expand Down Expand Up @@ -287,6 +288,128 @@ bool TrimAndTakeMessageHeaders(ref SequenceReader<byte> reader, bool trailers)
}
}

/// <summary>
/// Non-throwing version of ParseRequest. Returns HttpParseResult instead of throwing.
/// </summary>
private HttpParseResult TryParseRequestNoThrow(ref SequenceReader<byte> reader)
{
switch (_requestProcessingStatus)
{
case RequestProcessingStatus.RequestPending:
if (reader.TryPeek(out byte b) && (b == ByteCR || b == ByteLF))
{
reader.AdvancePastAny(ByteCR, ByteLF);
}

if (reader.End)
{
return HttpParseResult.Incomplete;
}

TimeoutControl.ResetTimeout(ServerOptions.Limits.RequestHeadersTimeout, TimeoutReason.RequestHeaders);
_requestProcessingStatus = RequestProcessingStatus.ParsingRequestLine;
goto case RequestProcessingStatus.ParsingRequestLine;

case RequestProcessingStatus.ParsingRequestLine:
var startLineResult = TryTakeStartLineNoThrow(ref reader);
if (startLineResult.HasError)
{
return startLineResult;
}
if (startLineResult.IsComplete)
{
_requestProcessingStatus = RequestProcessingStatus.ParsingHeaders;
goto case RequestProcessingStatus.ParsingHeaders;
}
return HttpParseResult.Incomplete;

case RequestProcessingStatus.ParsingHeaders:
var headersResult = TryTakeMessageHeadersNoThrow(ref reader, trailers: false);
if (headersResult.HasError)
{
return headersResult;
}
if (headersResult.IsComplete)
{
_requestProcessingStatus = RequestProcessingStatus.AppStarted;
return HttpParseResult.Complete;
}
return HttpParseResult.Incomplete;
}

return HttpParseResult.Incomplete;
}

private HttpParseResult TryTakeStartLineNoThrow(ref SequenceReader<byte> reader)
{
if (reader.Remaining >= ServerOptions.Limits.MaxRequestLineSize)
{
return TryTrimAndTakeStartLineNoThrow(ref reader);
}

return ((HttpParser<Http1ParsingHandler>)_parser).TryParseRequestLine(new Http1ParsingHandler(this), ref reader);

HttpParseResult TryTrimAndTakeStartLineNoThrow(ref SequenceReader<byte> reader)
{
var trimmedBuffer = reader.Sequence.Slice(reader.Position, ServerOptions.Limits.MaxRequestLineSize);
var trimmedReader = new SequenceReader<byte>(trimmedBuffer);

var result = ((HttpParser<Http1ParsingHandler>)_parser).TryParseRequestLine(new Http1ParsingHandler(this), ref trimmedReader);
if (result.HasError)
{
return result;
}
if (!result.IsComplete)
{
return HttpParseResult.Error(RequestRejectionReason.RequestLineTooLong);
}

reader.Advance(trimmedReader.Consumed);
return HttpParseResult.Complete;
}
}

private HttpParseResult TryTakeMessageHeadersNoThrow(ref SequenceReader<byte> reader, bool trailers)
{
if (reader.Remaining > _remainingRequestHeadersBytesAllowed)
{
return TryTrimAndTakeMessageHeadersNoThrow(ref reader, trailers);
}

var alreadyConsumed = reader.Consumed;
var result = ((HttpParser<Http1ParsingHandler>)_parser).TryParseHeaders(new Http1ParsingHandler(this, trailers), ref reader);
_remainingRequestHeadersBytesAllowed -= reader.Consumed - alreadyConsumed;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous code put this in a finally for some reason. I think all cases where TryParseHeaders throws we would mark endConnection and so wouldn't care about the value of _remainingRequestHeadersBytesAllowed anymore. Just want to call this out in case I'm wrong.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. When an error occurs, endConnection is set to true and the connection is closed, so the value of _remainingRequestHeadersBytesAllowed doesn't matter after that point.


if (result.IsComplete)
{
TimeoutControl.CancelTimeout();
}

return result;

HttpParseResult TryTrimAndTakeMessageHeadersNoThrow(ref SequenceReader<byte> reader, bool trailers)
{
var trimmedBuffer = reader.Sequence.Slice(reader.Position, _remainingRequestHeadersBytesAllowed);
var trimmedReader = new SequenceReader<byte>(trimmedBuffer);

var result = ((HttpParser<Http1ParsingHandler>)_parser).TryParseHeaders(new Http1ParsingHandler(this, trailers), ref trimmedReader);
_remainingRequestHeadersBytesAllowed -= trimmedReader.Consumed;

if (result.HasError)
{
return result;
}
if (!result.IsComplete)
{
return HttpParseResult.Error(RequestRejectionReason.HeadersExceedMaxTotalSize);
}

TimeoutControl.CancelTimeout();
reader.Advance(trimmedReader.Consumed);
return HttpParseResult.Complete;
}
}

public void OnStartLine(HttpVersionAndMethod versionAndMethod, TargetOffsetPathLength targetPath, Span<byte> startLine)
{
// Null characters are not allowed and should have been checked by HttpParser before calling this method
Expand Down Expand Up @@ -721,37 +844,26 @@ protected override bool BeginRead(out ValueTask<ReadResult> awaitable)
protected override bool TryParseRequest(ReadResult result, out bool endConnection)
{
var reader = new SequenceReader<byte>(result.Buffer);
var isConsumed = false;
try
{
isConsumed = ParseRequest(ref reader);
}
catch (InvalidOperationException) when (_requestProcessingStatus == RequestProcessingStatus.ParsingHeaders)
{
KestrelBadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders);
throw;
}
#pragma warning disable CS0618 // Type or member is obsolete
catch (BadHttpRequestException ex)

// Use non-throwing parser path for performance
var parseResult = TryParseRequestNoThrow(ref reader);
var isConsumed = parseResult.IsComplete;

// Handle parse errors without exceptions
if (parseResult.HasError)
{
// Create exception and call OnBadRequest BEFORE AdvanceTo, as that may invalidate the buffer
var ex = CreateBadRequestException(parseResult, result.Buffer);
OnBadRequest(result.Buffer, ex);

// Avoid re-throwing - handle the bad request state directly here
// to eliminate exception propagation overhead through async state machines
Input.AdvanceTo(reader.Position, result.Buffer.End);

SetBadRequestState(ex);
endConnection = true;
return true;
}
#pragma warning restore CS0618 // Type or member is obsolete
catch (Exception)
{
KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.OtherError);
throw;
}
finally
{
Input.AdvanceTo(reader.Position, isConsumed ? reader.Position : result.Buffer.End);
}

Input.AdvanceTo(reader.Position, isConsumed ? reader.Position : result.Buffer.End);

if (result.IsCompleted)
{
Expand All @@ -761,25 +873,35 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio
endConnection = true;
return true;
case RequestProcessingStatus.ParsingRequestLine:
KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestLine);
break;
{
var ex = KestrelBadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine);
OnBadRequest(result.Buffer, ex);
SetBadRequestState(ex);
endConnection = true;
return true;
}
case RequestProcessingStatus.ParsingHeaders:
KestrelBadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders);
break;
{
var ex = KestrelBadHttpRequestException.GetException(RequestRejectionReason.MalformedRequestInvalidHeaders);
OnBadRequest(result.Buffer, ex);
SetBadRequestState(ex);
endConnection = true;
return true;
}
}
}
else if (!_keepAlive && _requestProcessingStatus == RequestProcessingStatus.RequestPending)
{
// Stop the request processing loop if the server is shutting down or there was a keep-alive timeout
// and there is no ongoing request.
endConnection = true;
return true;
}
else if (RequestTimedOut)
{
// In this case, there is an ongoing request but the start line/header parsing has timed out, so send
// a 408 response.
KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestHeadersTimeout);
var ex = KestrelBadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout);
OnBadRequest(result.Buffer, ex);
SetBadRequestState(ex);
endConnection = true;
return true;
}

endConnection = false;
Expand Down Expand Up @@ -836,6 +958,34 @@ internal static ConnectionEndReason GetConnectionEndReason(Microsoft.AspNetCore.
}
}

#pragma warning disable CS0618 // Type or member is obsolete
/// <summary>
/// Creates a BadHttpRequestException with error detail extracted from the buffer.
/// </summary>
private BadHttpRequestException CreateBadRequestException(HttpParseResult parseResult, ReadOnlySequence<byte> buffer)
{
// Some error reasons don't use detail
if (parseResult.ErrorReason == RequestRejectionReason.InvalidRequestHeadersNoCRLF)
{
return KestrelBadHttpRequestException.GetException(parseResult.ErrorReason);
}

// Extract error detail from buffer if available, but only when logging is enabled
// to avoid leaking internal details in production.
if (Log.IsEnabled(LogLevel.Information) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of our exception logic uses HttpParser._showErrorDetails and some of it uses LogLevel.Information. Seems like we're losing that differentiator.

parseResult.ErrorLength > 0 &&
parseResult.ErrorOffset + parseResult.ErrorLength <= buffer.Length)
{
var errorSlice = buffer.Slice(parseResult.ErrorOffset, parseResult.ErrorLength);
var errorBytes = errorSlice.IsSingleSegment ? errorSlice.FirstSpan : errorSlice.ToArray();
var detail = errorBytes.GetAsciiStringEscaped(Constants.MaxExceptionDetailSize);
return KestrelBadHttpRequestException.GetException(parseResult.ErrorReason, detail);
}

return KestrelBadHttpRequestException.GetException(parseResult.ErrorReason);
}
#pragma warning restore CS0618 // Type or member is obsolete

#pragma warning disable CS0618 // Type or member is obsolete
private void OnBadRequest(ReadOnlySequence<byte> requestData, BadHttpRequestException ex)
#pragma warning restore CS0618 // Type or member is obsolete
Expand Down
Loading
Loading