8
8
using System . Net . Http ;
9
9
using System . Net . Http . HPack ;
10
10
using System . Security . Authentication ;
11
+ using System . Text ;
11
12
using Microsoft . AspNetCore . Connections ;
12
13
using Microsoft . AspNetCore . Connections . Features ;
13
14
using Microsoft . AspNetCore . Hosting . Server ;
@@ -23,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
23
24
internal partial class Http2Connection : IHttp2StreamLifetimeHandler , IHttpStreamHeadersHandler , IRequestProcessor
24
25
{
25
26
public static ReadOnlySpan < byte > ClientPreface => ClientPrefaceBytes ;
27
+ public static byte [ ] ? InvalidHttp1xErrorResponseBytes ;
26
28
27
29
private const PseudoHeaderFields _mandatoryRequestPseudoHeaderFields =
28
30
PseudoHeaderFields . Method | PseudoHeaderFields . Path | PseudoHeaderFields . Scheme ;
@@ -402,8 +404,40 @@ private void ValidateTlsRequirements()
402
404
}
403
405
}
404
406
407
+ [ Flags ]
408
+ private enum ReadPrefaceState
409
+ {
410
+ None = 0 ,
411
+ Preface = 1 ,
412
+ Http1x = 2 ,
413
+ All = Preface | Http1x
414
+ }
415
+
405
416
private async Task < bool > TryReadPrefaceAsync ( )
406
417
{
418
+ // HTTP/1.x and HTTP/2 support connections without TLS. That means ALPN hasn't been used to ensure both sides are
419
+ // using the same protocol. A common problem is someone using HTTP/1.x to talk to a HTTP/2 only endpoint.
420
+ //
421
+ // HTTP/2 starts a connection with a preface. This method reads and validates it. If the connection doesn't start
422
+ // with the preface, and it isn't using TLS, then we attempt to detect what the client is trying to do and send
423
+ // back a friendly error message.
424
+ //
425
+ // Outcomes from this method:
426
+ // 1. Successfully read HTTP/2 preface. Connection continues to be established.
427
+ // 2. Detect HTTP/1.x request. Send back HTTP/1.x 400 response.
428
+ // 3. Unknown content. Report HTTP/2 PROTOCOL_ERROR to client.
429
+ // 4. Timeout while waiting for content.
430
+ //
431
+ // Future improvement: Detect TLS frame. Useful for people starting TLS connection with a non-TLS endpoint.
432
+ var state = ReadPrefaceState . All ;
433
+
434
+ // With TLS, ALPN should have already errored if the wrong HTTP version is used.
435
+ // Only perform additional validation if endpoint doesn't use TLS.
436
+ if ( ConnectionFeatures . Get < ITlsHandshakeFeature > ( ) != null )
437
+ {
438
+ state ^= ReadPrefaceState . Http1x ;
439
+ }
440
+
407
441
while ( _isClosed == 0 )
408
442
{
409
443
var result = await Input . ReadAsync ( ) ;
@@ -415,9 +449,55 @@ private async Task<bool> TryReadPrefaceAsync()
415
449
{
416
450
if ( ! readableBuffer . IsEmpty )
417
451
{
418
- if ( ParsePreface ( readableBuffer , out consumed , out examined ) )
452
+ if ( state . HasFlag ( ReadPrefaceState . Preface ) )
453
+ {
454
+ if ( readableBuffer . Length >= ClientPreface . Length )
455
+ {
456
+ if ( IsPreface ( readableBuffer , out consumed , out examined ) )
457
+ {
458
+ return true ;
459
+ }
460
+ else
461
+ {
462
+ state ^= ReadPrefaceState . Preface ;
463
+ }
464
+ }
465
+ }
466
+
467
+ if ( state . HasFlag ( ReadPrefaceState . Http1x ) )
468
+ {
469
+ if ( ParseHttp1x ( readableBuffer , out var detectedVersion ) )
470
+ {
471
+ if ( detectedVersion == HttpVersion . Http10 || detectedVersion == HttpVersion . Http11 )
472
+ {
473
+ Log . PossibleInvalidHttpVersionDetected ( ConnectionId , HttpVersion . Http2 , detectedVersion ) ;
474
+
475
+ var responseBytes = InvalidHttp1xErrorResponseBytes ??= Encoding . ASCII . GetBytes (
476
+ "HTTP/1.1 400 Bad Request\r \n " +
477
+ "Connection: close\r \n " +
478
+ "Content-Type: text/plain\r \n " +
479
+ "Content-Length: 56\r \n " +
480
+ "\r \n " +
481
+ "An HTTP/1.x request was sent to an HTTP/2 only endpoint." ) ;
482
+
483
+ await _context . Transport . Output . WriteAsync ( responseBytes ) ;
484
+
485
+ // Close connection here so a GOAWAY frame isn't written.
486
+ TryClose ( ) ;
487
+
488
+ return false ;
489
+ }
490
+ else
491
+ {
492
+ state ^= ReadPrefaceState . Http1x ;
493
+ }
494
+ }
495
+ }
496
+
497
+ // Tested all states. Return HTTP/2 protocol error.
498
+ if ( state == ReadPrefaceState . None )
419
499
{
420
- return true ;
500
+ throw new Http2ConnectionErrorException ( CoreStrings . Http2ErrorInvalidPreface , Http2ErrorCode . PROTOCOL_ERROR ) ;
421
501
}
422
502
}
423
503
@@ -437,22 +517,44 @@ private async Task<bool> TryReadPrefaceAsync()
437
517
return false ;
438
518
}
439
519
440
- private static bool ParsePreface ( in ReadOnlySequence < byte > buffer , out SequencePosition consumed , out SequencePosition examined )
520
+ private bool ParseHttp1x ( ReadOnlySequence < byte > buffer , out HttpVersion httpVersion )
441
521
{
442
- consumed = buffer . Start ;
443
- examined = buffer . End ;
522
+ httpVersion = HttpVersion . Unknown ;
444
523
445
- if ( buffer . Length < ClientPreface . Length )
524
+ var reader = new SequenceReader < byte > ( buffer . Length > Limits . MaxRequestLineSize ? buffer . Slice ( 0 , Limits . MaxRequestLineSize ) : buffer ) ;
525
+ if ( reader . TryReadTo ( out ReadOnlySpan < byte > requestLine , ( byte ) '\n ' ) )
446
526
{
447
- return false ;
527
+ // Line should be long enough for HTTP/1.X and end with \r\n
528
+ if ( requestLine . Length > 10 && requestLine [ requestLine . Length - 1 ] == ( byte ) '\r ' )
529
+ {
530
+ httpVersion = HttpUtilities . GetKnownVersion ( requestLine . Slice ( requestLine . Length - 9 , 8 ) ) ;
531
+ }
532
+
533
+ return true ;
534
+ }
535
+
536
+ // Couldn't find newline within max request line size so this isn't valid HTTP/1.x.
537
+ if ( buffer . Length > Limits . MaxRequestLineSize )
538
+ {
539
+ return true ;
448
540
}
449
541
542
+ return false ;
543
+ }
544
+
545
+ private static bool IsPreface ( in ReadOnlySequence < byte > buffer , out SequencePosition consumed , out SequencePosition examined )
546
+ {
547
+ consumed = buffer . Start ;
548
+ examined = buffer . End ;
549
+
550
+ Debug . Assert ( buffer . Length >= ClientPreface . Length , "Not enough content to match preface." ) ;
551
+
450
552
var preface = buffer . Slice ( 0 , ClientPreface . Length ) ;
451
553
var span = preface . ToSpan ( ) ;
452
554
453
555
if ( ! span . SequenceEqual ( ClientPreface ) )
454
556
{
455
- throw new Http2ConnectionErrorException ( CoreStrings . Http2ErrorInvalidPreface , Http2ErrorCode . PROTOCOL_ERROR ) ;
557
+ return false ;
456
558
}
457
559
458
560
consumed = examined = preface . End ;
0 commit comments