Try fast search NHibernate

10 January 2017

Azure IoT Hub a puro REST

El año pasado, en una reunión con el colorado el ruso y el pibe, me pidieron un ejemplo de uso de Azure IoT Hub sin usar el SDK. Yo pensaba que con un poco de GoogleFu algo iba a encontrar pero… nada.

En lugar de seguir buscando codigo y ya que todo, o casi, en Azure tiene API REST empecé a leer la documentación de la API y a codear.

Si quieren probar el codigo de este post, a parte un account de Azure, necesitan crear un IoTHub y tener a mano tres parametros:
1) el host del IoTHub que creaste
HostName
2) El nombre de la policy. En este caso, aunque ya tienen policies definida por default, le conviene crear una policy nueva.
PolicyName
3) la key de la policy. Como key pueden usar la primary o la secondary; en muchos servicios en Azure siempre tienen dos keys para cambiar/regenerar la key sin sufrir downtime.
SAS

Ya tenemos los ingredientes arriba la mesada y podemos empezar a prepararlos para la cocción.

Ya hace un tiempito que en Azure muchos servicios gozan de la fantastica SAS (Shared Access Signature) que, basicamente, nos permite compartir recursos y/o servicios sin por eso tener que hacer “viajar” las keys o utilizar procesos de auth que pueden ser más complejos como OAuth2. La SAS viaja plácidamente en el query string o en los headers de un request REST. En este caso usaremos la SAS para evitar el obscuro MitM (man in the middle) creando una SAS, usable por un tiempo relativamente corto (TTL time to live), para enviar mensajes al Azure IoT Hub. Una SAS para, condimentar un request al IoT Hub, se prepara de esta forma:

    private static readonly DateTime epochTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);

    public static string SharedAccessSignature(string hostUrl, string policyName, string policyAccessKey, TimeSpan timeToLive)
    {
      if (string.IsNullOrWhiteSpace(hostUrl))
      {
        throw new ArgumentNullException(nameof(hostUrl));
      }

      var expires = Convert.ToInt64(DateTime.UtcNow.Add(timeToLive).Subtract(epochTime).TotalSeconds).ToString(CultureInfo.InvariantCulture);
      var resourceUri = WebUtility.UrlEncode(hostUrl.ToLowerInvariant());
      var toSign = string.Concat(resourceUri, "\n", expires);
      var signed = Sign(toSign, policyAccessKey);

      var sb = new StringBuilder();
      sb.Append("sr=").Append(resourceUri)
        .Append("&sig=").Append(WebUtility.UrlEncode(signed))
        .Append("&se=").Append(expires);
      if (!string.IsNullOrEmpty(policyName))
      {
        sb.Append("&skn=").Append(WebUtility.UrlEncode(policyName));
      }
      return sb.ToString();
    }

    private static string Sign(string requestString, string key)
    {
      using (var hmacshA256 = new HMACSHA256(Convert.FromBase64String(key)))
      {
        var hash = hmacshA256.ComputeHash(Encoding.UTF8.GetBytes(requestString));
        return Convert.ToBase64String(hash);
      }
    }
Ingredientes y condimento preparados podemos empezar cocinando la entrada o sea el check de existencia y la registración de un device en el IoT Hub… el device se puede registrar en el IoT Hub a mano (desde el portal de Azure) pero va a resultar medio engorroso si planean agregar una estación de monitoreo sin mucha cerimonia.
    public static async Task CreateIfNotExists(HttpClient httpClient, string deviceId)
    {
      if (await Exists(httpClient, deviceId))
      {
        return;
      }
      var jsonMessage =
        $"{{\"deviceId\": \"{deviceId}\", \"status\": \"enabled\", \"statusReason\": \"Listo para enviar info\"}}";

      var request = new HttpRequestMessage(HttpMethod.Put, $"devices/{deviceId}?api-version=2016-02-03")
      {
        Content = new StringContent(jsonMessage, Encoding.ASCII, "application/json")
      };
      var response = await httpClient.SendAsync(request);
      if (!response.IsSuccessStatusCode)
      {
        throw new IOException("No fue posible registrar la estación de medición.");
      }
    }

    public static async Task<bool> Exists(HttpClient httpClient, string deviceId)
    {
      var request = new HttpRequestMessage(HttpMethod.Get, $"devices/{deviceId}?api-version=2016-02-03");
      request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
      var response = await httpClient.SendAsync(request);
      return response.IsSuccessStatusCode;
    }
