diff --git a/iothub/device/src/Edge/EdgeModuleClientHelper.cs b/iothub/device/src/Edge/EdgeModuleClientHelper.cs index 58b828d9e6..1b6fe6fd03 100644 --- a/iothub/device/src/Edge/EdgeModuleClientHelper.cs +++ b/iothub/device/src/Edge/EdgeModuleClientHelper.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; +using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices.Client.HsmAuthentication; using static System.Runtime.InteropServices.RuntimeInformation; @@ -84,7 +85,8 @@ internal static IotHubConnectionCredentials CreateIotHubConnectionCredentialsFro internal static async Task CreateCertificateValidatorFromEnvironmentAsync( ITrustBundleProvider trustBundleProvider, - IotHubClientOptions options) + IotHubClientOptions options, + CancellationToken cancellationToken) { Debug.Assert(options != null); @@ -122,7 +124,7 @@ internal static async Task CreateCertificateValidatorFrom if (!string.IsNullOrEmpty(gateway)) { IList certs = await trustBundleProvider - .GetTrustBundleAsync(new Uri(edgeWorkloadUri), EdgeHsmApiVersion) + .GetTrustBundleAsync(new Uri(edgeWorkloadUri), EdgeHsmApiVersion, cancellationToken) .ConfigureAwait(false); certificateValidator = CreateCertificateValidator(certs, options); } diff --git a/iothub/device/src/Edge/ITrustBundleProvider.cs b/iothub/device/src/Edge/ITrustBundleProvider.cs index 978a89ba26..83b9ffa737 100644 --- a/iothub/device/src/Edge/ITrustBundleProvider.cs +++ b/iothub/device/src/Edge/ITrustBundleProvider.cs @@ -4,12 +4,13 @@ using System; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.Azure.Devices.Client { internal interface ITrustBundleProvider { - Task> GetTrustBundleAsync(Uri providerUri, string defaultApiVersion); + Task> GetTrustBundleAsync(Uri providerUri, string defaultApiVersion, CancellationToken cancellationToken); } } diff --git a/iothub/device/src/Edge/TrustBundleProvider.cs b/iothub/device/src/Edge/TrustBundleProvider.cs index 61eafa5c75..0568ccd97a 100644 --- a/iothub/device/src/Edge/TrustBundleProvider.cs +++ b/iothub/device/src/Edge/TrustBundleProvider.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices.Client.HsmAuthentication; using Microsoft.Azure.Devices.Client.HsmAuthentication.GeneratedCode; @@ -17,7 +18,7 @@ internal sealed class TrustBundleProvider : ITrustBundleProvider { private static readonly IIotHubClientRetryPolicy s_retryPolicy = new IotHubClientExponentialBackoffRetryPolicy(3, TimeSpan.FromSeconds(30)); - public async Task> GetTrustBundleAsync(Uri providerUri, string apiVersion) + public async Task> GetTrustBundleAsync(Uri providerUri, string apiVersion, CancellationToken cancellationToken) { try { @@ -26,7 +27,7 @@ public async Task> GetTrustBundleAsync(Uri providerUri, { BaseUrl = HttpClientHelper.GetBaseUri(providerUri) }; - TrustBundleResponse response = await GetTrustBundleWithRetryAsync(hsmHttpClient, apiVersion).ConfigureAwait(false); + TrustBundleResponse response = await GetTrustBundleWithRetryAsync(hsmHttpClient, apiVersion, cancellationToken).ConfigureAwait(false); IList certs = ParseCertificates(response.Certificate); return certs; @@ -45,13 +46,15 @@ public async Task> GetTrustBundleAsync(Uri providerUri, private static async Task GetTrustBundleWithRetryAsync( HttpHsmClient hsmHttpClient, - string apiVersion) + string apiVersion, + CancellationToken cancellationToken) { var transientRetryPolicy = new RetryHandler(s_retryPolicy); return await transientRetryPolicy .RunWithRetryAsync( () => hsmHttpClient.TrustBundleAsync(apiVersion), - (Exception ex) => ex is SwaggerException se && se.StatusCode >= 500) + (Exception ex) => ex is SwaggerException se && se.StatusCode >= 500, + cancellationToken) .ConfigureAwait(false); } diff --git a/iothub/device/src/IotHubModuleClient.cs b/iothub/device/src/IotHubModuleClient.cs index 36ff1b6ce8..771c93bbfc 100644 --- a/iothub/device/src/IotHubModuleClient.cs +++ b/iothub/device/src/IotHubModuleClient.cs @@ -21,6 +21,9 @@ public class IotHubModuleClient : IotHubBaseClient private const string ModuleMethodUriFormat = "/twins/{0}/modules/{1}/methods?" + ClientApiVersionHelper.ApiVersionQueryStringLatest; private const string DeviceMethodUriFormat = "/twins/{0}/methods?" + ClientApiVersionHelper.ApiVersionQueryStringLatest; + private const string IotDeviceModuleMethodInvokeErrorMessage = "This API call is relevant only for IoT Edge modules. Please make sure your client is initialized correctly with a gateway hostname. " + + "For subscribing to IoT device module direct method invocations, see SetDirectMethodCallbackAsync(...)."; + /// /// Creates a disposable client from the specified connection string. /// @@ -93,7 +96,7 @@ internal IotHubModuleClient(IotHubConnectionCredentials iotHubConnectionCredenti if (Logging.IsEnabled) Logging.CreateClient( this, - $"HostName={IotHubConnectionCredentials.HostName};DeviceId={IotHubConnectionCredentials.DeviceId};ModuleId={IotHubConnectionCredentials.ModuleId}", + $"HostName={IotHubConnectionCredentials.HostName};DeviceId={IotHubConnectionCredentials.DeviceId};ModuleId={IotHubConnectionCredentials.ModuleId};isEdgeModule={IotHubConnectionCredentials.IsEdgeModule}", _clientOptions); } @@ -101,21 +104,26 @@ internal IotHubModuleClient(IotHubConnectionCredentials iotHubConnectionCredenti /// Creates a disposable IotHubModuleClient instance in an IoT Edge deployment based on environment variables. /// /// The options that allow configuration of the module client instance during initialization. + /// A cancellation token to cancel the operation. /// A disposable client instance. /// The required environmental variables were missing. Check the exception thrown for additional details. + /// The operation has been canceled. /// /// /// await using var client = await IotHubModuleClient.CreateFromEnvironmentAsync(new IotHubClientOptions(new IotHubClientMqttSettings(IotHubClientTransportProtocol.WebSocket))); /// /// - public static async Task CreateFromEnvironmentAsync(IotHubClientOptions options = default) + public static async Task CreateFromEnvironmentAsync(IotHubClientOptions options = default, CancellationToken cancellationToken = default) { IotHubClientOptions clientOptions = options != null ? options.Clone() : new(); IotHubConnectionCredentials iotHubConnectionCredentials = EdgeModuleClientHelper.CreateIotHubConnectionCredentialsFromEnvironment(); - ICertificateValidator certificateValidator = await EdgeModuleClientHelper.CreateCertificateValidatorFromEnvironmentAsync(new TrustBundleProvider(), clientOptions); + ICertificateValidator certificateValidator = await EdgeModuleClientHelper.CreateCertificateValidatorFromEnvironmentAsync( + new TrustBundleProvider(), + clientOptions, + cancellationToken); return new IotHubModuleClient(iotHubConnectionCredentials, options, certificateValidator); } @@ -236,6 +244,7 @@ public async Task SendMessagesToRouteAsync(string outputName, IEnumerableThe result of the method invocation. /// or is null. /// The client instance is not already open. + /// An IoT device module is used to invoke this API. /// The operation has been canceled. /// An error occured when communicating with IoT hub service. /// The client has been disposed. @@ -248,6 +257,12 @@ public Task InvokeMethodAsync(string deviceId, EdgeModuleD { Argument.AssertNotNullOrWhiteSpace(deviceId, nameof(deviceId)); Argument.AssertNotNull(methodRequest, nameof(methodRequest)); + + if (!IotHubConnectionCredentials.IsEdgeModule) + { + throw new InvalidOperationException(IotDeviceModuleMethodInvokeErrorMessage); + } + cancellationToken.ThrowIfCancellationRequested(); return InvokeMethodAsync(GetDeviceMethodUri(deviceId), methodRequest, cancellationToken); @@ -270,6 +285,7 @@ public Task InvokeMethodAsync(string deviceId, EdgeModuleD /// The result of the method invocation. /// , or is null. /// The client instance is not already open. + /// An IoT device module is used to invoke this API. /// The operation has been canceled. /// An error occured when communicating with IoT hub service. /// The client has been disposed. @@ -283,6 +299,12 @@ public Task InvokeMethodAsync(string deviceId, string modu Argument.AssertNotNullOrWhiteSpace(deviceId, nameof(deviceId)); Argument.AssertNotNullOrWhiteSpace(moduleId, nameof(moduleId)); Argument.AssertNotNull(methodRequest, nameof(methodRequest)); + + if (!IotHubConnectionCredentials.IsEdgeModule) + { + throw new InvalidOperationException(IotDeviceModuleMethodInvokeErrorMessage); + } + cancellationToken.ThrowIfCancellationRequested(); return InvokeMethodAsync(GetModuleMethodUri(deviceId, moduleId), methodRequest, cancellationToken); diff --git a/iothub/device/tests/Edge/EdgeModuleClientHelperTest.cs b/iothub/device/tests/Edge/EdgeModuleClientHelperTest.cs index def964b37e..c55c194f57 100644 --- a/iothub/device/tests/Edge/EdgeModuleClientHelperTest.cs +++ b/iothub/device/tests/Edge/EdgeModuleClientHelperTest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -186,11 +187,11 @@ public async Task TestCreate_FromEnvironment_SetAmqpTransportSettings_ShouldCrea var options = new IotHubClientOptions(settings); var trustBundle = new Mock(); trustBundle - .Setup(x => x.GetTrustBundleAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.GetTrustBundleAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => Task.FromResult>(new List(0))); IotHubConnectionCredentials creds = EdgeModuleClientHelper.CreateIotHubConnectionCredentialsFromEnvironment(); ICertificateValidator certValidator = await EdgeModuleClientHelper - .CreateCertificateValidatorFromEnvironmentAsync(trustBundle.Object, options) + .CreateCertificateValidatorFromEnvironmentAsync(trustBundle.Object, options, CancellationToken.None) .ConfigureAwait(false); // act @@ -223,11 +224,11 @@ public async Task TestCreate_FromEnvironment_SetMqttTransportSettings_ShouldCrea var options = new IotHubClientOptions(settings); var trustBundle = new Mock(); trustBundle - .Setup(x => x.GetTrustBundleAsync(It.IsAny(), It.IsAny())) + .Setup(x => x.GetTrustBundleAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(() => Task.FromResult>(new List(0))); IotHubConnectionCredentials creds = EdgeModuleClientHelper.CreateIotHubConnectionCredentialsFromEnvironment(); ICertificateValidator certValidator = await EdgeModuleClientHelper - .CreateCertificateValidatorFromEnvironmentAsync(trustBundle.Object, options) + .CreateCertificateValidatorFromEnvironmentAsync(trustBundle.Object, options, CancellationToken.None) .ConfigureAwait(false); // act diff --git a/iothub/device/tests/IotHubModuleClientDisposeTests.cs b/iothub/device/tests/IotHubModuleClientDisposeTests.cs index b860a67758..72cb6e3031 100644 --- a/iothub/device/tests/IotHubModuleClientDisposeTests.cs +++ b/iothub/device/tests/IotHubModuleClientDisposeTests.cs @@ -27,7 +27,7 @@ public static async Task ClassInitializeAsync(TestContext context) string testSharedAccessKey = Convert.ToBase64String(rndBytes); var csBuilder = new IotHubConnectionString( "contoso.azure-devices.net", - null, + "my-gateway", "deviceId", "moduleId", null, diff --git a/iothub/device/tests/IotHubModuleClientTests.cs b/iothub/device/tests/IotHubModuleClientTests.cs index 160c522972..9bd9d0793d 100644 --- a/iothub/device/tests/IotHubModuleClientTests.cs +++ b/iothub/device/tests/IotHubModuleClientTests.cs @@ -27,7 +27,7 @@ public class IotHubModuleClientTests private const string FakeSharedAccessKey = "dGVzdFN0cmluZzQ="; private static readonly string s_connectionStringWithModuleId = $"GatewayHostName={FakeGatewayHostName};HostName={FakeHostName};DeviceId={DeviceId};ModuleId={ModuleId};SharedAccessKey={FakeSharedAccessKey}"; private static readonly string s_connectionStringWithoutModuleId = $"GatewayHostName={FakeGatewayHostName};HostName={FakeHostName};DeviceId={DeviceId};SharedAccessKey={FakeSharedAccessKey}"; - private static readonly string s_fakeConnectionString = $"HostName={FakeHostName};SharedAccessKeyName=AllAccessKey;DeviceId={DeviceId};ModuleId={ModuleId};SharedAccessKey={FakeSharedAccessKey}"; + private static readonly string s_fakeConnectionStringWithoutGateway = $"HostName={FakeHostName};SharedAccessKeyName=AllAccessKey;DeviceId={DeviceId};ModuleId={ModuleId};SharedAccessKey={FakeSharedAccessKey}"; public const string NoModuleTwinJson = "{ \"maxConnections\": 10 }"; @@ -60,7 +60,7 @@ public async Task IotHubModuleClient_CreateFromConnectionString_WithNoModuleId_T [TestMethod] public async Task IotHubModuleClient_CreateFromConnectionString_NoTransportSettings_Succeeds() { - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); moduleClient.Should().NotBeNull(); } @@ -83,7 +83,7 @@ public async Task IotHubModuleClient_CreateFromConnectionString_WithClientOption }; // act - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString, clientOptions); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway, clientOptions); // assert moduleClient.Should().NotBeNull(); @@ -94,7 +94,7 @@ public async Task IotHubModuleClient_SetReceiveCallbackAsync_SetCallback_Mqtt() { // arrange var options = new IotHubClientOptions(new IotHubClientMqttSettings()); - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString, options); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway, options); var innerHandler = new Mock(); moduleClient.InnerHandler = innerHandler.Object; @@ -115,7 +115,7 @@ public async Task IotHubModuleClient_SetReceiveCallbackAsync_SetCallback_Mqtt() public async Task IotHubModuleClient_OnReceiveEventMessageCalled_DefaultCallbackCalled() { // arrange - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); var innerHandler = new Mock(); moduleClient.InnerHandler = innerHandler.Object; @@ -146,7 +146,7 @@ public async Task IotHubModuleClient_SendMessagesToRoute_ThrowsSocketExceptionAs { // arrange string messageId = Guid.NewGuid().ToString(); - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); var innerHandler = new Mock(); // This is used to simulate the transport level socket exception innerHandler @@ -170,7 +170,7 @@ public async Task IotHubModuleClient_SendMessagesToRoute_ThrowsWebSocketExceptio { // arrange string messageId = Guid.NewGuid().ToString(); - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); var innerHandler = new Mock(); // This is used to simulate the transport level websocket exception innerHandler @@ -193,7 +193,7 @@ public async Task IotHubModuleClient_SendMessagesToRoute_ThrowsWebSocketExceptio public async Task IotHubModuleClient_InvokeMethodAsync_EdgeDevice_NullMethodRequest_Throws_NullException() { // arrange - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); var DirectMethodRequest = new EdgeModuleDirectMethodRequest("TestMethodName") { PayloadConvention = DefaultPayloadConvention.Instance, @@ -210,7 +210,7 @@ public async Task IotHubModuleClient_InvokeMethodAsync_EdgeDevice_NullMethodRequ public async Task IotHubModuleClient_InvokeMethodAsync_EdgeModule_NullMethodRequest_Throws_NullException() { // arrange - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); var DirectMethodRequest = new EdgeModuleDirectMethodRequest("TestMethodName") { PayloadConvention = DefaultPayloadConvention.Instance, @@ -227,7 +227,7 @@ public async Task IotHubModuleClient_InvokeMethodAsync_EdgeModule_NullMethodRequ public async Task IotHubModuleClient_InvokeMethodAsync_WithoutExplicitOpenAsync_Throws_InvalidOperationException() { // arrange - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); var DirectMethodRequest = new EdgeModuleDirectMethodRequest("TestMethodName") { PayloadConvention = DefaultPayloadConvention.Instance, @@ -245,7 +245,7 @@ public async Task MessageIdDefaultNotSet_SendEventDoesNotSetMessageId() { // arrange string messageId = Guid.NewGuid().ToString(); - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); var innerHandler = new Mock(); innerHandler @@ -272,7 +272,7 @@ public async Task MessageIdDefaultNotSet_SendEventBatchDoesNotSetMessageId() { // arrange string messageId = Guid.NewGuid().ToString(); - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString, new IotHubClientOptions(new IotHubClientAmqpSettings())); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway, new IotHubClientOptions(new IotHubClientAmqpSettings())); var innerHandler = new Mock(); innerHandler @@ -298,7 +298,7 @@ public async Task MessageIdDefaultNotSet_SendEventBatchDoesNotSetMessageId() public async Task IotHubModuleClient_SendTelemetryAsync_WithoutExplicitOpenAsync_ThrowsInvalidOperationException() { // arrange - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); // act Func act = async () => await moduleClient.SendTelemetryAsync(new TelemetryMessage()); @@ -311,7 +311,7 @@ public async Task IotHubModuleClient_SendTelemetryAsync_WithoutExplicitOpenAsync public async Task IotHubModuleClient_SendTelemetryBatchAsync_WithoutExplicitOpenAsync_ThrowsInvalidOperationException() { // arrange - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); // act Func act = async () => await moduleClient.SendTelemetryAsync(new List { new TelemetryMessage(), new TelemetryMessage() }); @@ -324,7 +324,7 @@ public async Task IotHubModuleClient_SendTelemetryBatchAsync_WithoutExplicitOpen public async Task IotHubModuleClient_SetMethodHandlerUnset_WhenNoMethodHandler() { // arrange - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); var innerHandler = new Mock(); moduleClient.InnerHandler = innerHandler.Object; @@ -342,7 +342,7 @@ public async Task IotHubModuleClient_SetMethodHandlerUnset_WhenNoMethodHandler() public async Task IotHubModuleClient_SetMethodHandler_UnsetLastMethodHandler() { // arrange - await using var moduleClient = new IotHubModuleClient(s_fakeConnectionString); + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); var innerHandler = new Mock(); moduleClient.InnerHandler = innerHandler.Object; @@ -398,6 +398,32 @@ public async Task IotHubModuleClient_SetMethodHandler_UnsetLastMethodHandler() methodCallbackCalled.Should().BeFalse(); } + [TestMethod] + public async Task IotHubModuleClient_InvokeMethodAsync_ToEdgeDevice_WithoutGatewayHostname_ThrowsInvalidOperationException() + { + // arrange + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); + + // act + Func act = async () => await moduleClient.InvokeMethodAsync(DeviceId, new EdgeModuleDirectMethodRequest("FakeMethodName"), CancellationToken.None); + + // assert + await act.Should().ThrowAsync(); + } + + [TestMethod] + public async Task IotHubModuleClient_InvokeMethodAsync_ToEdgeModule_WithoutGatewayHostname_ThrowsInvalidOperationException() + { + // arrange + await using var moduleClient = new IotHubModuleClient(s_fakeConnectionStringWithoutGateway); + + // act + Func act = async () => await moduleClient.InvokeMethodAsync(DeviceId, ModuleId, new EdgeModuleDirectMethodRequest("FakeMethodName"), CancellationToken.None); + + // assert + await act.Should().ThrowAsync(); + } + private class CustomDirectMethodPayload { [JsonProperty("grade")]