Skip to content

Commit 2e66c71

Browse files
JamesNKhalter73
andauthored
Detect HTTP/2 connection sent to HTTP/1.1 endpoint (#39402)
Co-authored-by: Stephen Halter <halter73@gmail.com>
1 parent 516aa52 commit 2e66c71

File tree

4 files changed

+134
-30
lines changed

4 files changed

+134
-30
lines changed

src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs

+56-1
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66
using System.Globalization;
77
using System.IO.Pipelines;
88
using Microsoft.AspNetCore.Connections;
9+
using Microsoft.AspNetCore.Connections.Features;
910
using Microsoft.AspNetCore.Http.Features;
11+
using Microsoft.AspNetCore.Internal;
1012
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
1113

1214
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
1315

1416
internal partial class Http1Connection : HttpProtocol, IRequestProcessor, IHttpOutputAborter
1517
{
18+
internal static ReadOnlySpan<byte> Http2GoAwayHttp11RequiredBytes => new byte[17] { 0, 0, 8, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13 };
19+
1620
private const byte ByteAsterisk = (byte)'*';
1721
private const byte ByteForwardSlash = (byte)'/';
1822
private const string Asterisk = "*";
@@ -39,6 +43,9 @@ internal partial class Http1Connection : HttpProtocol, IRequestProcessor, IHttpO
3943

4044
private long _remainingRequestHeadersBytesAllowed;
4145

46+
// Tracks whether a HTTP/2 preface was detected during the first request.
47+
private bool _http2PrefaceDetected;
48+
4249
public Http1Connection(HttpConnectionContext context)
4350
{
4451
Initialize(context);
@@ -618,7 +625,6 @@ protected override void OnReset()
618625
_requestTargetForm = HttpRequestTarget.Unknown;
619626
_absoluteRequestTarget = null;
620627
_remainingRequestHeadersBytesAllowed = ServerOptions.Limits.MaxRequestHeadersTotalSize + 2;
621-
_requestCount++;
622628

623629
MinResponseDataRate = ServerOptions.Limits.MinResponseDataRate;
624630

@@ -642,6 +648,7 @@ protected override void BeginRequestProcessing()
642648
{
643649
// Reset the features and timeout.
644650
Reset();
651+
_requestCount++;
645652
TimeoutControl.SetTimeout(_keepAliveTicks, TimeoutReason.KeepAlive);
646653
}
647654

@@ -667,6 +674,14 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio
667674
}
668675
throw;
669676
}
677+
#pragma warning disable CS0618 // Type or member is obsolete
678+
catch (BadHttpRequestException ex)
679+
{
680+
DetectHttp2Preface(result.Buffer, ex);
681+
682+
throw;
683+
}
684+
#pragma warning restore CS0618 // Type or member is obsolete
670685
finally
671686
{
672687
Input.AdvanceTo(reader.Position, isConsumed ? reader.Position : result.Buffer.End);
@@ -713,5 +728,45 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio
713728
}
714729
}
715730