Como notarán, hasta ahora, son todo metodos de una pequeña utility (todo los metodos son static) que llamé, con muchisima fantasia: DeviceRest . Otra peculiaridad de estos métodos es el hecho que reciben un HttpClient ; el motivo será más claro cocinando el plato principal.
  public class MeditionInfoSender
  {
    private readonly string stationId;
    private HttpClient currentHttpClient;
    private readonly string iotHubHost;
    private readonly string iotHubPolicyName;
    private readonly string iotHubPolicyKey;

    public MeditionInfoSender(string iotHubHost, string iotHubPolicyName, string iotHubPolicyKey, string stationId)
    {
      if (string.IsNullOrWhiteSpace(iotHubHost))
      {
        throw new ArgumentNullException(nameof(iotHubHost));
      }
      if (string.IsNullOrWhiteSpace(iotHubPolicyName))
      {
        throw new ArgumentNullException(nameof(iotHubPolicyName));
      }
      if (string.IsNullOrWhiteSpace(iotHubPolicyKey))
      {
        throw new ArgumentNullException(nameof(iotHubPolicyKey));
      }
      if (string.IsNullOrWhiteSpace(stationId))
      {
        throw new ArgumentNullException(nameof(stationId));
      }
      this.stationId = stationId;
      this.iotHubHost = iotHubHost;
      this.iotHubPolicyName = iotHubPolicyName;
      this.iotHubPolicyKey = iotHubPolicyKey;
    }

    public void InitializeStation()
    {
      using (var httpClient = new HttpClient())
      {
        httpClient.BaseAddress = new UriBuilder {Scheme = "https", Host = iotHubHost}.Uri;
        var hubSharedAccessSignature = DeviceRest.SharedAccessSignature(iotHubHost
          , iotHubPolicyName
          , iotHubPolicyKey
          , TimeSpan.FromMinutes(1));
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("SharedAccessSignature", hubSharedAccessSignature);
        DeviceRest.CreateIfNotExists(httpClient, stationId).Wait();
      }
      currentHttpClient = CreateHttpClient();
    }

    public async Task<bool> Send(string jsonMessage)
    {
      if (string.IsNullOrWhiteSpace(jsonMessage))
      {
        return false;
      }
      var request = new HttpRequestMessage(HttpMethod.Post, $"devices/{stationId}/messages/events?api-version=2016-02-03")
      {
        Content = new StringContent(jsonMessage, Encoding.ASCII, "application/json")
      };
      try
      {
        var httpClient = GetHttpClient();
        var response = await httpClient.SendAsync(request);
        return response.IsSuccessStatusCode;
      }
      catch (Exception)
      {
        return false;
      }
    }

    private HttpClient GetHttpClient()=> currentHttpClient ?? (currentHttpClient= CreateHttpClient());

    private HttpClient CreateHttpClient()
    {
      var httpClient = new HttpClient(new SharedAccessSignatureAuthHandler(iotHubHost, iotHubPolicyName, iotHubPolicyKey))
      {
        BaseAddress = new UriBuilder {Scheme = "https", Host = iotHubHost}.Uri,
      };
      return httpClient;
    }

    private class SharedAccessSignatureAuthHandler : HttpClientHandler
    {
      private AuthenticationHeaderValue currentSas;
      private readonly TimeSpan maxSasTtl = TimeSpan.FromMinutes(23);
      private readonly Stopwatch timer = new Stopwatch();
      private readonly string iotHubHost;
      private readonly string iotHubPolicyName;
      private readonly string iotHubPolicyKey;

      public SharedAccessSignatureAuthHandler(string iotHubHost, string iotHubPolicyName, string iotHubPolicyKey)
      {
        if (string.IsNullOrWhiteSpace(iotHubHost))
        {
          throw new ArgumentNullException(nameof(iotHubHost));
        }
        if (string.IsNullOrWhiteSpace(iotHubPolicyName))
        {
          throw new ArgumentNullException(nameof(iotHubPolicyName));
        }
        if (string.IsNullOrWhiteSpace(iotHubPolicyKey))
        {
          throw new ArgumentNullException(nameof(iotHubPolicyKey));
        }
        this.iotHubHost = iotHubHost;
        this.iotHubPolicyName = iotHubPolicyName;
        this.iotHubPolicyKey = iotHubPolicyKey;
      }

      protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
      {
        if (timer.Elapsed >= maxSasTtl || currentSas == null)
        {
          SetCurrentAuth();
        }
        request.Headers.Authorization = currentSas;
        return base.SendAsync(request, cancellationToken);
      }

      private void SetCurrentAuth()
      {
        timer.Reset();
        var hubSharedAccessSignature = DeviceRest.SharedAccessSignature(iotHubHost
          , iotHubPolicyName
          , iotHubPolicyKey
          , maxSasTtl.Add(TimeSpan.FromMinutes(3)));
        currentSas = new AuthenticationHeaderValue("SharedAccessSignature", hubSharedAccessSignature);
        timer.Start();
      }
    }
  }
Esta clase se ocupa de toda (lo que necesité) la comunicación con el IoT Hub usando su API REST. La parte más compleja (bueno… ponele…) es la gestión de la instancia de HttpClient que se usa a cada request.En el pipeline del HttpClient se usa una instancia de HttpClientHandler encargada de generar una SAS, con un TTL de 3 minutos, y adornar el request agregando el Authorization Header (el handler es la clase SharedAccessSignatureAuthHandler ).

Ya está el plato cocinado falta solo ver como se come…

Considerando que ustedes sabrán como obtener el stationId (que para el IoT Hub es el Device-ID), el primer bocón sería mas o menos así:
      sender = new MeditionInfoSender(iotHubHost, iotHubPolicyName, iotHubPolicyKey, stationId);
      Console.WriteLine($"Inicializando estación de medición '{stationId}'...");
      sender.InitializeStation();
      Console.WriteLine($"Estación '{stationId}' inicializada.");
Considerando que ustedes sabrán como obtener/construir el iotJsonMessage , el envío de un mensaje al IoT Hub (todos los otros bocones) se reduce a lo siguiente:
        var sent = await sender.Send(iotJsonMessage);

Esta receta es para la versión ligth/zero que pueden consumir quienes están a dieta o le resulta pesado el SDK o simplemente se divierte cocinando comida casera (pueden usarlo para traducirlo en el lenguaje de su device). Los que pueden ir al fast-food sería mejor que usen el SDK que le corresponda; le paso la que ya es solo la landing page del SDK (en el texto de la pagina encontrarán en link al repo de cada lenguaje): https://github.com/Azure/azure-iot-sdks

Como postre: el codigo de este post, as is, se usó en un par de proyectos .NET Core para los siguientes runtimes:
  "runtimes": {
    "win10-x64": {},
    "ubuntu.14.04-x64": {},
    "debian.8-x64": {} 
  }
o sea que funcionó en windows, ubuntu y docker (meterlo en un RaspberryPi, con UWP, no cuesta mucho)… y si! .NET, al fin, es multiplataforma y OSS.

No comments:

Post a Comment