/// <summary> /// Accepts unidirectional streams (control, QPack, ...) from the server. /// </summary> private async Task AcceptStreamsAsync() { try { while (true) { ValueTask <QuicStream> streamTask; lock (SyncObj) { if (ShuttingDown) { return; } // No cancellation token is needed here; we expect the operation to cancel itself when _connection is disposed. streamTask = _connection !.AcceptInboundStreamAsync(CancellationToken.None); } QuicStream stream = await streamTask.ConfigureAwait(false); // This process is cleaned up when _connection is disposed, and errors are observed via Abort(). _ = ProcessServerStreamAsync(stream); } } catch (QuicException ex) when(ex.QuicError == QuicError.OperationAborted) { // Shutdown initiated by us, no need to abort. } catch (QuicException ex) when(ex.QuicError == QuicError.ConnectionAborted) { Debug.Assert(ex.ApplicationErrorCode.HasValue); Http3ErrorCode code = (Http3ErrorCode)ex.ApplicationErrorCode.Value; Abort(HttpProtocolException.CreateHttp3ConnectionException(code, SR.net_http_http3_connection_close)); } catch (Exception ex) { Abort(ex); } }
message); // message private async Task SendSettingsAsync() { try { _clientControl = await _connection !.OpenOutboundStreamAsync(QuicStreamType.Unidirectional).ConfigureAwait(false); // Server MUST NOT abort our control stream, setup a continuation which will react accordingly _ = _clientControl.WritesClosed.ContinueWith(t => { if (t.Exception?.InnerException is QuicException ex && ex.QuicError == QuicError.StreamAborted) { Abort(HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.ClosedCriticalStream)); } }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Current); await _clientControl.WriteAsync(_pool.Settings.Http3SettingsFrame, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { Abort(ex); } }
/// <summary> /// Reads the server's control stream. /// </summary> private async Task ProcessServerControlStreamAsync(QuicStream stream, ArrayBuffer buffer) { try { using (buffer) { // Read the first frame of the control stream. Per spec: // A SETTINGS frame MUST be sent as the first frame of each control stream. (Http3FrameType? frameType, long payloadLength) = await ReadFrameEnvelopeAsync().ConfigureAwait(false); if (frameType == null) { // Connection closed prematurely, expected SETTINGS frame. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.ClosedCriticalStream); } if (frameType != Http3FrameType.Settings) { throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.MissingSettings); } await ProcessSettingsFrameAsync(payloadLength).ConfigureAwait(false); // Read subsequent frames. while (true) { (frameType, payloadLength) = await ReadFrameEnvelopeAsync().ConfigureAwait(false); switch (frameType) { case Http3FrameType.GoAway: await ProcessGoAwayFrameAsync(payloadLength).ConfigureAwait(false); break; case Http3FrameType.Settings: // If an endpoint receives a second SETTINGS frame on the control stream, the endpoint MUST respond with a connection error of type H3_FRAME_UNEXPECTED. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.UnexpectedFrame); case Http3FrameType.Headers: // Servers should not send these frames to a control stream. case Http3FrameType.Data: case Http3FrameType.MaxPushId: case Http3FrameType.ReservedHttp2Priority: // These frames are explicitly reserved and must never be sent. case Http3FrameType.ReservedHttp2Ping: case Http3FrameType.ReservedHttp2WindowUpdate: case Http3FrameType.ReservedHttp2Continuation: throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.UnexpectedFrame); case Http3FrameType.PushPromise: case Http3FrameType.CancelPush: // Because we haven't sent any MAX_PUSH_ID frame, it is invalid to receive any push-related frames as they will all reference a too-large ID. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.IdError); case null: // End of stream reached. If we're shutting down, stop looping. Otherwise, this is an error (this stream should not be closed for life of connection). bool shuttingDown; lock (SyncObj) { shuttingDown = ShuttingDown; } if (!shuttingDown) { throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.ClosedCriticalStream); } return; default: await SkipUnknownPayloadAsync(frameType.GetValueOrDefault(), payloadLength).ConfigureAwait(false); break; } } } } catch (QuicException ex) when(ex.QuicError == QuicError.StreamAborted) { // Peers MUST NOT close the control stream throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.ClosedCriticalStream); } async ValueTask <(Http3FrameType?frameType, long payloadLength)> ReadFrameEnvelopeAsync() { long frameType, payloadLength; int bytesRead; while (!Http3Frame.TryReadIntegerPair(buffer.ActiveSpan, out frameType, out payloadLength, out bytesRead)) { buffer.EnsureAvailableSpace(VariableLengthIntegerHelper.MaximumEncodedLength * 2); bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).ConfigureAwait(false); if (bytesRead != 0) { buffer.Commit(bytesRead); } else if (buffer.ActiveLength == 0) { // End of stream. return(null, 0); } else { // Our buffer has partial frame data in it but not enough to complete the read: bail out. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.FrameError); } } buffer.Discard(bytesRead); return((Http3FrameType)frameType, payloadLength); } async ValueTask ProcessSettingsFrameAsync(long settingsPayloadLength) { while (settingsPayloadLength != 0) { long settingId, settingValue; int bytesRead; while (!Http3Frame.TryReadIntegerPair(buffer.ActiveSpan, out settingId, out settingValue, out bytesRead)) { buffer.EnsureAvailableSpace(VariableLengthIntegerHelper.MaximumEncodedLength * 2); bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).ConfigureAwait(false); if (bytesRead != 0) { buffer.Commit(bytesRead); } else { // Our buffer has partial frame data in it but not enough to complete the read: bail out. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.FrameError); } } settingsPayloadLength -= bytesRead; if (settingsPayloadLength < 0) { // An integer was encoded past the payload length. // A frame payload that contains additional bytes after the identified fields or a frame payload that terminates before the end of the identified fields MUST be treated as a connection error of type H3_FRAME_ERROR. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.FrameError); } buffer.Discard(bytesRead); switch ((Http3SettingType)settingId) { case Http3SettingType.MaxHeaderListSize: _maximumHeadersLength = (int)Math.Min(settingValue, int.MaxValue); break; case Http3SettingType.ReservedHttp2EnablePush: case Http3SettingType.ReservedHttp2MaxConcurrentStreams: case Http3SettingType.ReservedHttp2InitialWindowSize: case Http3SettingType.ReservedHttp2MaxFrameSize: // Per https://tools.ietf.org/html/draft-ietf-quic-http-31#section-7.2.4.1 // these settings IDs are reserved and must never be sent. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.SettingsError); } } } async ValueTask ProcessGoAwayFrameAsync(long goawayPayloadLength) { long firstRejectedStreamId; int bytesRead; while (!VariableLengthIntegerHelper.TryRead(buffer.ActiveSpan, out firstRejectedStreamId, out bytesRead)) { buffer.EnsureAvailableSpace(VariableLengthIntegerHelper.MaximumEncodedLength); bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).ConfigureAwait(false); if (bytesRead != 0) { buffer.Commit(bytesRead); } else { // Our buffer has partial frame data in it but not enough to complete the read: bail out. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.FrameError); } } buffer.Discard(bytesRead); if (bytesRead != goawayPayloadLength) { // Frame contains unknown extra data after the integer. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.FrameError); } OnServerGoAway(firstRejectedStreamId); } async ValueTask SkipUnknownPayloadAsync(Http3FrameType frameType, long payloadLength) { while (payloadLength != 0) { if (buffer.ActiveLength == 0) { int bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).ConfigureAwait(false); if (bytesRead != 0) { buffer.Commit(bytesRead); } else { // Our buffer has partial frame data in it but not enough to complete the read: bail out. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.FrameError); } } long readLength = Math.Min(payloadLength, buffer.ActiveLength); buffer.Discard((int)readLength); payloadLength -= readLength; } } }
/// <summary> /// Routes a stream to an appropriate stream-type-specific processor /// </summary> private async Task ProcessServerStreamAsync(QuicStream stream) { ArrayBuffer buffer = default; try { await using (stream.ConfigureAwait(false)) { if (stream.CanWrite) { // Server initiated bidirectional streams are either push streams or extensions, and we support neither. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.StreamCreationError); } buffer = new ArrayBuffer(initialSize: 32, usePool: true); int bytesRead; try { bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).ConfigureAwait(false); } catch (QuicException ex) when(ex.QuicError == QuicError.StreamAborted) { // Treat identical to receiving 0. See below comment. bytesRead = 0; } if (bytesRead == 0) { // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-unidirectional-streams // A sender can close or reset a unidirectional stream unless otherwise specified. A receiver MUST // tolerate unidirectional streams being closed or reset prior to the reception of the unidirectional // stream header. return; } buffer.Commit(bytesRead); // Stream type is a variable-length integer, but we only check the first byte. There is no known type requiring more than 1 byte. switch (buffer.ActiveSpan[0]) { case (byte)Http3StreamType.Control: if (Interlocked.Exchange(ref _haveServerControlStream, 1) != 0) { // A second control stream has been received. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.StreamCreationError); } // Discard the stream type header. buffer.Discard(1); // Ownership of buffer is transferred to ProcessServerControlStreamAsync. ArrayBuffer bufferCopy = buffer; buffer = default; await ProcessServerControlStreamAsync(stream, bufferCopy).ConfigureAwait(false); return; case (byte)Http3StreamType.QPackDecoder: if (Interlocked.Exchange(ref _haveServerQpackDecodeStream, 1) != 0) { // A second QPack decode stream has been received. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.StreamCreationError); } // The stream must not be closed, but we aren't using QPACK right now -- ignore. buffer.Dispose(); await stream.CopyToAsync(Stream.Null).ConfigureAwait(false); return; case (byte)Http3StreamType.QPackEncoder: if (Interlocked.Exchange(ref _haveServerQpackEncodeStream, 1) != 0) { // A second QPack encode stream has been received. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.StreamCreationError); } // We haven't enabled QPack in our SETTINGS frame, so we shouldn't receive any meaningful data here. // However, the standard says the stream must not be closed for the lifetime of the connection. Just ignore any data. buffer.Dispose(); await stream.CopyToAsync(Stream.Null).ConfigureAwait(false); return; case (byte)Http3StreamType.Push: // We don't support push streams. // Because no maximum push stream ID was negotiated via a MAX_PUSH_ID frame, server should not have sent this. Abort the connection with H3_ID_ERROR. throw HttpProtocolException.CreateHttp3ConnectionException(Http3ErrorCode.IdError); default: // Unknown stream type. Per spec, these must be ignored and aborted but not be considered a connection-level error. if (NetEventSource.Log.IsEnabled()) { // Read the rest of the integer, which might be more than 1 byte, so we can log it. long unknownStreamType; while (!VariableLengthIntegerHelper.TryRead(buffer.ActiveSpan, out unknownStreamType, out _)) { buffer.EnsureAvailableSpace(VariableLengthIntegerHelper.MaximumEncodedLength); bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).ConfigureAwait(false); if (bytesRead == 0) { unknownStreamType = -1; break; } buffer.Commit(bytesRead); } NetEventSource.Info(this, $"Ignoring server-initiated stream of unknown type {unknownStreamType}."); } stream.Abort(QuicAbortDirection.Read, (long)Http3ErrorCode.StreamCreationError); stream.Dispose(); return; } } } catch (QuicException ex) when(ex.QuicError == QuicError.OperationAborted) { // ignore the exception, we have already closed the connection } catch (Exception ex) { Abort(ex); } finally { buffer.Dispose(); } }