22// The .NET Foundation licenses this file to you under the MIT license.
33
44using System . Buffers ;
5+ using System . Diagnostics ;
56using System . Globalization ;
67using System . IO . Pipelines ;
78using System . Net . Http ;
@@ -19,13 +20,18 @@ internal abstract class Http3ControlStream : IHttp3Stream, IThreadPoolWorkItem
1920 private const int EncoderStreamTypeId = 2 ;
2021 private const int DecoderStreamTypeId = 3 ;
2122
23+ // Arbitrarily chosen max frame length
24+ // ControlStream frames currently are very small, either a single variable length integer (max 8 bytes), two variable length integers,
25+ // or in the case of SETTINGS a small collection of two variable length integers
26+ // We'll use a generous value of 10k in case new optional frame(s) are added that might be a little larger than the current frames.
27+ private const int MaxFrameSize = 10_000 ;
28+
2229 private readonly Http3FrameWriter _frameWriter ;
2330 private readonly Http3StreamContext _context ;
2431 private readonly Http3PeerSettings _serverPeerSettings ;
2532 private readonly IStreamIdFeature _streamIdFeature ;
2633 private readonly IStreamClosedFeature _streamClosedFeature ;
2734 private readonly IProtocolErrorCodeFeature _errorCodeFeature ;
28- private readonly Http3RawFrame _incomingFrame = new Http3RawFrame ( ) ;
2935 private volatile int _isClosed ;
3036 private long _headerType ;
3137 private readonly object _completionLock = new ( ) ;
@@ -159,9 +165,9 @@ private async ValueTask<long> TryReadStreamHeaderAsync()
159165 {
160166 if ( ! readableBuffer . IsEmpty )
161167 {
162- var id = VariableLengthIntegerHelper . GetInteger ( readableBuffer , out consumed , out examined ) ;
163- if ( id != - 1 )
168+ if ( VariableLengthIntegerHelper . TryGetInteger ( readableBuffer , out consumed , out var id ) )
164169 {
170+ examined = consumed ;
165171 return id ;
166172 }
167173 }
@@ -240,13 +246,17 @@ public async Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> appli
240246 }
241247 finally
242248 {
249+ await _context . StreamContext . DisposeAsync ( ) ;
250+
243251 ApplyCompletionFlag ( StreamCompletionFlags . Completed ) ;
244252 _context . StreamLifetimeHandler . OnStreamCompleted ( this ) ;
245253 }
246254 }
247255
248256 private async Task HandleControlStream ( )
249257 {
258+ var incomingFrame = new Http3RawFrame ( ) ;
259+ var isContinuedFrame = false ;
250260 while ( _isClosed == 0 )
251261 {
252262 var result = await Input . ReadAsync ( ) ;
@@ -259,12 +269,33 @@ private async Task HandleControlStream()
259269 if ( ! readableBuffer . IsEmpty )
260270 {
261271 // need to kick off httpprotocol process request async here.
262- while ( Http3FrameReader . TryReadFrame ( ref readableBuffer , _incomingFrame , out var framePayload ) )
272+ while ( Http3FrameReader . TryReadFrame ( ref readableBuffer , incomingFrame , isContinuedFrame , out var framePayload ) )
263273 {
264- Log . Http3FrameReceived ( _context . ConnectionId , _streamIdFeature . StreamId , _incomingFrame ) ;
265-
266- consumed = examined = framePayload . End ;
267- await ProcessHttp3ControlStream ( framePayload ) ;
274+ Debug . Assert ( incomingFrame . RemainingLength >= framePayload . Length ) ;
275+
276+ // Only log when parsing the beginning of the frame
277+ if ( ! isContinuedFrame )
278+ {
279+ Log . Http3FrameReceived ( _context . ConnectionId , _streamIdFeature . StreamId , incomingFrame ) ;
280+ }
281+
282+ examined = framePayload . End ;
283+ await ProcessHttp3ControlStream ( incomingFrame , isContinuedFrame , framePayload , out consumed ) ;
284+
285+ if ( incomingFrame . RemainingLength == framePayload . Length )
286+ {
287+ Debug . Assert ( framePayload . Slice ( 0 , consumed ) . Length == framePayload . Length ) ;
288+
289+ incomingFrame . RemainingLength = 0 ;
290+ isContinuedFrame = false ;
291+ }
292+ else
293+ {
294+ incomingFrame . RemainingLength -= framePayload . Slice ( 0 , consumed ) . Length ;
295+ isContinuedFrame = true ;
296+
297+ Debug . Assert ( incomingFrame . RemainingLength > 0 ) ;
298+ }
268299 }
269300 }
270301
@@ -294,56 +325,71 @@ private async ValueTask HandleEncodingDecodingTask()
294325 }
295326 }
296327
297- private ValueTask ProcessHttp3ControlStream ( in ReadOnlySequence < byte > payload )
328+ private ValueTask ProcessHttp3ControlStream ( Http3RawFrame incomingFrame , bool isContinuedFrame , in ReadOnlySequence < byte > payload , out SequencePosition consumed )
298329 {
299- switch ( _incomingFrame . Type )
330+ // default to consuming the entire payload, this is so that we don't need to set consumed from all the frame types that aren't implemented yet.
331+ // individual frame types can set consumed if they're implemented and want to be able to partially consume the payload.
332+ consumed = payload . End ;
333+ switch ( incomingFrame . Type )
300334 {
301335 case Http3FrameType . Data :
302336 case Http3FrameType . Headers :
303337 case Http3FrameType . PushPromise :
304- // https://quicwg. org/base-drafts/draft-ietf-quic-http .html#section-7.2
305- throw new Http3ConnectionErrorException ( CoreStrings . FormatHttp3ErrorUnsupportedFrameOnControlStream ( _incomingFrame . FormattedType ) , Http3ErrorCode . UnexpectedFrame , ConnectionEndReason . UnexpectedFrame ) ;
338+ // https://www.rfc-editor. org/rfc/rfc9114 .html#section-8.1-2.12.1
339+ throw new Http3ConnectionErrorException ( CoreStrings . FormatHttp3ErrorUnsupportedFrameOnControlStream ( incomingFrame . FormattedType ) , Http3ErrorCode . UnexpectedFrame , ConnectionEndReason . UnexpectedFrame ) ;
306340 case Http3FrameType . Settings :
307- return ProcessSettingsFrameAsync ( payload ) ;
341+ CheckMaxFrameSize ( incomingFrame ) ;
342+ return ProcessSettingsFrameAsync ( isContinuedFrame , payload , out consumed ) ;
308343 case Http3FrameType . GoAway :
309- return ProcessGoAwayFrameAsync ( ) ;
344+ return ProcessGoAwayFrameAsync ( isContinuedFrame , incomingFrame , payload , out consumed ) ;
310345 case Http3FrameType . CancelPush :
311- return ProcessCancelPushFrameAsync ( ) ;
346+ return ProcessCancelPushFrameAsync ( incomingFrame , payload , out consumed ) ;
312347 case Http3FrameType . MaxPushId :
313- return ProcessMaxPushIdFrameAsync ( ) ;
348+ return ProcessMaxPushIdFrameAsync ( incomingFrame , payload , out consumed ) ;
314349 default :
315- return ProcessUnknownFrameAsync ( _incomingFrame . Type ) ;
350+ CheckMaxFrameSize ( incomingFrame ) ;
351+ return ProcessUnknownFrameAsync ( incomingFrame . Type ) ;
316352 }
317- }
318353
319- private ValueTask ProcessSettingsFrameAsync ( ReadOnlySequence < byte > payload )
320- {
321- if ( _haveReceivedSettingsFrame )
354+ static void CheckMaxFrameSize ( Http3RawFrame http3RawFrame )
322355 {
323- // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-settings
324- throw new Http3ConnectionErrorException ( CoreStrings . Http3ErrorControlStreamMultipleSettingsFrames , Http3ErrorCode . UnexpectedFrame , ConnectionEndReason . UnexpectedFrame ) ;
356+ // Not part of the RFC, but it's a good idea to limit the size of frames when we know they're supposed to be small.
357+ if ( http3RawFrame . RemainingLength >= MaxFrameSize )
358+ {
359+ throw new Http3ConnectionErrorException ( CoreStrings . FormatHttp3ControlStreamFrameTooLarge ( http3RawFrame . FormattedType ) , Http3ErrorCode . FrameError , ConnectionEndReason . InvalidFrameLength ) ;
360+ }
325361 }
362+ }
326363
327- _haveReceivedSettingsFrame = true ;
328- _streamClosedFeature . OnClosed ( static state =>
364+ private ValueTask ProcessSettingsFrameAsync ( bool isContinuedFrame , ReadOnlySequence < byte > payload , out SequencePosition consumed )
365+ {
366+ if ( ! isContinuedFrame )
329367 {
330- var stream = ( Http3ControlStream ) state ! ;
331- stream . OnStreamClosed ( ) ;
332- } , this ) ;
368+ if ( _haveReceivedSettingsFrame )
369+ {
370+ // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4
371+ throw new Http3ConnectionErrorException ( CoreStrings . Http3ErrorControlStreamMultipleSettingsFrames , Http3ErrorCode . UnexpectedFrame , ConnectionEndReason . UnexpectedFrame ) ;
372+ }
373+
374+ _haveReceivedSettingsFrame = true ;
375+ _streamClosedFeature . OnClosed ( static state =>
376+ {
377+ var stream = ( Http3ControlStream ) state ! ;
378+ stream . OnStreamClosed ( ) ;
379+ } , this ) ;
380+ }
333381
334382 while ( true )
335383 {
336- var id = VariableLengthIntegerHelper . GetInteger ( payload , out var consumed , out _ ) ;
337- if ( id == - 1 )
384+ if ( ! VariableLengthIntegerHelper . TryGetInteger ( payload , out consumed , out var id ) )
338385 {
339386 break ;
340387 }
341388
342- payload = payload . Slice ( consumed ) ;
343-
344- var value = VariableLengthIntegerHelper . GetInteger ( payload , out consumed , out _ ) ;
345- if ( value == - 1 )
389+ if ( ! VariableLengthIntegerHelper . TryGetInteger ( payload . Slice ( consumed ) , out consumed , out var value ) )
346390 {
391+ // Reset consumed to very start even though we successfully read 1 varint. It's because we want to keep the id for when we have the value as well.
392+ consumed = payload . Start ;
347393 break ;
348394 }
349395
@@ -382,37 +428,48 @@ private void ProcessSetting(long id, long value)
382428 }
383429 }
384430
385- private ValueTask ProcessGoAwayFrameAsync ( )
431+ private ValueTask ProcessGoAwayFrameAsync ( bool isContinuedFrame , Http3RawFrame incomingFrame , ReadOnlySequence < byte > payload , out SequencePosition consumed )
386432 {
387- EnsureSettingsFrame ( Http3FrameType . GoAway ) ;
433+ // https://www.rfc-editor.org/rfc/rfc9114.html#name-goaway
434+
435+ // We've already triggered RequestClose since isContinuedFrame is only true
436+ // after we've already parsed the frame type and called the processing function at least once.
437+ if ( ! isContinuedFrame )
438+ {
439+ EnsureSettingsFrame ( Http3FrameType . GoAway ) ;
388440
389- // StopProcessingNextRequest must be called before RequestClose to ensure it's considered client initiated.
390- _context . Connection . StopProcessingNextRequest ( serverInitiated : false , ConnectionEndReason . ClientGoAway ) ;
391- _context . ConnectionContext . Features . Get < IConnectionLifetimeNotificationFeature > ( ) ? . RequestClose ( ) ;
441+ // StopProcessingNextRequest must be called before RequestClose to ensure it's considered client initiated.
442+ _context . Connection . StopProcessingNextRequest ( serverInitiated : false , ConnectionEndReason . ClientGoAway ) ;
443+ _context . ConnectionContext . Features . Get < IConnectionLifetimeNotificationFeature > ( ) ? . RequestClose ( ) ;
444+ }
392445
393- // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-goaway
394- // PUSH is not implemented so nothing to do.
446+ // PUSH is not implemented but we still want to parse the frame to do error checking
447+ ParseVarIntWithFrameLengthValidation ( incomingFrame , payload , out consumed ) ;
395448
396449 // TODO: Double check the connection remains open.
397450 return default ;
398451 }
399452
400- private ValueTask ProcessCancelPushFrameAsync ( )
453+ private ValueTask ProcessCancelPushFrameAsync ( Http3RawFrame incomingFrame , ReadOnlySequence < byte > payload , out SequencePosition consumed )
401454 {
455+ // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.3
456+
402457 EnsureSettingsFrame ( Http3FrameType . CancelPush ) ;
403458
404- // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-cancel_push
405- // PUSH is not implemented so nothing to do.
459+ // PUSH is not implemented but we still want to parse the frame to do error checking
460+ ParseVarIntWithFrameLengthValidation ( incomingFrame , payload , out consumed ) ;
406461
407462 return default ;
408463 }
409464
410- private ValueTask ProcessMaxPushIdFrameAsync ( )
465+ private ValueTask ProcessMaxPushIdFrameAsync ( Http3RawFrame incomingFrame , ReadOnlySequence < byte > payload , out SequencePosition consumed )
411466 {
467+ // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.7
468+
412469 EnsureSettingsFrame ( Http3FrameType . MaxPushId ) ;
413470
414- // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-cancel_push
415- // PUSH is not implemented so nothing to do.
471+ // PUSH is not implemented but we still want to parse the frame to do error checking
472+ ParseVarIntWithFrameLengthValidation ( incomingFrame , payload , out consumed ) ;
416473
417474 return default ;
418475 }
@@ -426,6 +483,23 @@ private ValueTask ProcessUnknownFrameAsync(Http3FrameType frameType)
426483 return default ;
427484 }
428485
486+ // Used for frame types that aren't (fully) implemented yet and contain a single var int as part of their framing. (CancelPush, MaxPushId, GoAway)
487+ // We want to throw an error if the length field of the frame is larger than the spec defined format of the frame.
488+ private static void ParseVarIntWithFrameLengthValidation ( Http3RawFrame incomingFrame , ReadOnlySequence < byte > payload , out SequencePosition consumed )
489+ {
490+ if ( ! VariableLengthIntegerHelper . TryGetInteger ( payload , out consumed , out _ ) )
491+ {
492+ return ;
493+ }
494+
495+ if ( incomingFrame . RemainingLength > payload . Slice ( 0 , consumed ) . Length )
496+ {
497+ // https://www.rfc-editor.org/rfc/rfc9114.html#section-10.8
498+ // An implementation MUST ensure that the length of a frame exactly matches the length of the fields it contains.
499+ throw new Http3ConnectionErrorException ( CoreStrings . FormatHttp3ControlStreamFrameTooLarge ( Http3Formatting . ToFormattedType ( incomingFrame . Type ) ) , Http3ErrorCode . FrameError , ConnectionEndReason . InvalidFrameLength ) ;
500+ }
501+ }
502+
429503 private void EnsureSettingsFrame ( Http3FrameType frameType )
430504 {
431505 if ( ! _haveReceivedSettingsFrame )
0 commit comments