731+
#pragma warning disable CS0618 // Type or member is obsolete
732+
private void DetectHttp2Preface(ReadOnlySequence<byte> requestData, BadHttpRequestException ex)
733+
#pragma warning restore CS0618 // Type or member is obsolete
734+
{
735+
const int PrefaceLineLength = 16;
736+
737+
// Only check for HTTP/2 preface on non-TLS connection.
738+
// When TLS is used then ALPN is used to negotiate correct version.
739+
if (ConnectionFeatures.Get<ITlsHandshakeFeature>() == null)
740+
{
741+
// If there is an unrecognized HTTP version, it is the first request on the connection, and the request line
742+
// bytes matches the HTTP/2 preface request line bytes then log and return a HTTP/2 GOAWAY frame.
743+
if (ex.Reason == RequestRejectionReason.UnrecognizedHTTPVersion
744+
&& _requestCount == 1
745+
&& requestData.Length >= PrefaceLineLength)
746+
{
747+
var clientPrefaceRequestLine = Http2.Http2Connection.ClientPreface.Slice(0, PrefaceLineLength);
748+
var currentRequestLine = requestData.Slice(0, PrefaceLineLength).ToSpan();
749+
if (currentRequestLine.SequenceEqual(clientPrefaceRequestLine))
750+
{
751+
Log.PossibleInvalidHttpVersionDetected(ConnectionId, Http.HttpVersion.Http11, Http.HttpVersion.Http2);
752+
753+
// Can't write GOAWAY here. Set flag so TryProduceInvalidRequestResponse writes GOAWAY.
754+
_http2PrefaceDetected = true;
755+
}
756+
}
757+
}
758+
}
759+
760+
protected override Task TryProduceInvalidRequestResponse()
761+
{
762+
if (_http2PrefaceDetected)
763+
{
764+
_context.Transport.Output.Write(Http2GoAwayHttp11RequiredBytes);
765+
return _context.Transport.Output.FlushAsync().GetAsTask();
766+
}
767+
768+
return base.TryProduceInvalidRequestResponse();
769+
}
770+
716771
void IRequestProcessor.Tick(DateTimeOffset now) { }
717772
}

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs

+9-4
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,10 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
593593
{
594594
try
595595
{
596-
await TryProduceInvalidRequestResponse();
596+
if (_requestRejectedException != null)
597+
{
598+
await TryProduceInvalidRequestResponse();
599+
}
597600
}
598601
catch (Exception ex)
599602
{
@@ -985,10 +988,12 @@ private void VerifyInitializeState(int firstWriteByteCount)
985988
VerifyAndUpdateWrite(firstWriteByteCount);
986989
}
987990

988-
protected Task TryProduceInvalidRequestResponse()
991+
protected virtual Task TryProduceInvalidRequestResponse()
989992
{
990-
// If _requestAborted is set, the connection has already been closed.
991-
if (_requestRejectedException != null && !_connectionAborted)
993+
Debug.Assert(_requestRejectedException != null);
994+
995+
// If _connectionAborted is set, the connection has already been closed.
996+
if (!_connectionAborted)
992997
{
993998
return ProduceEnd();
994999
}

src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs

+50-25
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Buffers;
55
using System.Collections;
6+
using System.IO.Pipelines;
67
using System.Text;
78
using Microsoft.AspNetCore.Connections;
89
using Microsoft.AspNetCore.Http;
@@ -146,21 +147,6 @@ public void ResetResetsScheme()
146147
Assert.Equal("http", ((IFeatureCollection)_http1Connection).Get<IHttpRequestFeature>().Scheme);
147148
}
148149

149-
[Fact]
150-
public void ResetResetsTraceIdentifier()
151-
{
152-
_http1Connection.TraceIdentifier = "xyz";
153-
154-
_http1Connection.Reset();
155-
156-
var nextId = ((IFeatureCollection)_http1Connection).Get<IHttpRequestIdentifierFeature>().TraceIdentifier;
157-
Assert.NotEqual("xyz", nextId);
158-
159-
_http1Connection.Reset();
160-
var secondId = ((IFeatureCollection)_http1Connection).Get<IHttpRequestIdentifierFeature>().TraceIdentifier;
161-
Assert.NotEqual(nextId, secondId);
162-
}
163-
164150
[Fact]
165151
public void ResetResetsMinRequestBodyDataRate()
166152
{
@@ -182,31 +168,73 @@ public void ResetResetsMinResponseDataRate()
182168
}
183169

184170
[Fact]
185-
public void TraceIdentifierCountsRequestsPerHttp1Connection()
171+
public async Task TraceIdentifierCountsRequestsPerHttp1Connection()
186172
{
187173
var connectionId = _http1ConnectionContext.ConnectionId;
188174
var feature = ((IFeatureCollection)_http1Connection).Get<IHttpRequestIdentifierFeature>();
189-
// Reset() is called once in the test ctor
175+
176+
var requestProcessingTask = _http1Connection.ProcessRequestsAsync(new DummyApplication());
177+
190178
var count = 1;
191-
void Reset()
179+
async Task SendRequestAsync()
192180
{
193-
_http1Connection.Reset();
181+
var data = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n");
182+
await _application.Output.WriteAsync(data);
183+
184+
while (true)
185+
{
186+
var read = await _application.Input.ReadAsync();
187+
SequencePosition consumed = read.Buffer.Start;
188+
SequencePosition examined = read.Buffer.End;
189+
try
190+
{
191+
if (TryReadResponse(read, out consumed, out examined))
192+
{
193+
break;
194+
}
195+
}
196+
finally
197+
{
198+
_application.Input.AdvanceTo(consumed, examined);
199+
}
200+
}
201+
194202
count++;
195203
}
196204

205+
static bool TryReadResponse(ReadResult read, out SequencePosition consumed, out SequencePosition examined)
206+
{
207+
consumed = read.Buffer.Start;
208+
examined = read.Buffer.End;
209+
210+
SequenceReader<byte> reader = new SequenceReader<byte>(read.Buffer);
211+
if (reader.TryReadTo(out ReadOnlySequence<byte> _, new byte[] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' }, advancePastDelimiter: true))
212+
{
213+
consumed = reader.Position;
214+
examined = reader.Position;
215+
return true;
216+
}
217+
218+
return false;
219+
}
220+
197221
var nextId = feature.TraceIdentifier;
198222
Assert.Equal($"{connectionId}:00000001", nextId);
199223

200-
Reset();
224+
await SendRequestAsync();
225+
201226
var secondId = feature.TraceIdentifier;
202227
Assert.Equal($"{connectionId}:00000002", secondId);
203228

204-
var big = 1_000_000;
229+
var big = 10_000;
205230
while (big-- > 0)
206231
{
207-
Reset();
232+
await SendRequestAsync();
208233
}
209234
Assert.Equal($"{connectionId}:{count:X8}", feature.TraceIdentifier);
235+
236+
_http1Connection.StopProcessingNextRequest();
237+
await requestProcessingTask.DefaultTimeout();
210238
}
211239

212240
[Fact]
@@ -216,9 +244,6 @@ public void TraceIdentifierGeneratesWhenNull()
216244
var id = _http1Connection.TraceIdentifier;
217245
Assert.NotNull(id);
218246
Assert.Equal(id, _http1Connection.TraceIdentifier);
219-
220-
_http1Connection.Reset();
221-
Assert.NotEqual(id, _http1Connection.TraceIdentifier);
222247
}
223248

224249
[Fact]

src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs

+19
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5+
using System.Text;
56
using Microsoft.AspNetCore.Http.Features;
67
using Microsoft.AspNetCore.Server.Kestrel.Core;
8+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
79
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
810
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
911
using Microsoft.AspNetCore.Testing;
@@ -193,6 +195,23 @@ await client.SendAll(
193195
}
194196
}
195197

198+
[Fact]
199+
public async Task BadRequestForHttp2()
200+
{
201+
await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory)))
202+
{
203+
using (var client = server.CreateConnection())
204+
{
205+
await client.Stream.WriteAsync(Core.Internal.Http2.Http2Connection.ClientPreface.ToArray()).DefaultTimeout();
206+
207+
var data = await client.Stream.ReadAtLeastLengthAsync(17);
208+
209+
Assert.Equal(Http1Connection.Http2GoAwayHttp11RequiredBytes.ToArray(), data);
210+
Assert.Empty(await client.Stream.ReadUntilEndAsync().DefaultTimeout());
211+
}
212+
}
213+
}
214+
196215
private class BadRequestEventListener : IObserver<KeyValuePair<string, object>>, IDisposable
197216
{
198217
private IDisposable _subscription;

0 commit comments

Comments
 (0)