| File: Internal\HttpConnection.cs | Web Access |
| Project: src\aspnetcore\src\Servers\Kestrel\Core\src\Microsoft.AspNetCore.Server.Kestrel.Core.csproj (Microsoft.AspNetCore.Server.Kestrel.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; /// <remarks> /// Instantiated by <see cref="HttpConnectionMiddleware{TContext}"/> when a connection is received. /// <para/> /// Not related, type-wise, to <see cref="Http1Connection{TContext}"/>, <see cref="Http2Connection"/>, /// or <see cref="Http3Connection"/>. It does, however, instantiate one of those types as its /// <see cref="_requestProcessor"/> based on the protocol. /// </remarks> internal sealed class HttpConnection : ITimeoutHandler { private static ReadOnlySpan<byte> Http2Id => "h2"u8; private readonly BaseHttpConnectionContext _context; private readonly TimeProvider _timeProvider; private readonly TimeoutControl _timeoutControl; private readonly Lock _protocolSelectionLock = new(); private ProtocolSelectionState _protocolSelectionState = ProtocolSelectionState.Initializing; private Http1Connection? _http1Connection; // Internal for testing internal IRequestProcessor? _requestProcessor; public HttpConnection(BaseHttpConnectionContext context) { _context = context; _timeProvider = _context.ServiceContext.TimeProvider; _timeoutControl = new TimeoutControl(this, _timeProvider); // Tests override the timeout control sometimes _context.TimeoutControl ??= _timeoutControl; } private KestrelTrace Log => _context.ServiceContext.Log; public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> httpApplication) where TContext : notnull { IConnectionMetricsTagsFeature? connectionMetricsTagsFeature = null; try { connectionMetricsTagsFeature = _context.ConnectionFeatures.Get<IConnectionMetricsTagsFeature>(); // Ensure TimeoutControl._lastTimestamp is initialized before anything that could set timeouts runs. _timeoutControl.Initialize(); IRequestProcessor? requestProcessor = null; switch (SelectProtocol()) { case HttpProtocols.Http1: // _http1Connection must be initialized before adding the connection to the connection manager requestProcessor = _http1Connection = new Http1Connection<TContext>((HttpConnectionContext)_context); _protocolSelectionState = ProtocolSelectionState.Selected; AddMetricsHttpProtocolTag(KestrelMetrics.Http11); break; case HttpProtocols.Http2: // _http2Connection must be initialized before yielding control to the transport thread, // to prevent a race condition where _http2Connection.Abort() is called just as // _http2Connection is about to be initialized. requestProcessor = new Http2Connection((HttpConnectionContext)_context); _protocolSelectionState = ProtocolSelectionState.Selected; AddMetricsHttpProtocolTag(KestrelMetrics.Http2); break; case HttpProtocols.Http3: requestProcessor = new Http3Connection((HttpMultiplexedConnectionContext)_context); _protocolSelectionState = ProtocolSelectionState.Selected; AddMetricsHttpProtocolTag(KestrelMetrics.Http3); break; case HttpProtocols.None: // An error was already logged in SelectProtocol(), but we should close the connection. break; default: // SelectProtocol() only returns Http1, Http2, Http3 or None. throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2 or None."); } _requestProcessor = requestProcessor; if (requestProcessor != null) { var connectionHeartbeatFeature = _context.ConnectionFeatures.Get<IConnectionHeartbeatFeature>(); var connectionLifetimeNotificationFeature = _context.ConnectionFeatures.Get<IConnectionLifetimeNotificationFeature>(); // These features should never be null in Kestrel itself, if this middleware is ever refactored to run outside of kestrel, // we'll need to handle these missing. Debug.Assert(connectionHeartbeatFeature != null, nameof(IConnectionHeartbeatFeature) + " is missing!"); Debug.Assert(connectionLifetimeNotificationFeature != null, nameof(IConnectionLifetimeNotificationFeature) + " is missing!"); // Register the various callbacks once we're going to start processing requests // The heart beat for various timeouts connectionHeartbeatFeature?.OnHeartbeat(state => ((HttpConnection)state).Tick(), this); // Register for graceful shutdown of the server using var shutdownRegistration = connectionLifetimeNotificationFeature?.ConnectionClosedRequested.Register(state => ((HttpConnection)state!).StopProcessingNextRequest(ConnectionEndReason.GracefulAppShutdown), this); // Register for connection close using var closedRegistration = _context.ConnectionContext.ConnectionClosed.Register(state => ((HttpConnection)state!).OnConnectionClosed(), this); await requestProcessor.ProcessRequestsAsync(httpApplication); } } catch (Exception ex) { Log.LogCritical(0, ex, $"Unexpected exception in {nameof(HttpConnection)}.{nameof(ProcessRequestsAsync)}."); } finally { // Before exiting HTTP layer, set the end reason on the context as a connection metrics tag. if (_context.MetricsContext.ConnectionEndReason is { } connectionEndReason) { KestrelMetrics.AddConnectionEndReason(connectionMetricsTagsFeature, connectionEndReason); } } } private void AddMetricsHttpProtocolTag(string httpVersion) { if (_context.ConnectionContext.Features.Get<IConnectionMetricsTagsFeature>() is { } metricsTags) { metricsTags.Tags.Add(new KeyValuePair<string, object?>("network.protocol.name", "http")); metricsTags.Tags.Add(new KeyValuePair<string, object?>("network.protocol.version", httpVersion)); } } // For testing only internal void Initialize(IRequestProcessor requestProcessor) { _requestProcessor = requestProcessor; _http1Connection = requestProcessor as Http1Connection; _protocolSelectionState = ProtocolSelectionState.Selected; } private void StopProcessingNextRequest(ConnectionEndReason reason) { ProtocolSelectionState previousState; lock (_protocolSelectionLock) { previousState = _protocolSelectionState; Debug.Assert(previousState != ProtocolSelectionState.Initializing, "The state should never be initializing"); } switch (previousState) { case ProtocolSelectionState.Selected: _requestProcessor!.StopProcessingNextRequest(reason); break; case ProtocolSelectionState.Aborted: break; } } private void OnConnectionClosed() { ProtocolSelectionState previousState; lock (_protocolSelectionLock) { previousState = _protocolSelectionState; Debug.Assert(previousState != ProtocolSelectionState.Initializing, "The state should never be initializing"); } switch (previousState) { case ProtocolSelectionState.Selected: _requestProcessor!.OnInputOrOutputCompleted(); break; case ProtocolSelectionState.Aborted: break; } } private void Abort(ConnectionAbortedException ex, ConnectionEndReason reason) { ProtocolSelectionState previousState; lock (_protocolSelectionLock) { previousState = _protocolSelectionState; Debug.Assert(previousState != ProtocolSelectionState.Initializing, "The state should never be initializing"); _protocolSelectionState = ProtocolSelectionState.Aborted; } switch (previousState) { case ProtocolSelectionState.Selected: _requestProcessor!.Abort(ex, reason); break; case ProtocolSelectionState.Aborted: break; } } private HttpProtocols SelectProtocol() { var hasTls = _context.ConnectionFeatures.Get<ITlsConnectionFeature>() != null; var applicationProtocol = _context.ConnectionFeatures.Get<ITlsApplicationProtocolFeature>()?.ApplicationProtocol ?? new ReadOnlyMemory<byte>(); var isMultiplexTransport = _context is HttpMultiplexedConnectionContext; var http1Enabled = _context.Protocols.HasFlag(HttpProtocols.Http1); var http2Enabled = _context.Protocols.HasFlag(HttpProtocols.Http2); var http3Enabled = _context.Protocols.HasFlag(HttpProtocols.Http3); string? error = null; if (_context.Protocols == HttpProtocols.None) { error = CoreStrings.EndPointRequiresAtLeastOneProtocol; } if (isMultiplexTransport) { if (http3Enabled) { return HttpProtocols.Http3; } error = $"Protocols {_context.Protocols} not supported on multiplexed transport."; } if (!http1Enabled && http2Enabled && hasTls && !Http2Id.SequenceEqual(applicationProtocol.Span)) { error = CoreStrings.EndPointHttp2NotNegotiated; } if (error != null) { Log.LogError(0, error); return HttpProtocols.None; } if (!hasTls && http1Enabled) { // Even if Http2 was enabled, default to Http1 because it's ambiguous without ALPN. return HttpProtocols.Http1; } return http2Enabled && (!hasTls || Http2Id.SequenceEqual(applicationProtocol.Span)) ? HttpProtocols.Http2 : HttpProtocols.Http1; } private void Tick() { if (_protocolSelectionState == ProtocolSelectionState.Aborted) { // It's safe to check for timeouts on a dead connection, // but try not to in order to avoid extraneous logs. return; } var timestamp = _timeProvider.GetTimestamp(); _timeoutControl.Tick(timestamp); _requestProcessor!.Tick(timestamp); } public void OnTimeout(TimeoutReason reason) { // In the cases that don't log directly here, we expect the setter of the timeout to also be the input // reader, so when the read is canceled or aborted, the reader should write the appropriate log. switch (reason) { case TimeoutReason.KeepAlive: _requestProcessor!.StopProcessingNextRequest(ConnectionEndReason.KeepAliveTimeout); break; case TimeoutReason.RequestHeaders: _requestProcessor!.HandleRequestHeadersTimeout(); break; case TimeoutReason.ReadDataRate: _requestProcessor!.HandleReadDataRateTimeout(); break; case TimeoutReason.WriteDataRate: Log.ResponseMinimumDataRateNotSatisfied(_context.ConnectionId, _http1Connection?.TraceIdentifier); Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied), ConnectionEndReason.MinResponseDataRate); break; case TimeoutReason.RequestBodyDrain: case TimeoutReason.TimeoutFeature: Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedOutByServer), ConnectionEndReason.ServerTimeout); break; default: Debug.Assert(false, "Invalid TimeoutReason"); break; } } private enum ProtocolSelectionState { Initializing, Selected, Aborted } }