Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HttpClient: Adds detection of DNS changes through use of SocketsHttpHandler for .NET 6 and above #3762

Merged
merged 39 commits into from
Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d461613
initial commit
NaluTripician Mar 14, 2023
9ae170d
removed unneeded usings
NaluTripician Mar 14, 2023
6040c54
added validation callback, still needs tests
NaluTripician Mar 15, 2023
5562f00
nits + fixes
NaluTripician Mar 15, 2023
1644165
added additional test
NaluTripician Mar 16, 2023
f3245e7
test change
NaluTripician Mar 16, 2023
8bc9f23
removed unneeded Dispose calls
NaluTripician Mar 16, 2023
db022f7
removed unnneed dispose calls
NaluTripician Mar 16, 2023
6f188cf
requested changes
NaluTripician Mar 17, 2023
65c8eb9
added pooledConnectionLifetime as client option
NaluTripician Mar 17, 2023
da8e381
nit
NaluTripician Mar 17, 2023
6f5a290
Update Microsoft.Azure.Cosmos/src/HttpClient/CosmosHttpClientCore.cs
NaluTripician Mar 17, 2023
8127fab
Update Microsoft.Azure.Cosmos/src/CosmosClientOptions.cs
NaluTripician Mar 17, 2023
9675ccf
suggested changes
NaluTripician Mar 17, 2023
fb54c24
remove test, reorder usings
NaluTripician Mar 17, 2023
237dd77
updated contracts
NaluTripician Mar 20, 2023
5c56bee
removed all non XXXAPI.json changes from UpdateContracts run
NaluTripician Mar 20, 2023
81c6d9d
Merge branch 'master' into users/nalutripician/EnsureDNSChanges
NaluTripician Mar 23, 2023
8dbf95e
Merge branch 'master' into users/nalutripician/EnsureDNSChanges
NaluTripician Mar 30, 2023
f1698ed
removed public surface, added random timespan
NaluTripician Mar 30, 2023
5b8fa7f
Merge branch 'users/nalutripician/EnsureDNSChanges' of https://github…
NaluTripician Mar 30, 2023
0e75424
removed change from unrelated file
NaluTripician Mar 30, 2023
814b2ef
Update Microsoft.Azure.Cosmos/src/HttpClient/CosmosHttpClientCore.cs
NaluTripician Mar 30, 2023
39b8da8
added thread safe random method
NaluTripician Mar 30, 2023
04984cf
added thread safe random method
NaluTripician Mar 30, 2023
7f52f15
Merge branch 'master' into users/nalutripician/EnsureDNSChanges
NaluTripician Mar 30, 2023
e762fc0
nit
NaluTripician Mar 31, 2023
c02c5ce
Merge branch 'users/nalutripician/EnsureDNSChanges' of https://github…
NaluTripician Mar 31, 2023
2f7348a
fixed merge mistake
NaluTripician Apr 3, 2023
254c13e
Merge branch 'master' into users/nalutripician/EnsureDNSChanges
NaluTripician Apr 4, 2023
d55ecf0
added reflection failsafe/error tracing
NaluTripician Apr 4, 2023
dc0758f
nits
NaluTripician Apr 4, 2023
ce5db56
added back removed if
NaluTripician Apr 4, 2023
3cb66e0
fixed formatting
NaluTripician Apr 4, 2023
51f3be2
Merge branch 'master' into users/nalutripician/EnsureDNSChanges
ealsur Apr 4, 2023
fd74417
Merge branch 'master' into users/nalutripician/EnsureDNSChanges
ealsur Apr 4, 2023
efd660a
Merge branch 'master' into users/nalutripician/EnsureDNSChanges
ealsur Apr 6, 2023
e6f76c3
Merge branch 'master' into users/nalutripician/EnsureDNSChanges
NaluTripician Apr 7, 2023
af5afcc
changed random method, fixed serverCertificateCustomValidation
NaluTripician Apr 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 89 additions & 3 deletions Microsoft.Azure.Cosmos/src/HttpClient/CosmosHttpClientCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ namespace Microsoft.Azure.Cosmos
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos.Core.Trace;
using Microsoft.Azure.Cosmos.Resource.CosmosExceptions;
using Microsoft.Azure.Cosmos.Tracing;
using Microsoft.Azure.Cosmos.Tracing.TraceData;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Collections;
Expand Down Expand Up @@ -99,7 +99,93 @@ public static CosmosHttpClient CreateWithConnectionPolicy(
eventSource: eventSource);
}

