Not for the restless, HTTP access to the Azure IoT Hub

The Azure IoT Hub is accessible using multiple protocols. You can use MQTT, AMQP and HTTP. It’s even possible to run MQTT and AMQP over HTTP using web sockets (in case your firewall is closed).

This week, I had to connect a device to the IoT Hub running its own propriety runtime environment. The only way to communicate was HTTP.

Luckily, still HTTP is supported but communication works a bit different compared to using the IoT Hub SDK’s which Microsoft is offering.

Yes, at first it seems easy to just make a POST or GET to a REST endpoint. But looking at the security, just providing the Device connection string is not enough. You have to extract an SAS (Shared Access Signature) token first.

Let’s see how you can use REST.

Retrieving an SAS token using Device Explorer

It’s not shown in the original documentation but before you can send D2C telemetry or receive C2D messages, you have to generate an SAS token.

An SAS token is just a secret token which is valid for a limited time (Time To Live (TTL) in days) and for a specific user/device. Normally, SAS tokens are provided using a ‘second channel’. So you first get this token from one source/channel using some kind of security. Next, you use the token for one of more calls to the logic on another channel you wanted to access in the first place. Once the token times out, you will have to get another one.

So you can have a more granular way to block or give access to a device.

Here, we use the Device Explorer as a second channel. First, select the specific device in the IoT Hub:

Next, open the dialog for the SAS token generation:

Select ‘generate’ and see that the token is actually generated.

Be aware: we only need the last part. So skip the first part “HostName=[name of IoT Hub].azure-devices.net;DeviceId=[name of device];SharedAccessSignature=”

Copy this SAS token for future usage.

Note: It must be clear that you need both the IoT Hub connection string and access to a device to generate this token.

Sending telemetry

Finally, we can send a message. Here is the C# code for an HTTP POST call in a console app:

Console.WriteLine("Start sending...");

string iotHub = "[name of your IoT Hub]";
string deviceId = "[name of your device]";
string api = "2016-11-14";// "2016-02-03";
string restUriPost = $"https://{iotHub}.azure-devices.net/devices/{deviceId}/messages/events?api-version={api}";
string sas = "[your SAS token]";

var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", sas);

var body = new{Temperature=22.2, Humidity=45.2, Signal="ok" };
var content = JsonConvert.SerializeObject(body);
var stringContent = new StringContent(content, Encoding.UTF8, "application/json");

var resultPost = client.PostAsync(restUriPost, stringContent).Result;

Console.WriteLine($"Sent {resultPost}");

Note: We reference this Newtonsoft.Json NUGET package

This results in a 204 response from the ‘server’. And this is a good thing:

In the Device Explorer we see the arrival of the message:

Regarding the code, we use this POST action. And in the body, an anonymous class is represented as JSON.

We have to combine some variables to get a full URL. The API version is a bit awkward. I found two versions of them on the internet.

I have no idea what the differences are.

The primary change in the 2016-11-14 API version is the introduction of Device Twin API support. Documentation of the runtime API started with this API version only.

So, do not pass the date of ‘today’, just stick to one of this 🙂

The SAS token is put in the header. So make sure you use HTTPS. Otherwise, your token is passed as plain text and accessible for everyone.

Receiving messages, the basics

So sending an event seems pretty easy. How about receiving messages?

If we use MQTT of AMQP, this is simple. The communication is stateful so we only need to pass a callback function.

Using HTTP, You need to check the availability of a message yourself, over and over again (polling).

Note: HTTP C2D messages are heavily throttled. Microsoft has advised a polling interval of 25 minutes using HTTP.

And just sending C2D messages is not enough. The device has to acknowledge the received messages, this is called ‘complete’:

But the device can also make the choice to reject the message (it’s put in a dead letter box, never to be seen again) or it can abandon it (the message is ignored for now and will be picked up again another time).

Receiving, completing and rejecting

Once you have seen this diagram, it’s clear that you need to make at least two calls: one for getting the message and one for ‘deleting it’ in the IoT Hub.

So let’s make two Rest calls, GET and DELETE:

Console.WriteLine("Start sending...");

string iotHub = "[name of your IoT Hub]"; 
string deviceId = "[name of your device]"; 
string api = "2016-11-14";// "2016-02-03";
string restUriGet = $"https://{iotHub}.azure-devices.net/devices/{deviceId}/messages/deviceBound?api-version={api}";
string sas = "[your SAS token]";
var clientGet = new HttpClient();
clientGet.DefaultRequestHeaders.Add("Authorization", sas);

