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 14 commits
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
54 changes: 54 additions & 0 deletions e2e/test/iothub/FileUploadE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Azure.Devices.Client;
using Microsoft.Azure.Devices.Client.Transport;
using Microsoft.Azure.Devices.E2ETests.Helpers;
Expand Down Expand Up @@ -127,6 +130,49 @@ public async Task FileUpload_SmallFile_Http_GranularSteps_Proxy()
await UploadFileGranularAsync(fileStreamSource, filename, fileUploadTransportSettings).ConfigureAwait(false);
}

// File upload requests can be configured to use a user-provided HttpClient
[TestMethod]
public async Task FileUpload_UsesCustomHttpClient()
{
using TestDevice testDevice =
await TestDevice.GetTestDeviceAsync(_devicePrefix, TestDeviceType.Sasl).ConfigureAwait(false);

using var CustomHttpMessageHandler = new CustomHttpMessageHandler();
var fileUploadSettings = new Http1TransportSettings()
{
// This HttpClient should throw a NotImplementedException whenever it makes an HTTP
// request
HttpClient = new HttpClient(CustomHttpMessageHandler),
};

var clientOptions = new ClientOptions()
{
FileUploadTransportSettings = fileUploadSettings,
};

using var deviceClient =
DeviceClient.CreateFromConnectionString(testDevice.ConnectionString, clientOptions);

await deviceClient.OpenAsync().ConfigureAwait(false);

var request = new FileUploadSasUriRequest()
{
BlobName = "someBlobName",
};

try
{
await deviceClient.GetFileUploadSasUriAsync(request).ConfigureAwait(false);
}
catch (Exception e)
{
// The custom HttpMessageHandler throws NotImplementedException when making any Http request.
// So if this exception is not thrown, then the client didn't use the custom HttpMessageHandler
e.InnerException.Should().BeOfType(typeof(NotImplementedException),
"The provided custom HttpMessageHandler throws NotImplementedException when making any HTTP request");
}
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
}

private async Task UploadFileGranularAsync(Stream source, string filename, Http1TransportSettings fileUploadTransportSettings, bool useX509auth = false)
{
using TestDevice testDevice = await TestDevice.GetTestDeviceAsync(
Expand Down Expand Up @@ -265,6 +311,14 @@ private static async Task<string> GetTestFileNameAsync(int fileSize)
return filePath;
}

private class CustomHttpMessageHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
throw new NotImplementedException("Deliberately not implemented for test purposes");
}
}

[ClassCleanup]
public static void CleanupCertificates()
{
Expand Down
41 changes: 41 additions & 0 deletions e2e/test/iothub/messaging/MessageSendE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Azure.Devices.Client;
Expand Down Expand Up @@ -416,6 +418,37 @@ public async Task Message_DeviceSendMessageWayOverAllowedSize_Http()
await SendSingleMessage(TestDeviceType.Sasl, Client.TransportType.Http1, OverlyExceedAllowedMessageSizeInBytes).ConfigureAwait(false);
}

[TestMethod]
[Timeout(TestTimeoutMilliseconds)]
public async Task Message_DeviceSendSingleWithCustomHttpClient_Http()
{
using var httpMessageHandler = new CustomHttpMessageHandler();
var httpTransportSettings = new Http1TransportSettings()
{
HttpClient = new HttpClient(httpMessageHandler)
};
var transportSettings = new ITransportSettings[] { httpTransportSettings };

using TestDevice testDevice =
await TestDevice.GetTestDeviceAsync(_devicePrefix).ConfigureAwait(false);

using DeviceClient deviceClient = testDevice.CreateDeviceClient(transportSettings);

await deviceClient.OpenAsync().ConfigureAwait(false);
using var message = new Client.Message();
try
{
await deviceClient.SendEventAsync(message).ConfigureAwait(false);
}
catch (Exception e)
{
// The custom HttpMessageHandler throws NotImplementedException when making any Http request.
// So if this exception is not thrown, then the client didn't use the custom HttpMessageHandler
e.InnerException.Should().BeOfType(typeof(NotImplementedException),
"The provided custom HttpMessageHandler throws NotImplementedException when making any HTTP request");
}
}

private async Task SendSingleMessage(TestDeviceType type, Client.TransportType transport, int messageSize = 0)
{
using TestDevice testDevice = await TestDevice.GetTestDeviceAsync(_devicePrefix, type).ConfigureAwait(false);
Expand Down Expand Up @@ -532,5 +565,13 @@ public static Client.Message ComposeD2cTestMessageOfSpecifiedSize(int messageSiz

return message;
}

private class CustomHttpMessageHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
throw new NotImplementedException("Deliberately not implemented for test purposes");
}
}
}
}
16 changes: 16 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,20 @@ public TransportType GetTransportType()

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

/// <summary>
/// The HTTP client to use for all HTTP operations.
/// </summary>
/// <remarks>
/// If not provided, an HTTP client will be created for you based on the other settings provided.
/// <para>
/// This HTTP client instance will be disposed when the device/module client using it is disposed.
/// </para>
/// <para>
/// If provided, all other HTTP-specific settings (such as proxy, SSL protocols, and certificate revocation check)
/// on this class will be ignored and must be specified on this HttpClient instance.
/// </para>
/// </remarks>
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
public HttpClient HttpClient { get; set; }
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
}
}
44 changes: 23 additions & 21 deletions iothub/device/src/ModuleClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -829,54 +829,56 @@ private async Task<MethodResponse> InvokeMethodAsync(Uri uri, MethodRequest meth
}
methodRequest.ThrowIfNull(nameof(methodRequest));

HttpClientHandler httpClientHandler = null;
HttpMessageHandler httpMessageHandler = null;
Func<object, X509Certificate, X509Chain, SslPolicyErrors, bool> customCertificateValidation = _certValidator.GetCustomCertificateValidation();

try
{
var context = new PipelineContext
{
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
if (InternalClient.Certificate != null)
{
transportSettings.ClientCertificate = InternalClient.Certificate;
}

if (customCertificateValidation != null)
{
TlsVersions.Instance.SetLegacyAcceptableVersions();

#if !NET451
httpClientHandler = new HttpClientHandler
httpMessageHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = customCertificateValidation,
SslProtocols = TlsVersions.Instance.Preferred,
};
#else
httpClientHandler = new WebRequestHandler();
((WebRequestHandler)httpClientHandler).ServerCertificateValidationCallback = (sender, certificate, chain, errors) =>
httpMessageHandler = new WebRequestHandler();
((WebRequestHandler)httpMessageHandler).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;
transportSettings.HttpClient = new HttpClient(httpMessageHandler, true);
#endif
}

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);

return new MethodResponse(Encoding.UTF8.GetBytes(result.GetPayloadAsJson()), result.Status);
}
finally
{
httpClientHandler?.Dispose();
httpMessageHandler?.Dispose();
timtay-microsoft marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
Loading