public static HttpMessageHandler CreateHttpClientHandler(int gatewayModeMaxConnectionLimit, IWebProxy webProxy, Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> serverCertificateCustomValidationCallback)
public static HttpMessageHandler CreateHttpClientHandler(
NaluTripician marked this conversation as resolved.
Show resolved Hide resolved
int gatewayModeMaxConnectionLimit,
IWebProxy webProxy,
Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> serverCertificateCustomValidationCallback)
{
// TODO: Remove type check and use #if NET6_0_OR_GREATER when multitargetting is possible
kirankumarkolli marked this conversation as resolved.
Show resolved Hide resolved
Type socketHandlerType = Type.GetType("System.Net.Http.SocketsHttpHandler, System.Net.Http");
NaluTripician marked this conversation as resolved.
Show resolved Hide resolved

if (socketHandlerType != null)
NaluTripician marked this conversation as resolved.
Show resolved Hide resolved
{
try
{
return CosmosHttpClientCore.CreateSocketsHttpHandlerHelper(gatewayModeMaxConnectionLimit, webProxy, serverCertificateCustomValidationCallback);
}
NaluTripician marked this conversation as resolved.
Show resolved Hide resolved
catch (Exception e)
{
DefaultTrace.TraceError("Failed to create SocketsHttpHandler: {0}", e);
ealsur marked this conversation as resolved.
Show resolved Hide resolved
}
}

return CosmosHttpClientCore.CreateHttpClientHandlerHelper(gatewayModeMaxConnectionLimit, webProxy, serverCertificateCustomValidationCallback);
}

public static HttpMessageHandler CreateSocketsHttpHandlerHelper(
int gatewayModeMaxConnectionLimit,
IWebProxy webProxy,
Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> serverCertificateCustomValidationCallback)
{
// TODO: Remove Reflection when multitargetting is possible
Type socketHandlerType = Type.GetType("System.Net.Http.SocketsHttpHandler, System.Net.Http");
NaluTripician marked this conversation as resolved.
Show resolved Hide resolved

object socketHttpHandler = Activator.CreateInstance(socketHandlerType);

PropertyInfo pooledConnectionLifetimeInfo = socketHandlerType.GetProperty("PooledConnectionLifetime");

//Sets the timeout for unused connections to a random time between 5 minutes and 5 minutes and 30 seconds.
//This is to avoid the issue where a large number of connections are closed at the same time.
NaluTripician marked this conversation as resolved.
Show resolved Hide resolved
Type threadSafeRandomType = Type.GetType("System.Random+ThreadSafeRandom, System.Private.CoreLib");
kirankumarkolli marked this conversation as resolved.
Show resolved Hide resolved

object random = Activator.CreateInstance(threadSafeRandomType);

MethodInfo nextDoubleInfo = threadSafeRandomType.GetMethod("NextDouble");

TimeSpan connectionTimeSpan = TimeSpan.FromMinutes(5) + TimeSpan.FromSeconds(30 * (double)nextDoubleInfo.Invoke(random, null));
pooledConnectionLifetimeInfo.SetValue(socketHttpHandler, connectionTimeSpan);

// Proxy is only set by users and can cause not supported exception on some platforms
if (webProxy != null)
{
PropertyInfo webProxyInfo = socketHandlerType.GetProperty("Proxy");
webProxyInfo.SetValue(socketHttpHandler, webProxy);
}

// https://docs.microsoft.com/en-us/archive/blogs/timomta/controlling-the-number-of-outgoing-connections-from-httpclient-net-core-or-full-framework
try
{
PropertyInfo maxConnectionsPerServerInfo = socketHandlerType.GetProperty("MaxConnectionsPerServer");
maxConnectionsPerServerInfo.SetValue(socketHttpHandler, gatewayModeMaxConnectionLimit);

if (serverCertificateCustomValidationCallback != null)
{
//Get SslOptions Property
PropertyInfo sslOptionsInfo = socketHandlerType.GetProperty("SslOptions");
object sslOptions = sslOptionsInfo.GetValue(socketHttpHandler);

//Set SslOptions Property with custom certificate validation
PropertyInfo remoteCertificateValidationCallbackInfo = sslOptions.GetType().GetProperty("RemoteCertificateValidationCallback");
remoteCertificateValidationCallbackInfo.SetValue(
sslOptions,
new RemoteCertificateValidationCallback((object _, X509Certificate certificate, X509Chain x509Chain, SslPolicyErrors sslPolicyErrors) => serverCertificateCustomValidationCallback(
certificate is { } ? new X509Certificate2(certificate) : null,
x509Chain,
sslPolicyErrors)));
}
}
// MaxConnectionsPerServer is not supported on some platforms.
catch (PlatformNotSupportedException)
ealsur marked this conversation as resolved.
Show resolved Hide resolved
{
}

return (HttpMessageHandler)socketHttpHandler;
}

