csharp/Azure-Samples/digital-twins-samples/DeviceSimulator/DeviceSimulator/IotHubConnection.cs

IotHubConnection.cs
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using System.Globalization;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Azure.Amqp;
using Microsoft.Azure.Amqp.Framing;
using Microsoft.Azure.Amqp.Sasl;
using Microsoft.Azure.Amqp.Transport;
using Microsoft.Azure.Devices;

namespace DeviceSimulator
{
    /// 
    /// A transient connection to the IoT Hub service, providing a set of utility-type operations that
    /// span the service client and device roles.
    /// 
    public static clast IotHubConnection
    {
        /// The regular expression used to parse the Event Hub name from the IoT Hub redirection address.
        private static readonly Regex EventHubNameExpression = new Regex(
            @":\d+\/(?.*)\/\$management",
            RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant);

        /// 
        /// Requests connection string for the built-in Event Hubs messaging endpoint of the astociated IoT Hub.
        /// 
        /// The connection string for the IoT Hub instance to request the Event Hubs connection string from.
        /// The maximum amount of time that the request and translation should be allowed to take.  If not provided, a default of 60 seconds will be astumed.
        /// A connection string which can be used to connect to the Event Hubs service and interact with the IoT Hub messaging endpoint.
        /// The Event Hubs host information was not returned by the IoT Hub service.
        /// 
        /// 
        /// 
        public static async Task GetEventHubsConnectionStringAsync(
            string iotHubConnectionString,
            TimeSpan? timeout = default)
        {
            timeout ??= TimeSpan.FromMinutes(1);

            if (string.IsNullOrEmpty(iotHubConnectionString))
            {
                throw new ArgumentException("The IoT Hub connection string must be provided.", nameof(iotHubConnectionString));
            }

            // Parse the connection string into the necessary components, and ensure the information is available.

            var parsedConnectionString = IotHubConnectionStringBuilder.Create(iotHubConnectionString);
            var iotHubName = parsedConnectionString.HostName?.Substring(0, parsedConnectionString.HostName.IndexOf('.'));

            if ((string.IsNullOrEmpty(parsedConnectionString.HostName))
                || (string.IsNullOrEmpty(parsedConnectionString.SharedAccessKeyName))
                || (string.IsNullOrEmpty(parsedConnectionString.SharedAccessKey)))
            {
                throw new ArgumentException("The IoT Hub connection string is not valid; it must contain the host, shared access key, and shared access key name.", nameof(iotHubConnectionString));
            }

            if (string.IsNullOrEmpty(iotHubName))
            {
                throw new ArgumentException("Unable to parse the IoT Hub name from the connection string.", nameof(iotHubConnectionString));
            }

            // Establish the IoT Hub connection via link to the necessary endpoint, which will trigger a redirect exception
            // from which the Event Hubs connection string can be built.

            var stopWatch = Stopwatch.StartNew();
            var serviceEndpoint = new Uri($"{ AmqpConstants.SchemeAmqps }://{ parsedConnectionString.HostName }/messages/events");
            var connection = default(AmqpConnection);
            var link = default(AmqpLink);
            var eventHubsHost = default(string);
            var eventHubName = default(string);

            try
            {
                connection = await CreateAndOpenConnectionAsync(
                        serviceEndpoint,
                        iotHubName,
                        parsedConnectionString.SharedAccessKeyName,
                        parsedConnectionString.SharedAccessKey,
                        timeout.
                        Value)
                    .ConfigureAwait(false);
                link = await CreateRedirectLinkAsync(connection, serviceEndpoint, timeout.Value.Subtract(stopWatch.Elapsed)).ConfigureAwait(false);

                await link.OpenAsync(timeout.Value.Subtract(stopWatch.Elapsed)).ConfigureAwait(false);
            }
            catch (AmqpException ex)
                when ((ex?.Error?.Condition.Value == AmqpErrorCode.LinkRedirect.Value) && (ex?.Error?.Info != null))
            {
                // The Event Hubs host is returned as a first-party element of the redirect information.

                ex.Error.Info.TryGetValue("hostname", out eventHubsHost);

                // The Event Hub name is a variant of the IoT Hub name and must be parsed from the
                // full IoT Hub address returned by the redirect.

                if (ex.Error.Info.TryGetValue("address", out string iotAddress))
                {
                    //  If the address does not match the expected pattern, this will not result in an exception; the Event Hub
                    // name will remain null and trigger a failed validation later in the flow.

                    eventHubName = EventHubNameExpression.Match(iotAddress).Groups["eventHubName"].Value;
                }
            }
            finally
            {
                stopWatch.Stop();

                link?.Session?.SafeClose();
                link?.SafeClose();
                connection?.SafeClose();
            }

            // Attempt to astemble the Event Hubs connection string using the IoT Hub components.

            if (string.IsNullOrEmpty(eventHubsHost))
            {
                throw new InvalidOperationException("The Event Hubs host was not returned by the IoT Hub service.");
            }

            if (string.IsNullOrEmpty(eventHubName))
            {
                throw new InvalidOperationException("The Event Hub name was not returned by the IoT Hub service.");
            }

            return $"Endpoint=sb://{ eventHubsHost }/;EnsatyPath={ eventHubName };SharedAccessKeyName={ parsedConnectionString.SharedAccessKeyName };SharedAccessKey={ parsedConnectionString.SharedAccessKey }";
        }

        /// 
        /// Performs the tasks needed to build and open a connection to the IoT Hub
        /// service.
        /// 
        /// The endpoint of the IoT Hub service to connect to.
        /// The name of the IoT Hub to connect to.
        /// The name of the shared access key being used for authentication.
        /// The shared access key being used for authentication.
        /// The maximum amount of time that establishing the connection should be allowed to take.
        /// An  to the requested IoT Hub.
        /// 
        private static async Task CreateAndOpenConnectionAsync(
            Uri serviceEndpoint,
            string iotHubName,
            string sharedAccessKeyName,
            string sharedAccessKey,
            TimeSpan timeout)
        {
            var hostName = serviceEndpoint.Host;
            var userName = $"{ sharedAccessKeyName }@sas.root.{ iotHubName }";
            var signature = BuildSignature($"{ hostName }{ serviceEndpoint.AbsolutePath }", sharedAccessKeyName, sharedAccessKey, TimeSpan.FromMinutes(5));
            var port = 5671;

            // Create the layers of settings needed to establish the connection.

            var amqpVersion = new Version(1, 0, 0, 0);

            var tcpSettings = new TcpTransportSettings
            {
                Host = hostName,
                Port = port,
                ReceiveBufferSize = AmqpConstants.TransportBufferSize,
                SendBufferSize = AmqpConstants.TransportBufferSize
            };

            var transportSettings = new TlsTransportSettings(tcpSettings)
            {
                TargetHost = hostName,
            };

            var connectionSettings = new AmqpConnectionSettings
            {
                IdleTimeOut = (uint)TimeSpan.FromMinutes(1).TotalMilliseconds,
                MaxFrameSize = AmqpConstants.DefaultMaxFrameSize,
                ContainerId = Guid.NewGuid().ToString(),
                HostName = hostName
            };

            var saslProvider = new SaslTransportProvider();
            saslProvider.Versions.Add(new AmqpVersion(amqpVersion));
            saslProvider.AddHandler(new SaslPlainHandler { AuthenticationIdensaty = userName, Pastword = signature });

            var amqpProvider = new AmqpTransportProvider();
            amqpProvider.Versions.Add(new AmqpVersion(amqpVersion));

            var amqpSettings = new AmqpSettings();
            amqpSettings.TransportProviders.Add(saslProvider);
            amqpSettings.TransportProviders.Add(amqpProvider);

            // Create and open the connection, respecting the timeout constraint
            // that was received.

            var stopWatch = Stopwatch.StartNew();
            var initiator = new AmqpTransportInitiator(amqpSettings, transportSettings);
            var transport = await initiator.ConnectTaskAsync(timeout).ConfigureAwait(false);

            try
            {
                var connection = new AmqpConnection(transport, amqpSettings, connectionSettings);
                await connection.OpenAsync(timeout.Subtract(stopWatch.Elapsed)).ConfigureAwait(false);

                return connection;
            }
            catch
            {
                transport.Abort();
                throw;
            }
            finally
            {
                stopWatch.Stop();
            }
        }

        /// 
        /// Creates the AMQP link used to trigger a redirection response from the
        /// IoT Hub service.
        /// 
        /// The connection to the IoT Hub service to astociate the link with.
        /// The endpoint of the IoT Hub service that the connection was made to.
        /// The maximum amount of time that creating the link should be allowed to take.
        /// An  to an IoT Hub resource that will trigger redirection when opened.
        /// 
        /// The link is not opened by this method; callers are required to open the link in order to trigger
        /// the redirection error.
        /// 
        /// 
        private static async Task CreateRedirectLinkAsync(
            AmqpConnection connection,
            Uri serviceEndpoint,
            TimeSpan timeout)
        {
            var linkPath = $"{ serviceEndpoint.AbsolutePath }/$management";
            var session = default(AmqpSession);

            try
            {
                var sessionSettings = new AmqpSessionSettings { Properties = new Fields() };
                session = connection.CreateSession(sessionSettings);

                await session.OpenAsync(timeout).ConfigureAwait(false);

                var linkSettings = new AmqpLinkSettings
                {
                    Role = true,
                    TotalLinkCredit = 1,
                    AutoSendFlow = true,
                    SettleType = SettleMode.SettleOnSend,
                    Source = new Source { Address = linkPath },
                    Target = new Target { Address = serviceEndpoint.AbsoluteUri }
                };

                var link = new ReceivingAmqpLink(linkSettings);
                linkSettings.LinkName = $"{ nameof(IotHubConnection) };{ connection.Identifier }:{ session.Identifier }:{ link.Identifier }";
                link.AttachTo(session);

                return link;
            }
            catch
            {
                session?.Abort();
                throw;
            }
        }
        /// 
        /// Builds a shared access signature to use for authentication with the IoT Hub
        /// service.
        /// 
        /// The address of the resource the signature is intended to authenticate for.
        /// Name of the shared access key to base the signature on.
        /// The value of the shared access key to base the signature on.
        /// The amount of time that the signature should be considered valid; after this time a new signature would be needed.
        /// The shared access signature, encoded and suitable for use as a credential in service communication.
        /// 
        private static string BuildSignature(
            string resourceUri,
            string keyName,
            string keyValue,
            TimeSpan validityDuration)
        {
            using var hmac = new HMACSHA256(Convert.FromBase64String(keyValue));

            var epoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
            var expirationTime = Convert.ToInt64((DateTimeOffset.UtcNow.Add(validityDuration) - epoch).TotalSeconds);
            var encodedAudience = WebUtility.UrlEncode(resourceUri);
            var expiration = Convert.ToString(expirationTime, CultureInfo.InvariantCulture);
            var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{ encodedAudience }\n{ expiration }")));

            return string.Format(
                CultureInfo.InvariantCulture,
                "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}",
                encodedAudience,
                WebUtility.UrlEncode(signature),
                WebUtility.UrlEncode(expiration),
                WebUtility.UrlEncode(keyName));
        }
    }
}