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

Change how HTTP/2 frames are parsed and generated #2893

Merged
merged 1 commit into from
Sep 17, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions src/Kestrel.Core/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -572,13 +572,13 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="GreaterThanZeroRequired" xml:space="preserve">
<value>A value greater than zero is required.</value>
</data>
<data name="Http2FrameMissingFields" xml:space="preserve">
<value>The frame is too short to contain the fields indicated by the given flags.</value>
</data>
<data name="ArgumentOutOfRange" xml:space="preserve">
<value>A value between {min} and {max} is required.</value>
</data>
<data name="HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock" xml:space="preserve">
<value>Dynamic tables size update did not occur at the beginning of the first header block.</value>
</data>
<data name="HPackErrorNotEnoughBuffer" xml:space="preserve">
<value>The given buffer was too small to encode any headers.</value>
</data>
</root>
11 changes: 8 additions & 3 deletions src/Kestrel.Core/Internal/Http2/HPack/HPackDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Buffers;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
Expand Down Expand Up @@ -109,11 +110,15 @@ internal HPackDecoder(int maxDynamicTableSize, DynamicTable dynamicTable)
_dynamicTable = dynamicTable;
}

public void Decode(Span<byte> data, bool endHeaders, IHttpHeadersHandler handler)
public void Decode(ReadOnlySequence<byte> data, bool endHeaders, IHttpHeadersHandler handler)
{
for (var i = 0; i < data.Length; i++)
foreach (var segment in data)
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
{
OnByte(data[i], handler);
var span = segment.Span;
for (var i = 0; i < span.Length; i++)
{
OnByte(span[i], handler);
}
}

if (endHeaders)
Expand Down
11 changes: 10 additions & 1 deletion src/Kestrel.Core/Internal/Http2/HPack/HPackEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,29 @@ public bool BeginEncode(int statusCode, IEnumerable<KeyValuePair<string, string>
_enumerator.MoveNext();

var statusCodeLength = EncodeStatusCode(statusCode, buffer);
var done = Encode(buffer.Slice(statusCodeLength), out var headersLength);
var done = Encode(buffer.Slice(statusCodeLength), throwIfNoneEncoded: false, out var headersLength);
length = statusCodeLength + headersLength;

return done;
}

public bool Encode(Span<byte> buffer, out int length)
{
return Encode(buffer, throwIfNoneEncoded: true, out length);
}

private bool Encode(Span<byte> buffer, bool throwIfNoneEncoded, out int length)
{
length = 0;

do
{
if (!EncodeHeader(_enumerator.Current.Key, _enumerator.Current.Value, buffer.Slice(length), out var headerLength))
{
if (length == 0 && throwIfNoneEncoded)
{
throw new HPackEncodingException(CoreStrings.HPackErrorNotEnoughBuffer);
Copy link
Member

Choose a reason for hiding this comment

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

I know we don't plan for this to be permanent, but I would still like a test verifying this gets thrown from the first body write.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added

}
return false;
}

Expand Down
19 changes: 19 additions & 0 deletions src/Kestrel.Core/Internal/Http2/HPack/HPackEncodingException.cs
Original file line number Diff line number Diff line change
@@ -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;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
{
public class HPackEncodingException : Exception
{
public HPackEncodingException(string message)
: base(message)
{
}
public HPackEncodingException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}
73 changes: 34 additions & 39 deletions src/Kestrel.Core/Internal/Http2/Http2Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private enum PseudoHeaderFields
private readonly Http2PeerSettings _serverSettings = new Http2PeerSettings();
private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings();

private readonly Http2Frame _incomingFrame;
private readonly Http2Frame _incomingFrame = new Http2Frame();

private Http2Stream _currentHeadersStream;
private RequestHeaderParsingState _requestHeaderParsingState;
Expand All @@ -92,7 +92,6 @@ public Http2Connection(Http2ConnectionContext context)
_serverSettings.MaxFrameSize = (uint)context.ServiceContext.ServerOptions.Limits.Http2.MaxFrameSize;
_serverSettings.HeaderTableSize = (uint)context.ServiceContext.ServerOptions.Limits.Http2.HeaderTableSize;
_hpackDecoder = new HPackDecoder((int)_serverSettings.HeaderTableSize);
_incomingFrame = new Http2Frame(_serverSettings.MaxFrameSize);
_serverSettings.MaxHeaderListSize = (uint)context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize;
}

Expand Down Expand Up @@ -188,16 +187,21 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
var result = await Input.ReadAsync();
var readableBuffer = result.Buffer;
var consumed = readableBuffer.Start;
var examined = readableBuffer.End;
var examined = readableBuffer.Start;

try
{
if (!readableBuffer.IsEmpty)
{
if (Http2FrameReader.ReadFrame(readableBuffer, _incomingFrame, _serverSettings.MaxFrameSize, out consumed, out examined))
if (Http2FrameReader.ReadFrame(readableBuffer, _incomingFrame, _serverSettings.MaxFrameSize, out var framePayload))
{
Log.Http2FrameReceived(ConnectionId, _incomingFrame);
await ProcessFrameAsync(application);
consumed = examined = framePayload.End;
await ProcessFrameAsync(application, framePayload);
}
else
{
examined = readableBuffer.End;
}
}

Expand Down Expand Up @@ -372,7 +376,7 @@ private bool ParsePreface(ReadOnlySequence<byte> readableBuffer, out SequencePos
return true;
}

private Task ProcessFrameAsync<TContext>(IHttpApplication<TContext> application)
private Task ProcessFrameAsync<TContext>(IHttpApplication<TContext> application, ReadOnlySequence<byte> payload)
{
// http://httpwg.org/specs/rfc7540.html#rfc.section.5.1.1
// Streams initiated by a client MUST use odd-numbered stream identifiers; ...
Expand All @@ -386,31 +390,31 @@ private Task ProcessFrameAsync<TContext>(IHttpApplication<TContext> application)
switch (_incomingFrame.Type)
{
case Http2FrameType.DATA:
return ProcessDataFrameAsync();
return ProcessDataFrameAsync(payload);
case Http2FrameType.HEADERS:
return ProcessHeadersFrameAsync(application);
return ProcessHeadersFrameAsync(application, payload);
case Http2FrameType.PRIORITY:
return ProcessPriorityFrameAsync();
case Http2FrameType.RST_STREAM:
return ProcessRstStreamFrameAsync();
case Http2FrameType.SETTINGS:
return ProcessSettingsFrameAsync();
return ProcessSettingsFrameAsync(payload);
case Http2FrameType.PUSH_PROMISE:
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorPushPromiseReceived, Http2ErrorCode.PROTOCOL_ERROR);
case Http2FrameType.PING:
return ProcessPingFrameAsync();
return ProcessPingFrameAsync(payload);
case Http2FrameType.GOAWAY:
return ProcessGoAwayFrameAsync();
case Http2FrameType.WINDOW_UPDATE:
return ProcessWindowUpdateFrameAsync();
case Http2FrameType.CONTINUATION:
return ProcessContinuationFrameAsync(application);
return ProcessContinuationFrameAsync(application, payload);
default:
return ProcessUnknownFrameAsync();
}
}

private Task ProcessDataFrameAsync()
private Task ProcessDataFrameAsync(ReadOnlySequence<byte> payload)
{
if (_currentHeadersStream != null)
{
Expand All @@ -422,12 +426,6 @@ private Task ProcessDataFrameAsync()
throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR);
}

// The Padding field is missing
if (_incomingFrame.DataPayloadOffset > _incomingFrame.PayloadLength)
{
throw new Http2ConnectionErrorException(CoreStrings.Http2FrameMissingFields, Http2ErrorCode.PROTOCOL_ERROR);
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
}

if (_incomingFrame.DataHasPadding && _incomingFrame.DataPadLength >= _incomingFrame.PayloadLength)
{
throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorPaddingTooLong(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR);
Expand All @@ -449,7 +447,7 @@ private Task ProcessDataFrameAsync()
throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(_incomingFrame.Type, stream.StreamId), Http2ErrorCode.STREAM_CLOSED);
}

return stream.OnDataAsync(_incomingFrame);
return stream.OnDataAsync(_incomingFrame, payload);
}

// If we couldn't find the stream, it was either alive previously but closed with
Expand All @@ -466,7 +464,7 @@ private Task ProcessDataFrameAsync()
throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamClosed(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.STREAM_CLOSED);
}

