HTTP/2: connection error when receiving frames disallowed by stream states.

This commit is contained in:
Cesar Blum Silveira 2017-10-02 10:24:57 -07:00 committed by GitHub
parent bb9840a552
commit 1b1137b880
8 changed files with 210 additions and 75 deletions

View File

@ -14,7 +14,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
public class Http1OutputProducer : IHttpOutputProducer
{
private static readonly ArraySegment<byte> _emptyData = new ArraySegment<byte>(new byte[0]);
private static readonly ArraySegment<byte> _continueBytes = new ArraySegment<byte>(Encoding.ASCII.GetBytes("HTTP/1.1 100 Continue\r\n\r\n"));
private static readonly byte[] _bytesHttpVersion11 = Encoding.ASCII.GetBytes("HTTP/1.1 ");
private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n");
@ -71,7 +70,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
{
return WriteAsync(_emptyData, cancellationToken);
return WriteAsync(Constants.EmptyData, cancellationToken);
}
public void Write<T>(Action<WritableBuffer, T> callback, T state)

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
private readonly Http2Frame _incomingFrame = new Http2Frame();
private Http2Stream _currentHeadersStream;
private int _lastStreamId;
private int _highestOpenedStreamId;
private bool _stopping;
@ -156,7 +156,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
stream.Abort(error);
}
await _frameWriter.WriteGoAwayAsync(_lastStreamId, errorCode);
await _frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, errorCode);
}
finally
{
@ -247,16 +247,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR);
}
if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream) && !stream.HasReceivedEndStream)
ThrowIfIncomingFrameSentToIdleStream();
if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream) && !stream.EndStreamReceived)
{
return stream.OnDataAsync(_incomingFrame.DataPayload,
endStream: (_incomingFrame.DataFlags & Http2DataFrameFlags.END_STREAM) == Http2DataFrameFlags.END_STREAM);
}
return _frameWriter.WriteRstStreamAsync(_incomingFrame.StreamId, Http2ErrorCode.STREAM_CLOSED);
// http://httpwg.org/specs/rfc7540.html#rfc.section.5.1
//
// ...an endpoint that receives any frames after receiving a frame with the
// END_STREAM flag set MUST treat that as a connection error (Section 5.4.1)
// of type STREAM_CLOSED, unless the frame is permitted as described below.
//
// (The allowed frame types for this situation are WINDOW_UPDATE, RST_STREAM and PRIORITY)
//
// If we couldn't find the stream, it was either alive previously but closed with
// END_STREAM or RST_STREAM, or it was implicitly closed when the client opened
// a new stream with a higher ID. Per the spec, we should send RST_STREAM if
// the stream was closed with RST_STREAM or implicitly, but the spec also says
// in http://httpwg.org/specs/rfc7540.html#rfc.section.5.4.1 that
//
// An endpoint can end a connection at any time. In particular, an endpoint MAY
// choose to treat a stream error as a connection error.
//
// We choose to do that here so we don't have to keep state to track implicitly closed
// streams vs. streams closed with END_STREAM or RST_STREAM.
throw new Http2ConnectionErrorException(Http2ErrorCode.STREAM_CLOSED);
}
private Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext> application)
private async Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext> application)
{
if (_currentHeadersStream != null)
{
@ -278,33 +299,66 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR);
}
_currentHeadersStream = new Http2Stream<TContext>(application, new Http2StreamContext
if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream))
{
ConnectionId = ConnectionId,
StreamId = _incomingFrame.StreamId,
ServiceContext = _context.ServiceContext,
ConnectionFeatures = _context.ConnectionFeatures,
PipeFactory = _context.PipeFactory,
LocalEndPoint = _context.LocalEndPoint,
RemoteEndPoint = _context.RemoteEndPoint,
StreamLifetimeHandler = this,
FrameWriter = _frameWriter
});
_currentHeadersStream.ExpectData = (_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == 0;
_currentHeadersStream.Reset();
_streams[_incomingFrame.StreamId] = _currentHeadersStream;
_hpackDecoder.Decode(_incomingFrame.HeadersPayload, _currentHeadersStream.RequestHeaders);
if ((_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS)
{
_lastStreamId = _incomingFrame.StreamId;
_ = _currentHeadersStream.ProcessRequestsAsync();
_currentHeadersStream = null;
// http://httpwg.org/specs/rfc7540.html#rfc.section.5.1
//
// ...an endpoint that receives any frames after receiving a frame with the
// END_STREAM flag set MUST treat that as a connection error (Section 5.4.1)
// of type STREAM_CLOSED, unless the frame is permitted as described below.
//
// (The allowed frame types for this situation are WINDOW_UPDATE, RST_STREAM and PRIORITY)
if (stream.EndStreamReceived)
{
throw new Http2ConnectionErrorException(Http2ErrorCode.STREAM_CLOSED);
}
// TODO: trailers
}
else if (_incomingFrame.StreamId <= _highestOpenedStreamId)
{
// http://httpwg.org/specs/rfc7540.html#rfc.section.5.1.1
//
// The first use of a new stream identifier implicitly closes all streams in the "idle"
// state that might have been initiated by that peer with a lower-valued stream identifier.
//
// If we couldn't find the stream, it was previously closed (either implicitly or with
// END_STREAM or RST_STREAM).
throw new Http2ConnectionErrorException(Http2ErrorCode.STREAM_CLOSED);
}
else
{
// Start a new stream
_currentHeadersStream = new Http2Stream<TContext>(application, new Http2StreamContext
{
ConnectionId = ConnectionId,
StreamId = _incomingFrame.StreamId,
ServiceContext = _context.ServiceContext,
ConnectionFeatures = _context.ConnectionFeatures,
PipeFactory = _context.PipeFactory,
LocalEndPoint = _context.LocalEndPoint,
RemoteEndPoint = _context.RemoteEndPoint,
StreamLifetimeHandler = this,
FrameWriter = _frameWriter
});
return Task.CompletedTask;
if ((_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == Http2HeadersFrameFlags.END_STREAM)
{
await _currentHeadersStream.OnDataAsync(Constants.EmptyData, endStream: true);
}
_currentHeadersStream.Reset();
_streams[_incomingFrame.StreamId] = _currentHeadersStream;
_hpackDecoder.Decode(_incomingFrame.HeadersPayload, _currentHeadersStream.RequestHeaders);
if ((_incomingFrame.HeadersFlags & Http2HeadersFrameFlags.END_HEADERS) == Http2HeadersFrameFlags.END_HEADERS)
{
_highestOpenedStreamId = _incomingFrame.StreamId;
_ = _currentHeadersStream.ProcessRequestsAsync();
_currentHeadersStream = null;
}
}
}
private Task ProcessPriorityFrameAsync()
@ -349,14 +403,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
throw new Http2ConnectionErrorException(Http2ErrorCode.FRAME_SIZE_ERROR);
}
ThrowIfIncomingFrameSentToIdleStream();
if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream))
{
stream.Abort(error: null);
}
else if (_incomingFrame.StreamId > _lastStreamId)
{
throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR);
}
return Task.CompletedTask;
}
@ -449,16 +501,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
throw new Http2ConnectionErrorException(Http2ErrorCode.FRAME_SIZE_ERROR);
}
if (_incomingFrame.StreamId == 0)
ThrowIfIncomingFrameSentToIdleStream();
if (_incomingFrame.WindowUpdateSizeIncrement == 0)
{
if (_incomingFrame.WindowUpdateSizeIncrement == 0)
if (_incomingFrame.StreamId == 0)
{
throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR);
}
}
else
{
if (_incomingFrame.WindowUpdateSizeIncrement == 0)
else
{
return _frameWriter.WriteRstStreamAsync(_incomingFrame.StreamId, Http2ErrorCode.PROTOCOL_ERROR);
}
@ -478,7 +529,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
if ((_incomingFrame.ContinuationFlags & Http2ContinuationFrameFlags.END_HEADERS) == Http2ContinuationFrameFlags.END_HEADERS)
{
_lastStreamId = _currentHeadersStream.StreamId;
_highestOpenedStreamId = _currentHeadersStream.StreamId;
_ = _currentHeadersStream.ProcessRequestsAsync();
_currentHeadersStream = null;
}
@ -496,6 +547,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
return Task.CompletedTask;
}
private void ThrowIfIncomingFrameSentToIdleStream()
{
// http://httpwg.org/specs/rfc7540.html#rfc.section.5.1
// 5.1. Stream states
// ...
// idle:
// ...
// Receiving any frame other than HEADERS or PRIORITY on a stream in this state MUST be
// treated as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
//
// If the stream ID in the incoming frame is higher than the highest opened stream ID so
// far, then the incoming frame's target stream is in the idle state, which is the implicit
// initial state for all streams.
if (_incomingFrame.StreamId > _highestOpenedStreamId)
{
throw new Http2ConnectionErrorException(Http2ErrorCode.PROTOCOL_ERROR);
}
}
void IHttp2StreamLifetimeHandler.OnStreamCompleted(int streamId)
{
_streams.TryRemove(streamId, out _);

View File

@ -7,16 +7,13 @@ using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
public class Http2FrameWriter : IHttp2FrameWriter
{
private static readonly ArraySegment<byte> _emptyData = new ArraySegment<byte>(new byte[0]);
private readonly Http2Frame _outgoingFrame = new Http2Frame();
private readonly object _writeLock = new object();
private readonly HPackEncoder _hpackEncoder = new HPackEncoder();
@ -48,7 +45,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public Task FlushAsync(CancellationToken cancellationToken)
{
return WriteAsync(_emptyData);
return WriteAsync(Constants.EmptyData);
}
public Task Write100ContinueAsync(int streamId)

View File

@ -29,9 +29,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
HttpRequestHeaders headers,
Http2Stream context)
{
if (!context.ExpectData)
if (context.EndStreamReceived)
{
return MessageBody.ZeroContentLengthClose;
return ZeroContentLengthClose;
}
return new ForHttp2(context);

View File

@ -6,13 +6,12 @@ using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
public class Http2OutputProducer : IHttpOutputProducer
{
private static readonly ArraySegment<byte> _emptyData = new ArraySegment<byte>(new byte[0]);
private readonly int _streamId;
private readonly IHttp2FrameWriter _frameWriter;
@ -47,7 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public Task WriteStreamSuffixAsync(CancellationToken cancellationToken)
{
return _frameWriter.WriteDataAsync(_streamId, _emptyData, endStream: true, cancellationToken: cancellationToken);
return _frameWriter.WriteDataAsync(_streamId, Constants.EmptyData, endStream: true, cancellationToken: cancellationToken);
}
public void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders)

View File

@ -24,12 +24,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public int StreamId => _context.StreamId;
public bool HasReceivedEndStream { get; private set; }
public bool EndStreamReceived { get; private set; }
protected IHttp2StreamLifetimeHandler StreamLifetimeHandler => _context.StreamLifetimeHandler;
public bool ExpectData { get; set; }
public override bool IsUpgradableRequest => false;
protected override void OnReset()
@ -91,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
if (endStream)
{
HasReceivedEndStream = true;
EndStreamReceived = true;
RequestBodyPipe.Writer.Complete();
}
}

View File

@ -32,5 +32,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
public const string ServerName = "Kestrel";
public static readonly TimeSpan RequestBodyDrainTimeout = TimeSpan.FromSeconds(5);
public static readonly ArraySegment<byte> EmptyData = new ArraySegment<byte>(new byte[0]);
}
}

