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

Commit

Permalink
Change how HTTP/2 frames are parsed and generated #2858
Browse files Browse the repository at this point in the history
  • Loading branch information
Tratcher committed Sep 6, 2018
1 parent b8e5669 commit f0e0c8f
Show file tree
Hide file tree
Showing 23 changed files with 959 additions and 564 deletions.
3 changes: 0 additions & 3 deletions src/Kestrel.Core/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -572,9 +572,6 @@ 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>
Expand Down
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 @@ -108,11 +109,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)
{
OnByte(data[i], handler);
var span = segment.Span;
for (var i = 0; i < span.Length; i++)
{
OnByte(span[i], handler);
}
}

if (endHeaders && _state != State.Ready)
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 ArgumentOutOfRangeException("The given buffer was too small to encode any headers.");
}
return false;
}

Expand Down
78 changes: 38 additions & 40 deletions src/Kestrel.Core/Internal/Http2/Http2Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ private enum PseudoHeaderFields
private readonly Http2PeerSettings _serverSettings = new Http2PeerSettings();
private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings();

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

private Http2Stream _currentHeadersStream;
private RequestHeaderParsingState _requestHeaderParsingState;
Expand All @@ -92,7 +93,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 +188,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 (_frameReader.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 +377,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 +391,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 +427,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);
}

if (_incomingFrame.DataHasPadding && _incomingFrame.DataPadLength >= _incomingFrame.PayloadLength)
{
throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorPaddingTooLong(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR);
Expand All @@ -449,7 +448,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 +465,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 +477,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 +511,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 +548,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 +601,7 @@ private Task ProcessRstStreamFrameAsync()
return Task.CompletedTask;
}

private async Task ProcessSettingsFrameAsync()
private Task ProcessSettingsFrameAsync(ReadOnlySequence<byte> payload)
{
if (_currentHeadersStream != null)
{
Expand All @@ -625,7 +620,7 @@ private async Task ProcessSettingsFrameAsync()
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorSettingsAckLengthNotZero, Http2ErrorCode.FRAME_SIZE_ERROR);
}

return;
return Task.CompletedTask;
}

if (_incomingFrame.PayloadLength % 6 != 0)
Expand All @@ -639,14 +634,15 @@ private async Task ProcessSettingsFrameAsync()
var previousInitialWindowSize = (int)_clientSettings.InitialWindowSize;
var previousMaxFrameSize = _clientSettings.MaxFrameSize;

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

// Ack before we update the windows, they could send data immediately.
await _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));
}

// This difference can be negative.
Expand All @@ -665,6 +661,8 @@ private async Task ProcessSettingsFrameAsync()
}
}
}

return ackTask;
}
catch (Http2SettingsParameterOutOfRangeException ex)
{
Expand All @@ -674,7 +672,7 @@ private async Task ProcessSettingsFrameAsync()
}
}

private Task ProcessPingFrameAsync()
private Task ProcessPingFrameAsync(ReadOnlySequence<byte> payload)
{
if (_currentHeadersStream != null)
{
Expand All @@ -697,7 +695,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 @@ -775,7 +773,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 @@ -789,11 +787,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 @@ -807,7 +805,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 @@ -836,7 +834,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
27 changes: 6 additions & 21 deletions src/Kestrel.Core/Internal/Http2/Http2Frame.Data.cs
Original file line number Diff line number Diff line change
@@ -1,8 +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.

using System;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
/*
Expand All @@ -26,32 +24,19 @@ public Http2DataFrameFlags DataFlags

public bool DataHasPadding => (DataFlags & Http2DataFrameFlags.PADDED) == Http2DataFrameFlags.PADDED;

public byte DataPadLength
{
get => DataHasPadding ? Payload[0] : (byte)0;
set => Payload[0] = value;
}

public int DataPayloadOffset => DataHasPadding ? 1 : 0;
public byte DataPadLength { get; set; }

private int DataPayloadLength => PayloadLength - DataPayloadOffset - DataPadLength;
private int DataPayloadOffset => DataHasPadding ? 1 : 0;

public Span<byte> DataPayload => Payload.Slice(DataPayloadOffset, DataPayloadLength);
public int DataPayloadLength => PayloadLength - DataPayloadOffset - DataPadLength;

public void PrepareData(int streamId, byte? padLength = null)
{
var padded = padLength != null;

PayloadLength = (int)_maxFrameSize;
PayloadLength = 0;
Type = Http2FrameType.DATA;
DataFlags = padded ? Http2DataFrameFlags.PADDED : Http2DataFrameFlags.NONE;
DataFlags = padLength.HasValue ? Http2DataFrameFlags.PADDED : Http2DataFrameFlags.NONE;
StreamId = streamId;

if (padded)
{
DataPadLength = padLength.Value;
Payload.Slice(PayloadLength - padLength.Value).Fill(0);
}
DataPadLength = padLength ?? 0;
}
}
}
Loading

0 comments on commit f0e0c8f

Please sign in to comment.