public static HttpMessageHandler CreateHttpClientHandlerHelper(
int gatewayModeMaxConnectionLimit,
IWebProxy webProxy,
Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> serverCertificateCustomValidationCallback)
{
HttpClientHandler httpClientHandler = new HttpClientHandler();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,6 @@ public async Task Verify_CertificateCallBackGetsCalled_ForTCP_HTTP()
{
await database?.DeleteStreamAsync();
}

}

[TestMethod]
Expand Down Expand Up @@ -878,7 +877,7 @@ public async Task HttpClientConnectionLimitTest()
))
{
CosmosHttpClient cosmosHttpClient = cosmosClient.DocumentClient.httpClient;
HttpClientHandler httpClientHandler = (HttpClientHandler)cosmosHttpClient.HttpMessageHandler;
SocketsHttpHandler httpClientHandler = (SocketsHttpHandler)cosmosHttpClient.HttpMessageHandler;
Assert.AreEqual(gatewayConnectionLimit, httpClientHandler.MaxConnectionsPerServer);

Cosmos.Database database = await cosmosClient.CreateDatabaseAsync(Guid.NewGuid().ToString());
Expand All @@ -895,7 +894,7 @@ public async Task HttpClientConnectionLimitTest()

await Task.WhenAll(creates);

// Clean up the database and container
// Clean up the database
await database.DeleteAsync();
}

Expand All @@ -907,6 +906,20 @@ public async Task HttpClientConnectionLimitTest()
$"Before connections: {JsonConvert.SerializeObject(excludeConnections)}; After connections: {JsonConvert.SerializeObject(afterConnections)}");
}

[TestMethod]
public void PooledConnectionLifetimeTest()
{
//Create Cosmos Client
using CosmosClient cosmosClient = new CosmosClient(
accountEndpoint: "https://localhost:8081",
authKeyOrResourceToken: Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())));

//Assert type of message handler
Type socketHandlerType = Type.GetType("System.Net.Http.SocketsHttpHandler, System.Net.Http");
Type clientMessageHandlerType = cosmosClient.ClientContext.DocumentClient.httpClient.HttpMessageHandler.GetType();
Assert.AreEqual(socketHandlerType, clientMessageHandlerType);
}

