From 90b4e11ce61b5d6f87b34fd6e4de11bfb3dd98ab Mon Sep 17 00:00:00 2001 From: Marek Safar Date: Mon, 8 Mar 2021 18:26:48 +0100 Subject: [PATCH] if-def less version --- .../src/System.Net.Http.csproj | 7 +- .../Http/SocketsHttpHandler/ConnectHelper.cs | 17 - .../HttpConnectionPool.NoQuic.cs | 24 + .../HttpConnectionPool.Quic.cs | 437 ++++++++++++++++++ .../SocketsHttpHandler/HttpConnectionPool.cs | 406 +--------------- .../HttpConnectionSettings.NoQuic.cs | 10 + .../HttpConnectionSettings.Quic.cs | 40 ++ .../HttpConnectionSettings.cs | 35 +- 8 files changed, 540 insertions(+), 436 deletions(-) create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.NoQuic.cs create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.Quic.cs create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.NoQuic.cs create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.Quic.cs diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index c64b152dd488e2..3b08e2e0b0cbc3 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -13,7 +13,6 @@ $(DefineConstants);TARGET_BROWSER - $(DefineConstants);HTTP3_SUPPORTED true @@ -219,6 +218,12 @@ + + + + + + diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs index 3b47491b0afde9..a3734b2ec4c888 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs @@ -100,23 +100,6 @@ private static async ValueTask EstablishSslConnectionAsyncCore(bool a return sslStream; } -#if HTTP3_SUPPORTED - public static async ValueTask ConnectQuicAsync(QuicImplementationProvider quicImplementationProvider, DnsEndPoint endPoint, SslClientAuthenticationOptions? clientAuthenticationOptions, CancellationToken cancellationToken) - { - QuicConnection con = new QuicConnection(quicImplementationProvider, endPoint, clientAuthenticationOptions); - try - { - await con.ConnectAsync(cancellationToken).ConfigureAwait(false); - return con; - } - catch (Exception ex) - { - con.Dispose(); - throw CreateWrappedException(ex, endPoint.Host, endPoint.Port, cancellationToken); - } - } -#endif - internal static Exception CreateWrappedException(Exception error, string host, int port, CancellationToken cancellationToken) { return CancellationHelper.ShouldWrapInOperationCanceledException(error, cancellationToken) ? diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.NoQuic.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.NoQuic.cs new file mode 100644 index 00000000000000..ed28ff658972f0 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.NoQuic.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http +{ + internal sealed partial class HttpConnectionPool + { + private static bool IsHttp3Enabled => false; + + private ValueTask<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>? + GetHttp3ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken) => null; + + private bool IsAltSvcBlocked(HttpAuthority authority) => false; + + private bool ProcessAltSvc(HttpResponseMessage response, HttpConnectionBase? connection) => false; + + public void OnNetworkChanged() + { + } + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.Quic.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.Quic.cs new file mode 100644 index 00000000000000..cf87afd7724c95 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.Quic.cs @@ -0,0 +1,437 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Net.Http.Headers; +using System.Net.Http.HPack; +using System.Net.Http.QPack; +using System.Net.Quic; +using System.Net.Quic.Implementations; +using System.Net.Security; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Authentication; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Internal; + +namespace System.Net.Http +{ + internal sealed partial class HttpConnectionPool : IDisposable + { + /// Initially set to null, this can be set to enable HTTP/3 based on Alt-Svc. + private volatile HttpAuthority? _http3Authority; + + /// If true, the will persist across a network change. If false, it will be reset to . + private bool _persistAuthority; + + /// A timer to expire and return the pool to . Initialized on first use. + private Timer? _authorityExpireTimer; + + /// + /// When an Alt-Svc authority fails due to 421 Misdirected Request, it is placed in the blocklist to be ignored + /// for milliseconds. Initialized on first use. + /// + private volatile HashSet? _altSvcBlocklist; + private CancellationTokenSource? _altSvcBlocklistTimerCancellation; + private volatile bool _altSvcEnabled; + + /// + /// If exceeds this size, Alt-Svc will be disabled entirely for milliseconds. + /// This is to prevent a failing server from bloating the dictionary beyond a reasonable value. + /// + private const int MaxAltSvcIgnoreListSize = 8; + + /// The time, in milliseconds, that an authority should remain in . + private const int AltSvcBlocklistTimeoutInMilliseconds = 10 * 60 * 1000; + + private bool IsHttp3Enabled; + private Http3Connection? _http3Connection; + private SemaphoreSlim? _http3ConnectionCreateLock; + internal byte[]? _http3EncodedAuthorityHostHeader; + private SslClientAuthenticationOptions? _sslOptionsHttp3; + + private static readonly List s_http3ApplicationProtocols = new List() { Http3Connection.Http3ApplicationProtocol31, Http3Connection.Http3ApplicationProtocol30, Http3Connection.Http3ApplicationProtocol29 }; + + private ValueTask<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>? + GetHttp3ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + HttpAuthority? authority = _http3Authority; + // H3 is explicitly requested, assume prenegotiated H3. + if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower) + { + authority = authority ?? _originAuthority; + } + if (authority != null) + { +#pragma warning disable CA2012 + if (IsAltSvcBlocked(authority)) + { + return ValueTask.FromException<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>( + new HttpRequestException(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, 3))); + } + + return GetHttp3ConnectionAsync(request, authority, cancellationToken); +#pragma warning restore CA2012 + } + + return null; + } + + private async ValueTask<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)> + GetHttp3ConnectionAsync(HttpRequestMessage request, HttpAuthority authority, CancellationToken cancellationToken) + { + Debug.Assert(_kind == HttpConnectionKind.Https); + Debug.Assert(IsHttp3Enabled == true); + + Http3Connection? http3Connection = Volatile.Read(ref _http3Connection); + + if (http3Connection != null) + { + TimeSpan pooledConnectionLifetime = _poolManager.Settings._pooledConnectionLifetime; + if (http3Connection.LifetimeExpired(Environment.TickCount64, pooledConnectionLifetime) || http3Connection.Authority != authority) + { + // Connection expired. + http3Connection.Dispose(); + InvalidateHttp3Connection(http3Connection); + } + else + { + // Connection exists and it is still good to use. + if (NetEventSource.Log.IsEnabled()) Trace("Using existing HTTP3 connection."); + _usedSinceLastCleanup = true; + return (http3Connection, false, null); + } + } + + // Ensure that the connection creation semaphore is created + if (_http3ConnectionCreateLock == null) + { + lock (SyncObj) + { + if (_http3ConnectionCreateLock == null) + { + _http3ConnectionCreateLock = new SemaphoreSlim(1); + } + } + } + + await _http3ConnectionCreateLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (_http3Connection != null) + { + // Someone beat us to creating the connection. + + if (NetEventSource.Log.IsEnabled()) + { + Trace("Using existing HTTP3 connection."); + } + + return (_http3Connection, false, null); + } + + if (NetEventSource.Log.IsEnabled()) + { + Trace("Attempting new HTTP3 connection."); + } + + QuicConnection quicConnection; + try + { + quicConnection = await ConnectQuicAsync(Settings._quicImplementationProvider ?? QuicImplementationProviders.Default, new DnsEndPoint(authority.IdnHost, authority.Port), _sslOptionsHttp3, cancellationToken).ConfigureAwait(false); + } + catch + { + // Disables HTTP/3 until server announces it can handle it via Alt-Svc. + BlocklistAuthority(authority); + throw; + } + + //TODO: NegotiatedApplicationProtocol not yet implemented. +#if false + if (quicConnection.NegotiatedApplicationProtocol != SslApplicationProtocol.Http3) + { + BlocklistAuthority(authority); + throw new HttpRequestException("QUIC connected but no HTTP/3 indicated via ALPN.", null, RequestRetryType.RetryOnSameOrNextProxy); + } +#endif + + http3Connection = new Http3Connection(this, _originAuthority, authority, quicConnection); + _http3Connection = http3Connection; + + if (NetEventSource.Log.IsEnabled()) + { + Trace("New HTTP3 connection established."); + } + + return (http3Connection, true, null); + } + finally + { + _http3ConnectionCreateLock.Release(); + } + } + + private static async ValueTask ConnectQuicAsync(QuicImplementationProvider quicImplementationProvider, DnsEndPoint endPoint, SslClientAuthenticationOptions? clientAuthenticationOptions, CancellationToken cancellationToken) + { + QuicConnection con = new QuicConnection(quicImplementationProvider, endPoint, clientAuthenticationOptions); + try + { + await con.ConnectAsync(cancellationToken).ConfigureAwait(false); + return con; + } + catch (Exception ex) + { + con.Dispose(); + throw ConnectHelper.CreateWrappedException(ex, endPoint.Host, endPoint.Port, cancellationToken); + } + } + + partial void InitializeHttp3EncodedAuthorityHostHeader(string hostHeader) + { + _http3EncodedAuthorityHostHeader = QPackEncoder.EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(H3StaticTable.Authority, hostHeader); + } + + partial void InitializeHttp3SslOptions(string sslHostName) + { + _sslOptionsHttp3 = ConstructSslOptions(_poolManager, sslHostName); + _sslOptionsHttp3.ApplicationProtocols = s_http3ApplicationProtocols; + } + + partial void InitializeHttpsConnectionKind() + { + IsHttp3Enabled = _poolManager.Settings._maxHttpVersion >= HttpVersion.Version30 && (_poolManager.Settings._quicImplementationProvider ?? QuicImplementationProviders.Default).IsSupported; + _altSvcEnabled = IsHttp3Enabled; + } + + partial void DisposeHttp3Objects() + { + if (_authorityExpireTimer != null) + { + _authorityExpireTimer.Dispose(); + _authorityExpireTimer = null; + } + + if (_altSvcBlocklistTimerCancellation != null) + { + _altSvcBlocklistTimerCancellation.Cancel(); + _altSvcBlocklistTimerCancellation.Dispose(); + _altSvcBlocklistTimerCancellation = null; + } + } + + public void InvalidateHttp3Connection(Http3Connection connection) + { + lock (SyncObj) + { + if (_http3Connection == connection) + { + _http3Connection = null; + } + } + } + + private bool ProcessAltSvc(HttpResponseMessage response, HttpConnectionBase? connection) + { + // Check for the Alt-Svc header, to upgrade to HTTP/3. + if (_altSvcEnabled && response.Headers.TryGetValues(KnownHeaders.AltSvc.Descriptor, out IEnumerable? altSvcHeaderValues)) + { + HandleAltSvc(altSvcHeaderValues, response.Headers.Age); + } + + // If an Alt-Svc authority returns 421, it means it can't actually handle the request. + // An authority is supposed to be able to handle ALL requests to the origin, so this is a server bug. + // In this case, we blocklist the authority and retry the request at the origin. + if (response.StatusCode == HttpStatusCode.MisdirectedRequest && connection is Http3Connection h3Connection && h3Connection.Authority != _originAuthority) + { + response.Dispose(); + BlocklistAuthority(h3Connection.Authority); + return true; + } + + return false; + } + + /// + /// Expires the current Alt-Svc authority, resetting the connection back to origin. + /// + partial void ExpireAltSvcAuthority(bool expireTimer) + { + // If we ever support prenegotiated HTTP/3, this should be set to origin, not nulled out. + _http3Authority = null; + if (expireTimer) + { + Debug.Assert(_authorityExpireTimer != null); + _authorityExpireTimer.Change(Timeout.Infinite, Timeout.Infinite); + } + } + + /// + /// Checks whether the given is on the currext Alt-Svc blocklist. + /// + /// + private bool IsAltSvcBlocked(HttpAuthority authority) + { + if (_altSvcBlocklist != null) + { + lock (_altSvcBlocklist) + { + return _altSvcBlocklist.Contains(authority); + } + } + return false; + } + + /// + /// Blocklists an authority and resets the current authority back to origin. + /// If the number of blocklisted authorities exceeds , + /// Alt-Svc will be disabled entirely for a period of time. + /// + /// + /// This is called when we get a "421 Misdirected Request" from an alternate authority. + /// A future strategy would be to retry the individual request on an older protocol, we'd want to have + /// some logic to blocklist after some number of failures to avoid doubling our request latency. + /// + /// For now, the spec states alternate authorities should be able to handle ALL requests, so this + /// is treated as an exceptional error by immediately blocklisting the authority. + /// + internal void BlocklistAuthority(HttpAuthority badAuthority) + { + Debug.Assert(badAuthority != null); + + HashSet? altSvcBlocklist = _altSvcBlocklist; + + if (altSvcBlocklist == null) + { + lock (SyncObj) + { + altSvcBlocklist = _altSvcBlocklist; + if (altSvcBlocklist == null) + { + altSvcBlocklist = new HashSet(); + _altSvcBlocklistTimerCancellation = new CancellationTokenSource(); + _altSvcBlocklist = altSvcBlocklist; + } + } + } + + bool added, disabled = false; + + lock (altSvcBlocklist) + { + added = altSvcBlocklist.Add(badAuthority); + + if (added && altSvcBlocklist.Count >= MaxAltSvcIgnoreListSize && _altSvcEnabled) + { + _altSvcEnabled = false; + disabled = true; + } + } + + lock (SyncObj) + { + if (_http3Authority == badAuthority) + { + ExpireAltSvcAuthority(); + } + } + + Debug.Assert(_altSvcBlocklistTimerCancellation != null); + if (added) + { + _ = Task.Delay(AltSvcBlocklistTimeoutInMilliseconds) + .ContinueWith(t => + { + lock (altSvcBlocklist) + { + altSvcBlocklist.Remove(badAuthority); + } + }, _altSvcBlocklistTimerCancellation.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } + + if (disabled) + { + _ = Task.Delay(AltSvcBlocklistTimeoutInMilliseconds) + .ContinueWith(t => + { + _altSvcEnabled = true; + }, _altSvcBlocklistTimerCancellation.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } + } + + public void OnNetworkChanged() + { + lock (SyncObj) + { + if (_http3Authority != null && _persistAuthority == false) + { + ExpireAltSvcAuthority(); + } + } + } + + partial void HandleAltSvcQuic(HttpAuthority? nextAuthority, TimeSpan nextAuthorityMaxAge, bool nextAuthorityPersist) + { + // There's a race here in checking _http3Authority outside of the lock, + // but there's really no bad behavior if _http3Authority changes in the mean time. + if (nextAuthority != null && !nextAuthority.Equals(_http3Authority)) + { + // Clamp the max age to 30 days... this is arbitrary but prevents passing a too-large TimeSpan to the Timer. + if (nextAuthorityMaxAge.Ticks > (30 * TimeSpan.TicksPerDay)) + { + nextAuthorityMaxAge = TimeSpan.FromTicks(30 * TimeSpan.TicksPerDay); + } + + lock (SyncObj) + { + if (_authorityExpireTimer == null) + { + var thisRef = new WeakReference(this); + + bool restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + _authorityExpireTimer = new Timer(static o => + { + var wr = (WeakReference)o!; + if (wr.TryGetTarget(out HttpConnectionPool? @this)) + { + @this.ExpireAltSvcAuthority(expireTimer: false); + } + }, thisRef, nextAuthorityMaxAge, Timeout.InfiniteTimeSpan); + } + finally + { + if (restoreFlow) ExecutionContext.RestoreFlow(); + } + } + else + { + _authorityExpireTimer.Change(nextAuthorityMaxAge, Timeout.InfiniteTimeSpan); + } + + _http3Authority = nextAuthority; + _persistAuthority = nextAuthorityPersist; + } + + if (!nextAuthorityPersist) + { + _poolManager.StartMonitoringNetworkChanges(); + } + } + } + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 55e644c0cbdd6f..b9b141bfee482c 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -9,7 +9,6 @@ using System.Net.Http.Headers; using System.Net.Http.HPack; using System.Net.Http.QPack; -using System.Net.Quic; using System.Net.Security; using System.Net.Sockets; using System.Runtime.CompilerServices; @@ -23,7 +22,7 @@ namespace System.Net.Http { /// Provides a pool of connections to the same endpoint. - internal sealed class HttpConnectionPool : IDisposable + internal sealed partial class HttpConnectionPool : IDisposable { private static readonly bool s_isWindows7Or2008R2 = GetIsWindows7Or2008R2(); @@ -34,32 +33,6 @@ internal sealed class HttpConnectionPool : IDisposable /// The origin authority used to construct the . private readonly HttpAuthority? _originAuthority; - /// Initially set to null, this can be set to enable HTTP/3 based on Alt-Svc. - private volatile HttpAuthority? _http3Authority; - - /// A timer to expire and return the pool to . Initialized on first use. - private Timer? _authorityExpireTimer; - - /// If true, the will persist across a network change. If false, it will be reset to . - private bool _persistAuthority; - - /// - /// When an Alt-Svc authority fails due to 421 Misdirected Request, it is placed in the blocklist to be ignored - /// for milliseconds. Initialized on first use. - /// - private volatile HashSet? _altSvcBlocklist; - private CancellationTokenSource? _altSvcBlocklistTimerCancellation; - private volatile bool _altSvcEnabled; - - /// - /// If exceeds this size, Alt-Svc will be disabled entirely for milliseconds. - /// This is to prevent a failing server from bloating the dictionary beyond a reasonable value. - /// - private const int MaxAltSvcIgnoreListSize = 8; - - /// The time, in milliseconds, that an authority should remain in . - private const int AltSvcBlocklistTimeoutInMilliseconds = 10 * 60 * 1000; - /// List of idle connections stored in the pool. private readonly List _idleConnections = new List(); /// The maximum number of connections allowed to be associated with the pool. @@ -72,16 +45,6 @@ internal sealed class HttpConnectionPool : IDisposable private byte[]? _http2AltSvcOriginUri; internal readonly byte[]? _http2EncodedAuthorityHostHeader; -#if HTTP3_SUPPORTED - private readonly bool _http3Enabled; - private Http3Connection? _http3Connection; - private SemaphoreSlim? _http3ConnectionCreateLock; - internal readonly byte[]? _http3EncodedAuthorityHostHeader; - private readonly SslClientAuthenticationOptions? _sslOptionsHttp3; -#else - private const bool _http3Enabled = false; -#endif - /// For non-proxy connection pools, this is the host name in bytes; for proxies, null. private readonly byte[]? _hostHeaderValueBytes; /// Options specialized and cached for this pool and its key. @@ -140,10 +103,7 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK Debug.Assert(sslHostName != null); Debug.Assert(proxyUri == null); -#if HTTP3_SUPPORTED - _http3Enabled = _poolManager.Settings._maxHttpVersion >= HttpVersion.Version30 && (_poolManager.Settings._quicImplementationProvider ?? QuicImplementationProviders.Default).IsSupported; - _altSvcEnabled = _http3Enabled; -#endif + InitializeHttpsConnectionKind(); break; case HttpConnectionKind.Proxy: @@ -203,9 +163,7 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK if (sslHostName == null) { _http2EncodedAuthorityHostHeader = HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(H2StaticTable.Authority, hostHeader); -#if HTTP3_SUPPORTED - _http3EncodedAuthorityHostHeader = QPackEncoder.EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(H3StaticTable.Authority, hostHeader); -#endif + InitializeHttp3EncodedAuthorityHostHeader(hostHeader); } } @@ -237,18 +195,13 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK Debug.Assert(hostHeader != null); _http2EncodedAuthorityHostHeader = HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(H2StaticTable.Authority, hostHeader); -#if HTTP3_SUPPORTED - _http3EncodedAuthorityHostHeader = QPackEncoder.EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(H3StaticTable.Authority, hostHeader); -#endif + InitializeHttp3EncodedAuthorityHostHeader(hostHeader); } -#if HTTP3_SUPPORTED - if (_http3Enabled) + if (IsHttp3Enabled) { - _sslOptionsHttp3 = ConstructSslOptions(poolManager, sslHostName); - _sslOptionsHttp3.ApplicationProtocols = s_http3ApplicationProtocols; + InitializeHttp3SslOptions (sslHostName); } -#endif } // Set up for PreAuthenticate. Access to this cache is guarded by a lock on the cache itself. @@ -260,9 +213,6 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK if (NetEventSource.Log.IsEnabled()) Trace($"{this}"); } -#if HTTP3_SUPPORTED - private static readonly List s_http3ApplicationProtocols = new List() { Http3Connection.Http3ApplicationProtocol31, Http3Connection.Http3ApplicationProtocol30, Http3Connection.Http3ApplicationProtocol29 }; -#endif private static readonly List s_http2ApplicationProtocols = new List() { SslApplicationProtocol.Http2, SslApplicationProtocol.Http11 }; private static readonly List s_http2OnlyApplicationProtocols = new List() { SslApplicationProtocol.Http2 }; @@ -346,7 +296,7 @@ public byte[] Http2AltSvcOriginUri // Do not even attempt at getting/creating a connection if it's already obvious we cannot provided the one requested. if (request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower) { - if (request.Version.Major == 3 && !_http3Enabled) + if (request.Version.Major == 3 && !IsHttp3Enabled) { return ValueTask.FromException<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>( new HttpRequestException(SR.Format(SR.net_http_requested_version_not_enabled, request.Version, request.VersionPolicy, 3))); @@ -358,28 +308,14 @@ public byte[] Http2AltSvcOriginUri } } -#if HTTP3_SUPPORTED // Either H3 explicitly requested or secured upgraded allowed. - if (_http3Enabled && (request.Version.Major >= 3 || (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher && IsSecure))) + if (IsHttp3Enabled && (request.Version.Major >= 3 || (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher && IsSecure))) { - HttpAuthority? authority = _http3Authority; - // H3 is explicitly requested, assume prenegotiated H3. - if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower) - { - authority = authority ?? _originAuthority; - } - if (authority != null) - { - if (IsAltSvcBlocked(authority)) - { - return ValueTask.FromException<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)>( - new HttpRequestException(SR.Format(SR.net_http_requested_version_cannot_establish, request.Version, request.VersionPolicy, 3))); - } - - return GetHttp3ConnectionAsync(request, authority, cancellationToken); - } + var result = GetHttp3ConnectionAsync(request, cancellationToken); + if (result != null) + return result.GetValueOrDefault(); } -#endif + // If we got here, we cannot provide HTTP/3 connection. Do not continue if downgrade is not allowed. if (request.Version.Major >= 3 && request.VersionPolicy != HttpVersionPolicy.RequestVersionOrLower) { @@ -752,114 +688,6 @@ private void AddHttp2Connection(Http2Connection newConnection) } } -#if HTTP3_SUPPORTED - private async ValueTask<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)> - GetHttp3ConnectionAsync(HttpRequestMessage request, HttpAuthority authority, CancellationToken cancellationToken) - { - Debug.Assert(_kind == HttpConnectionKind.Https); - Debug.Assert(_http3Enabled == true); - - Http3Connection? http3Connection = Volatile.Read(ref _http3Connection); - - if (http3Connection != null) - { - TimeSpan pooledConnectionLifetime = _poolManager.Settings._pooledConnectionLifetime; - if (http3Connection.LifetimeExpired(Environment.TickCount64, pooledConnectionLifetime) || http3Connection.Authority != authority) - { - // Connection expired. - http3Connection.Dispose(); - InvalidateHttp3Connection(http3Connection); - } - else - { - // Connection exists and it is still good to use. - if (NetEventSource.Log.IsEnabled()) Trace("Using existing HTTP3 connection."); - _usedSinceLastCleanup = true; - return (http3Connection, false, null); - } - } - - // Ensure that the connection creation semaphore is created - if (_http3ConnectionCreateLock == null) - { - lock (SyncObj) - { - if (_http3ConnectionCreateLock == null) - { - _http3ConnectionCreateLock = new SemaphoreSlim(1); - } - } - } - - await _http3ConnectionCreateLock.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - if (_http3Connection != null) - { - // Someone beat us to creating the connection. - - if (NetEventSource.Log.IsEnabled()) - { - Trace("Using existing HTTP3 connection."); - } - - return (_http3Connection, false, null); - } - - if (NetEventSource.Log.IsEnabled()) - { - Trace("Attempting new HTTP3 connection."); - } - - QuicConnection quicConnection; - try - { - quicConnection = await ConnectHelper.ConnectQuicAsync(Settings._quicImplementationProvider ?? QuicImplementationProviders.Default, new DnsEndPoint(authority.IdnHost, authority.Port), _sslOptionsHttp3, cancellationToken).ConfigureAwait(false); - } - catch - { - // Disables HTTP/3 until server announces it can handle it via Alt-Svc. - BlocklistAuthority(authority); - throw; - } - - //TODO: NegotiatedApplicationProtocol not yet implemented. -#if false - if (quicConnection.NegotiatedApplicationProtocol != SslApplicationProtocol.Http3) - { - BlocklistAuthority(authority); - throw new HttpRequestException("QUIC connected but no HTTP/3 indicated via ALPN.", null, RequestRetryType.RetryOnSameOrNextProxy); - } -#endif - - http3Connection = new Http3Connection(this, _originAuthority, authority, quicConnection); - _http3Connection = http3Connection; - - if (NetEventSource.Log.IsEnabled()) - { - Trace("New HTTP3 connection established."); - } - - return (http3Connection, true, null); - } - finally - { - _http3ConnectionCreateLock.Release(); - } - } - - public void InvalidateHttp3Connection(Http3Connection connection) - { - lock (SyncObj) - { - if (_http3Connection == connection) - { - _http3Connection = null; - } - } - } -#endif - public async ValueTask SendWithRetryAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken) { while (true) @@ -937,23 +765,9 @@ public async ValueTask SendWithRetryAsync(HttpRequestMessag continue; } -#if HTTP3_SUPPORTED - // Check for the Alt-Svc header, to upgrade to HTTP/3. - if (_altSvcEnabled && response.Headers.TryGetValues(KnownHeaders.AltSvc.Descriptor, out IEnumerable? altSvcHeaderValues)) - { - HandleAltSvc(altSvcHeaderValues, response.Headers.Age); - } - - // If an Alt-Svc authority returns 421, it means it can't actually handle the request. - // An authority is supposed to be able to handle ALL requests to the origin, so this is a server bug. - // In this case, we blocklist the authority and retry the request at the origin. - if (response.StatusCode == HttpStatusCode.MisdirectedRequest && connection is Http3Connection h3Connection && h3Connection.Authority != _originAuthority) - { - response.Dispose(); - BlocklistAuthority(h3Connection.Authority); + if (ProcessAltSvc(response, connection)) continue; - } -#endif + return response; } } @@ -980,8 +794,6 @@ internal void HandleAltSvc(IEnumerable altSvcHeaderValues, TimeSpan? res if (value == AltSvcHeaderValue.Clear) { ExpireAltSvcAuthority(); - Debug.Assert(_authorityExpireTimer != null); - _authorityExpireTimer.Change(Timeout.Infinite, Timeout.Infinite); break; } @@ -1012,176 +824,7 @@ internal void HandleAltSvc(IEnumerable altSvcHeaderValues, TimeSpan? res } } - // There's a race here in checking _http3Authority outside of the lock, - // but there's really no bad behavior if _http3Authority changes in the mean time. - if (nextAuthority != null && !nextAuthority.Equals(_http3Authority)) - { - // Clamp the max age to 30 days... this is arbitrary but prevents passing a too-large TimeSpan to the Timer. - if (nextAuthorityMaxAge.Ticks > (30 * TimeSpan.TicksPerDay)) - { - nextAuthorityMaxAge = TimeSpan.FromTicks(30 * TimeSpan.TicksPerDay); - } - - lock (SyncObj) - { - if (_authorityExpireTimer == null) - { - var thisRef = new WeakReference(this); - - bool restoreFlow = false; - try - { - if (!ExecutionContext.IsFlowSuppressed()) - { - ExecutionContext.SuppressFlow(); - restoreFlow = true; - } - - _authorityExpireTimer = new Timer(static o => - { - var wr = (WeakReference)o!; - if (wr.TryGetTarget(out HttpConnectionPool? @this)) - { - @this.ExpireAltSvcAuthority(); - } - }, thisRef, nextAuthorityMaxAge, Timeout.InfiniteTimeSpan); - } - finally - { - if (restoreFlow) ExecutionContext.RestoreFlow(); - } - } - else - { - _authorityExpireTimer.Change(nextAuthorityMaxAge, Timeout.InfiniteTimeSpan); - } - - _http3Authority = nextAuthority; - _persistAuthority = nextAuthorityPersist; - } - - if (!nextAuthorityPersist) - { - _poolManager.StartMonitoringNetworkChanges(); - } - } - } - - /// - /// Expires the current Alt-Svc authority, resetting the connection back to origin. - /// - private void ExpireAltSvcAuthority() - { - // If we ever support prenegotiated HTTP/3, this should be set to origin, not nulled out. - _http3Authority = null; - } - - /// - /// Checks whether the given is on the currext Alt-Svc blocklist. - /// - /// - private bool IsAltSvcBlocked(HttpAuthority authority) - { - if (_altSvcBlocklist != null) - { - lock (_altSvcBlocklist) - { - return _altSvcBlocklist.Contains(authority); - } - } - return false; - } - - /// - /// Blocklists an authority and resets the current authority back to origin. - /// If the number of blocklisted authorities exceeds , - /// Alt-Svc will be disabled entirely for a period of time. - /// - /// - /// This is called when we get a "421 Misdirected Request" from an alternate authority. - /// A future strategy would be to retry the individual request on an older protocol, we'd want to have - /// some logic to blocklist after some number of failures to avoid doubling our request latency. - /// - /// For now, the spec states alternate authorities should be able to handle ALL requests, so this - /// is treated as an exceptional error by immediately blocklisting the authority. - /// - internal void BlocklistAuthority(HttpAuthority badAuthority) - { - Debug.Assert(badAuthority != null); - - HashSet? altSvcBlocklist = _altSvcBlocklist; - - if (altSvcBlocklist == null) - { - lock (SyncObj) - { - altSvcBlocklist = _altSvcBlocklist; - if (altSvcBlocklist == null) - { - altSvcBlocklist = new HashSet(); - _altSvcBlocklistTimerCancellation = new CancellationTokenSource(); - _altSvcBlocklist = altSvcBlocklist; - } - } - } - - bool added, disabled = false; - - lock (altSvcBlocklist) - { - added = altSvcBlocklist.Add(badAuthority); - - if (added && altSvcBlocklist.Count >= MaxAltSvcIgnoreListSize && _altSvcEnabled) - { - _altSvcEnabled = false; - disabled = true; - } - } - - lock (SyncObj) - { - if (_http3Authority == badAuthority) - { - ExpireAltSvcAuthority(); - Debug.Assert(_authorityExpireTimer != null); - _authorityExpireTimer.Change(Timeout.Infinite, Timeout.Infinite); - } - } - - Debug.Assert(_altSvcBlocklistTimerCancellation != null); - if (added) - { - _ = Task.Delay(AltSvcBlocklistTimeoutInMilliseconds) - .ContinueWith(t => - { - lock (altSvcBlocklist) - { - altSvcBlocklist.Remove(badAuthority); - } - }, _altSvcBlocklistTimerCancellation.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } - - if (disabled) - { - _ = Task.Delay(AltSvcBlocklistTimeoutInMilliseconds) - .ContinueWith(t => - { - _altSvcEnabled = true; - }, _altSvcBlocklistTimerCancellation.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } - } - - public void OnNetworkChanged() - { - lock (SyncObj) - { - if (_http3Authority != null && _persistAuthority == false) - { - ExpireAltSvcAuthority(); - Debug.Assert(_authorityExpireTimer != null); - _authorityExpireTimer.Change(Timeout.Infinite, Timeout.Infinite); - } - } + HandleAltSvcQuic(nextAuthority, nextAuthorityMaxAge, nextAuthorityPersist); } public async Task SendWithNtConnectionAuthAsync(HttpConnection connection, HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken) @@ -1691,18 +1334,7 @@ public void Dispose() _http2Connections = null; } - if (_authorityExpireTimer != null) - { - _authorityExpireTimer.Dispose(); - _authorityExpireTimer = null; - } - - if (_altSvcBlocklistTimerCancellation != null) - { - _altSvcBlocklistTimerCancellation.Cancel(); - _altSvcBlocklistTimerCancellation.Dispose(); - _altSvcBlocklistTimerCancellation = null; - } + DisposeHttp3Objects(); } Debug.Assert(list.Count == 0, $"Expected {nameof(list)}.{nameof(list.Count)} == 0"); } @@ -1877,6 +1509,12 @@ internal void HeartBeat() } } + partial void DisposeHttp3Objects(); + partial void ExpireAltSvcAuthority(bool expireTimer = true); + partial void HandleAltSvcQuic(HttpAuthority? nextAuthority, TimeSpan nextAuthorityMaxAge, bool nextAuthorityPersist); + partial void InitializeHttp3EncodedAuthorityHostHeader(string hostHeader); + partial void InitializeHttp3SslOptions(string sslHostName); + partial void InitializeHttpsConnectionKind(); // For diagnostic purposes public override string ToString() => diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.NoQuic.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.NoQuic.cs new file mode 100644 index 00000000000000..4c70769cd5b6ad --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.NoQuic.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Net.Http +{ + internal sealed partial class HttpConnectionSettings + { + private static bool AllowDraftHttp3 => false; + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.Quic.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.Quic.cs new file mode 100644 index 00000000000000..d5ae4069b01e96 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.Quic.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Net.Http +{ + internal sealed partial class HttpConnectionSettings + { + private const string Http3DraftSupportEnvironmentVariableSettingName = "DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP3DRAFTSUPPORT"; + private const string Http3DraftSupportAppCtxSettingName = "System.Net.SocketsHttpHandler.Http3DraftSupport"; + + private static bool AllowDraftHttp3 + { + get + { + // Default to allowing draft HTTP/3, but enable that to be overridden + // by an AppContext switch, or by an environment variable being set to false/0. + + // First check for the AppContext switch, giving it priority over the environment variable. + if (AppContext.TryGetSwitch(Http3DraftSupportAppCtxSettingName, out bool allowHttp3)) + { + return allowHttp3; + } + + // AppContext switch wasn't used. Check the environment variable. + string? envVar = Environment.GetEnvironmentVariable(Http3DraftSupportEnvironmentVariableSettingName); + if (envVar != null && (envVar.Equals("false", StringComparison.OrdinalIgnoreCase) || envVar.Equals("0"))) + { + // Disallow HTTP/3 protocol for HTTP endpoints. + return false; + } + + // Default to allow. + return true; + } + } + + private byte[]? _http3SettingsFrame; + internal byte[] Http3SettingsFrame => _http3SettingsFrame ??= Http3Connection.BuildSettingsFrame(this); + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs index b5dab70c653339..0b4bb12bd6b7ae 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionSettings.cs @@ -12,12 +12,10 @@ namespace System.Net.Http { /// Provides a state bag of settings for configuring HTTP connections. - internal sealed class HttpConnectionSettings + internal sealed partial class HttpConnectionSettings { private const string Http2SupportEnvironmentVariableSettingName = "DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP2SUPPORT"; private const string Http2SupportAppCtxSettingName = "System.Net.Http.SocketsHttpHandler.Http2Support"; - private const string Http3DraftSupportEnvironmentVariableSettingName = "DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP3DRAFTSUPPORT"; - private const string Http3DraftSupportAppCtxSettingName = "System.Net.SocketsHttpHandler.Http3DraftSupport"; internal DecompressionMethods _automaticDecompression = HttpHandlerDefaults.DefaultAutomaticDecompression; @@ -149,37 +147,6 @@ private static bool AllowHttp2 } } - private static bool AllowDraftHttp3 - { - get - { - // Default to allowing draft HTTP/3, but enable that to be overridden - // by an AppContext switch, or by an environment variable being set to false/0. - - // First check for the AppContext switch, giving it priority over the environment variable. - if (AppContext.TryGetSwitch(Http3DraftSupportAppCtxSettingName, out bool allowHttp3)) - { - return allowHttp3; - } - - // AppContext switch wasn't used. Check the environment variable. - string? envVar = Environment.GetEnvironmentVariable(Http3DraftSupportEnvironmentVariableSettingName); - if (envVar != null && (envVar.Equals("false", StringComparison.OrdinalIgnoreCase) || envVar.Equals("0"))) - { - // Disallow HTTP/3 protocol for HTTP endpoints. - return false; - } - - // Default to allow. - return true; - } - } - public bool EnableMultipleHttp2Connections => _enableMultipleHttp2Connections; - -#if HTTP3_SUPPORTED - private byte[]? _http3SettingsFrame; - internal byte[] Http3SettingsFrame => _http3SettingsFrame ??= Http3Connection.BuildSettingsFrame(this); -#endif } }