private async Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext> application)
private Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext> application, ReadOnlySequence<byte> payload)
{
if (_currentHeadersStream != null)
{
Expand All @@ -478,12 +476,6 @@ private async Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext>
throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR);
}

// Padding or priority fields are missing
if (_incomingFrame.HeadersPayloadOffset > _incomingFrame.PayloadLength)
{
throw new Http2ConnectionErrorException(CoreStrings.Http2FrameMissingFields, Http2ErrorCode.PROTOCOL_ERROR);
}

if (_incomingFrame.HeadersHasPadding && _incomingFrame.HeadersPadLength >= _incomingFrame.PayloadLength - 1)
{
throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorPaddingTooLong(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR);
Expand Down Expand Up @@ -518,7 +510,8 @@ private async Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext>
_currentHeadersStream = stream;
_requestHeaderParsingState = RequestHeaderParsingState.Trailers;

await DecodeTrailersAsync(_incomingFrame.HeadersEndHeaders, _incomingFrame.HeadersPayload);
var headersPayload = payload.Slice(0, _incomingFrame.HeadersPayloadLength); // Minus padding
return DecodeTrailersAsync(_incomingFrame.HeadersEndHeaders, headersPayload);
}
else if (_incomingFrame.StreamId <= _highestOpenedStreamId)
{
Expand Down Expand Up @@ -554,7 +547,8 @@ private async Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext>
_currentHeadersStream.Reset();
_headerFlags = _incomingFrame.HeadersFlags;

await DecodeHeadersAsync(application, _incomingFrame.HeadersEndHeaders, _incomingFrame.HeadersPayload);
var headersPayload = payload.Slice(0, _incomingFrame.HeadersPayloadLength); // Minus padding
return DecodeHeadersAsync(application, _incomingFrame.HeadersEndHeaders, headersPayload);
}
}

