2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
4
using System . Buffers ;
5
+ using System . Diagnostics ;
5
6
using System . Globalization ;
6
7
using System . IO . Pipelines ;
7
8
using System . Net . Http ;
@@ -19,13 +20,18 @@ internal abstract class Http3ControlStream : IHttp3Stream, IThreadPoolWorkItem
19
20
private const int EncoderStreamTypeId = 2 ;
20
21
private const int DecoderStreamTypeId = 3 ;
21
22
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
+
22
29
private readonly Http3FrameWriter _frameWriter ;
23
30
private readonly Http3StreamContext _context ;
24
31
private readonly Http3PeerSettings _serverPeerSettings ;
25
32
private readonly IStreamIdFeature _streamIdFeature ;
26
33
private readonly IStreamClosedFeature _streamClosedFeature ;
27
34
private readonly IProtocolErrorCodeFeature _errorCodeFeature ;
28
- private readonly Http3RawFrame _incomingFrame = new Http3RawFrame ( ) ;
29
35
private volatile int _isClosed ;
30
36
private long _headerType ;
31
37
private readonly object _completionLock = new ( ) ;
@@ -159,9 +165,9 @@ private async ValueTask<long> TryReadStreamHeaderAsync()
159
165
{
160
166
if ( ! readableBuffer . IsEmpty )
161
167
{
162
- var id = VariableLengthIntegerHelper . GetInteger ( readableBuffer , out consumed , out examined ) ;
163
- if ( id != - 1 )
168
+ if ( VariableLengthIntegerHelper . TryGetInteger ( readableBuffer , out consumed , out var id ) )
164
169
{
170
+ examined = consumed ;
165
171
return id ;
166
172
}
167
173
}
@@ -240,13 +246,17 @@ public async Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> appli
240
246
}
241
247
finally
242
248
{
249
+ await _context . StreamContext . DisposeAsync ( ) ;
250
+
243
251
ApplyCompletionFlag ( StreamCompletionFlags . Completed ) ;
244
252
_context . StreamLifetimeHandler . OnStreamCompleted ( this ) ;
245
253
}
246
254
}
247
255
248
256
private async Task HandleControlStream ( )
249
257
{
258
+ var incomingFrame = new Http3RawFrame ( ) ;
259
+ var isContinuedFrame = false ;
250
260
while ( _isClosed == 0 )
251
261
{
252
262
var result = await Input . ReadAsync ( ) ;
@@ -259,12 +269,33 @@ private async Task HandleControlStream()
259
269
if ( ! readableBuffer . IsEmpty )
260
270
{
261
271
// 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 ) )
263
273
{
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
+ }
268
299
}
269
300
}
270
301
@@ -294,56 +325,71 @@ private async ValueTask HandleEncodingDecodingTask()
294
325
}
295
326
}
296
327
297
- private ValueTask ProcessHttp3ControlStream ( in ReadOnlySequence < byte > payload )
328
+ private ValueTask ProcessHttp3ControlStream ( Http3RawFrame incomingFrame , bool isContinuedFrame , in ReadOnlySequence < byte > payload , out SequencePosition consumed )
298
329
{
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 )
300
334
{
301
335
case Http3FrameType . Data :
302
336
case Http3FrameType . Headers :
303
337
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 ) ;
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 ) ;
306
340
case Http3FrameType . Settings :
307
- return ProcessSettingsFrameAsync ( payload ) ;
341
+ CheckMaxFrameSize ( incomingFrame ) ;
342
+ return ProcessSettingsFrameAsync ( isContinuedFrame , payload , out consumed ) ;
308
343
case Http3FrameType . GoAway :
309
- return ProcessGoAwayFrameAsync ( ) ;
344
+ return ProcessGoAwayFrameAsync ( isContinuedFrame , incomingFrame , payload , out consumed ) ;
310
345
case Http3FrameType . CancelPush :
311
- return ProcessCancelPushFrameAsync ( ) ;
346
+ return ProcessCancelPushFrameAsync ( incomingFrame , payload , out consumed ) ;
312
347
case Http3FrameType . MaxPushId :
313
- return ProcessMaxPushIdFrameAsync ( ) ;
348
+ return ProcessMaxPushIdFrameAsync ( incomingFrame , payload , out consumed ) ;
314
349
default :
315
- return ProcessUnknownFrameAsync ( _incomingFrame . Type ) ;
350
+ CheckMaxFrameSize ( incomingFrame ) ;
351
+ return ProcessUnknownFrameAsync ( incomingFrame . Type ) ;
316
352
}
317
- }
318
353
319
- private ValueTask ProcessSettingsFrameAsync ( ReadOnlySequence < byte > payload )
320
- {
321
- if ( _haveReceivedSettingsFrame )
354
+ static void CheckMaxFrameSize ( Http3RawFrame http3RawFrame )
322
355
{
323
- // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-settings
324
- throw new Http3ConnectionErrorException ( CoreStrings . Http3ErrorControlStreamMultipleSettingsFrames , Http3ErrorCode . 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 ) ;
360
+ }
325
361
}
362
+ }
326
363
327
- _haveReceivedSettingsFrame = true ;
328
- _streamClosedFeature . OnClosed ( static state =>
364
+ private ValueTask ProcessSettingsFrameAsync ( bool isContinuedFrame , ReadOnlySequence < byte > payload , out SequencePosition consumed )
365
+ {
366
+ if ( ! isContinuedFrame )
329
367
{
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 ) ;
372
+ }
373
+
374
+ _haveReceivedSettingsFrame = true ;
375
+ _streamClosedFeature . OnClosed ( static state =>
376
+ {
377
+ var stream = ( Http3ControlStream ) state ! ;
378
+ stream . OnStreamClosed ( ) ;
379
+ } , this ) ;
380
+ }
333
381
334
382
while ( true )
335
383
{
336
- var id = VariableLengthIntegerHelper . GetInteger ( payload , out var consumed , out _ ) ;
337
- if ( id == - 1 )
384
+ if ( ! VariableLengthIntegerHelper . TryGetInteger ( payload , out consumed , out var id ) )
338
385
{
339
386
break ;
340
387
}
341
388
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 ) )
346
390
{
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 ;
347
393
break ;
348
394
}
349
395
@@ -382,37 +428,48 @@ private void ProcessSetting(long id, long value)
382
428
}
383
429
}
384
430
385
- private ValueTask ProcessGoAwayFrameAsync ( )
431
+ private ValueTask ProcessGoAwayFrameAsync ( bool isContinuedFrame , Http3RawFrame incomingFrame , ReadOnlySequence < byte > payload , out SequencePosition consumed )
386
432
{
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 ) ;
388
440
389
- // StopProcessingNextRequest must be called before RequestClose to ensure it's considered client initiated.
390
- _context . Connection . StopProcessingNextRequest ( serverInitiated : false ) ;
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 ) ;
443
+ _context . ConnectionContext . Features . Get < IConnectionLifetimeNotificationFeature > ( ) ? . RequestClose ( ) ;
444
+ }
392
445
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 ) ;
395
448
396
449
// TODO: Double check the connection remains open.
397
450
return default ;
398
451
}
399
452
400
- private ValueTask ProcessCancelPushFrameAsync ( )
453
+ private ValueTask ProcessCancelPushFrameAsync ( Http3RawFrame incomingFrame , ReadOnlySequence < byte > payload , out SequencePosition consumed )
401
454
{
455
+ // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.3
456
+
402
457
EnsureSettingsFrame ( Http3FrameType . CancelPush ) ;
403
458
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 ) ;
406
461
407
462
return default ;
408
463
}
409
464
410
- private ValueTask ProcessMaxPushIdFrameAsync ( )
465
+ private ValueTask ProcessMaxPushIdFrameAsync ( Http3RawFrame incomingFrame , ReadOnlySequence < byte > payload , out SequencePosition consumed )
411
466
{
467
+ // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.7
468
+
412
469
EnsureSettingsFrame ( Http3FrameType . MaxPushId ) ;
413
470
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 ) ;
416
473
417
474
return default ;
418
475
}
@@ -426,6 +483,23 @@ private ValueTask ProcessUnknownFrameAsync(Http3FrameType frameType)
426
483
return default ;
427
484
}
428
485
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 ) ;
500
+ }
501
+ }
502
+
429
503
private void EnsureSettingsFrame ( Http3FrameType frameType )
430
504
{
431
505
if ( ! _haveReceivedSettingsFrame )
0 commit comments