var resultGet = clientGet.GetAsync(restUriGet).Result;

var message = resultGet.Content.ReadAsStringAsync().Result;

var etag = resultGet.Headers.ETag?.Tag.Replace("\"", string.Empty);

Console.WriteLine($"Message received: '{message}' with etag '{etag}'");

if (etag != null)
{
  var restUriComplete = $"https://{iotHub}.azure-devices.net/devices/{deviceId}/messages/deviceBound/{etag}?api-version={api}";

  var reject = false;

  if (reject)
  {
     // the message is now a dead letter 
     restUriComplete = restUriComplete + "&reject"; 
  }
  var clientDeletion = new HttpClient();
  clientDeletion.DefaultRequestHeaders.Add("Authorization", sas);

  var resultDelete = clientDeletion.DeleteAsync(restUriComplete).Result;

  Console.WriteLine($"Delete {resultDelete.IsSuccessStatusCode} {resultDelete.StatusCode} {resultDelete.ReasonPhrase}"); 
}
else
{
  Console.WriteLine("No etag, no message available from queue");
}

The most important part of this code is the ETag. Once a message is received, it is uniquely identified by this ETag. So we need to provide this ETag of the message we want to complete or reject.

Note: Microsoft stated “Currently, the use of the ETag in the header does not comply with RFC 7232. A fix for this issue is currently in our backlog.” So maybe this ETag is subject to change?

First, we provide a C2D message using the Device Explorer:

and then we run our code:

The GET call receives the message. Great. And then we send this DELETE call. Again, we receive a 204 No Content response of this second call.

To reject the message, just add “&reject” to the DELETE call.

Receiving, Abandoning

The last call is Abandoning, which is for some unknown reason a Post with an empty body (well, we do not delete the message and we change something on the server so a Get is not appropriate. This leaves a Post action).

We first send a C2D message:

And we handle it with the following logic:

if (etag != null)
{
  string restUriAbandon = $"https://{iotHub}.azure-devices.net/devices/{deviceId}/messages/deviceBound/{etag}/abandon?api-version={api}";
  var clientPost = new HttpClient();
  clientPost.DefaultRequestHeaders.Add("Authorization", sas);

  var emptyContent = new StringContent(string.Empty, Encoding.UTF8, "text/plain");

  var resultPost= clientPost.PostAsync(restUriAbandon, emptyContent).Result;

  Console.WriteLine($"Post {resultPost.IsSuccessStatusCode} {resultPost.StatusCode} {resultPost.ReasonPhrase}");
}

And when we execute this code, we get a nice 204 No Content response:

If we run this call over and over, we retrieve the same message again and again!

Create an SAS token yourself

In the examples above, we used an SAS token created by the Device Explorer. But if you want to program it yourself, this is the code to use:

private static async Task<String> getSASSignature()
{
  var deviceId = "[your device name]";
  var fullIotHubName = "[IoT Hub name].azure-devices.net";
  var connectionStringIoTHub = "[connection string to your IoT Hub]";

  try
  {
    var registryManager = RegistryManager.CreateFromConnectionString(connectionStringIoTHub);
    var device = await registryManager.GetDeviceAsync(deviceId);
    var primaryKey = device.Authentication.SymmetricKey.PrimaryKey;

    var sasBuilder = new SharedAccessSignatureBuilder()
    {
      Key = primaryKey,
      Target = $"{fullIotHubName}/devices/{WebUtility.UrlEncode(deviceId)}",
      TimeToLive = TimeSpan.FromDays(Convert.ToDouble(42)) // 42 days
    };

    return sasBuilder.ToSignature();
  }
  catch (Exception ex)
  {
    Console.WriteLine($"Exception: {ex.Message}");

    return null;
  }
}

Note: we reference this Microsoft.Azure.Devices NUGET package

Here we generate an SAS token which is valid for a certain device on a certain IoT Hub for a period of 42 days:

This string is the complete SAS token.

So you need a connection string to the IoT Hub (using the iothubowner policy).

You could even put this in an Azure Function.

Conclusion

If HTTP is your weapon of choice, here you have a head start on sending and receiving messages.

Advertenties