public static byte[] BuildSettingsFrame(HttpConnectionSettings settings)
        {
            Span <byte> buffer = stackalloc byte[4 + VariableLengthIntegerHelper.MaximumEncodedLength];

            int integerLength = VariableLengthIntegerHelper.WriteInteger(buffer.Slice(4), settings._maxResponseHeadersLength * 1024L);
            int payloadLength = 1 + integerLength; // includes the setting ID and the integer value.

            Debug.Assert(payloadLength <= VariableLengthIntegerHelper.OneByteLimit);

            buffer[0] = (byte)Http3StreamType.Control;
            buffer[1] = (byte)Http3FrameType.Settings;
            buffer[2] = (byte)payloadLength;
            buffer[3] = (byte)Http3SettingType.MaxHeaderListSize;

            return(buffer.Slice(4 + integerLength).ToArray());
        }
        public const int MaximumEncodedFrameEnvelopeLength = 1 + VariableLengthIntegerHelper.MaximumEncodedLength; // Frame type + payload length.

        /// <summary>
        /// Reads two variable-length integers.
        /// </summary>
        public static bool TryReadIntegerPair(ReadOnlySpan <byte> buffer, out long a, out long b, out int bytesRead)
        {
            if (VariableLengthIntegerHelper.TryRead(buffer, out a, out int aLength))
            {
                buffer = buffer.Slice(aLength);
                if (VariableLengthIntegerHelper.TryRead(buffer, out b, out int bLength))
                {
                    bytesRead = aLength + bLength;
                    return(true);
                }
            }

            b         = 0;
            bytesRead = 0;
            return(false);
        }
        //  0                   1                   2                   3
        //  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
        // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        // |                           Type (i)                          ...
        // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        // |                          Length (i)                         ...
        // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        // |                       Frame Payload (*)                     ...
        // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        public static bool TryWriteFrameEnvelope(Http3FrameType frameType, long payloadLength, Span <byte> buffer, out int bytesWritten)
        {
            Debug.Assert(VariableLengthIntegerHelper.GetByteCount((long)frameType) == 1, $"{nameof(TryWriteFrameEnvelope)} assumes {nameof(frameType)} will fit within a single byte varint.");

            if (buffer.Length != 0)
            {
                buffer[0] = (byte)frameType;
                buffer    = buffer.Slice(1);

                if (VariableLengthIntegerHelper.TryWrite(buffer, payloadLength, out int payloadLengthEncodedLength))
                {
                    bytesWritten = payloadLengthEncodedLength + 1;
                    return(true);
                }
            }

            bytesWritten = 0;
            return(false);
        }
        /// <summary>
        /// Reads the server's control stream.
        /// </summary>
        private async Task ProcessServerControlStreamAsync(QuicStream stream, ArrayBuffer buffer)
        {
            (Http3FrameType? frameType, long payloadLength) = await ReadFrameEnvelopeAsync().ConfigureAwait(false);

            if (frameType == null)
            {
                // Connection closed prematurely, expected SETTINGS frame.
                throw new Http3ConnectionException(Http3ErrorCode.ClosedCriticalStream);
            }

            if (frameType != Http3FrameType.Settings)
            {
                // Expected SETTINGS as first frame of control stream.
                throw new Http3ConnectionException(Http3ErrorCode.MissingSettings);
            }

            await ProcessSettingsFrameAsync(payloadLength).ConfigureAwait(false);

            while (true)
            {
                (frameType, payloadLength) = await ReadFrameEnvelopeAsync().ConfigureAwait(false);

                switch (frameType)
                {
                case Http3FrameType.GoAway:
                    await ProcessGoAwayFameAsync(payloadLength).ConfigureAwait(false);

                    break;

                case Http3FrameType.Settings:
                    // Only a single SETTINGS frame is supported.
                    throw new Http3ConnectionException(Http3ErrorCode.UnexpectedFrame);

                case Http3FrameType.Headers:
                case Http3FrameType.Data:
                case Http3FrameType.MaxPushId:
                case Http3FrameType.DuplicatePush:
                    // Servers should not send these frames to a control stream.
                    throw new Http3ConnectionException(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 new Http3ConnectionException(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 new Http3ConnectionException(Http3ErrorCode.ClosedCriticalStream);
                    }
                    return;

                default:
                    await SkipUnknownPayloadAsync(frameType.GetValueOrDefault(), payloadLength).ConfigureAwait(false);

                    break;
                }
            }

            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 new Http3ConnectionException(Http3ErrorCode.FrameError);
                    }
                }

                buffer.Discard(bytesRead);

                return((Http3FrameType)frameType, payloadLength);
            }

            async ValueTask ProcessSettingsFrameAsync(long settingsPayloadLength)
            {
                if (settingsPayloadLength > MaximumSettingsPayloadLength)
                {
                    if (NetEventSource.Log.IsEnabled())
                    {
                        Trace($"Received SETTINGS frame with {settingsPayloadLength} byte payload exceeding the {MaximumSettingsPayloadLength} byte maximum.");
                    }
                    throw new Http3ConnectionException(Http3ErrorCode.ExcessiveLoad);
                }

                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 new Http3ConnectionException(Http3ErrorCode.FrameError);
                        }
                    }

                    settingsPayloadLength -= bytesRead;

                    // Only support this single setting. Skip others.
                    if (settingId == (long)Http3SettingType.MaxHeaderListSize)
                    {
                        _maximumHeadersLength = (int)Math.Min(settingValue, int.MaxValue);
                    }
                }
            }

            async ValueTask ProcessGoAwayFameAsync(long goawayPayloadLength)
            {
                long lastStreamId;
                int  bytesRead;

                while (!VariableLengthIntegerHelper.TryRead(buffer.AvailableSpan, out lastStreamId, 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 new Http3ConnectionException(Http3ErrorCode.FrameError);
                    }
                }

                buffer.Discard(bytesRead);
                if (bytesRead != goawayPayloadLength)
                {
                    // Frame contains unknown extra data after the integer.
                    throw new Http3ConnectionException(Http3ErrorCode.FrameError);
                }

                OnServerGoAway(lastStreamId);
            }

            async ValueTask SkipUnknownPayloadAsync(Http3FrameType frameType, long payloadLength)
            {
                if (payloadLength > MaximumUnknownFramePayloadLength)
                {
                    Trace($"Received unknown frame type 0x{(long)frameType:x} with {payloadLength} byte payload exceeding the {MaximumUnknownFramePayloadLength} byte maximum.");
                    throw new Http3ConnectionException(Http3ErrorCode.ExcessiveLoad);
                }

                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 new Http3ConnectionException(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)
        {
            try
            {
                await using (stream.ConfigureAwait(false))
                    using (var buffer = new ArrayBuffer(initialSize: 32, usePool: true))
                    {
                        if (stream.CanWrite)
                        {
                            // Server initiated bidirectional streams are either push streams or extensions, and we support neither.
                            throw new Http3ConnectionException(Http3ErrorCode.StreamCreationError);
                        }

                        int bytesRead;

                        try
                        {
                            bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).ConfigureAwait(false);
                        }
                        catch (QuicStreamAbortedException)
                        {
                            // 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 new Http3ConnectionException(Http3ErrorCode.StreamCreationError);
                            }

                            // Discard the stream type header.
                            buffer.Discard(1);

                            await ProcessServerControlStreamAsync(stream, buffer).ConfigureAwait(false);

                            return;

                        case (byte)Http3StreamType.QPackDecoder:
                            if (Interlocked.Exchange(ref _haveServerQpackDecodeStream, 1) != 0)
                            {
                                // A second QPack decode stream has been received.
                                throw new Http3ConnectionException(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 new Http3ConnectionException(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.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 new Http3ConnectionException(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.AbortWrite((long)Http3ErrorCode.StreamCreationError);
                            return;
                        }
                    }
            }
            catch (Exception ex)
            {
                Abort(ex);
            }
        }
Exemple #6
0
        /// <summary>
        /// Reads the server's control stream.
        /// </summary>
        private async Task ProcessServerControlStreamAsync(QuicStream stream, ArrayBuffer buffer)
        {
            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 new Http3ConnectionException(Http3ErrorCode.ClosedCriticalStream);
                }

                if (frameType != Http3FrameType.Settings)
                {
                    throw new Http3ConnectionException(Http3ErrorCode.MissingSettings);
                }

                await ProcessSettingsFrameAsync(payloadLength).ConfigureAwait(false);

                // Read subsequent frames.

                while (true)
                {
                    (frameType, payloadLength) = await ReadFrameEnvelopeAsync().ConfigureAwait(false);

                    switch (frameType)
                    {
                    case Http3FrameType.GoAway:
                        await ProcessGoAwayFameAsync(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 new Http3ConnectionException(Http3ErrorCode.UnexpectedFrame);

                    case Http3FrameType.Headers:
                    case Http3FrameType.Data:
                    case Http3FrameType.MaxPushId:
                    case Http3FrameType.DuplicatePush:
                        // Servers should not send these frames to a control stream.
                        throw new Http3ConnectionException(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 new Http3ConnectionException(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 new Http3ConnectionException(Http3ErrorCode.ClosedCriticalStream);
                        }
                        return;

                    default:
                        await SkipUnknownPayloadAsync(frameType.GetValueOrDefault(), payloadLength).ConfigureAwait(false);

                        break;
                    }
                }
            }

            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 new Http3ConnectionException(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 new Http3ConnectionException(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 new Http3ConnectionException(Http3ErrorCode.FrameError);
                    }

                    buffer.Discard(bytesRead);

                    // Only support this single setting. Skip others.
                    if (settingId == (long)Http3SettingType.MaxHeaderListSize)
                    {
                        _maximumHeadersLength = (int)Math.Min(settingValue, int.MaxValue);
                    }
                }
            }

            async ValueTask ProcessGoAwayFameAsync(long goawayPayloadLength)
            {
                long lastStreamId;
                int  bytesRead;

                while (!VariableLengthIntegerHelper.TryRead(buffer.AvailableSpan, out lastStreamId, 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 new Http3ConnectionException(Http3ErrorCode.FrameError);
                    }
                }

                buffer.Discard(bytesRead);
                if (bytesRead != goawayPayloadLength)
                {
                    // Frame contains unknown extra data after the integer.
                    throw new Http3ConnectionException(Http3ErrorCode.FrameError);
                }

                OnServerGoAway(lastStreamId);
            }

            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 new Http3ConnectionException(Http3ErrorCode.FrameError);
                        }
                    }

                    long readLength = Math.Min(payloadLength, buffer.ActiveLength);
                    buffer.Discard((int)readLength);
                    payloadLength -= readLength;
                }
            }
        }