Exemple #1
0
        /// <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);
            }
        }
Exemple #2
0
            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);
            }
        }
Exemple #3
0
        /// <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;
                }
            }
        }
Exemple #4
0
        /// <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();
            }
        }