|
// 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.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.NetworkInformation;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
namespace System.Net.Http
{
// General flow of requests through the various layers:
//
// (1) HttpConnectionPoolManager.SendAsync: Does proxy lookup
// (2) HttpConnectionPoolManager.SendAsyncCore: Find or create connection pool
// (3) HttpConnectionPool.SendAsync: Handle basic/digest request auth
// (4) HttpConnectionPool.SendWithProxyAuthAsync: Handle basic/digest proxy auth
// (5) HttpConnectionPool.SendWithRetryAsync: Retrieve connection from pool, or create new
// Also, handle retry for failures on connection reuse
// (6) HttpConnection.SendAsync: Handle negotiate/ntlm connection auth
// (7) HttpConnection.SendWithNtProxyAuthAsync: Handle negotiate/ntlm proxy auth
// (8) HttpConnection.SendAsyncCore: Write request to connection and read response
// Also, handle cookie processing
//
// Redirect and decompression handling are done above HttpConnectionPoolManager,
// in RedirectHandler and DecompressionHandler respectively.
/// <summary>Provides a set of connection pools, each for its own endpoint.</summary>
internal sealed class HttpConnectionPoolManager : IDisposable
{
/// <summary>How frequently an operation should be initiated to clean out old pools and connections in those pools.</summary>
private readonly TimeSpan _cleanPoolTimeout;
/// <summary>The pools, indexed by endpoint.</summary>
private readonly ConcurrentDictionary<HttpConnectionKey, HttpConnectionPool> _pools;
/// <summary>Timer used to initiate cleaning of the pools.</summary>
private readonly Timer? _cleaningTimer;
/// <summary>Heart beat timer currently used for Http2 ping only.</summary>
private readonly Timer? _heartBeatTimer;
private readonly HttpConnectionSettings _settings;
private readonly IWebProxy? _proxy;
private readonly ICredentials? _proxyCredentials;
#if !ILLUMOS && !SOLARIS
private NetworkChangeCleanup? _networkChangeCleanup;
#endif
/// <summary>
/// Keeps track of whether or not the cleanup timer is running. It helps us avoid the expensive
/// <see cref="ConcurrentDictionary{TKey,TValue}.IsEmpty"/> call.
/// </summary>
private bool _timerIsRunning;
/// <summary>Object used to synchronize access to state in the pool.</summary>
private object SyncObj => _pools;
/// <summary>Initializes the pools.</summary>
public HttpConnectionPoolManager(HttpConnectionSettings settings)
{
_settings = settings;
_pools = new ConcurrentDictionary<HttpConnectionKey, HttpConnectionPool>();
// As an optimization, we can sometimes avoid the overheads associated with
// storing connections. This is possible when we would immediately terminate
// connections anyway due to either the idle timeout or the lifetime being
// set to zero, as in that case the timeout effectively immediately expires.
// However, we can only do such optimizations if we're not also tracking
// connections per server, as we use data in the associated data structures
// to do that tracking.
bool avoidStoringConnections =
settings._maxConnectionsPerServer == int.MaxValue &&
(settings._pooledConnectionIdleTimeout == TimeSpan.Zero ||
settings._pooledConnectionLifetime == TimeSpan.Zero);
// Start out with the timer not running, since we have no pools.
// When it does run, run it with a frequency based on the idle timeout.
if (!avoidStoringConnections)
{
if (settings._pooledConnectionIdleTimeout == Timeout.InfiniteTimeSpan)
{
const int DefaultScavengeSeconds = 30;
_cleanPoolTimeout = TimeSpan.FromSeconds(DefaultScavengeSeconds);
}
else
{
const int ScavengesPerIdle = 4;
const int MinScavengeSeconds = 1;
TimeSpan timerPeriod = settings._pooledConnectionIdleTimeout / ScavengesPerIdle;
_cleanPoolTimeout = timerPeriod.TotalSeconds >= MinScavengeSeconds ? timerPeriod : TimeSpan.FromSeconds(MinScavengeSeconds);
}
using (ExecutionContext.SuppressFlow()) // Don't capture the current ExecutionContext and its AsyncLocals onto the timer causing them to live forever
{
// Create the timer. Ensure the Timer has a weak reference to this manager; otherwise, it
// can introduce a cycle that keeps the HttpConnectionPoolManager rooted by the Timer
// implementation until the handler is Disposed (or indefinitely if it's not).
var thisRef = new WeakReference<HttpConnectionPoolManager>(this);
_cleaningTimer = new Timer(static s =>
{
var wr = (WeakReference<HttpConnectionPoolManager>)s!;
if (wr.TryGetTarget(out HttpConnectionPoolManager? thisRef))
{
thisRef.RemoveStalePools();
}
}, thisRef, Timeout.Infinite, Timeout.Infinite);
// For now heart beat is used only for ping functionality.
if (_settings._keepAlivePingDelay != Timeout.InfiniteTimeSpan)
{
long heartBeatInterval = (long)Math.Max(1000, Math.Min(_settings._keepAlivePingDelay.TotalMilliseconds, _settings._keepAlivePingTimeout.TotalMilliseconds) / 4);
_heartBeatTimer = new Timer(static state =>
{
var wr = (WeakReference<HttpConnectionPoolManager>)state!;
if (wr.TryGetTarget(out HttpConnectionPoolManager? thisRef))
{
thisRef.HeartBeat();
}
}, thisRef, heartBeatInterval, heartBeatInterval);
}
}
}
// Figure out proxy stuff.
if (settings._useProxy)
{
_proxy = settings._proxy ?? HttpClient.DefaultProxy;
if (_proxy != null)
{
_proxyCredentials = _proxy.Credentials ?? settings._defaultProxyCredentials;
}
}
}
#if !ILLUMOS && !SOLARIS
/// <summary>
/// Starts monitoring for network changes. Upon a change, <see cref="HttpConnectionPool.OnNetworkChanged"/> will be
/// called for every <see cref="HttpConnectionPool"/> in the <see cref="HttpConnectionPoolManager"/>.
/// </summary>
public void StartMonitoringNetworkChanges()
{
if (_networkChangeCleanup != null)
{
return;
}
// Monitor network changes to invalidate Alt-Svc headers.
// A weak reference is used to avoid NetworkChange.NetworkAddressChanged keeping a non-disposed connection pool alive.
NetworkAddressChangedEventHandler networkChangedDelegate;
{ // scope to avoid closure if _networkChangeCleanup != null
var poolsRef = new WeakReference<ConcurrentDictionary<HttpConnectionKey, HttpConnectionPool>>(_pools);
networkChangedDelegate = delegate
{
if (poolsRef.TryGetTarget(out ConcurrentDictionary<HttpConnectionKey, HttpConnectionPool>? pools))
{
foreach (HttpConnectionPool pool in pools.Values)
{
pool.OnNetworkChanged();
}
}
};
}
var cleanup = new NetworkChangeCleanup(networkChangedDelegate);
if (Interlocked.CompareExchange(ref _networkChangeCleanup, cleanup, null) != null)
{
// We lost a race, another thread already started monitoring.
GC.SuppressFinalize(cleanup);
return;
}
// RFC: https://tools.ietf.org/html/rfc7838#section-2.2
// When alternative services are used to send a client to the most
// optimal server, a change in network configuration can result in
// cached values becoming suboptimal. Therefore, clients SHOULD remove
// from cache all alternative services that lack the "persist" flag with
// the value "1" when they detect such a change, when information about
// network state is available.
try
{
using (ExecutionContext.SuppressFlow())
{
NetworkChange.NetworkAddressChanged += networkChangedDelegate;
}
}
catch (NetworkInformationException e)
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"Exception when subscribing to NetworkChange.NetworkAddressChanged: {e}");
// We can't monitor network changes, so technically "information
// about network state is not available" and we can just keep
// all Alt-Svc entries until their expiration time.
//
// keep the _networkChangeCleanup field assigned so we don't try again needlessly
}
}
private sealed class NetworkChangeCleanup : IDisposable
{
private readonly NetworkAddressChangedEventHandler _handler;
public NetworkChangeCleanup(NetworkAddressChangedEventHandler handler)
{
_handler = handler;
}
// If user never disposes the HttpClient, use finalizer to remove from NetworkChange.NetworkAddressChanged.
// _handler will be rooted in NetworkChange, so should be safe to use here.
~NetworkChangeCleanup() => NetworkChange.NetworkAddressChanged -= _handler;
public void Dispose()
{
NetworkChange.NetworkAddressChanged -= _handler;
GC.SuppressFinalize(this);
}
}
#endif
public HttpConnectionSettings Settings => _settings;
public ICredentials? ProxyCredentials => _proxyCredentials;
private static string ParseHostNameFromHeader(string hostHeader)
{
// See if we need to trim off a port.
int colonPos = hostHeader.IndexOf(':');
if (colonPos >= 0)
{
// There is colon, which could either be a port separator or a separator in
// an IPv6 address. See if this is an IPv6 address; if it's not, use everything
// before the colon as the host name, and if it is, use everything before the last
// colon iff the last colon is after the end of the IPv6 address (otherwise it's a
// part of the address).
int ipV6AddressEnd = hostHeader.IndexOf(']');
if (ipV6AddressEnd == -1)
{
return hostHeader.Substring(0, colonPos);
}
else
{
colonPos = hostHeader.LastIndexOf(':');
if (colonPos > ipV6AddressEnd)
{
return hostHeader.Substring(0, colonPos);
}
}
}
return hostHeader;
}
private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? proxyUri, bool isProxyConnect)
{
Uri? uri = request.RequestUri;
Debug.Assert(uri != null);
if (isProxyConnect)
{
Debug.Assert(uri == proxyUri);
return new HttpConnectionKey(HttpConnectionKind.ProxyConnect, uri.IdnHost, uri.Port, null, proxyUri, GetIdentityIfDefaultCredentialsUsed(_settings._defaultCredentialsUsedForProxy));
}
string? sslHostName = null;
if (HttpUtilities.IsSupportedSecureScheme(uri.Scheme))
{
string? hostHeader = request.Headers.Host;
if (hostHeader != null)
{
sslHostName = ParseHostNameFromHeader(hostHeader);
}
else
{
// No explicit Host header. Use host from uri.
sslHostName = uri.IdnHost;
}
}
string identity = GetIdentityIfDefaultCredentialsUsed(proxyUri != null ? _settings._defaultCredentialsUsedForProxy : _settings._defaultCredentialsUsedForServer);
if (proxyUri != null)
{
Debug.Assert(HttpUtilities.IsSupportedProxyScheme(proxyUri.Scheme));
if (HttpUtilities.IsSocksScheme(proxyUri.Scheme))
{
// Socks proxy
if (sslHostName != null)
{
return new HttpConnectionKey(HttpConnectionKind.SslSocksTunnel, uri.IdnHost, uri.Port, sslHostName, proxyUri, identity);
}
else
{
return new HttpConnectionKey(HttpConnectionKind.SocksTunnel, uri.IdnHost, uri.Port, null, proxyUri, identity);
}
}
else if (sslHostName == null)
{
if (HttpUtilities.IsNonSecureWebSocketScheme(uri.Scheme))
{
// Non-secure websocket connection through proxy to the destination.
return new HttpConnectionKey(HttpConnectionKind.ProxyTunnel, uri.IdnHost, uri.Port, null, proxyUri, identity);
}
else
{
// Standard HTTP proxy usage for non-secure requests
// The destination host and port are ignored here, since these connections
// will be shared across any requests that use the proxy.
return new HttpConnectionKey(HttpConnectionKind.Proxy, null, 0, null, proxyUri, identity);
}
}
else
{
// Tunnel SSL connection through proxy to the destination.
return new HttpConnectionKey(HttpConnectionKind.SslProxyTunnel, uri.IdnHost, uri.Port, sslHostName, proxyUri, identity);
}
}
else if (sslHostName != null)
{
return new HttpConnectionKey(HttpConnectionKind.Https, uri.IdnHost, uri.Port, sslHostName, null, identity);
}
else
{
return new HttpConnectionKey(HttpConnectionKind.Http, uri.IdnHost, uri.Port, null, null, identity);
}
}
public ValueTask<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request, Uri? proxyUri, bool async, bool doRequestAuth, bool isProxyConnect, CancellationToken cancellationToken)
{
HttpConnectionKey key = GetConnectionKey(request, proxyUri, isProxyConnect);
HttpConnectionPool? pool;
while (!_pools.TryGetValue(key, out pool))
{
pool = new HttpConnectionPool(this, key.Kind, key.Host, key.Port, key.SslHostName, key.ProxyUri);
if (_cleaningTimer == null)
{
// There's no cleaning timer, which means we're not adding connections into pools, but we still need
// the pool object for this request. We don't need or want to add the pool to the pools, though,
// since we don't want it to sit there forever, which it would without the cleaning timer.
break;
}
if (_pools.TryAdd(key, pool))
{
// We need to ensure the cleanup timer is running if it isn't
// already now that we added a new connection pool.
lock (SyncObj)
{
if (!_timerIsRunning)
{
SetCleaningTimer(_cleanPoolTimeout);
}
}
break;
}
// We created a pool and tried to add it to our pools, but some other thread got there before us.
// We don't need to Dispose the pool, as that's only needed when it contains connections
// that need to be closed.
}
return pool.SendAsync(request, async, doRequestAuth, cancellationToken);
}
public ValueTask<HttpResponseMessage> SendProxyConnectAsync(HttpRequestMessage request, Uri proxyUri, bool async, CancellationToken cancellationToken)
{
return SendAsyncCore(request, proxyUri, async, doRequestAuth: false, isProxyConnect: true, cancellationToken);
}
public ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
{
if (_proxy == null)
{
return SendAsyncCore(request, null, async, doRequestAuth, isProxyConnect: false, cancellationToken);
}
// Do proxy lookup.
Uri? proxyUri = null;
try
{
Debug.Assert(request.RequestUri != null);
if (!_proxy.IsBypassed(request.RequestUri))
{
if (_proxy is IMultiWebProxy multiWebProxy)
{
MultiProxy multiProxy = multiWebProxy.GetMultiProxy(request.RequestUri);
if (multiProxy.ReadNext(out proxyUri, out bool isFinalProxy) && !isFinalProxy)
{
return SendAsyncMultiProxy(request, async, doRequestAuth, multiProxy, proxyUri, cancellationToken);
}
}
else
{
proxyUri = _proxy.GetProxy(request.RequestUri);
}
}
}
catch (Exception ex)
{
// Eat any exception from the IWebProxy and just treat it as no proxy.
// This matches the behavior of other handlers.
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"Exception from {_proxy.GetType().Name}.GetProxy({request.RequestUri}): {ex}");
}
if (proxyUri != null && !HttpUtilities.IsSupportedProxyScheme(proxyUri.Scheme))
{
throw new NotSupportedException(SR.net_http_invalid_proxy_scheme);
}
return SendAsyncCore(request, proxyUri, async, doRequestAuth, isProxyConnect: false, cancellationToken);
}
/// <summary>
/// Iterates a request over a set of proxies until one works, or all proxies have failed.
/// </summary>
/// <param name="request">The request message.</param>
/// <param name="async">Whether to execute the request synchronously or asynchronously.</param>
/// <param name="doRequestAuth">Whether to perform request authentication.</param>
/// <param name="multiProxy">The set of proxies to use.</param>
/// <param name="firstProxy">The first proxy try.</param>
/// <param name="cancellationToken">The cancellation token to use for the operation.</param>
private async ValueTask<HttpResponseMessage> SendAsyncMultiProxy(HttpRequestMessage request, bool async, bool doRequestAuth, MultiProxy multiProxy, Uri? firstProxy, CancellationToken cancellationToken)
{
HttpRequestException rethrowException;
do
{
try
{
return await SendAsyncCore(request, firstProxy, async, doRequestAuth, isProxyConnect: false, cancellationToken).ConfigureAwait(false);
}
catch (HttpRequestException ex) when (ex.AllowRetry != RequestRetryType.NoRetry)
{
rethrowException = ex;
}
}
while (multiProxy.ReadNext(out firstProxy, out _));
ExceptionDispatchInfo.Throw(rethrowException);
return null; // should never be reached: VS doesn't realize Throw() never returns.
}
/// <summary>Disposes of the pools, disposing of each individual pool.</summary>
public void Dispose()
{
_cleaningTimer?.Dispose();
_heartBeatTimer?.Dispose();
foreach (KeyValuePair<HttpConnectionKey, HttpConnectionPool> pool in _pools)
{
pool.Value.Dispose();
}
#if !ILLUMOS && !SOLARIS
_networkChangeCleanup?.Dispose();
#endif
}
/// <summary>Sets <see cref="_cleaningTimer"/> and <see cref="_timerIsRunning"/> based on the specified timeout.</summary>
private void SetCleaningTimer(TimeSpan timeout)
{
if (_cleaningTimer!.Change(timeout, Timeout.InfiniteTimeSpan))
{
_timerIsRunning = timeout != Timeout.InfiniteTimeSpan;
}
}
/// <summary>Removes unusable connections from each pool, and removes stale pools entirely.</summary>
private void RemoveStalePools()
{
Debug.Assert(_cleaningTimer != null);
// Iterate through each pool in the set of pools. For each, ask it to clear out
// any unusable connections (e.g. those which have expired, those which have been closed, etc.)
// The pool may detect that it's empty and long unused, in which case it'll dispose of itself,
// such that any connections returned to the pool to be cached will be disposed of. In such
// a case, we also remove the pool from the set of pools to avoid a leak.
foreach (KeyValuePair<HttpConnectionKey, HttpConnectionPool> entry in _pools)
{
if (entry.Value.CleanCacheAndDisposeIfUnused())
{
_pools.TryRemove(entry.Key, out _);
}
}
// Restart the timer if we have any pools to clean up.
lock (SyncObj)
{
SetCleaningTimer(!_pools.IsEmpty ? _cleanPoolTimeout : Timeout.InfiniteTimeSpan);
}
// NOTE: There is a possible race condition with regards to a pool getting cleaned up at the same
// time it's about to be used for another request. The timer cleanup could start running, see that
// a pool is empty, and initiate its disposal. Concurrently, the pools could hand out the pool
// to a request looking to get a connection, because the pool may not have been removed yet
// from the pools. Worst case here is that connection will end up getting returned to an
// already disposed pool, in which case the connection will also end up getting disposed rather
// than reused. This should be a rare occurrence, so for now we don't worry about it. In the
// future, there are a variety of possible ways to address it, such as allowing connections to
// be returned to pools they weren't associated with.
}
private void HeartBeat()
{
foreach (KeyValuePair<HttpConnectionKey, HttpConnectionPool> pool in _pools)
{
pool.Value.HeartBeat();
}
}
private static string GetIdentityIfDefaultCredentialsUsed(bool defaultCredentialsUsed)
{
return defaultCredentialsUsed ? CurrentUserIdentityProvider.GetIdentity() : string.Empty;
}
internal readonly struct HttpConnectionKey : IEquatable<HttpConnectionKey>
{
public readonly HttpConnectionKind Kind;
public readonly string? Host;
public readonly int Port;
public readonly string? SslHostName; // null if not SSL
public readonly Uri? ProxyUri;
public readonly string Identity;
public HttpConnectionKey(HttpConnectionKind kind, string? host, int port, string? sslHostName, Uri? proxyUri, string identity)
{
Kind = kind;
Host = host;
Port = port;
SslHostName = sslHostName;
ProxyUri = proxyUri;
Identity = identity;
}
// In the common case, SslHostName (when present) is equal to Host. If so, don't include in hash.
public override int GetHashCode() =>
(SslHostName == Host ?
HashCode.Combine(Kind, Host, Port, ProxyUri, Identity) :
HashCode.Combine(Kind, Host, Port, SslHostName, ProxyUri, Identity));
public override bool Equals([NotNullWhen(true)] object? obj) =>
obj is HttpConnectionKey hck &&
Equals(hck);
public bool Equals(HttpConnectionKey other) =>
Kind == other.Kind &&
Host == other.Host &&
Port == other.Port &&
ProxyUri == other.ProxyUri &&
SslHostName == other.SslHostName &&
Identity == other.Identity;
}
}
}
|