Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 39ed24e

Browse files
author
Geoff Kizer
committed
reorganize pool manager logic and rework HttpConnectionKey to support ssl proxy tunneling
1 parent 823e019 commit 39ed24e

File tree

9 files changed

+194
-179
lines changed

9 files changed

+194
-179
lines changed

src/System.Net.Http/src/System.Net.Http.csproj

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,8 @@
138138
<Compile Include="System\Net\Http\SocketsHttpHandler\EmptyReadStream.cs" />
139139
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnection.cs" />
140140
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnectionHandler.cs" />
141-
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnectionKey.cs" />
142141
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnectionPool.cs" />
143-
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnectionPools.cs" />
142+
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnectionPoolManager.cs" />
144143
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnectionResponseContent.cs" />
145144
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpConnectionSettings.cs" />
146145
<Compile Include="System\Net\Http\SocketsHttpHandler\HttpContentDuplexStream.cs" />
@@ -464,4 +463,4 @@
464463
<Reference Include="System.Security.Cryptography.Primitives" />
465464
</ItemGroup>
466465
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
467-
</Project>
466+
</Project>

src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,8 @@ public CertificateCallbackMapper(Func<HttpRequestMessage, X509Certificate2, X509
3232
}
3333
}
3434

35-
public static async ValueTask<Stream> ConnectAsync(HttpConnectionKey key, CancellationToken cancellationToken)
35+
public static async ValueTask<Stream> ConnectAsync(string host, int port, CancellationToken cancellationToken)
3636
{
37-
string host = key.Host;
38-
int port = key.Port;
39-
4037
try
4138
{
4239
// Rather than creating a new Socket and calling ConnectAsync on it, we use the static

src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public HttpConnection(
7979
_pool = pool;
8080
_stream = stream;
8181
_transportContext = transportContext;
82-
_usingProxy = pool.Pools.UsingProxy;
82+
_usingProxy = pool.UsingProxy;
8383
_idnHostAsciiBytes = pool.IdnHostAsciiBytes;
8484

8585
_writeBuffer = new byte[InitialWriteBufferSize];
@@ -89,11 +89,11 @@ public HttpConnection(
8989

9090
if (NetEventSource.IsEnabled)
9191
{
92-
if (_stream is SslStream sslStream)
92+
if (pool.IsSecure)
9393
{
94+
var sslStream = (SslStream)_stream;
9495
Trace(
95-
$"Secure connection created to {pool.Key.Host}:{pool.Key.Port}. " +
96-
$"SslHostName:{pool.Key.SslHostName}. " +
96+
$"Secure connection created to {pool}. " +
9797
$"SslProtocol:{sslStream.SslProtocol}, " +
9898
$"CipherAlgorithm:{sslStream.CipherAlgorithm}, CipherStrength:{sslStream.CipherStrength}, " +
9999
$"HashAlgorithm:{sslStream.HashAlgorithm}, HashStrength:{sslStream.HashStrength}, " +
@@ -102,7 +102,7 @@ public HttpConnection(
102102
}
103103
else
104104
{
105-
Trace($"Connection created to {pool.Key.Host}:{pool.Key.Port}.");
105+
Trace($"Connection created to {pool}.");
106106
}
107107
}
108108
}
@@ -261,9 +261,9 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, Can
261261

262262
// Determine cookies to send
263263
string cookiesFromContainer = null;
264-
if (_pool.Pools.Settings._useCookies)
264+
if (_pool.Settings._useCookies)
265265
{
266-
cookiesFromContainer = _pool.Pools.Settings._cookieContainer.GetCookieHeader(request.RequestUri);
266+
cookiesFromContainer = _pool.Settings._cookieContainer.GetCookieHeader(request.RequestUri);
267267
if (cookiesFromContainer == "")
268268
{
269269
cookiesFromContainer = null;
@@ -357,7 +357,7 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, Can
357357
}
358358

359359
// Start to read response.
360-
_allowedReadLineBytes = _pool.Pools.Settings._maxResponseHeadersLength * 1024;
360+
_allowedReadLineBytes = _pool.Settings._maxResponseHeadersLength * 1024;
361361

362362
// We should not have any buffered data here; if there was, it should have been treated as an error
363363
// by the previous request handling. (Note we do not support HTTP pipelining.)
@@ -501,9 +501,9 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, Can
501501
if (NetEventSource.IsEnabled) Trace($"Received response: {response}");
502502

503503
// Process Set-Cookie headers.
504-
if (_pool.Pools.Settings._useCookies)
504+
if (_pool.Settings._useCookies)
505505
{
506-
CookieHelper.ProcessReceivedCookies(response, _pool.Pools.Settings._cookieContainer);
506+
CookieHelper.ProcessReceivedCookies(response, _pool.Settings._cookieContainer);
507507
}
508508

509509
return response;
@@ -1290,7 +1290,7 @@ private static bool EqualsOrdinal(string left, Span<byte> right)
12901290
return true;
12911291
}
12921292

1293-
public sealed override string ToString() => $"{nameof(HttpConnection)}({_pool.Key})"; // Description for diagnostic purposes
1293+
public sealed override string ToString() => $"{nameof(HttpConnection)}({_pool})"; // Description for diagnostic purposes
12941294

12951295
private static void ThrowInvalidHttpResponse() => throw new HttpRequestException(SR.net_http_invalid_response);
12961296

src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionHandler.cs

Lines changed: 5 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,70 +9,23 @@ namespace System.Net.Http
99
{
1010
internal sealed class HttpConnectionHandler : HttpMessageHandler
1111
{
12-
private readonly HttpConnectionPools _connectionPools;
12+
private readonly HttpConnectionPoolManager _poolManager;
1313

14-
public HttpConnectionHandler(HttpConnectionSettings settings)
14+
public HttpConnectionHandler(HttpConnectionPoolManager poolManager)
1515
{
16-
_connectionPools = new HttpConnectionPools(settings, usingProxy: false);
16+
_poolManager = poolManager;
1717
}
1818

1919
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
2020
{
21-
Uri uri = request.RequestUri;
22-
HttpConnectionKey key = new HttpConnectionKey(uri.IdnHost, uri.Port, GetSslHostName(request));
23-
HttpConnectionPool pool = _connectionPools.GetOrAddPool(key);
24-
return pool.SendAsync(request, cancellationToken);
25-
}
26-
27-
private static string GetSslHostName(HttpRequestMessage request)
28-
{
29-
Uri uri = request.RequestUri;
30-
31-
if (!HttpUtilities.IsSupportedSecureScheme(uri.Scheme))
32-
{
33-
// Not using SSL.
34-
return null;
35-
}
36-
37-
string hostHeader = request.Headers.Host;
38-
if (hostHeader == null)
39-
{
40-
// No explicit Host header. Use host from uri.
41-
return request.RequestUri.IdnHost;
42-
}
43-
44-
// There is a host header. Use it, but first see if we need to trim off a port.
45-
int colonPos = hostHeader.IndexOf(':');
46-
if (colonPos >= 0)
47-
{
48-
// There is colon, which could either be a port separator or a separator in
49-
// an IPv6 address. See if this is an IPv6 address; if it's not, use everything
50-
// before the colon as the host name, and if it is, use everything before the last
51-
// colon iff the last colon is after the end of the IPv6 address (otherwise it's a
52-
// part of the address).
53-
int ipV6AddressEnd = hostHeader.IndexOf(']');
54-
if (ipV6AddressEnd == -1)
55-
{
56-
return hostHeader.Substring(0, colonPos);
57-
}
58-
else
59-
{
60-
colonPos = hostHeader.LastIndexOf(':');
61-
if (colonPos > ipV6AddressEnd)
62-
{
63-
return hostHeader.Substring(0, colonPos);
64-
}
65-
}
66-
}
67-
68-
return hostHeader;
21+
return _poolManager.SendAsync(request, null, cancellationToken);
6922
}
7023

7124
protected override void Dispose(bool disposing)
7225
{
7326
if (disposing)
7427
{
75-
_connectionPools.Dispose();
28+
_poolManager.Dispose();
7629
}
7730

7831
base.Dispose(disposing);

src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionKey.cs

Lines changed: 0 additions & 46 deletions
This file was deleted.

src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ namespace System.Net.Http
1717
/// <summary>Provides a pool of connections to the same endpoint.</summary>
1818
internal sealed class HttpConnectionPool : IDisposable
1919
{
20-
private readonly HttpConnectionPools _pools;
21-
private readonly HttpConnectionKey _key;
20+
private readonly HttpConnectionPoolManager _poolManager;
21+
private readonly string _host;
22+
private readonly int _port;
23+
private readonly Uri _proxyUri;
2224

2325
/// <summary>List of idle connections stored in the pool.</summary>
2426
private readonly List<CachedConnection> _idleConnections = new List<CachedConnection>();
@@ -44,39 +46,38 @@ internal sealed class HttpConnectionPool : IDisposable
4446

4547
/// <summary>Initializes the pool.</summary>
4648
/// <param name="maxConnections">The maximum number of connections allowed to be associated with the pool at any given time.</param>
47-
public HttpConnectionPool(HttpConnectionPools pools, HttpConnectionKey key, int maxConnections = int.MaxValue) // int.MaxValue treated as infinite
49+
///
50+
public HttpConnectionPool(HttpConnectionPoolManager poolManager, string host, int port, string sslHostName, Uri proxyUri, int maxConnections)
4851
{
49-
_pools = pools;
50-
_key = key;
52+
_poolManager = poolManager;
53+
_host = host;
54+
_port = port;
55+
_proxyUri = proxyUri;
5156
_maxConnections = maxConnections;
5257

53-
// Precalculate ASCII bytes for header name
54-
// We don't do this for proxy connections because the actual host header varies on a proxy connection.
55-
if (!pools.UsingProxy)
58+
if (sslHostName != null)
5659
{
57-
// CONSIDER: Cache more than just host name -- port, header name, etc
58-
59-
// Note the IDN hostname should always be ASCII, since it's already been IDNA encoded.
60-
_idnHostAsciiBytes = Encoding.ASCII.GetBytes(key.Host);
61-
Debug.Assert(Encoding.ASCII.GetString(_idnHostAsciiBytes) == key.Host);
62-
}
63-
else
64-
{
65-
// Proxy connections should never use SSL
66-
Debug.Assert(!key.IsSecure);
60+
// Precalculate cached SSL options to use for all connections.
61+
_sslOptions = _poolManager.Settings._sslOptions?.ShallowClone() ?? new SslClientAuthenticationOptions();
62+
_sslOptions.ApplicationProtocols = null; // explicitly ignore any ApplicationProtocols set
63+
_sslOptions.TargetHost = sslHostName; // always use the key's name rather than whatever was specified
6764
}
6865

69-
if (key.IsSecure)
66+
if (_host != null)
7067
{
71-
// Precalculate cached SSL options to use for all connections.
72-
_sslOptions = _pools.Settings._sslOptions?.ShallowClone() ?? new SslClientAuthenticationOptions();
73-
_sslOptions.ApplicationProtocols = null; // explicitly ignore any ApplicationProtocols set
74-
_sslOptions.TargetHost = key.SslHostName; // always use the key's name rather than whatever was specified
68+
// Precalculate ASCII bytes for header name
69+
// Note that if _host is null, this is a (non-tunneled) proxy connection, and we can't cache the hostname.
70+
// CONSIDER: Cache more than just host name -- port, header name, etc
71+
72+
// Note the IDN hostname should always be ASCII, since it's already been IDNA encoded.
73+
_idnHostAsciiBytes = Encoding.ASCII.GetBytes(_host);
74+
Debug.Assert(Encoding.ASCII.GetString(_idnHostAsciiBytes) == _host);
7575
}
7676
}
7777

78-
public HttpConnectionKey Key => _key;
79-
public HttpConnectionPools Pools => _pools;
78+
public HttpConnectionSettings Settings => _poolManager.Settings;
79+
public bool IsSecure => _sslOptions != null;
80+
public bool UsingProxy => (_proxyUri != null && !IsSecure); // Tunnel doesn't count, only direct proxy usage
8081
public byte[] IdnHostAsciiBytes => _idnHostAsciiBytes;
8182

8283
/// <summary>Object used to synchronize access to state in the pool.</summary>
@@ -89,8 +90,8 @@ private ValueTask<HttpConnection> GetConnectionAsync(HttpRequestMessage request,
8990
return new ValueTask<HttpConnection>(Task.FromCanceled<HttpConnection>(cancellationToken));
9091
}
9192

92-
TimeSpan pooledConnectionLifetime = _pools.Settings._pooledConnectionLifetime;
93-
TimeSpan pooledConnectionIdleTimeout = _pools.Settings._pooledConnectionIdleTimeout;
93+
TimeSpan pooledConnectionLifetime = _poolManager.Settings._pooledConnectionLifetime;
94+
TimeSpan pooledConnectionIdleTimeout = _poolManager.Settings._pooledConnectionIdleTimeout;
9495
DateTimeOffset now = DateTimeOffset.UtcNow;
9596
List<CachedConnection> list = _idleConnections;
9697
lock (SyncObj)
@@ -199,10 +200,15 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, Can
199200

200201
private async ValueTask<HttpConnection> CreateConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
201202
{
202-
Stream stream = await ConnectHelper.ConnectAsync(_key, cancellationToken).ConfigureAwait(false);
203+
Stream stream =
204+
_proxyUri != null ?
205+
(_sslOptions != null ?
206+
throw new NotSupportedException("SSL Proxy tunneling not currently supported") :
207+
await ConnectHelper.ConnectAsync(_proxyUri.IdnHost, _proxyUri.Port, cancellationToken)) :
208+
await ConnectHelper.ConnectAsync(_host, _port, cancellationToken);
203209

204210
TransportContext transportContext = null;
205-
if (_key.IsSecure)
211+
if (_sslOptions != null)
206212
{
207213
SslStream sslStream = await ConnectHelper.EstablishSslConnectionAsync(_sslOptions, request, stream, cancellationToken).ConfigureAwait(false);
208214
stream = sslStream;
@@ -473,8 +479,8 @@ public void Dispose()
473479
/// </returns>
474480
public bool CleanCacheAndDisposeIfUnused()
475481
{
476-
TimeSpan pooledConnectionLifetime = _pools.Settings._pooledConnectionLifetime;
477-
TimeSpan pooledConnectionIdleTimeout = _pools.Settings._pooledConnectionIdleTimeout;
482+
TimeSpan pooledConnectionLifetime = _poolManager.Settings._pooledConnectionLifetime;
483+
TimeSpan pooledConnectionIdleTimeout = _poolManager.Settings._pooledConnectionIdleTimeout;
478484

479485
List<CachedConnection> list = _idleConnections;
480486
List<HttpConnection> toDispose = null;
@@ -560,7 +566,16 @@ public bool CleanCacheAndDisposeIfUnused()
560566
return false;
561567
}
562568

563-
public override string ToString() => $"{nameof(HttpConnectionPool)}(Connections:{_associatedConnectionCount})"; // Description for diagnostic purposes
569+
// For diagnostic purposes
570+
public override string ToString() =>
571+
$"{nameof(HttpConnectionPool)}" +
572+
(_proxyUri == null ?
573+
(_sslOptions == null ?
574+
$"http://{_host}:{_port}" :
575+
$"https://{_host}:{_port}" + (_sslOptions.TargetHost != _host ? $", SSL TargetHost={_sslOptions.TargetHost}" : null)) :
576+
(_sslOptions == null ?
577+
$"Proxy {_proxyUri}" :
578+
$"https://{_host}:{_port}/ tunnelled via Proxy {_proxyUri}" + (_sslOptions.TargetHost != _host ? $", SSL TargetHost={_sslOptions.TargetHost}" : null)));
564579

565580
private void Trace(string message, [CallerMemberName] string memberName = null) =>
566581
NetEventSource.Log.HandlerMessage(

0 commit comments

Comments
 (0)