-
Notifications
You must be signed in to change notification settings - Fork 4.5k
/
HttpConnectionPoolManager.cs
558 lines (496 loc) · 25.8 KB
/
HttpConnectionPoolManager.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
// 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;
}
}
}