|
| 1 | +// Copyright (c) .NET Foundation. All rights reserved. |
| 2 | +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. |
| 3 | + |
| 4 | +using System; |
| 5 | +using System.Diagnostics; |
| 6 | +using System.Net; |
| 7 | +using System.Threading.Tasks; |
| 8 | +using Microsoft.AspNetCore.Connections; |
| 9 | +using Microsoft.AspNetCore.Connections.Features; |
| 10 | +using Microsoft.AspNetCore.Hosting.Server; |
| 11 | +using Microsoft.AspNetCore.Http.Features; |
| 12 | +using Microsoft.AspNetCore.Server.Kestrel.Core.Features; |
| 13 | +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; |
| 14 | +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; |
| 15 | +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; |
| 16 | +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; |
| 17 | +using Microsoft.Extensions.Logging; |
| 18 | + |
| 19 | +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal |
| 20 | +{ |
| 21 | + internal class Http3ConnectionTemp : ITimeoutHandler |
| 22 | + { |
| 23 | + // Use C#7.3's ReadOnlySpan<byte> optimization for static data https://vcsjones.com/2019/02/01/csharp-readonly-span-bytes-static/ |
| 24 | + private static ReadOnlySpan<byte> Http2Id => new[] { (byte)'h', (byte)'2' }; |
| 25 | + |
| 26 | + private readonly Http3ConnectionContext _context; |
| 27 | + private readonly ISystemClock _systemClock; |
| 28 | + private readonly TimeoutControl _timeoutControl; |
| 29 | + |
| 30 | + private readonly object _protocolSelectionLock = new object(); |
| 31 | + private ProtocolSelectionState _protocolSelectionState = ProtocolSelectionState.Initializing; |
| 32 | + private IRequestProcessor _requestProcessor; |
| 33 | + private Http1Connection _http1Connection; |
| 34 | + |
| 35 | + public Http3ConnectionTemp(Http3ConnectionContext context) |
| 36 | + { |
| 37 | + _context = context; |
| 38 | + _systemClock = _context.ServiceContext.SystemClock; |
| 39 | + |
| 40 | + _timeoutControl = new TimeoutControl(this); |
| 41 | + |
| 42 | + // Tests override the timeout control sometimes |
| 43 | + _context.TimeoutControl ??= _timeoutControl; |
| 44 | + } |
| 45 | + |
| 46 | + private IKestrelTrace Log => _context.ServiceContext.Log; |
| 47 | + |
| 48 | + public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> httpApplication) |
| 49 | + { |
| 50 | + try |
| 51 | + { |
| 52 | + // Ensure TimeoutControl._lastTimestamp is initialized before anything that could set timeouts runs. |
| 53 | + _timeoutControl.Initialize(_systemClock.UtcNowTicks); |
| 54 | + |
| 55 | + IRequestProcessor requestProcessor = null; |
| 56 | + |
| 57 | + requestProcessor = new Http3Connection(_context); |
| 58 | + _protocolSelectionState = ProtocolSelectionState.Selected; |
| 59 | + |
| 60 | + _requestProcessor = requestProcessor; |
| 61 | + |
| 62 | + if (requestProcessor != null) |
| 63 | + { |
| 64 | + var connectionHeartbeatFeature = _context.ConnectionFeatures.Get<IConnectionHeartbeatFeature>(); |
| 65 | + var connectionLifetimeNotificationFeature = _context.ConnectionFeatures.Get<IConnectionLifetimeNotificationFeature>(); |
| 66 | + |
| 67 | + // These features should never be null in Kestrel itself, if this middleware is ever refactored to run outside of kestrel, |
| 68 | + // we'll need to handle these missing. |
| 69 | + Debug.Assert(connectionHeartbeatFeature != null, nameof(IConnectionHeartbeatFeature) + " is missing!"); |
| 70 | + Debug.Assert(connectionLifetimeNotificationFeature != null, nameof(IConnectionLifetimeNotificationFeature) + " is missing!"); |
| 71 | + |
| 72 | + // Register the various callbacks once we're going to start processing requests |
| 73 | + |
| 74 | + // The heart beat for various timeouts |
| 75 | + connectionHeartbeatFeature?.OnHeartbeat(state => ((Http3ConnectionTemp)state).Tick(), this); |
| 76 | + |
| 77 | + // Register for graceful shutdown of the server |
| 78 | + using var shutdownRegistration = connectionLifetimeNotificationFeature?.ConnectionClosedRequested.Register(state => ((Http3ConnectionTemp)state).StopProcessingNextRequest(), this); |
| 79 | + |
| 80 | + // Register for connection close |
| 81 | + using var closedRegistration = _context.MultiplexedConnectionContext.ConnectionClosed.Register(state => ((Http3ConnectionTemp)state).OnConnectionClosed(), this); |
| 82 | + |
| 83 | + await requestProcessor.ProcessRequestsAsync(httpApplication); |
| 84 | + } |
| 85 | + } |
| 86 | + catch (Exception ex) |
| 87 | + { |
| 88 | + Log.LogCritical(0, ex, $"Unexpected exception in {nameof(HttpConnection)}.{nameof(ProcessRequestsAsync)}."); |
| 89 | + } |
| 90 | + finally |
| 91 | + { |
| 92 | + if (_http1Connection?.IsUpgraded == true) |
| 93 | + { |
| 94 | + _context.ServiceContext.ConnectionManager.UpgradedConnectionCount.ReleaseOne(); |
| 95 | + } |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + // For testing only |
| 100 | + internal void Initialize(IRequestProcessor requestProcessor) |
| 101 | + { |
| 102 | + _requestProcessor = requestProcessor; |
| 103 | + _http1Connection = requestProcessor as Http1Connection; |
| 104 | + _protocolSelectionState = ProtocolSelectionState.Selected; |
| 105 | + } |
| 106 | + |
| 107 | + private void StopProcessingNextRequest() |
| 108 | + { |
| 109 | + ProtocolSelectionState previousState; |
| 110 | + lock (_protocolSelectionLock) |
| 111 | + { |
| 112 | + previousState = _protocolSelectionState; |
| 113 | + Debug.Assert(previousState != ProtocolSelectionState.Initializing, "The state should never be initializing"); |
| 114 | + |
| 115 | + switch (_protocolSelectionState) |
| 116 | + { |
| 117 | + case ProtocolSelectionState.Selected: |
| 118 | + case ProtocolSelectionState.Aborted: |
| 119 | + break; |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + switch (previousState) |
| 124 | + { |
| 125 | + case ProtocolSelectionState.Selected: |
| 126 | + _requestProcessor.StopProcessingNextRequest(); |
| 127 | + break; |
| 128 | + case ProtocolSelectionState.Aborted: |
| 129 | + break; |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + private void OnConnectionClosed() |
| 134 | + { |
| 135 | + ProtocolSelectionState previousState; |
| 136 | + lock (_protocolSelectionLock) |
| 137 | + { |
| 138 | + previousState = _protocolSelectionState; |
| 139 | + Debug.Assert(previousState != ProtocolSelectionState.Initializing, "The state should never be initializing"); |
| 140 | + |
| 141 | + switch (_protocolSelectionState) |
| 142 | + { |
| 143 | + case ProtocolSelectionState.Selected: |
| 144 | + case ProtocolSelectionState.Aborted: |
| 145 | + break; |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + switch (previousState) |
| 150 | + { |
| 151 | + case ProtocolSelectionState.Selected: |
| 152 | + _requestProcessor.OnInputOrOutputCompleted(); |
| 153 | + break; |
| 154 | + case ProtocolSelectionState.Aborted: |
| 155 | + break; |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + private void Abort(ConnectionAbortedException ex) |
| 160 | + { |
| 161 | + ProtocolSelectionState previousState; |
| 162 | + |
| 163 | + lock (_protocolSelectionLock) |
| 164 | + { |
| 165 | + previousState = _protocolSelectionState; |
| 166 | + Debug.Assert(previousState != ProtocolSelectionState.Initializing, "The state should never be initializing"); |
| 167 | + |
| 168 | + _protocolSelectionState = ProtocolSelectionState.Aborted; |
| 169 | + } |
| 170 | + |
| 171 | + switch (previousState) |
| 172 | + { |
| 173 | + case ProtocolSelectionState.Selected: |
| 174 | + _requestProcessor.Abort(ex); |
| 175 | + break; |
| 176 | + case ProtocolSelectionState.Aborted: |
| 177 | + break; |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + |
| 182 | + private void Tick() |
| 183 | + { |
| 184 | + if (_protocolSelectionState == ProtocolSelectionState.Aborted) |
| 185 | + { |
| 186 | + // It's safe to check for timeouts on a dead connection, |
| 187 | + // but try not to in order to avoid extraneous logs. |
| 188 | + return; |
| 189 | + } |
| 190 | + |
| 191 | + // It's safe to use UtcNowUnsynchronized since Tick is called by the Heartbeat. |
| 192 | + var now = _systemClock.UtcNowUnsynchronized; |
| 193 | + _timeoutControl.Tick(now); |
| 194 | + _requestProcessor.Tick(now); |
| 195 | + } |
| 196 | + |
| 197 | + public void OnTimeout(TimeoutReason reason) |
| 198 | + { |
| 199 | + // In the cases that don't log directly here, we expect the setter of the timeout to also be the input |
| 200 | + // reader, so when the read is canceled or aborted, the reader should write the appropriate log. |
| 201 | + switch (reason) |
| 202 | + { |
| 203 | + case TimeoutReason.KeepAlive: |
| 204 | + _requestProcessor.StopProcessingNextRequest(); |
| 205 | + break; |
| 206 | + case TimeoutReason.RequestHeaders: |
| 207 | + _requestProcessor.HandleRequestHeadersTimeout(); |
| 208 | + break; |
| 209 | + case TimeoutReason.ReadDataRate: |
| 210 | + _requestProcessor.HandleReadDataRateTimeout(); |
| 211 | + break; |
| 212 | + case TimeoutReason.WriteDataRate: |
| 213 | + Log.ResponseMinimumDataRateNotSatisfied(_context.ConnectionId, _http1Connection?.TraceIdentifier); |
| 214 | + Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)); |
| 215 | + break; |
| 216 | + case TimeoutReason.RequestBodyDrain: |
| 217 | + case TimeoutReason.TimeoutFeature: |
| 218 | + Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedOutByServer)); |
| 219 | + break; |
| 220 | + default: |
| 221 | + Debug.Assert(false, "Invalid TimeoutReason"); |
| 222 | + break; |
| 223 | + } |
| 224 | + } |
| 225 | + |
| 226 | + private enum ProtocolSelectionState |
| 227 | + { |
| 228 | + Initializing, |
| 229 | + Selected, |
| 230 | + Aborted |
| 231 | + } |
| 232 | + } |
| 233 | +} |
0 commit comments