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

Allow users to pass in HttpClient to use for file upload operations #3293

Merged
merged 23 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
41 changes: 28 additions & 13 deletions common/src/service/HttpClientHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Amqp.Transport;
using Microsoft.Azure.Devices.Common;
using Microsoft.Azure.Devices.Common.Exceptions;
using Microsoft.Azure.Devices.Common.Extensions;
Expand Down Expand Up @@ -885,26 +887,39 @@ internal static HttpClient CreateDefaultClient(IWebProxy webProxy, Uri baseUri,

internal static HttpMessageHandler CreateDefaultHttpMessageHandler(IWebProxy webProxy, Uri baseUri, int connectionLeaseTimeoutMilliseconds)
{
#pragma warning disable CA2000 // Dispose objects before losing scope (object is returned by this method, so the caller is responsible for disposing it)
#if NETCOREAPP && !NETCOREAPP2_0 && !NETCOREAPP1_0 && !NETCOREAPP1_1
// SocketsHttpHandler is only available in netcoreapp2.1 and onwards
var httpMessageHandler = new SocketsHttpHandler();
httpMessageHandler.SslOptions.EnabledSslProtocols = TlsVersions.Instance.Preferred;
HttpMessageHandler httpMessageHandler = null;

#if NETCOREAPP2_1_OR_GREATER || NET5_0_OR_GREATER
var socketsHandler = new SocketsHttpHandler();
socketsHandler.SslOptions.EnabledSslProtocols = TlsVersions.Instance.Preferred;

if (!TlsVersions.Instance.CertificateRevocationCheck)
{
socketsHandler.SslOptions.CertificateRevocationCheckMode = X509RevocationMode.NoCheck;
}

if (webProxy != DefaultWebProxySettings.Instance)
{
socketsHandler.UseProxy = webProxy != null;
socketsHandler.Proxy = webProxy;
}

httpMessageHandler = socketsHandler;
#else
var httpMessageHandler = new HttpClientHandler();
var httpClientHandler = new HttpClientHandler();
#if !NET451
httpMessageHandler.SslProtocols = TlsVersions.Instance.Preferred;
httpMessageHandler.CheckCertificateRevocationList = TlsVersions.Instance.CertificateRevocationCheck;
httpClientHandler.SslProtocols = TlsVersions.Instance.Preferred;
httpClientHandler.CheckCertificateRevocationList = TlsVersions.Instance.CertificateRevocationCheck;
#endif
#endif
#pragma warning restore CA2000 // Dispose objects before losing scope

if (webProxy != DefaultWebProxySettings.Instance)
{
httpMessageHandler.UseProxy = webProxy != null;
httpMessageHandler.Proxy = webProxy;
httpClientHandler.UseProxy = webProxy != null;
httpClientHandler.Proxy = webProxy;
}

httpMessageHandler = httpClientHandler;
#endif

ServicePointHelpers.SetLimits(httpMessageHandler, baseUri, connectionLeaseTimeoutMilliseconds);

return httpMessageHandler;
Expand Down
7 changes: 7 additions & 0 deletions iothub/device/src/ClientSettings/Http1TransportSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Azure.Devices.Shared;

Expand Down Expand Up @@ -47,5 +48,11 @@ public TransportType GetTransportType()

/// <inheritdoc/>
public IWebProxy Proxy { get; set; }

/// <summary>
/// The handler that this client will instantiate its HttpClient with. If this value
/// is provided, all other options will be ignored in favor of this handler.
/// </summary>
public HttpMessageHandler HttpMessageHandler { get; set; }
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
}
}
34 changes: 17 additions & 17 deletions iothub/device/src/ModuleClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,21 @@ private async Task<MethodResponse> InvokeMethodAsync(Uri uri, MethodRequest meth

try
{
var context = new PipelineContext()
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
{
ProductInfo = new ProductInfo
{
Extra = InternalClient.ProductInfo
}
};

var transportSettings = new Http1TransportSettings();
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
//We need to add the certificate to the httpTransport if DeviceAuthenticationWithX509Certificate
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
if (InternalClient.Certificate != null)
{
transportSettings.ClientCertificate = InternalClient.Certificate;
}

if (customCertificateValidation != null)
{
TlsVersions.Instance.SetLegacyAcceptableVersions();
Expand All @@ -845,30 +860,15 @@ private async Task<MethodResponse> InvokeMethodAsync(Uri uri, MethodRequest meth
SslProtocols = TlsVersions.Instance.Preferred,
};
#else
httpClientHandler = new WebRequestHandler();
transportSettings.HttpMessageHandler = new WebRequestHandler();
((WebRequestHandler)httpClientHandler).ServerCertificateValidationCallback = (sender, certificate, chain, errors) =>
{
return customCertificateValidation(sender, certificate, chain, errors);
};
#endif
}

var context = new PipelineContext()
{
ProductInfo = new ProductInfo
{
Extra = InternalClient.ProductInfo
}
};

var transportSettings = new Http1TransportSettings();
//We need to add the certificate to the httpTransport if DeviceAuthenticationWithX509Certificate
if (InternalClient.Certificate != null)
{
transportSettings.ClientCertificate = InternalClient.Certificate;
}

using var httpTransport = new HttpTransportHandler(context, InternalClient.IotHubConnectionString, transportSettings, httpClientHandler);
using var httpTransport = new HttpTransportHandler(context, InternalClient.IotHubConnectionString, transportSettings);
var methodInvokeRequest = new MethodInvokeRequest(methodRequest.Name, methodRequest.DataAsJson, methodRequest.ResponseTimeout, methodRequest.ConnectionTimeout);
MethodInvokeResponse result = await httpTransport.InvokeMethodAsync(methodInvokeRequest, uri, cancellationToken).ConfigureAwait(false);

Expand Down
119 changes: 66 additions & 53 deletions iothub/device/src/Transport/Http/HttpClientHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,91 +38,104 @@ internal sealed class HttpClientHelper : IHttpClientHelper
private readonly IReadOnlyDictionary<HttpStatusCode, Func<HttpResponseMessage, Task<Exception>>> _defaultErrorMapping;
private readonly bool _usingX509ClientCert;
private HttpClient _httpClientObj;
private HttpClientHandler _httpClientHandler;
private HttpMessageHandler _httpMessageHandler;
private bool _isDisposed;
private readonly ProductInfo _productInfo;
private readonly bool _isClientPrimaryTransportHandler;

// These default values are consistent with Azure.Core default values:
// https://github.com/Azure/azure-sdk-for-net/blob/7e3cf643977591e9041f4c628fd4d28237398e0b/sdk/core/Azure.Core/src/Pipeline/ServicePointHelpers.cs#L28
private const int DefaultMaxConnectionsPerServer = 50;

// How long, in milliseconds, a given cached TCP connection created by this client's HTTP layer will live before being closed.
// If this value is set to any negative value, the connection lease will be infinite. If this value is set to 0, then the TCP connection will close after
// each HTTP request and a new TCP connection will be opened upon the next request.
//
// By closing cached TCP connections and opening a new one upon the next request, the underlying HTTP client has a chance to do a DNS lookup
// to validate that it will send the requests to the correct IP address. While it is atypical for a given IoT hub to change its IP address, it does
// happen when a given IoT hub fails over into a different region.
//
// This default value is consistent with the default value used in Azure.Core
// https://github.com/Azure/azure-sdk-for-net/blob/7e3cf643977591e9041f4c628fd4d28237398e0b/sdk/core/Azure.Core/src/Pipeline/ServicePointHelpers.cs#L29
private static readonly TimeSpan s_defaultConnectionLeaseTimeout = TimeSpan.FromMinutes(5);

public HttpClientHelper(
Uri baseAddress,
IAuthorizationProvider authenticationHeaderProvider,
IDictionary<HttpStatusCode, Func<HttpResponseMessage, Task<Exception>>> defaultErrorMapping,
TimeSpan timeout,
Action<HttpClient> preRequestActionForAllRequests,
X509Certificate2 clientCert,
HttpClientHandler httpClientHandler,
Http1TransportSettings transportSettings,
ProductInfo productInfo,
IWebProxy proxy,
bool isClientPrimaryTransportHandler = false)
{
_baseAddress = baseAddress;
_authenticationHeaderProvider = authenticationHeaderProvider;
_defaultErrorMapping = new ReadOnlyDictionary<HttpStatusCode, Func<HttpResponseMessage, Task<Exception>>>(defaultErrorMapping);
_httpMessageHandler = transportSettings.HttpMessageHandler;

if (_httpMessageHandler == null)
{
#if NET451
TlsVersions.Instance.SetLegacyAcceptableVersions();
TlsVersions.Instance.SetLegacyAcceptableVersions();

_httpClientHandler = httpClientHandler;
var webRequestHandler = new WebRequestHandler();

if (clientCert != null)
{
if (_httpClientHandler == null)
if (transportSettings.ClientCertificate != null)
{
webRequestHandler.ClientCertificates.Add(transportSettings.ClientCertificate);
_usingX509ClientCert = true;
}
else
{
_httpClientHandler = new WebRequestHandler();
_usingX509ClientCert = false;
}

(_httpClientHandler as WebRequestHandler).ClientCertificates.Add(clientCert);
_usingX509ClientCert = true;
}
if (proxy != DefaultWebProxySettings.Instance)
{
webRequestHandler.UseProxy = proxy != null;
webRequestHandler.Proxy = proxy;
}

if (proxy != DefaultWebProxySettings.Instance)
{
if (_httpClientHandler == null)
_httpMessageHandler = webRequestHandler;
#elif NET5_0_OR_GREATER
var socketsHandler = new SocketsHttpHandler();
socketsHandler.SslOptions.EnabledSslProtocols = TlsVersions.Instance.Preferred;

if (!TlsVersions.Instance.CertificateRevocationCheck)
{
_httpClientHandler = new WebRequestHandler();
socketsHandler.SslOptions.CertificateRevocationCheckMode = X509RevocationMode.NoCheck;
}

_httpClientHandler.UseProxy = (proxy != null);
_httpClientHandler.Proxy = proxy;
}
if (transportSettings.ClientCertificate != null)
{
socketsHandler.SslOptions.ClientCertificates.Add(transportSettings.ClientCertificate);
}
else
{
_usingX509ClientCert = false;
}

if (proxy != DefaultWebProxySettings.Instance)
{
socketsHandler.UseProxy = proxy != null;
socketsHandler.Proxy = proxy;
}

_httpMessageHandler = socketsHandler;
#else
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
var httpClientHandler = new HttpClientHandler();
httpClientHandler.SslProtocols = TlsVersions.Instance.Preferred;
httpClientHandler.CheckCertificateRevocationList = TlsVersions.Instance.CertificateRevocationCheck;

_httpClientHandler = httpClientHandler ?? new HttpClientHandler();
_httpClientHandler.SslProtocols = TlsVersions.Instance.Preferred;
_httpClientHandler.CheckCertificateRevocationList = TlsVersions.Instance.CertificateRevocationCheck;
if (transportSettings.ClientCertificate != null)
{
httpClientHandler.ClientCertificates.Add(transportSettings.ClientCertificate);
_usingX509ClientCert = true;
}
else
{
_usingX509ClientCert = false;
}

if (clientCert != null)
{
_httpClientHandler.ClientCertificates.Add(clientCert);
_usingX509ClientCert = true;
}
if (proxy != DefaultWebProxySettings.Instance)
drwill-ms marked this conversation as resolved.
Show resolved Hide resolved
{
httpClientHandler.UseProxy = proxy != null;
httpClientHandler.Proxy = proxy;
}

if (proxy != DefaultWebProxySettings.Instance)
{
_httpClientHandler.UseProxy = proxy != null;
_httpClientHandler.Proxy = proxy;
}
_httpMessageHandler = httpClientHandler;
#endif

ServicePointHelpers.SetLimits(_httpClientHandler, _baseAddress);
ServicePointHelpers.SetLimits(_httpMessageHandler, _baseAddress);
}

_httpClientObj = _httpClientHandler != null ? new HttpClient(_httpClientHandler) : new HttpClient();
_httpClientObj = new HttpClient(_httpMessageHandler);
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved

_httpClientObj.BaseAddress = _baseAddress;
_httpClientObj.Timeout = timeout;
Expand Down Expand Up @@ -558,10 +571,10 @@ public void Dispose()

// HttpClientHandler that is used to create HttpClient will automatically be disposed when HttpClient is disposed
// But in case the client handler didn't end up being used by the HttpClient, we explicitly dispose it here.
if (_httpClientHandler != null)
if (_httpMessageHandler != null)
{
_httpClientHandler?.Dispose();
_httpClientHandler = null;
_httpMessageHandler?.Dispose();
_httpMessageHandler = null;
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
}

// The associated TokenRefresher should be disposed by the http client helper only when the http client
Expand Down
4 changes: 1 addition & 3 deletions iothub/device/src/Transport/Http/HttpTransportHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ internal HttpTransportHandler(
PipelineContext context,
IotHubConnectionString iotHubConnectionString,
Http1TransportSettings transportSettings,
HttpClientHandler httpClientHandler = null,
bool isClientPrimaryTransportHandler = false)
: base(context, transportSettings)
{
Expand All @@ -77,8 +76,7 @@ internal HttpTransportHandler(
ExceptionHandlingHelper.GetDefaultErrorMapping(),
s_defaultOperationTimeout,
null,
transportSettings.ClientCertificate,
httpClientHandler,
transportSettings,
productInfo,
transportSettings.Proxy,
isClientPrimaryTransportHandler);
Expand Down
7 changes: 5 additions & 2 deletions iothub/device/src/Transport/Http/ServicePointHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ public static void SetLimits(HttpMessageHandler messageHandler, Uri baseUri, int
ServicePoint servicePoint = ServicePointManager.FindServicePoint(baseUri);
servicePoint.ConnectionLeaseTimeout = connectionLeaseTimeoutMilliseconds;
break;
#if NETCOREAPP2_1_OR_GREATER || NET5_0_OR_GREATER
// SocketsHttpHandler is only available in netcore2.1 and onwards
#if NET5_0_OR_GREATER
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
// SocketsHttpHandler is only available in netcore2.1 and onwards and .NET 5 and onwards.
// This library does not target netcore2.1+ though, so there is no way to set these timeouts
// within this library for netcore2.1+ cases though. Users will need to pass in this handler
// themselves in netcore2.1+ cases
case SocketsHttpHandler socketsHttpHandler:
socketsHttpHandler.MaxConnectionsPerServer = DefaultMaxConnectionsPerServer;
socketsHttpHandler.PooledConnectionLifetime = TimeSpan.FromMilliseconds(connectionLeaseTimeoutMilliseconds);
Expand Down