public static IReadOnlyList<string> GetActiveConnections()
{
string testPid = Process.GetCurrentProcess().Id.ToString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ public void VerifyHttpClientHandlerIsSet()

CosmosClient cosmosClient = cosmosClientBuilder.Build();
CosmosHttpClient cosmosHttpClient = cosmosClient.DocumentClient.httpClient;
HttpClientHandler handler = (HttpClientHandler)cosmosHttpClient.HttpMessageHandler;
SocketsHttpHandler handler = (SocketsHttpHandler)cosmosHttpClient.HttpMessageHandler;

Assert.IsTrue(object.ReferenceEquals(webProxy, handler.Proxy));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@

namespace Microsoft.Azure.Cosmos.Tests
{
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Azure.Documents;
using System;
using System.Net.Http;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.IO;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos.Tracing;
using Microsoft.Azure.Cosmos.Tracing.TraceData;
using Microsoft.Azure.Documents;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class CosmosHttpClientCoreTests
Expand Down Expand Up @@ -410,6 +412,55 @@ async Task<HttpResponseMessage> sendFunc(HttpRequestMessage request, Cancellatio
Assert.AreEqual(HttpStatusCode.OK, responseMessage.StatusCode);
}

[TestMethod]
public void CreateSocketsHttpHandlerCreatesCorrectValueType()
{
int gatewayLimit = 10;
IWebProxy webProxy = null;
Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> serverCertificateCustomValidationCallback = (certificate2, x509Chain, sslPolicyErrors) => false;

HttpMessageHandler handler = CosmosHttpClientCore.CreateSocketsHttpHandlerHelper(
gatewayLimit,
webProxy,
serverCertificateCustomValidationCallback);

Assert.AreEqual(Type.GetType("System.Net.Http.SocketsHttpHandler, System.Net.Http"), handler.GetType());
SocketsHttpHandler socketsHandler = (SocketsHttpHandler)handler;

Assert.IsTrue(TimeSpan.FromMinutes(5.5) >= socketsHandler.PooledConnectionLifetime);
Assert.IsTrue(TimeSpan.FromMinutes(5) <= socketsHandler.PooledConnectionLifetime);
Assert.AreEqual(webProxy, socketsHandler.Proxy);
Assert.AreEqual(gatewayLimit, socketsHandler.MaxConnectionsPerServer);

//Create cert for test
X509Certificate2 x509Certificate2 = new CertificateRequest("cn=www.test", ECDsa.Create(), HashAlgorithmName.SHA256).CreateSelfSigned(DateTime.Now, DateTime.Now.AddYears(1));
X509Chain x509Chain = new X509Chain();
SslPolicyErrors sslPolicyErrors = new SslPolicyErrors();
Assert.IsFalse(socketsHandler.SslOptions.RemoteCertificateValidationCallback.Invoke(new object(), x509Certificate2, x509Chain, sslPolicyErrors));
}

[TestMethod]
public void CreateHttpClientHandlerCreatesCorrectValueType()
{
int gatewayLimit = 10;
IWebProxy webProxy = null;
Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> serverCertificateCustomValidationCallback = (certificate2, x509Chain, sslPolicyErrors) => false;

HttpMessageHandler handler = CosmosHttpClientCore.CreateHttpClientHandlerHelper(gatewayLimit, webProxy, serverCertificateCustomValidationCallback);

Assert.AreEqual(Type.GetType("System.Net.Http.HttpClientHandler, System.Net.Http"), handler.GetType());
HttpClientHandler clientHandler = (HttpClientHandler)handler;

Assert.AreEqual(webProxy, clientHandler.Proxy);
Assert.AreEqual(gatewayLimit, clientHandler.MaxConnectionsPerServer);

//Create cert for test
X509Certificate2 x509Certificate2 = new CertificateRequest("cn=www.test", ECDsa.Create(), HashAlgorithmName.SHA256).CreateSelfSigned(DateTime.Now, DateTime.Now.AddYears(1));
X509Chain x509Chain = new X509Chain();
SslPolicyErrors sslPolicyErrors = new SslPolicyErrors();
Assert.IsFalse(clientHandler.ServerCertificateCustomValidationCallback.Invoke(new HttpRequestMessage(), x509Certificate2, x509Chain, sslPolicyErrors));
}

private class MockMessageHandler : HttpMessageHandler
{
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendFunc;
Expand Down