Expand Down Expand Up @@ -606,7 +600,7 @@ private Task ProcessRstStreamFrameAsync()
return Task.CompletedTask;
}

private Task ProcessSettingsFrameAsync()
private Task ProcessSettingsFrameAsync(ReadOnlySequence<byte> payload)
{
if (_currentHeadersStream != null)
{
Expand Down Expand Up @@ -639,14 +633,15 @@ private Task ProcessSettingsFrameAsync()
var previousInitialWindowSize = (int)_clientSettings.InitialWindowSize;
var previousMaxFrameSize = _clientSettings.MaxFrameSize;

_clientSettings.Update(_incomingFrame.GetSettings());
_clientSettings.Update(Http2FrameReader.ReadSettings(payload));

// Ack before we update the windows, they could send data immediately.
var ackTask = _frameWriter.WriteSettingsAckAsync();
var ackTask = _frameWriter.WriteSettingsAckAsync();

if (_clientSettings.MaxFrameSize != previousMaxFrameSize)
{
_frameWriter.UpdateMaxFrameSize(_clientSettings.MaxFrameSize);
// Don't let the client choose an arbitrarily large size, this will be used for response buffers.
_frameWriter.UpdateMaxFrameSize(Math.Min(_clientSettings.MaxFrameSize, _serverSettings.MaxFrameSize));
Tratcher marked this conversation as resolved.
Show resolved Hide resolved
}

// This difference can be negative.
Expand Down Expand Up @@ -676,7 +671,7 @@ private Task ProcessSettingsFrameAsync()
}
}

private Task ProcessPingFrameAsync()
private Task ProcessPingFrameAsync(ReadOnlySequence<byte> payload)
{
if (_currentHeadersStream != null)
{
Expand All @@ -699,7 +694,7 @@ private Task ProcessPingFrameAsync()
return Task.CompletedTask;
}

return _frameWriter.WritePingAsync(Http2PingFrameFlags.ACK, _incomingFrame.Payload);
return _frameWriter.WritePingAsync(Http2PingFrameFlags.ACK, payload);
}

private Task ProcessGoAwayFrameAsync()
Expand Down Expand Up @@ -777,7 +772,7 @@ private Task ProcessWindowUpdateFrameAsync()
return Task.CompletedTask;
}

private Task ProcessContinuationFrameAsync<TContext>(IHttpApplication<TContext> application)
private Task ProcessContinuationFrameAsync<TContext>(IHttpApplication<TContext> application, ReadOnlySequence<byte> payload)
{
if (_currentHeadersStream == null)
{
Expand All @@ -791,11 +786,11 @@ private Task ProcessContinuationFrameAsync<TContext>(IHttpApplication<TContext>

if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
return DecodeTrailersAsync(_incomingFrame.ContinuationEndHeaders, _incomingFrame.Payload);
return DecodeTrailersAsync(_incomingFrame.ContinuationEndHeaders, payload);
}
else
{
return DecodeHeadersAsync(application, _incomingFrame.ContinuationEndHeaders, _incomingFrame.Payload);
return DecodeHeadersAsync(application, _incomingFrame.ContinuationEndHeaders, payload);
}
}

Expand All @@ -809,7 +804,7 @@ private Task ProcessUnknownFrameAsync()
return Task.CompletedTask;
}

private Task DecodeHeadersAsync<TContext>(IHttpApplication<TContext> application, bool endHeaders, Span<byte> payload)
private Task DecodeHeadersAsync<TContext>(IHttpApplication<TContext> application, bool endHeaders, ReadOnlySequence<byte> payload)
{
try
{
Expand Down Expand Up @@ -838,7 +833,7 @@ private Task DecodeHeadersAsync<TContext>(IHttpApplication<TContext> application
return Task.CompletedTask;
}

private Task DecodeTrailersAsync(bool endHeaders, Span<byte> payload)
private Task DecodeTrailersAsync(bool endHeaders, ReadOnlySequence<byte> payload)
{
_hpackDecoder.Decode(payload, endHeaders, handler: this);

Expand Down
3 changes: 1 addition & 2 deletions src/Kestrel.Core/Internal/Http2/Http2Frame.Continuation.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// 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.Core.Internal.Http2
{
/* https://tools.ietf.org/html/rfc7540#section-6.10
Expand All @@ -21,7 +20,7 @@ public Http2ContinuationFrameFlags ContinuationFlags

public void PrepareContinuation(Http2ContinuationFrameFlags flags, int streamId)
{
PayloadLength = (int)_maxFrameSize;
PayloadLength = 0;
Type = Http2FrameType.CONTINUATION;
ContinuationFlags = flags;
StreamId = streamId;
Expand Down
Loading