View File

@ -467,33 +467,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
[Fact]
public async Task DATA_Received_StreamIdle_StreamError()
public async Task DATA_Received_StreamIdle_ConnectionError()
{
await InitializeConnectionAsync(_noopApplication);
await SendDataAsync(1, _helloWorldBytes, endStream: false);
await WaitForStreamErrorAsync(expectedStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonRstStreamFrames: false);
await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false);
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task DATA_Received_StreamHalfClosedRemote_StreamError()
public async Task DATA_Received_StreamHalfClosedRemote_ConnectionError()
{
await InitializeConnectionAsync(_echoWaitForAbortApplication);
// Use _waitForAbortApplication so we know the stream will still be active when we send the illegal DATA frame
await InitializeConnectionAsync(_waitForAbortApplication);
await StartStreamAsync(1, _postRequestHeaders, endStream: false);
await SendDataAsync(1, _helloBytes, endStream: true);
await SendDataAsync(1, _worldBytes, endStream: true);
await StartStreamAsync(1, _postRequestHeaders, endStream: true);
await WaitForStreamErrorAsync(expectedStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonRstStreamFrames: true);
await SendDataAsync(1, _helloWorldBytes, endStream: false);
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: true);
await WaitForConnectionErrorAsync(expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task DATA_Received_StreamClosed_StreamError()
public async Task DATA_Received_StreamClosed_ConnectionError()
{
await InitializeConnectionAsync(_noopApplication);
@ -510,13 +507,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloWorldBytes, endStream: false);
await WaitForStreamErrorAsync(expectedStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonRstStreamFrames: false);
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
await WaitForConnectionErrorAsync(expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task DATA_Received_StreamClosedImplicitly_StreamError()
public async Task DATA_Received_StreamClosedImplicitly_ConnectionError()
{
// http://httpwg.org/specs/rfc7540.html#rfc.section.5.1.1
//
@ -540,9 +535,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloWorldBytes, endStream: true);
await WaitForStreamErrorAsync(expectedStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonRstStreamFrames: false);
await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
await WaitForConnectionErrorAsync(expectedLastStreamId: 3, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false);
}
[Fact]
@ -655,6 +648,63 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task HEADERS_Received_StreamClosed_ConnectionError()
{
await InitializeConnectionAsync(_noopApplication);
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
// Try to re-use the stream ID (http://httpwg.org/specs/rfc7540.html#rfc.section.5.1.1)
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await WaitForConnectionErrorAsync(expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task HEADERS_Received_StreamHalfClosedRemote_ConnectionError()
{
// Use _waitForAbortApplication so we know the stream will still be active when we send the illegal DATA frame
await InitializeConnectionAsync(_waitForAbortApplication);
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await SendHeadersAsync(1, Http2HeadersFrameFlags.NONE, _browserRequestHeaders);
await WaitForConnectionErrorAsync(expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task HEADERS_Received_StreamClosedImplicitly_ConnectionError()
{
await InitializeConnectionAsync(_noopApplication);
await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 3);
// Stream 1 was implicitly closed by opening stream 3 before (http://httpwg.org/specs/rfc7540.html#rfc.section.5.1.1)
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await WaitForConnectionErrorAsync(expectedLastStreamId: 3, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, ignoreNonGoAwayFrames: false);
}
[Theory]
[InlineData(0)]
[InlineData(1)]
@ -811,6 +861,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task RST_STREAM_Received_StreamIdle_ConnectionError()
{
await InitializeConnectionAsync(_noopApplication);
await SendRstStreamAsync(1);
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false);
}
[Theory]
[InlineData(3)]
[InlineData(5)]
@ -1077,6 +1137,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task WINDOW_UPDATE_Received_StreamIdle_ConnectionError()
{
await InitializeConnectionAsync(_waitForAbortApplication);
await SendWindowUpdateAsync(1, sizeIncrement: 1);
await WaitForConnectionErrorAsync(expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task WINDOW_UPDATE_Received_OnStream_SizeIncrementZero_StreamError()
{