1
0
Форкнуть 0
This commit is contained in:
Pericles Alves 2021-01-15 14:35:13 -08:00
Родитель d88de18fb5
Коммит ab8ee74240
103 изменённых файлов: 9539 добавлений и 0 удалений

47
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,47 @@
*.DS_Store
[Tt]humbs.db
#Visual Studio Files
[Oo]bj
[Bb]in
[Dd]ebug
[Bb]uild/
*.user
*.suo
*.exe
*.pdb
*.aps
*_i.c
*_p.c
*.ncb
*.tlb
*.tlh
*.[Cc]ache
*.bak
*.ncb
*.ilk
*.log
*.lib
*.sbr
*.sdf
ipch/
*.dbmdl
*.csproj.user
*.cache
*.swp
*.vspscc
*.vssscc
*.bak.*
*.bak
.vs/
#Tools
_ReSharper.*
_ReSharper*/
*.resharper
*.resharper.user
[Nn][Dd]epend[Oo]ut*/
Ankh.NoLoad
#Other
.svn

31
DeviceBridge.sln Normal file
Просмотреть файл

@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29911.98
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeviceBridge", "DeviceBridge\DeviceBridge.csproj", "{0956E364-75AF-4C03-A96C-DCA4D6058FA3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeviceBridgeTests", "DeviceBridgeTests\DeviceBridgeTests.csproj", "{9293F637-1AA6-4092-9E3B-A2966EA7C5CB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0956E364-75AF-4C03-A96C-DCA4D6058FA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0956E364-75AF-4C03-A96C-DCA4D6058FA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0956E364-75AF-4C03-A96C-DCA4D6058FA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0956E364-75AF-4C03-A96C-DCA4D6058FA3}.Release|Any CPU.Build.0 = Release|Any CPU
{9293F637-1AA6-4092-9E3B-A2966EA7C5CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9293F637-1AA6-4092-9E3B-A2966EA7C5CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9293F637-1AA6-4092-9E3B-A2966EA7C5CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9293F637-1AA6-4092-9E3B-A2966EA7C5CB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8C67BD65-1F81-4180-91B4-691483E7C916}
EndGlobalSection
EndGlobal

Просмотреть файл

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
namespace DeviceBridge.Common.Authentication
{
public static class SchemesNamesConst
{
public const string TokenAuthenticationDefaultScheme = "TokenAuthenticationScheme";
}
}

Просмотреть файл

@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using DeviceBridge.Providers;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NLog;
namespace DeviceBridge.Common.Authentication
{
public class TokenAuthenticationHandler : AuthenticationHandler<TokenAuthenticationOptions>
{
private ISecretsProvider secretsProvider;
private Logger logger;
public TokenAuthenticationHandler(IOptionsMonitor<TokenAuthenticationOptions> options, ILoggerFactory loggerFactory, NLog.Logger logger, UrlEncoder encoder, ISystemClock clock, IServiceProvider serviceProvider, ISecretsProvider secretsProvider)
: base(options, loggerFactory, encoder, clock)
{
ServiceProvider = serviceProvider;
this.secretsProvider = secretsProvider;
this.logger = logger;
}
public IServiceProvider ServiceProvider { get; set; }
protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var tmpLogger = this.logger.WithProperty("cv", Utils.GuidFromString(Request.HttpContext.TraceIdentifier));
tmpLogger.Info("Starting api key authentication.");
var masterApiKey = await this.secretsProvider.GetApiKey(this.logger);
var headers = Request.Headers;
var apiKey = headers["x-api-key"];
if (string.IsNullOrEmpty(apiKey))
{
tmpLogger.Info("Api key is null");
return AuthenticateResult.Fail("Api key is null");
}
bool isValidToken = masterApiKey.Equals(apiKey); // check token here
if (!isValidToken)
{
tmpLogger.Info($"Apikey authentication failed.");
return AuthenticateResult.Fail($"Apikey authentication failed.");
}
var claims = new[] { new Claim("apiKey", apiKey) };
var identity = new ClaimsIdentity(claims, nameof(TokenAuthenticationHandler));
var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), this.Scheme.Name);
tmpLogger.Info("Successfully authenticated using api key.");
return AuthenticateResult.Success(ticket);
}
}
}

Просмотреть файл

@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using Microsoft.AspNetCore.Authentication;
namespace DeviceBridge.Common.Authentication
{
public class TokenAuthenticationOptions : AuthenticationSchemeOptions
{
}
}

Просмотреть файл

@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using DeviceBridge.Common.Exceptions;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
namespace DeviceBridge.Common
{
internal class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
public ExceptionHandlingMiddleware(RequestDelegate next)
{
this._next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (BridgeException e)
{
context.Response.StatusCode = e.StatusCode;
throw e;
}
catch (Exception e)
{
context.Response.StatusCode = 500;
throw e;
}
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
namespace DeviceBridge.Common.Exceptions
{
[Serializable]
public abstract class BridgeException : Exception
{
public BridgeException(string message, int statusCode)
: base(message)
{
this.StatusCode = statusCode;
}
public BridgeException(string message, Exception inner, int statusCode)
: base(message, inner)
{
this.StatusCode = statusCode;
}
public int StatusCode { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using Microsoft.AspNetCore.Http;
namespace DeviceBridge.Common.Exceptions
{
public class DeviceConnectionNotFoundException : BridgeException
{
public DeviceConnectionNotFoundException(string deviceId)
: base($"Connection for device {deviceId} not found", StatusCodes.Status500InternalServerError)
{
}
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using Microsoft.AspNetCore.Http;
namespace DeviceBridge.Common.Exceptions
{
public class DeviceSdkTimeoutException : BridgeException
{
public DeviceSdkTimeoutException(string deviceId)
: base($"The device SDK timed out while executing the requested operation for device {deviceId}", StatusCodes.Status500InternalServerError)
{
}
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using Microsoft.AspNetCore.Http;
namespace DeviceBridge.Common.Exceptions
{
public class DpsRegistrationFailedWithUnknownStatusException : BridgeException
{
public DpsRegistrationFailedWithUnknownStatusException(string deviceId, string status, string substatus, int? errorCode, string errorMessage)
: base($"Failed to perform DPS registration for device {deviceId}: {status} {substatus} {errorCode} {errorMessage}", StatusCodes.Status500InternalServerError)
{
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using Microsoft.AspNetCore.Http;
namespace DeviceBridge.Common.Exceptions
{
public class EncryptionException : BridgeException
{
public EncryptionException()
: base("Error when trying to run encryption or decryption.", StatusCodes.Status500InternalServerError)
{
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using Microsoft.AspNetCore.Http;
namespace DeviceBridge.Common.Exceptions
{
public class StorageSetupIncompleteException : BridgeException
{
public StorageSetupIncompleteException(Exception inner)
: base("Storage setup incomplete: missing tables or stored procedures", inner, StatusCodes.Status500InternalServerError)
{
}
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using Microsoft.AspNetCore.Http;
namespace DeviceBridge.Common.Exceptions
{
public class UnknownDeviceSubscriptionTypeException : BridgeException
{
public UnknownDeviceSubscriptionTypeException(string type)
: base($"Unknown device subscription type {type}", StatusCodes.Status400BadRequest)
{
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using Microsoft.AspNetCore.Http;
namespace DeviceBridge.Common.Exceptions
{
public class UnknownStorageException : BridgeException
{
public UnknownStorageException(Exception inner)
: base("Unknown storage exception", inner, StatusCodes.Status500InternalServerError)
{
}
}
}

Просмотреть файл

@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
namespace DeviceBridge.Common
{
internal class RequestLoggingMiddleware
{
private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
private readonly RequestDelegate _next;
public RequestLoggingMiddleware(RequestDelegate next)
{
this._next = next;
}
public async Task Invoke(HttpContext context)
{
var request = await FormatRequest(context.Request);
var logger = _logger.WithProperty("cv", Utils.GuidFromString(context.TraceIdentifier));
logger.SetProperty("path", context.Request.Path.Value);
var regexResult = Regex.Match(context.Request.Path, @"(?<=devices\/).*?(?=\/)");
if (regexResult.Success)
{
var deviceId = regexResult.Groups[0].Value;
logger.SetProperty("deviceId", deviceId);
}
logger.Info(request);
var originalBodyStream = context.Response.Body;
using (var responseBody = new MemoryStream())
{
context.Response.Body = responseBody;
try
{
await _next(context);
}
catch (Exception e)
{
var tmpResponse = FormatResponse(context.Response);
logger.Error(e, tmpResponse);
await responseBody.CopyToAsync(originalBodyStream);
return;
}
var response = FormatResponse(context.Response);
logger.Info(response);
await responseBody.CopyToAsync(originalBodyStream);
}
}
private async Task<string> FormatRequest(HttpRequest request)
{
request.EnableBuffering();
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
await request.Body.ReadAsync(buffer, 0, buffer.Length);
var requestBody = Encoding.UTF8.GetString(buffer);
request.Body.Seek(0, SeekOrigin.Begin);
var builder = new StringBuilder(Environment.NewLine);
builder.AppendLine("{ headers: {");
foreach (var header in request.Headers)
{
var redactedHeaderValue = RedactHeaderValue(header);
builder.AppendLine($"{header.Key}:{redactedHeaderValue},");
}
builder.AppendLine($"}},body:{requestBody}}}");
return builder.ToString().Replace("\r\n", string.Empty);
}
private string RedactHeaderValue(System.Collections.Generic.KeyValuePair<string, StringValues> header)
{
if (header.Key.Equals("x-api-key"))
{
return "redacted";
}
return header.Value;
}
private string FormatResponse(HttpResponse response)
{
response.Body.Seek(0, SeekOrigin.Begin);
string responseBody = new StreamReader(response.Body).ReadToEnd();
response.Body.Seek(0, SeekOrigin.Begin);
return $"Response: {response.StatusCode}, {responseBody}";
}
}
}

Просмотреть файл

@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Security.Cryptography;
using System.Text;
using DeviceBridge.Providers;
using NLog;
public static class Utils
{
private static MD5 hasher = MD5.Create();
/// <summary>
/// Generates a GUID hashed from an input string.
/// </summary>
/// <param name="input">Input to generate the GUID from.</param>
/// <returns>GUID hashed from input.</returns>
public static Guid GuidFromString(string input)
{
return new Guid(hasher.ComputeHash(Encoding.Default.GetBytes(input)));
}
/// <summary>
/// Fetches the sql connection string.
/// </summary>
/// <param name="logger">Logger.</param>
/// <param name="secretsProvider">Secrets provider for retrieving credentials.</param>
/// <returns>The sql connection string.</returns>
public static string GetSqlConnectionString(Logger logger, SecretsProvider secretsProvider)
{
var sqlServerName = secretsProvider.GetSqlServerAsync(logger).Result;
var sqlDatabaseName = secretsProvider.GetSqlDatabaseAsync(logger).Result;
var sqlUsername = secretsProvider.GetSqlUsernameAsync(logger).Result;
var sqlPassword = secretsProvider.GetSqlPasswordAsync(logger).Result;
sqlPassword = sqlPassword.Replace("'", "''");
return $"Server=tcp:{sqlServerName},1433;Initial Catalog={sqlDatabaseName};Persist Security Info=False;User ID={sqlUsername};Password='{sqlPassword}';MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;";
}
}

Просмотреть файл

@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using NLog;
namespace DeviceBridge.Controllers
{
// TODO: add proper correlation middleware (that preserves the correlationId)
[Produces("application/json")]
public partial class BaseController : Controller
{
public BaseController(Logger baseLogger)
{
this.Logger = baseLogger.WithProperty("cv", Guid.NewGuid()); // Create a new logger instance to be used in the controller scope.
}
protected Logger Logger { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Configure logger
Logger.SetProperty("path", filterContext.HttpContext.Request.Path.Value);
Logger.SetProperty("cv", Utils.GuidFromString(filterContext.HttpContext.TraceIdentifier));
base.OnActionExecuting(filterContext);
}
}
}

Просмотреть файл

@ -0,0 +1,105 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using DeviceBridge.Models;
using DeviceBridge.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NLog;
namespace DeviceBridge.Controllers
{
[Route("devices/{deviceId}/[controller]")]
[ApiController]
public class ConnectionStatusController : BaseController
{
private readonly SubscriptionService _subscriptionService;
private readonly ConnectionManager _connectionManager;
public ConnectionStatusController(Logger logger, SubscriptionService subscriptionService, ConnectionManager connectionManager)
: base(logger)
{
_subscriptionService = subscriptionService;
_connectionManager = connectionManager;
}
/// <summary>
/// Gets that latest connection status for a device.
/// </summary>
/// <remarks>
/// For a detailed description of each status, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
/// </remarks>
/// <response code="200">The latest connection status and reason.</response>
/// <response code="404">If the connection status is not known (i.e., the device hasn't attempted to connect).</response>
[HttpGet]
[Route("")]
[NotFoundResultFilter]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<DeviceStatusResponseBody> GetCurrentConnectionStatus(string deviceId)
{
var deviceStatus = _connectionManager.GetDeviceStatus(deviceId);
return (deviceStatus != null) ? new DeviceStatusResponseBody()
{
Status = deviceStatus?.status.ToString(),
Reason = deviceStatus?.reason.ToString(),
}
: null;
}
/// <summary>
/// Gets the current connection status change subscription for a device.
/// </summary>
/// <response code="200">The current connection status subscription.</response>
/// <response code="404">If a subscription doesn't exist.</response>
[HttpGet]
[Route("sub")]
[NotFoundResultFilter]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<DeviceSubscription>> GetConnectionStatusSubscription(string deviceId, CancellationToken cancellationToken = default)
{
return await _subscriptionService.GetConnectionStatusSubscription(Logger, deviceId, cancellationToken);
}
/// <summary>
/// Creates or updates the current connection status change subscription for a device.
/// </summary>
/// <remarks>
/// When the internal connection status of a device changes, the service will send an event to the desired callback URL.
///
/// Example event:
/// {
/// "eventType": "string",
/// "deviceId": "string",
/// "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
/// "status": "string",
/// "reason": "string"
/// }
///
/// For a detailed description of each status, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
/// </remarks>
/// <response code="200">The created or updated connection status subscription.</response>
[HttpPut]
[Route("sub")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<DeviceSubscription>> CreateOrUpdateConnectionStatusSubscription(string deviceId, SubscriptionCreateOrUpdateBody body, CancellationToken cancellationToken = default)
{
return await _subscriptionService.CreateOrUpdateConnectionStatusSubscription(Logger, deviceId, body.CallbackUrl, cancellationToken);
}
/// <summary>
/// Deletes the current connection status change subscription for a device.
/// </summary>
/// <response code="204">Subscription deleted successfully.</response>
[HttpDelete]
[Route("sub")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DeleteConnectionStatusSubscription(string deviceId, CancellationToken cancellationToken = default)
{
await _subscriptionService.DeleteConnectionStatusSubscription(Logger, deviceId, cancellationToken);
return NoContent();
}
}
}

Просмотреть файл

@ -0,0 +1,88 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
using DeviceBridge.Models;
using DeviceBridge.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NLog;
namespace DeviceBridge.Controllers
{
[Route("devices/{deviceId}/[controller]")]
[ApiController]
public class DeviceBoundController : BaseController
{
private readonly SubscriptionService _subscriptionService;
public DeviceBoundController(Logger logger, SubscriptionService subscriptionService)
: base(logger)
{
_subscriptionService = subscriptionService;
}
/// <summary>
/// Gets the current C2D message subscription for a device.
/// </summary>
/// <response code="200">The current C2D message subscription.</response>
/// <response code="404">If a subscription doesn't exist.</response>
[HttpGet]
[Route("sub")]
[NotFoundResultFilter]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<DeviceSubscriptionWithStatus>> GetC2DMessageSubscription(string deviceId, CancellationToken cancellationToken = default)
{
return await _subscriptionService.GetDataSubscription(Logger, deviceId, DeviceSubscriptionType.C2DMessages, cancellationToken);
}
/// <summary>
/// Creates or updates the current C2D message subscription for a device.
/// </summary>
/// <remarks>
/// When the device receives a new C2D message from IoTHub, the service will send an event to the desired callback URL.
///
/// Example event:
/// {
/// "eventType": "string",
/// "deviceId": "string",
/// "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
/// "messageBody": {},
/// "properties": {
/// "prop1": "string",
/// "prop2": "string",
/// },
/// "messageId": "string",
/// "expirtyTimeUtC": "2020-12-04T01:06:14.251Z"
/// }
///
/// The response status code of the callback URL will determine how the service will acknowledge a message:
/// - Response code between 200 and 299: the service will complete the message.
/// - Response code between 400 and 499: the service will reject the message.
/// - Any other response status: the service will abandon the message, causing IotHub to redeliver it.
///
/// For a detailed overview of C2D messages, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d.
/// </remarks>
/// <response code="200">The created or updated C2D message subscription.</response>
[HttpPut]
[Route("sub")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<DeviceSubscriptionWithStatus>> CreateOrUpdateC2DMessageSubscription(string deviceId, SubscriptionCreateOrUpdateBody body, CancellationToken cancellationToken = default)
{
return await _subscriptionService.CreateOrUpdateDataSubscription(Logger, deviceId, DeviceSubscriptionType.C2DMessages, body.CallbackUrl, cancellationToken);
}
/// <summary>
/// Deletes the current C2D message subscription for a device.
/// </summary>
/// <response code="204">Subscription deleted successfully.</response>
[HttpDelete]
[Route("sub")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DeleteC2DMessageSubscription(string deviceId, CancellationToken cancellationToken = default)
{
await _subscriptionService.DeleteDataSubscription(Logger, deviceId, DeviceSubscriptionType.C2DMessages, cancellationToken);
return NoContent();
}
}
}

Просмотреть файл

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using DeviceBridge.Common.Exceptions;
namespace DeviceBridge.Controllers
{
/// <summary>
/// The response body returned if an error occours.
/// </summary>
public class HttpErrorBody
{
public HttpErrorBody(BridgeException e)
{
this.Message = e.Message;
}
public string Message { get; }
}
}

Просмотреть файл

@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using DeviceBridge.Models;
using DeviceBridge.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace DeviceBridge.Controllers
{
[Route("devices/{deviceId}/[controller]")]
[ApiController]
public class MessagesController : BaseController
{
private readonly IBridgeService _bridgeService;
public MessagesController(NLog.Logger logger, IBridgeService bridgeService)
: base(logger)
{
_bridgeService = bridgeService;
}
/// <summary>
/// Sends a device message to IoTHub.
/// </summary>
/// <remarks>
/// Example request:
///
/// POST /devices/{deviceId}/messages/events
/// {
/// "data": {
/// "temperature": 4.8,
/// "humidity": 31
/// }
/// }
/// .
/// </remarks>
/// <response code="200">Message sent successfully.</response>
[HttpPost]
[Route("events")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> SendMessage(string deviceId, MessageBody message, CancellationToken cancellationToken = default)
{
// Force timestamp to be interpreted as UTC.
if (message.CreationTimeUtc is DateTime)
{
message.CreationTimeUtc = DateTime.SpecifyKind((DateTime)message.CreationTimeUtc, DateTimeKind.Utc);
}
await _bridgeService.SendTelemetry(Logger, deviceId, message.Data, cancellationToken, message.Properties, message.ComponentName, message.CreationTimeUtc);
return Ok();
}
}
}

Просмотреть файл

@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using DeviceBridge.Models;
using DeviceBridge.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NLog;
namespace DeviceBridge.Controllers
{
[Route("devices/{deviceId}/[controller]")]
[ApiController]
public class MethodsController : BaseController
{
private readonly SubscriptionService _subscriptionService;
public MethodsController(Logger logger, SubscriptionService subscriptionService)
: base(logger)
{
_subscriptionService = subscriptionService;
}
/// <summary>
/// Gets the current direct methods subscription for a device.
/// </summary>
/// <response code="200">The current direct methods subscription.</response>
/// <response code="404">If a subscription doesn't exist.</response>
[HttpGet]
[Route("sub")]
[NotFoundResultFilter]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<DeviceSubscriptionWithStatus>> GetMethodsSubscription(string deviceId, CancellationToken cancellationToken = default)
{
return await _subscriptionService.GetDataSubscription(Logger, deviceId, DeviceSubscriptionType.Methods, cancellationToken);
}
/// <summary>
/// Creates or updates the current direct methods subscription for a device.
/// </summary>
/// <remarks>
/// When the device receives a direct method invocation from IoTHub, the service will send an event to the desired callback URL.
///
/// Example event:
/// {
/// "eventType": "string",
/// "deviceId": "string",
/// "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
/// "methodName": "string",
/// "requestData": {}
/// }
///
/// The callback may return an optional response body, which will be sent to IoTHub as the method response:
///
/// Example callback response:
/// {
/// "status": 200,
/// "payload": {}
/// }
/// .
/// </remarks>
/// <response code="200">The created or updated C2D message subscription.</response>
[HttpPut]
[Route("sub")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<DeviceSubscriptionWithStatus>> CreateOrUpdateMethodsSubscription(string deviceId, SubscriptionCreateOrUpdateBody body, CancellationToken cancellationToken = default)
{
return await _subscriptionService.CreateOrUpdateDataSubscription(Logger, deviceId, DeviceSubscriptionType.Methods, body.CallbackUrl, cancellationToken);
}
/// <summary>
/// Deletes the current direct methods subscription for a device.
/// </summary>
/// <response code="204">Subscription deleted successfully.</response>
[HttpDelete]
[Route("sub")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DeleteMethodsSubscription(string deviceId, CancellationToken cancellationToken = default)
{
await _subscriptionService.DeleteDataSubscription(Logger, deviceId, DeviceSubscriptionType.Methods, cancellationToken);
return NoContent();
}
}
}

Просмотреть файл

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
/// <summary>
/// Converts a null return value into a 404.
/// </summary>
public class NotFoundResultFilterAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
if (context.Result is ObjectResult objectResult && objectResult.Value == null)
{
context.Result = new NotFoundResult();
}
}
}

Просмотреть файл

@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
using DeviceBridge.Models;
using DeviceBridge.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace DeviceBridge.Controllers
{
[Route("devices/{deviceId}/[controller]")]
[ApiController]
public class RegistrationController : BaseController
{
private readonly ConnectionManager _connectionManager;
public RegistrationController(NLog.Logger logger, ConnectionManager connectionManager)
: base(logger)
{
_connectionManager = connectionManager;
}
/// <summary>
/// Performs DPS registration for a device, optionally assigning it to a model.
/// </summary>
/// <remarks>
/// The registration result is internally cached to be used in future connections.
/// This route is only intended for ahead-of-time registration of devices with the bridge and assignment to a specific model. To access all DPS registration features,
/// including sending custom registration payload and getting the assigned hub, please use the DPS REST API (https://docs.microsoft.com/en-us/rest/api/iot-dps/).
///
/// <b>NOTE:</b> DPS registration is a long-running operation, so calls to this route may take a long time to return. If this is a concern, use the DPS REST API directly, which provides
/// support for long-running operation status lookup.
/// </remarks>
/// <response code="200">Registration successful.</response>
[HttpPost]
[Route("")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> Register(string deviceId, RegistrationBody body, CancellationToken cancellationToken = default)
{
await _connectionManager.StandaloneDpsRegistrationAsync(Logger, deviceId, body.ModelId, cancellationToken);
return Ok();
}
}
}

Просмотреть файл

@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using DeviceBridge.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace DeviceBridge.Controllers
{
[Route("devices/{deviceId}/[controller]")]
[ApiController]
public class ResyncController : BaseController
{
private readonly SubscriptionService _subscriptionService;
public ResyncController(NLog.Logger logger, SubscriptionService subscriptionService)
: base(logger)
{
_subscriptionService = subscriptionService;
}
/// <summary>
/// Forces a full synchronization of all subscriptions for this device and attempts to restart any subscriptions in a stopped state.
/// </summary>
/// <remarks>
/// Internally it forces the reconnection of the device if it's in a permanent failure state, due for instance to:
/// - Bad credentials.
/// - Device was previously disabled in the cloud side.
/// - Automatic retries expired (e.g., due to a long period without network connectivity).
/// </remarks>
/// <response code="202">Resynchronization started.</response>
[HttpPost]
[Route("")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
public ActionResult Resync(string deviceId)
{
var _ = _subscriptionService.SynchronizeDeviceDbAndEngineDataSubscriptionsAsync(deviceId, false /* fetch latest subscriptions from DB */, true /* retry failed connection */);
return Accepted();
}
}
}

Просмотреть файл

@ -0,0 +1,127 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using DeviceBridge.Models;
using DeviceBridge.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
namespace DeviceBridge.Controllers
{
[Route("devices/{deviceId}/[controller]")]
[ApiController]
public class TwinController : BaseController
{
private readonly SubscriptionService _subscriptionService;
private readonly BridgeService _bridgeService;
public TwinController(Logger logger, SubscriptionService subscriptionService, BridgeService bridgeService)
: base(logger)
{
_subscriptionService = subscriptionService;
_bridgeService = bridgeService;
}
/// <summary>
/// Gets the device twin.
/// </summary>
/// <response code="200">The device twin.</response>
[HttpGet]
[Route("")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<DeviceTwin>> GetTwin(string deviceId, CancellationToken cancellationToken = default)
{
var response = new DeviceTwin()
{
Twin = new JRaw((await _bridgeService.GetTwin(Logger, deviceId, cancellationToken)).ToJson()),
};
return Content(JsonConvert.SerializeObject(response), "application/json");
}
/// <summary>
/// Updates reported properties in the device twin.
/// </summary>
/// <remarks>
/// Example request:
///
/// PATCH /devices/{deviceId}/properties/reported
/// {
/// "patch": {
/// "fanSpeed": 35,
/// "serial": "ABC"
/// }
/// }
/// .
/// </remarks>
/// <response code="204">Twin updated successfully.</response>
[HttpPatch]
[Route("properties/reported")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UpdateReportedProperties(string deviceId, ReportedPropertiesPatch body, CancellationToken cancellationToken = default)
{
await _bridgeService.UpdateReportedProperties(Logger, deviceId, body.Patch, cancellationToken);
return NoContent();
}
/// <summary>
/// Gets the current desired property change subscription for a device.
/// </summary>
/// <response code="200">The current desired property change subscription.</response>
/// <response code="404">If a subscription doesn't exist.</response>
[HttpGet]
[Route("properties/desired/sub")]
[NotFoundResultFilter]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<DeviceSubscriptionWithStatus>> GetDesiredPropertiesSubscription(string deviceId, CancellationToken cancellationToken = default)
{
return await _subscriptionService.GetDataSubscription(Logger, deviceId, DeviceSubscriptionType.DesiredProperties, cancellationToken);
}
/// <summary>
/// Creates or updates the current desired property change subscription for a device.
/// </summary>
/// <remarks>
/// When the device receives a new desired property change from IoTHub, the service will send an event to the desired callback URL.
///
/// Example event:
/// {
/// "eventType": "string",
/// "deviceId": "string",
/// "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
/// "desiredProperties": {
/// "prop1": "string",
/// "prop2": 12,
/// "prop3": {},
/// }
/// }
/// .
/// </remarks>
/// <response code="200">The created or updated C2D message subscription.</response>
[HttpPut]
[Route("properties/desired/sub")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<DeviceSubscriptionWithStatus>> CreateOrUpdateDesiredPropertiesSubscription(string deviceId, SubscriptionCreateOrUpdateBody body, CancellationToken cancellationToken = default)
{
return await _subscriptionService.CreateOrUpdateDataSubscription(Logger, deviceId, DeviceSubscriptionType.DesiredProperties, body.CallbackUrl, cancellationToken);
}
/// <summary>
/// Deletes the current desired property change subscription for a device.
/// </summary>
/// <response code="204">Subscription deleted successfully.</response>
[HttpDelete]
[Route("properties/desired/sub")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DeleteDesiredPropertiesSubscription(string deviceId, CancellationToken cancellationToken = default)
{
await _subscriptionService.DeleteDataSubscription(Logger, deviceId, DeviceSubscriptionType.DesiredProperties, cancellationToken);
return NoContent();
}
}
}

Просмотреть файл

@ -0,0 +1,70 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup>
<!--
Make sure any documentation comments which are included in code get checked for syntax during the build, but do
not report warnings for missing comments.
CS1573: Parameter 'parameter' has no matching param tag in the XML comment for 'parameter' (but other parameters do)
CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member'
-->
<DocumentationFile>$(OutputPath)$(AssemblyName).xml</DocumentationFile>
<NoWarn>$(NoWarn),1573,1591,1712</NoWarn>
</PropertyGroup>
<ItemGroup>
<Content Remove="NLog.config" />
</ItemGroup>
<ItemGroup>
<None Include="NLog.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.0" />
<PackageReference Include="Microsoft.AspNetCore.AzureKeyVault.HostingStartup" Version="2.0.4" />
<PackageReference Include="Microsoft.Azure.Devices" Version="1.28.1" />
<PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.33.1" />
<PackageReference Include="Microsoft.Azure.Devices.Provisioning.Client" Version="1.6.0" />
<PackageReference Include="Microsoft.Azure.Devices.Provisioning.Transport.Amqp" Version="1.3.1" />
<PackageReference Include="Microsoft.Azure.Devices.Provisioning.Transport.Http" Version="1.2.4" />
<PackageReference Include="Microsoft.Azure.KeyVault" Version="3.0.5" />
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.5.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.4" />
<PackageReference Include="NLog" Version="4.7.4" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.9.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Collections" Version="4.3.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
<PackageReference Include="System.Data.SqlClient" Version="4.8.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.6.0" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="stylecop.json" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,620 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>DeviceBridge</name>
</assembly>
<members>
<member name="M:DeviceBridge.Controllers.ConnectionStatusController.GetCurrentConnectionStatus(System.String)">
<summary>
Gets that latest connection status for a device.
</summary>
<remarks>
For a detailed description of each status, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
</remarks>
<response code="200">The latest connection status and reason.</response>
<response code="404">If the connection status is not known (i.e., the device hasn't attempted to connect).</response>
</member>
<member name="M:DeviceBridge.Controllers.ConnectionStatusController.GetConnectionStatusSubscription(System.String,System.Threading.CancellationToken)">
<summary>
Gets the current connection status change subscription for a device.
</summary>
<response code="200">The current connection status subscription.</response>
<response code="404">If a subscription doesn't exist.</response>
</member>
<member name="M:DeviceBridge.Controllers.ConnectionStatusController.CreateOrUpdateConnectionStatusSubscription(System.String,DeviceBridge.Models.SubscriptionCreateOrUpdateBody,System.Threading.CancellationToken)">
<summary>
Creates or updates the current connection status change subscription for a device.
</summary>
<remarks>
When the internal connection status of a device changes, the service will send an event to the desired callback URL.
Example event:
{
"eventType": "string",
"deviceId": "string",
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
"status": "string",
"reason": "string"
}
For a detailed description of each status, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
</remarks>
<response code="200">The created or updated connection status subscription.</response>
</member>
<member name="M:DeviceBridge.Controllers.ConnectionStatusController.DeleteConnectionStatusSubscription(System.String,System.Threading.CancellationToken)">
<summary>
Deletes the current connection status change subscription for a device.
</summary>
<response code="204">Subscription deleted successfully.</response>
</member>
<member name="M:DeviceBridge.Controllers.DeviceBoundController.GetC2DMessageSubscription(System.String,System.Threading.CancellationToken)">
<summary>
Gets the current C2D message subscription for a device.
</summary>
<response code="200">The current C2D message subscription.</response>
<response code="404">If a subscription doesn't exist.</response>
</member>
<member name="M:DeviceBridge.Controllers.DeviceBoundController.CreateOrUpdateC2DMessageSubscription(System.String,DeviceBridge.Models.SubscriptionCreateOrUpdateBody,System.Threading.CancellationToken)">
<summary>
Creates or updates the current C2D message subscription for a device.
</summary>
<remarks>
When the device receives a new C2D message from IoTHub, the service will send an event to the desired callback URL.
Example event:
{
"eventType": "string",
"deviceId": "string",
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
"messageBody": {},
"properties": {
"prop1": "string",
"prop2": "string",
},
"messageId": "string",
"expirtyTimeUtC": "2020-12-04T01:06:14.251Z"
}
The response status code of the callback URL will determine how the service will acknowledge a message:
- Response code between 200 and 299: the service will complete the message.
- Response code between 400 and 499: the service will reject the message.
- Any other response status: the service will abandon the message, causing IotHub to redeliver it.
For a detailed overview of C2D messages, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d.
</remarks>
<response code="200">The created or updated C2D message subscription.</response>
</member>
<member name="M:DeviceBridge.Controllers.DeviceBoundController.DeleteC2DMessageSubscription(System.String,System.Threading.CancellationToken)">
<summary>
Deletes the current C2D message subscription for a device.
</summary>
<response code="204">Subscription deleted successfully.</response>
</member>
<member name="T:DeviceBridge.Controllers.HttpErrorBody">
<summary>
The response body returned if an error occours.
</summary>
</member>
<member name="M:DeviceBridge.Controllers.MessagesController.SendMessage(System.String,DeviceBridge.Models.MessageBody,System.Threading.CancellationToken)">
<summary>
Sends a device message to IoTHub.
</summary>
<remarks>
Example request:
POST /devices/{deviceId}/messages/events
{
"data": {
"temperature": 4.8,
"humidity": 31
}
}
.
</remarks>
<response code="200">Message sent successfully.</response>
</member>
<member name="M:DeviceBridge.Controllers.MethodsController.GetMethodsSubscription(System.String,System.Threading.CancellationToken)">
<summary>
Gets the current direct methods subscription for a device.
</summary>
<response code="200">The current direct methods subscription.</response>
<response code="404">If a subscription doesn't exist.</response>
</member>
<member name="M:DeviceBridge.Controllers.MethodsController.CreateOrUpdateMethodsSubscription(System.String,DeviceBridge.Models.SubscriptionCreateOrUpdateBody,System.Threading.CancellationToken)">
<summary>
Creates or updates the current direct methods subscription for a device.
</summary>
<remarks>
When the device receives a direct method invocation from IoTHub, the service will send an event to the desired callback URL.
Example event:
{
"eventType": "string",
"deviceId": "string",
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
"methodName": "string",
"requestData": {}
}
The callback may return an optional response body, which will be sent to IoTHub as the method response:
Example callback response:
{
"status": 200,
"payload": {}
}
.
</remarks>
<response code="200">The created or updated C2D message subscription.</response>
</member>
<member name="M:DeviceBridge.Controllers.MethodsController.DeleteMethodsSubscription(System.String,System.Threading.CancellationToken)">
<summary>
Deletes the current direct methods subscription for a device.
</summary>
<response code="204">Subscription deleted successfully.</response>
</member>
<member name="M:DeviceBridge.Controllers.RegistrationController.Register(System.String,DeviceBridge.Models.RegistrationBody,System.Threading.CancellationToken)">
<summary>
Performs DPS registration for a device, optionally assigning it to a model.
</summary>
<remarks>
The registration result is internally cached to be used in future connections.
This route is only intended for ahead-of-time registration of devices with the bridge and assignment to a specific model. To access all DPS registration features,
including sending custom registration payload and getting the assigned hub, please use the DPS REST API (https://docs.microsoft.com/en-us/rest/api/iot-dps/).
<b>NOTE:</b> DPS registration is a long-running operation, so calls to this route may take a long time to return. If this is a concern, use the DPS REST API directly, which provides
support for long-running operation status lookup.
</remarks>
<response code="200">Registration successful.</response>
</member>
<member name="M:DeviceBridge.Controllers.ResyncController.Resync(System.String)">
<summary>
Forces a full synchronization of all subscriptions for this device and attempts to restart any subscriptions in a stopped state.
</summary>
<remarks>
Internally it forces the reconnection of the device if it's in a permanent failure state, due for instance to:
- Bad credentials.
- Device was previously disabled in the cloud side.
- Automatic retries expired (e.g., due to a long period without network connectivity).
</remarks>
<response code="202">Resynchronization started.</response>
</member>
<member name="M:DeviceBridge.Controllers.TwinController.GetTwin(System.String,System.Threading.CancellationToken)">
<summary>
Gets the device twin.
</summary>
<response code="200">The device twin.</response>
</member>
<member name="M:DeviceBridge.Controllers.TwinController.UpdateReportedProperties(System.String,DeviceBridge.Models.ReportedPropertiesPatch,System.Threading.CancellationToken)">
<summary>
Updates reported properties in the device twin.
</summary>
<remarks>
Example request:
PATCH /devices/{deviceId}/properties/reported
{
"patch": {
"fanSpeed": 35,
"serial": "ABC"
}
}
.
</remarks>
<response code="204">Twin updated successfully.</response>
</member>
<member name="M:DeviceBridge.Controllers.TwinController.GetDesiredPropertiesSubscription(System.String,System.Threading.CancellationToken)">
<summary>
Gets the current desired property change subscription for a device.
</summary>
<response code="200">The current desired property change subscription.</response>
<response code="404">If a subscription doesn't exist.</response>
</member>
<member name="M:DeviceBridge.Controllers.TwinController.CreateOrUpdateDesiredPropertiesSubscription(System.String,DeviceBridge.Models.SubscriptionCreateOrUpdateBody,System.Threading.CancellationToken)">
<summary>
Creates or updates the current desired property change subscription for a device.
</summary>
<remarks>
When the device receives a new desired property change from IoTHub, the service will send an event to the desired callback URL.
Example event:
{
"eventType": "string",
"deviceId": "string",
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
"desiredProperties": {
"prop1": "string",
"prop2": 12,
"prop3": {},
}
}
.
</remarks>
<response code="200">The created or updated C2D message subscription.</response>
</member>
<member name="M:DeviceBridge.Controllers.TwinController.DeleteDesiredPropertiesSubscription(System.String,System.Threading.CancellationToken)">
<summary>
Deletes the current desired property change subscription for a device.
</summary>
<response code="204">Subscription deleted successfully.</response>
</member>
<member name="F:DeviceBridge.Management.DbSchemaSetup.CreateUpsertDeviceSubscriptionProcedureQuery">
<summary>
Tries to create a device subscription. If one already exists, updates it.
Concurrent calls to this procedure will not generate a failure.
Outputs the creation time.
</summary>
</member>
<member name="F:DeviceBridge.Management.DbSchemaSetup.CreateUpsertHubCacheEntryProcedureQuery">
<summary>
Tries to add a hub cache entry for a device. If one already exists, updates it.
</summary>
</member>
<member name="F:DeviceBridge.Management.DbSchemaSetup.CreateGetHubCacheEntriesPagedProcedureQuery">
<summary>
Fetches a page of entries from the HubCache table.
The page index parameter is zero-based.
</summary>
</member>
<member name="F:DeviceBridge.Management.DbSchemaSetup.CreateGetDeviceSubscriptionsPagedProcedureQuery">
<summary>
Fetches a page of device subscriptions.
The page index parameter is zero-based.
Results are ordered by deviceId and subscriptionType.
</summary>
</member>
<member name="T:DeviceBridge.Management.EncryptionSetup">
<summary>
Encryption setup is responsible for creating encryption keys, and re-encrypting sensitive data in the database.
</summary>
</member>
<member name="M:DeviceBridge.Management.EncryptionSetup.Reencrypt">
<summary>
Creates and saves a new encryption key in the database.
Reencrypts all callback URL's in the database.
</summary>
<returns>Empty task.</returns>
</member>
<member name="M:DeviceBridge.Models.DeviceSubscriptionType.FromString(System.String)">
<summary>
Returns the corresponding singleton for a give subscription type.
</summary>
<exception cref="T:DeviceBridge.Common.Exceptions.UnknownDeviceSubscriptionTypeException">If the given value is not a valid subscription type.</exception>
<param name="value">The string representation subscription type.</param>
<returns>The corresponding singleton for the subscription type.</returns>
</member>
<member name="M:DeviceBridge.Models.DeviceSubscriptionType.IsDataSubscription">
<summary>
A data subscription deals with events that depend on a device connection (properties, methods, C2D messages).
A connection status subscription, in the other hand, is just a subscription to engine events that is always active and doesn't depend on a connection.
</summary>
<returns>Whether this is a data subscription or not.</returns>
</member>
<member name="T:DeviceBridge.Models.ReceiveMessageCallbackStatus">
<summary>Enum ReceiveMessageCallbackStatus.</summary>
</member>
<member name="F:DeviceBridge.Models.ReceiveMessageCallbackStatus.Accept">
<summary>Client should accept message.</summary>
</member>
<member name="F:DeviceBridge.Models.ReceiveMessageCallbackStatus.Reject">
<summary>Client should reject message.</summary>
</member>
<member name="F:DeviceBridge.Models.ReceiveMessageCallbackStatus.Abandon">
<summary>Client should abandon message.</summary>
</member>
<member name="F:DeviceBridge.Providers.StorageProvider.TableNotFoundErrorNumber">
<summary>
Taken from https://docs.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors.
</summary>
</member>
<member name="M:DeviceBridge.Providers.StorageProvider.ListAllSubscriptionsOrderedByDeviceId(NLog.Logger)">
<summary>
Lists all active subscriptions of all types ordered by device Id.
</summary>
<param name="logger">Logger to be used.</param>
<returns>List of all subscriptions of all types ordered by device Id.</returns>
</member>
<member name="M:DeviceBridge.Providers.StorageProvider.ListDeviceSubscriptions(NLog.Logger,System.String)">
<summary>
Lists all active subscriptions of all types for a device.
</summary>
<param name="logger">Logger to be used.</param>
<param name="deviceId">Id of the device to get the subscriptions for.</param>
<returns>List of subscriptions for the given device.</returns>
</member>
<member name="M:DeviceBridge.Providers.StorageProvider.GetDeviceSubscription(NLog.Logger,System.String,DeviceBridge.Models.DeviceSubscriptionType,System.Threading.CancellationToken)">
<summary>
Gets an active subscription of the specified type for a device, if one exists.
</summary>
<param name="logger">Logger to be used.</param>
<param name="deviceId">Id of the device to get the subscription for.</param>
<param name="subscriptionType">Type of the subscription to get.</param>
<param name="cancellationToken">Cancellation token.</param>
<returns>The subscription, if exists. Null otherwise.</returns>
</member>
<member name="M:DeviceBridge.Providers.StorageProvider.CreateOrUpdateDeviceSubscription(NLog.Logger,System.String,DeviceBridge.Models.DeviceSubscriptionType,System.String,System.Threading.CancellationToken)">
<summary>
Creates a subscription of the given type for the given device. If one already exists, it's updated with a new creation time and callback URL.
Returns the created or updated subscription.
</summary>
<param name="logger">Logger to be used.</param>
<param name="deviceId">Id of the device to create the subscription for.</param>
<param name="subscriptionType">Type of the subscription to be created.</param>
<param name="callbackUrl">Callback URL of the subscription.</param>
<param name="cancellationToken">Cancellation token.</param>
<returns>The created subscription.</returns>
</member>
<member name="M:DeviceBridge.Providers.StorageProvider.DeleteDeviceSubscription(NLog.Logger,System.String,DeviceBridge.Models.DeviceSubscriptionType,System.Threading.CancellationToken)">
<summary>
Deletes the subscription of the given type for a device, if one exists.
</summary>
<param name="logger">Logger to be used.</param>
<param name="deviceId">Id of the device to delete the subscription for.</param>
<param name="subscriptionType">Type of the subscription to be deleted.</param>
<param name="cancellationToken">Cancellation token.</param>
</member>
<member name="M:DeviceBridge.Providers.StorageProvider.GcHubCache(NLog.Logger)">
<summary>
Deletes from the hub cache any device that doesn't have a subscription and hasn't attempted to open a connection in the past week.
</summary>
<param name="logger">Logger to be used.</param>
</member>
<member name="M:DeviceBridge.Providers.StorageProvider.RenewHubCacheEntries(NLog.Logger,System.Collections.Generic.List{System.String})">
<summary>
Renews the Hub cache timestamp for a list of devices.
</summary>
<param name="logger">The logger instance to use.</param>
<param name="deviceIds">List of device Ids to renew.</param>
</member>
<member name="M:DeviceBridge.Providers.StorageProvider.AddOrUpdateHubCacheEntry(NLog.Logger,System.String,System.String)">
<summary>
Adds or updates a Hub cache entry for a device.
</summary>
<param name="logger">Logger to be used.</param>
<param name="deviceId">Id of the device for the new cache entry.</param>
<param name="hub">Hub to be added to the cache entry for the device.</param>
</member>
<member name="M:DeviceBridge.Providers.StorageProvider.ListHubCacheEntries(NLog.Logger)">
<summary>
Lists all entries in the Hub cache.
</summary>
<param name="logger">Logger to be used.</param>
<returns>List of all entries in the DB hub cache.</returns>
</member>
<member name="M:DeviceBridge.Providers.StorageProvider.Exec(NLog.Logger,System.String)">
<summary>
Executes an arbitrary SQL command against the DB.
</summary>
<param name="logger">Logger instance to use.</param>
<param name="sql">SQL command to run.</param>
</member>
<member name="M:DeviceBridge.Providers.StorageProvider.TranslateSqlException(System.Exception)">
<summary>
Translates SQL exceptions into service exceptions.
</summary>
<param name="e">Original SQL exception.</param>
<returns>The translated service exception.</returns>
</member>
<member name="T:DeviceBridge.Services.ConnectionManager">
<summary>
Manages SDK device connections. A connection can have two modes: permanent or temporary.
A permanent connection is one that should be kept open indefinitely, until the user explicitly decides to close it.
This connection type is used for any type of persistent subscription that needs an always-on connection, such as desired property changes.
A temporary connection is used for point-in-time operations, such as sending telemetry and getting the current device twin.
This type of connection lives for a few minutes (currently 9-10 mins) and is automatically closed. It's used to increase the chances
of a connection being already open when a point-in-time operation happens but also to make sure that connections don't stay
open for too long for silent devices.
Temporary connections are rewed whenever a new operation happens. Deleting a permanent connection falls back to a temporary connection if one exists.
</summary>
</member>
<member name="M:DeviceBridge.Services.ConnectionManager.StartExpiredConnectionCleanupAsync">
<summary>
Attempts to cleanup expired temporary connections every 10 seconds.
</summary>
</member>
<member name="M:DeviceBridge.Services.ConnectionManager.GetDeviceStatus(System.String)">
<summary>
See <see href="https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet">ConnectionStatus documentation</see>
for a detailed description of each status and reaosn.
</summary>
<param name="deviceId">Id of the device to get the status for.</param>
<returns>The last known connection status of the device or null if the device has never connected.</returns>
</member>
<member name="M:DeviceBridge.Services.ConnectionManager.GetDevicesThatConnectedSince(System.DateTime)">
<summary>
Gets the list of all devices that attempted to connect since a given timestamp.
</summary>
<param name="threshold">Timestamp to filter by.</param>
<returns>The list of device Ids that attempted to connect since the given timestamp.</returns>
</member>
<member name="M:DeviceBridge.Services.ConnectionManager.AssertDeviceConnectionOpenAsync(System.String,System.Boolean,System.Boolean,System.Nullable{System.Threading.CancellationToken})">
<summary>
Asserts that a permanent or temporary connection for this device is open.
A temporary connection is guaranteed to live for only a few minutes (currently 9-11 minutes).
</summary>
<param name="deviceId">Id of the device to open a connection for.</param>
<param name="temporary">Whether the requested connection is temporary or permanent.</param>
<param name="recreateFailedClient">Forces the recreation of the current client if it's in a permanent failure state (i.e., the SDK will not retry).</param>
<param name="cancellationToken">Optional cancellation token.</param>
</member>
<member name="M:DeviceBridge.Services.ConnectionManager.AssertDeviceConnectionClosedAsync(System.String,System.Boolean)">
<summary>
Asserts that the permanent or temporary connection for this device is closed. The temporary connection is only closed
if it has expired. The underlying connection is not actually closed if we're trying to delete a permanent connection and
a temporary one exists or vice-versa.
</summary>
<param name="deviceId">Id of the decide for which the connection should be closed.</param>
<param name="temporary">Whether the temporary or permanent connection should be closed.</param>
</member>
<member name="M:DeviceBridge.Services.ConnectionManager.StandaloneDpsRegistrationAsync(NLog.Logger,System.String,System.String,System.Nullable{System.Threading.CancellationToken})">
<summary>
Performs a standalone DPS registration (not part of a device connection). The registration data is cached for future connections.
</summary>
<param name="logger">Logger instance to use.</param>
<param name="deviceId">Id of the device to register.</param>
<param name="modelId">Optional model Id to assign the device to.</param>
<param name="cancellationToken">Optional cancellation token.</param>
</member>
<member name="M:DeviceBridge.Services.ConnectionManager.SetDesiredPropertyUpdateCallbackAsync(System.String,System.String,Microsoft.Azure.Devices.Client.DesiredPropertyUpdateCallback)">
<summary>
Sets the desired property change callback. The callback is not tied to a connection lifetime and will be active whenever the device
status is marked as online.
</summary>
<param name="deviceId">Id to the device to set the callback for.</param>
<param name="id">string identifying the callback, for tracking purposes.</param>
<param name="callback">The callback to be called when a desired property update is received.</param>
</member>
<member name="M:DeviceBridge.Services.ConnectionManager.SetMethodCallbackAsync(System.String,System.String,Microsoft.Azure.Devices.Client.MethodCallback)">
<summary>
Sets the direct method callback for a device. The callback is not tied to a connection lifetime and will be active whenever the device
status is marked as online.
</summary>
<param name="deviceId">Id to the device to set the callback for.</param>
<param name="id">string identifying the callback, for tracking purposes.</param>
<param name="callback">The callback to be called when a method invocation is received.</param>
</member>
<member name="M:DeviceBridge.Services.ConnectionManager.SetMessageCallbackAsync(System.String,System.String,System.Func{Microsoft.Azure.Devices.Client.Message,System.Threading.Tasks.Task{DeviceBridge.Models.ReceiveMessageCallbackStatus}})">
<summary>
Sets the direct message callback for a device. The callback is not tied to a connection lifetime and will be active whenever the device
status is marked as online.
</summary>
<param name="deviceId">Id to the device to set the callback for.</param>
<param name="id">string identifying the callback, for tracking purposes.</param>
<param name="callback">The callback to be called when a C2D message is received.</param>
</member>
<member name="M:DeviceBridge.Services.ConnectionManager.SetConnectionStatusCallback(System.String,System.Func{Microsoft.Azure.Devices.Client.ConnectionStatus,Microsoft.Azure.Devices.Client.ConnectionStatusChangeReason,System.Threading.Tasks.Task})">
<summary>
Sets the connection status change handler for a device.
</summary>
<param name="deviceId">Id of the device to set the callback for.</param>
<param name="callback">Callback to be called when the status of the device connection changes.</param>
</member>
<member name="M:DeviceBridge.Services.ConnectionManager.Dispose">
<summary>
Attempts to gracefully shutdown all SDK connections.
</summary>
</member>
<member name="M:DeviceBridge.Services.ConnectionManager.DpsRegisterInternalAsync(NLog.Logger,System.String,System.String,System.String,System.Nullable{System.Threading.CancellationToken})">
<summary>
Internal wrapper for DPS registration.
</summary>
<exception cref="T:DeviceBridge.Common.Exceptions.DpsRegistrationFailedWithUnknownStatusException">If the final registration status is not "assigned".</exception>
<param name="logger">Logger instance to use.</param>
<param name="deviceId">Id of the device to register.</param>
<param name="deviceKey">Key for the device.</param>
<param name="modelId">Optional model Id to be passed to DPS.</param>
<param name="cancellationToken">Optional cancellation token.</param>
<returns>The assigned hub for this device.</returns>
</member>
<member name="M:DeviceBridge.Services.ConnectionManager.BuildConnectionStatusChangeHandler(System.String)">
<summary>
Builds a connection change handler for a specific deviceId, which optionally calls a custom callback.
</summary>
<param name="deviceId">Id of the device for which to build the callback.</param>
<returns>The connection status change handler.</returns>
</member>
<member name="T:DeviceBridge.Services.CustomDeviceClientRetryPolicy">
<summary>
Extends the default SDK retry policy (ExponentialBackoff) to fail right away if the hub doesn't exist.
</summary>
</member>
<member name="M:DeviceBridge.Services.CustomDeviceClientRetryPolicy.ShouldRetry(System.Int32,System.Exception,System.TimeSpan@)">
<summary>
Returns true if, based on the parameters, the operation should be retried.
</summary>
<param name="currentRetryCount">How many times the operation has been retried.</param>
<param name="lastException">Operation exception.</param>
<param name="retryInterval">Next retry should be performed after this time interval.</param>
<returns>True if the operation should be retried, false otherwise.</returns>
</member>
<member name="T:DeviceBridge.Services.ExpiredConnectionCleanupHostedService">
<summary>
When the application starts, start the expired connection cleanup task.
</summary>
</member>
<member name="T:DeviceBridge.Services.HubCacheGcHostedService">
<summary>
Every 6 hours:
- renews the Hub cache entries for the devices that attempted to open a connection.
- runs the GC routine in the Hub cache, removing entries for any device that doesn't have a subscription and
hasn't connected in the last week.
</summary>
</member>
<member name="M:DeviceBridge.Services.SubscriptionService.StartDataSubscriptionsInitializationAsync">
<summary>
Starts the Initialization of data subscriptions for all devices based on the list fetched from the DB at service construction time.
For use during service startup.
</summary>
</member>
<member name="M:DeviceBridge.Services.SubscriptionService.SynchronizeDeviceDbAndEngineDataSubscriptionsAsync(System.String,System.Boolean,System.Boolean)">
<summary>
Asserts that the internal engine state (connection and callbacks) for a device reflects the subscriptions status and callbacks stored in the DB or in the initialization list.
Only applies to data subscriptions, i.e., not connection status subscriptions.
</summary>
<param name="deviceId">Id of the device to synchronize subscriptions for.</param>
<param name="useInitializationList">Whether subscriptions should be pulled from the initialization list or fetched from the DB.</param>
<param name="forceConnectionRetry">Whether to force a connection retry if the current connection is in a failed state.</param>
</member>
<member name="M:DeviceBridge.Services.SubscriptionService.ComputeDataSubscriptionStatus(System.String,DeviceBridge.Models.DeviceSubscriptionType,System.String)">
<summary>
Determines the status of a subscription based on the current state of the device client.
</summary>
<param name="deviceId">Id of the device for which to check the subscription status.</param>
<param name="subscriptionType">Type of the subscription that we want the status for.</param>
<param name="callbackUrl">URL for which we want to check the subscription status.</param>
<returns>Status of the subscription.</returns>
</member>
<member name="T:DeviceBridge.Services.SubscriptionStartupHostedService">
<summary>
When the application starts, initialize all device subscriptions that we have in the DB.
</summary>
</member>
<member name="T:DeviceBridge.Startup">
<summary>Class Startup.</summary>
</member>
<member name="M:DeviceBridge.Startup.#ctor(Microsoft.Extensions.Configuration.IConfiguration)">
<summary>Initializes a new instance of the <see cref="T:DeviceBridge.Startup"/> class.</summary>
<param name="configuration">The configuration.</param>
</member>
<member name="M:DeviceBridge.Startup.ConfigureServices(Microsoft.Extensions.DependencyInjection.IServiceCollection)">
<summary>This method gets called by the runtime. Use this method to add services to the container.</summary>
<param name="services">The services.</param>
</member>
<member name="M:DeviceBridge.Startup.Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder,Microsoft.AspNetCore.Hosting.IWebHostEnvironment,Microsoft.Extensions.Hosting.IHostApplicationLifetime,DeviceBridge.Services.ConnectionManager)">
<summary>This method gets called by the runtime. Use this method to configure the HTTP request pipeline..</summary>
<param name="app">The application.</param>
<param name="env">The env.</param>
<param name="lifetime">The lifetime.</param>
<param name="connectionManager">The connection manager.</param>
</member>
<member name="M:DeviceBridge.Startup.GetRetryPolicy">
<summary>
<para>Gets the retry policy, used in HttpClient.</para>
</summary>
<returns>IAsyncPolicy&lt;HttpResponseMessage&gt;.</returns>
</member>
<member name="M:Utils.GuidFromString(System.String)">
<summary>
Generates a GUID hashed from an input string.
</summary>
<param name="input">Input to generate the GUID from.</param>
<returns>GUID hashed from input.</returns>
</member>
<member name="M:Utils.GetSqlConnectionString(NLog.Logger,DeviceBridge.Providers.SecretsProvider)">
<summary>
Fetches the sql connection string.
</summary>
<param name="logger">Logger.</param>
<param name="secretsProvider">Secrets provider for retrieving credentials.</param>
<returns>The sql connection string.</returns>
</member>
<member name="T:NotFoundResultFilterAttribute">
<summary>
Converts a null return value into a 404.
</summary>
</member>
</members>
</doc>

Просмотреть файл

@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Not all elements must be documented")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1604:Element documentation should have summary", Justification = "Not all elements must be documented")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1615:Element return value should be documented", Justification = "Not all elements must be documented")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1611:Element parameters should be documented", Justification = "Not all elements must be documented")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1601:Partial elements should be documented", Justification = "Not all elements must be documented")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Ok to omit this prefix")]
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives should be placed correctly", Justification = "Conflicts with VS rule")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "Using underscore for private class fields")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1312:Variable names should begin with lower-case letter", Justification = "Prevents the use of underscore for unused local variable")]

Просмотреть файл

@ -0,0 +1,162 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Threading.Tasks;
using DeviceBridge.Providers;
using DeviceBridge.Services;
using NLog;
namespace DeviceBridge.Management
{
public class DbSchemaSetup
{
private const string CreateDeviceSubscriptionsTableQuery =
@"IF OBJECT_ID('dbo.DeviceSubscriptions', 'U') IS NULL
BEGIN
CREATE TABLE DeviceSubscriptions(
DeviceId VARCHAR(255),
SubscriptionType VARCHAR(20),
CallbackUrl NVARCHAR(MAX) NOT NULL,
CreatedAt DATETIME NOT NULL,
CONSTRAINT pk_device_subscriptions PRIMARY KEY (DeviceId, SubscriptionType)
);
END";
private const string CreateHubCacheTableQuery =
@"IF OBJECT_ID('dbo.HubCache', 'U') IS NULL
BEGIN
CREATE TABLE HubCache(
DeviceId VARCHAR(255) PRIMARY KEY,
Hub VARCHAR(255) NOT NULL,
RenewedAt DATETIME NOT NULL
);
END";
/// <summary>
/// Tries to create a device subscription. If one already exists, updates it.
/// Concurrent calls to this procedure will not generate a failure.
/// Outputs the creation time.
/// </summary>
private const string CreateUpsertDeviceSubscriptionProcedureQuery =
@"IF NOT EXISTS (SELECT * FROM sys.objects WHERE type = 'P' AND OBJECT_ID = OBJECT_ID('dbo.upsertDeviceSubscription'))
BEGIN
EXEC(N'
CREATE PROCEDURE upsertDeviceSubscription
@DeviceId VARCHAR(255),
@SubscriptionType VARCHAR(20),
@CallbackUrl NVARCHAR(MAX),
@CreatedAt DATETIME OUTPUT
AS
DECLARE @CurrentTime DATETIME;
SET @CurrentTime = GETUTCDATE();
SET @CreatedAt = @CurrentTime;
BEGIN TRY
INSERT INTO DeviceSubscriptions(DeviceId, SubscriptionType, CallbackUrl, CreatedAt) VALUES(@DeviceId, @SubscriptionType, @CallbackUrl, @CurrentTime);
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 2627 -- Primary key violation
-- This update is a best-effort attempt, i.e., the subscription might have been deleted between the test and set.
UPDATE DeviceSubscriptions SET CallbackUrl = @CallbackUrl, CreatedAt = @CurrentTime WHERE DeviceId = @DeviceId AND SubscriptionType = @SubscriptionType;
END CATCH
');
END";
/// <summary>
/// Tries to add a hub cache entry for a device. If one already exists, updates it.
/// </summary>
private const string CreateUpsertHubCacheEntryProcedureQuery =
@"IF NOT EXISTS (SELECT * FROM sys.objects WHERE type = 'P' AND OBJECT_ID = OBJECT_ID('dbo.upsertHubCacheEntry'))
BEGIN
EXEC(N'
CREATE PROCEDURE upsertHubCacheEntry
@DeviceId VARCHAR(255),
@Hub VARCHAR(255)
AS
BEGIN TRY
INSERT INTO HubCache(DeviceId, Hub, RenewedAt) VALUES(@DeviceId, @Hub, GETUTCDATE());
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 2627 -- Primary key violation
UPDATE HubCache SET Hub = @Hub, RenewedAt = GETUTCDATE() WHERE DeviceId = @DeviceId;
END CATCH
');
END";
/// <summary>
/// Fetches a page of entries from the HubCache table.
/// The page index parameter is zero-based.
/// </summary>
private const string CreateGetHubCacheEntriesPagedProcedureQuery =
@"IF NOT EXISTS (SELECT * FROM sys.objects WHERE type = 'P' AND OBJECT_ID = OBJECT_ID('dbo.getHubCacheEntriesPaged'))
BEGIN
EXEC(N'
CREATE PROCEDURE getHubCacheEntriesPaged
@PageIndex INT,
@RowsPerPage INT
AS
SELECT * FROM HubCache
ORDER BY DeviceId
OFFSET @PageIndex*@RowsPerPage ROWS
FETCH NEXT @RowsPerPage ROWS ONLY
');
END";
/// <summary>
/// Fetches a page of device subscriptions.
/// The page index parameter is zero-based.
/// Results are ordered by deviceId and subscriptionType.
/// </summary>
private const string CreateGetDeviceSubscriptionsPagedProcedureQuery =
@"IF NOT EXISTS (SELECT * FROM sys.objects WHERE type = 'P' AND OBJECT_ID = OBJECT_ID('dbo.getDeviceSubscriptionsPaged'))
BEGIN
EXEC(N'
CREATE PROCEDURE getDeviceSubscriptionsPaged
@PageIndex INT,
@RowsPerPage INT
AS
SELECT * FROM DeviceSubscriptions
ORDER BY DeviceId, SubscriptionType
OFFSET @PageIndex* @RowsPerPage ROWS
FETCH NEXT @RowsPerPage ROWS ONLY
');
END";
public async Task SetupDbSchema()
{
Logger logger = LogManager.GetCurrentClassLogger();
logger.Info("Initializing Key Vault and storage service.");
string kvUrl = Environment.GetEnvironmentVariable("KV_URL");
var secretsProvider = new SecretsProvider(kvUrl);
// Build connection string.
var sqlConnectionString = Utils.GetSqlConnectionString(logger, secretsProvider);
var encryptionService = new EncryptionService(logger, secretsProvider);
var storageProvider = new StorageProvider(sqlConnectionString, encryptionService);
// Run schema scripts.
logger.Info("Running DB schema setup scripts.");
logger.Info("Creating DeviceSubscriptions table");
await storageProvider.Exec(logger, CreateDeviceSubscriptionsTableQuery);
logger.Info("Creating HubCache table");
await storageProvider.Exec(logger, CreateHubCacheTableQuery);
logger.Info("Creating UpsertDeviceSubscription stored procedure");
await storageProvider.Exec(logger, CreateUpsertDeviceSubscriptionProcedureQuery);
logger.Info("Creating UpsertHubCacheEntry stored procedure");
await storageProvider.Exec(logger, CreateUpsertHubCacheEntryProcedureQuery);
logger.Info("Creating GetHubCacheEntriesPaged stored procedure");
await storageProvider.Exec(logger, CreateGetHubCacheEntriesPagedProcedureQuery);
logger.Info("Creating GetDeviceSubscriptionsPaged stored procedure");
await storageProvider.Exec(logger, CreateGetDeviceSubscriptionsPagedProcedureQuery);
logger.Info("Successfully executed DB schema setup.");
}
}
}

Просмотреть файл

@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using DeviceBridge.Providers;
using DeviceBridge.Services;
using NLog;
namespace DeviceBridge.Management
{
/// <summary>
/// Encryption setup is responsible for creating encryption keys, and re-encrypting sensitive data in the database.
/// </summary>
public class EncryptionSetup
{
/// <summary>
/// Creates and saves a new encryption key in the database.
/// Reencrypts all callback URL's in the database.
/// </summary>
/// <returns>Empty task.</returns>
public async Task Reencrypt()
{
Logger logger = LogManager.GetCurrentClassLogger();
logger.Info("Starting re-encryption.");
var kvUrl = Environment.GetEnvironmentVariable("KV_URL");
var secretsService = new SecretsProvider(kvUrl);
var sqlConnectionString = Utils.GetSqlConnectionString(logger, secretsService);
var secretsProvider = new SecretsProvider(kvUrl);
var encryptionService = new EncryptionService(logger, secretsProvider);
var storageProvider = new StorageProvider(sqlConnectionString, encryptionService);
var subs = await storageProvider.ListAllSubscriptionsOrderedByDeviceId(logger);
// Generate new key
await secretsProvider.PutEncyptionKey(logger, System.Text.Encoding.ASCII.GetString(Aes.Create().Key));
foreach (var sub in subs)
{
await storageProvider.CreateOrUpdateDeviceSubscription(logger, sub.DeviceId, sub.SubscriptionType, sub.CallbackUrl, CancellationToken.None);
}
logger.Info("Re-encryption complete.");
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DeviceBridge.Models
{
public class C2DMessageInvocationEventBody
{
[JsonProperty("eventType")]
public const string EventType = "C2DMessage";
[JsonProperty("deviceId")]
public string DeviceId { get; set; }
[JsonProperty("deviceReceivedAt")]
public DateTime DeviceReceivedAt { get; set; }
[JsonProperty("messageBody")]
public JRaw MessageBody { get; set; }
[JsonProperty("properties")]
public IDictionary<string, string> Properties { get; set; }
[JsonProperty("messageId")]
public string MessageId { get; set; }
[JsonProperty("expirtyTimeUtC")]
public DateTime ExpiryTimeUTC { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using Newtonsoft.Json;
namespace DeviceBridge.Models
{
public class ConnectionStatusChangeEventBody
{
[JsonProperty("eventType")]
public const string EventType = "ConnectionStatusChange";
[JsonProperty("deviceId")]
public string DeviceId { get; set; }
[JsonProperty("deviceReceivedAt")]
public DateTime DeviceReceivedAt { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("reason")]
public string Reason { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DeviceBridge.Models
{
public class DesiredPropertyUpdateEventBody
{
[JsonProperty("eventType")]
public const string EventType = "DesiredPropertyUpdate";
[JsonProperty("deviceId")]
public string DeviceId { get; set; }
[JsonProperty("deviceReceivedAt")]
public DateTime DeviceReceivedAt { get; set; }
[JsonProperty("desiredProperties")]
public JRaw DesiredProperties { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
namespace DeviceBridge.Models
{
public class DeviceStatusResponseBody
{
public string Status { get; set; }
public string Reason { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
namespace DeviceBridge.Models
{
public class DeviceSubscription
{
public DeviceSubscription()
{
}
public DeviceSubscription(DeviceSubscription original)
{
DeviceId = original.DeviceId;
SubscriptionType = original.SubscriptionType;
CallbackUrl = original.CallbackUrl;
CreatedAt = original.CreatedAt;
}
public string DeviceId { get; set; }
public DeviceSubscriptionType SubscriptionType { get; set; }
public string CallbackUrl { get; set; }
public DateTime CreatedAt { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Text.Json.Serialization;
using DeviceBridge.Common.Exceptions;
using Microsoft.OpenApi.Models;
namespace DeviceBridge.Models
{
[JsonConverter(typeof(DeviceSubscriptionTypeConverter))]
public class DeviceSubscriptionType
{
public static readonly OpenApiSchema Schema = new OpenApiSchema
{
Type = "string",
};
public static readonly DeviceSubscriptionType DesiredProperties = new DeviceSubscriptionType(DesiredPropertiesSubscriptionType);
public static readonly DeviceSubscriptionType Methods = new DeviceSubscriptionType(MethodsSubscriptionType);
public static readonly DeviceSubscriptionType C2DMessages = new DeviceSubscriptionType(C2DSubscriptionType);
public static readonly DeviceSubscriptionType ConnectionStatus = new DeviceSubscriptionType(ConnectionStatusSubscriptionType);
private const string DesiredPropertiesSubscriptionType = "DesiredProperties";
private const string MethodsSubscriptionType = "Methods";
private const string C2DSubscriptionType = "C2DMessages";
private const string ConnectionStatusSubscriptionType = "ConnectionStatus";
private string value;
private DeviceSubscriptionType(string value)
{
switch (value)
{
case DesiredPropertiesSubscriptionType:
case MethodsSubscriptionType:
case C2DSubscriptionType:
case ConnectionStatusSubscriptionType:
this.value = value;
break;
default:
throw new UnknownDeviceSubscriptionTypeException(value);
}
}
public static bool operator ==(DeviceSubscriptionType lhs, DeviceSubscriptionType rhs)
{
return (ReferenceEquals(lhs, null) && ReferenceEquals(rhs, null)) || (!ReferenceEquals(lhs, null) && lhs.Equals(rhs));
}
public static bool operator !=(DeviceSubscriptionType lhs, DeviceSubscriptionType rhs)
{
return !(lhs == rhs);
}
/// <summary>
/// Returns the corresponding singleton for a give subscription type.
/// </summary>
/// <exception cref="UnknownDeviceSubscriptionTypeException">If the given value is not a valid subscription type.</exception>
/// <param name="value">The string representation subscription type.</param>
/// <returns>The corresponding singleton for the subscription type.</returns>
public static DeviceSubscriptionType FromString(string value)
{
switch (value)
{
case DesiredPropertiesSubscriptionType:
return DesiredProperties;
case MethodsSubscriptionType:
return Methods;
case C2DSubscriptionType:
return C2DMessages;
case ConnectionStatusSubscriptionType:
return ConnectionStatus;
default:
throw new UnknownDeviceSubscriptionTypeException(value);
}
}
/// <summary>
/// A data subscription deals with events that depend on a device connection (properties, methods, C2D messages).
/// A connection status subscription, in the other hand, is just a subscription to engine events that is always active and doesn't depend on a connection.
/// </summary>
/// <returns>Whether this is a data subscription or not.</returns>
public bool IsDataSubscription()
{
return (this == DesiredProperties) || (this == Methods) || (this == C2DMessages);
}
public override string ToString()
{
return value;
}
public override bool Equals(object type)
{
if (ReferenceEquals(type, null))
{
return false;
}
return ReferenceEquals(this, type) || (value == (type as DeviceSubscriptionType).value);
}
public override int GetHashCode()
{
return value.GetHashCode();
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace DeviceBridge.Models
{
public class DeviceSubscriptionTypeConverter : JsonConverter<DeviceSubscriptionType>
{
public override DeviceSubscriptionType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, DeviceSubscriptionType value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
namespace DeviceBridge.Models
{
public class DeviceSubscriptionWithStatus : DeviceSubscription
{
public DeviceSubscriptionWithStatus(DeviceSubscription original)
: base(original)
{
}
public string Status { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Collections.Generic;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DeviceBridge.Models
{
public class DeviceTwin
{
public static readonly OpenApiSchema Schema = new OpenApiSchema()
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchema>()
{
{
"twin", new OpenApiSchema()
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchema>()
{
{
"properties", new OpenApiSchema()
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchema>()
{
{
"desired", new OpenApiSchema()
{
Type = "object",
}
},
{
"reported", new OpenApiSchema()
{
Type = "object",
}
},
},
}
},
},
}
},
},
};
[JsonProperty("twin")]
public JRaw Twin { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections.Generic;
namespace DeviceBridge.Models
{
public class HubCacheEntry
{
public string DeviceId { get; set; }
public string Hub { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace DeviceBridge.Models
{
public class MessageBody
{
[Required]
public IDictionary<string, object> Data { get; set; }
public IDictionary<string, string> Properties { get; set; }
public string ComponentName { get; set; }
public DateTime? CreationTimeUtc { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DeviceBridge.Models
{
public class MethodInvocationEventBody
{
[JsonProperty("eventType")]
public const string EventType = "DirectMethodInvocation";
[JsonProperty("deviceId")]
public string DeviceId { get; set; }
[JsonProperty("deviceReceivedAt")]
public DateTime DeviceReceivedAt { get; set; }
[JsonProperty("methodName")]
public string MethodName { get; set; }
[JsonProperty("requestData")]
public JRaw RequestData { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Text.Json;
namespace DeviceBridge.Models
{
public class MethodResponseBody
{
public JsonElement? Payload { get; set; }
public int? Status { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
namespace DeviceBridge.Models
{
/// <summary>Enum ReceiveMessageCallbackStatus.</summary>
public enum ReceiveMessageCallbackStatus
{
/// <summary>Client should accept message.</summary>
Accept,
/// <summary>Client should reject message.</summary>
Reject,
/// <summary>Client should abandon message.</summary>
Abandon,
}
}

Просмотреть файл

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
namespace DeviceBridge.Models
{
public class RegistrationBody
{
public string ModelId { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace DeviceBridge.Models
{
public class ReportedPropertiesPatch
{
[Required]
public IDictionary<string, object> Patch { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.ComponentModel.DataAnnotations;
namespace DeviceBridge.Models
{
public class SubscriptionCreateOrUpdateBody
{
[Required]
public string CallbackUrl { get; set; }
}
}

26
DeviceBridge/NLog.config Normal file
Просмотреть файл

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
autoReload="true"
throwExceptions="false"
internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log">
<targets>
<target name="logconsole" xsi:type="Console" >
<layout xsi:type="JsonLayout" includeAllProperties="true" maxRecursionLimit="20" excludeProperties="">
<attribute name='time' layout='${longdate}' />
<attribute name='level' layout='${level:upperCase=true}'/>
<attribute name='message' layout='${message}' />
<attribute name='exception' layout='${exception}' />
<attribute name='callsite' layout='${callsite}, ${callsite-linenumber}' />
</layout>
</target>
</targets>
<rules>
<logger name="*" minlevel="Debug" writeTo="logconsole" />
</rules>
</nlog>

73
DeviceBridge/Program.cs Normal file
Просмотреть файл

@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Linq;
using System.Threading;
using DeviceBridge.Common.Exceptions;
using DeviceBridge.Management;
using DeviceBridge.Providers;
using DeviceBridge.Services;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NLog.Web;
namespace DeviceBridge
{
public class Program
{
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>().UseUrls("http://localhost:5001");
});
}
private static void Main(string[] args)
{
var logger = NLogBuilder.ConfigureNLog("NLog.config").GetCurrentClassLogger();
// In setup mode only run setup tasks without bringing up the server.
if (args.Contains("--setup"))
{
logger.Info("Executing in setup mode.");
try
{
var dbSchemaSetup = new DbSchemaSetup();
dbSchemaSetup.SetupDbSchema().Wait();
var encryptionSetup = new EncryptionSetup();
encryptionSetup.Reencrypt().Wait();
return;
}
finally
{
NLog.LogManager.Shutdown();
}
}
try
{
CreateHostBuilder(args).Build().Run();
}
catch (Exception e)
{
// If the storage setup is not complete, wait 30 seconds before exiting. This gives more time for setup to finish before next execution is attempted.
if (e.InnerException is StorageSetupIncompleteException)
{
logger.Info("ERROR: DB setup is not complete. Please make sure that schema setup task finishes successfully. Process will exit in 30 seconds...");
Thread.Sleep(30000);
}
logger.Error(e);
}
finally
{
NLog.LogManager.Shutdown();
}
}
}
}

Просмотреть файл

@ -0,0 +1,26 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50744",
"sslPort": 44305
}
},
"profiles": {
"BridgeV2Service": {
"commandName": "Project",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"KV_URL": "<replace-with-local-development-keyvault-url>",
"MAX_POOL_SIZE": "50",
"DEVICE_RAMPUP_BATCH_SIZE": "150",
"DEVICE_RAMPUP_BATCH_INTERVAL_MS": "1000",
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUP__KEYVAULT__CONFIGURATIONENABLED": "true",
"HTTP_RETRY_LIMIT": "5"
}
}
}
}

Просмотреть файл

@ -0,0 +1,8 @@
{
"dependencies": {
"secrets1": {
"type": "secrets",
"connectionId": "ASPNETCORE_HOSTINGSTARTUP__KEYVAULT__CONFIGURATIONVAULT"
}
}
}

Просмотреть файл

@ -0,0 +1,8 @@
{
"dependencies": {
"secrets1": {
"type": "secrets.keyVault",
"connectionId": "ASPNETCORE_HOSTINGSTARTUP__KEYVAULT__CONFIGURATIONVAULT"
}
}
}

Просмотреть файл

@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.KeyVault.Models;
using NLog;
namespace DeviceBridge.Providers
{
public interface ISecretsProvider
{
Task PutSecretAsync(Logger logger, string secretName, string secretValue);
Task<string> GetIdScopeAsync(Logger logger);
Task<string> GetIotcSasKeyAsync(Logger logger);
Task<string> GetSqlPasswordAsync(Logger logger);
Task<string> GetSqlUsernameAsync(Logger logger);
Task<string> GetSqlServerAsync(Logger logger);
Task<string> GetSqlDatabaseAsync(Logger logger);
Task<string> GetApiKey(Logger logger);
Task<SecretBundle> GetEncryptionKey(Logger logger, string version = null);
Task<IDictionary<string, SecretBundle>> GetEncryptionKeyVersions(Logger logger);
}
}

Просмотреть файл

@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using DeviceBridge.Models;
using NLog;
namespace DeviceBridge.Providers
{
public interface IStorageProvider
{
Task<List<DeviceSubscription>> ListAllSubscriptionsOrderedByDeviceId(Logger logger);
Task<List<DeviceSubscription>> ListDeviceSubscriptions(Logger logger, string deviceId);
Task<DeviceSubscription> GetDeviceSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, CancellationToken cancellationToken);
Task<DeviceSubscription> CreateOrUpdateDeviceSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, string callbackUrl, CancellationToken cancellationToken);
Task DeleteDeviceSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, CancellationToken cancellationToken);
Task GcHubCache(Logger logger);
Task RenewHubCacheEntries(Logger logger, List<string> deviceIds);
Task AddOrUpdateHubCacheEntry(Logger logger, string deviceId, string hub);
Task<List<HubCacheEntry>> ListHubCacheEntries(Logger logger);
Task Exec(Logger logger, string sql);
}
}

Просмотреть файл

@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.KeyVault.Models;
using Microsoft.Azure.Services.AppAuthentication;
using NLog;
namespace DeviceBridge.Providers
{
public class SecretsProvider : ISecretsProvider
{
public const string IotcIdScope = "iotc-id-scope";
public const string IotcSasKey = "iotc-sas-key";
public const string IotcEncryptionKey = "iotc-encryption-key";
public const string SqlServer = "sql-server";
public const string SqlPassword = "sql-password";
public const string SqlUsername = "sql-username";
public const string SqlDatabase = "sql-database";
public const string ApiKeyName = "apiKey";
private readonly KeyVaultClient kvClient;
private readonly string kvUrl;
private string apiKey;
public SecretsProvider(string kvUrl)
{
var tokenProvider = new AzureServiceTokenProvider();
kvClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(tokenProvider.KeyVaultTokenCallback));
this.kvUrl = kvUrl;
}
public async Task PutSecretAsync(Logger logger, string secretName, string secretValue)
{
logger.Info("Adding secret {secretName} to Key Vault", secretName);
await kvClient.SetSecretAsync($"{kvUrl}", secretName, secretValue);
}
public async Task<string> GetIdScopeAsync(Logger logger)
{
return await GetSecretValueAsync(logger, IotcIdScope);
}
public async Task<string> GetIotcSasKeyAsync(Logger logger)
{
return await GetSecretValueAsync(logger, IotcSasKey);
}
public async Task<string> GetSqlPasswordAsync(Logger logger)
{
return await GetSecretValueAsync(logger, SqlPassword);
}
public async Task<string> GetSqlUsernameAsync(Logger logger)
{
return await GetSecretValueAsync(logger, SqlUsername);
}
public async Task<string> GetSqlServerAsync(Logger logger)
{
return await GetSecretValueAsync(logger, SqlServer);
}
public async Task<string> GetSqlDatabaseAsync(Logger logger)
{
return await GetSecretValueAsync(logger, SqlDatabase);
}
public async Task<string> GetApiKey(Logger logger)
{
if (apiKey == null)
{
apiKey = await GetSecretValueAsync(logger, ApiKeyName);
}
return apiKey;
}
public async Task<SecretBundle> GetEncryptionKey(Logger logger, string version = null)
{
return await GetSecretAsync(logger, IotcEncryptionKey, version);
}
public async Task PutEncyptionKey(Logger logger, string value)
{
await PutSecretAsync(logger, IotcEncryptionKey, value);
}
public async Task<IDictionary<string, SecretBundle>> GetEncryptionKeyVersions(Logger logger)
{
var versions = await kvClient.GetSecretVersionsAsync(kvUrl, IotcEncryptionKey);
var secrets = new Dictionary<string, SecretBundle>();
do
{
foreach (var version in versions)
{
secrets.Add(version.Identifier.Version, await GetEncryptionKey(logger, version.Identifier.Version));
}
}
while (versions.NextPageLink != null && (versions = await kvClient.GetSecretVersionsNextAsync(versions.NextPageLink)) != null);
return secrets;
}
private async Task<string> GetSecretValueAsync(Logger logger, string secretName, string secretVersion = null)
{
return (await GetSecretAsync(logger, secretName, secretVersion)).Value;
}
private async Task<SecretBundle> GetSecretAsync(Logger logger, string secretName, string secretVersion = null)
{
logger.Info("Getting secret {secretName} from Key Vault", secretName);
return (secretVersion == null) ? await kvClient.GetSecretAsync($"{kvUrl}", secretName) : await kvClient.GetSecretAsync($"{kvUrl}", secretName, secretVersion);
}
}
}

Просмотреть файл

@ -0,0 +1,451 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Threading;
using System.Threading.Tasks;
using DeviceBridge.Common.Exceptions;
using DeviceBridge.Models;
using DeviceBridge.Services;
using NLog;
namespace DeviceBridge.Providers
{
public class StorageProvider : IStorageProvider
{
/// <summary>
/// Taken from https://docs.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors.
/// </summary>
private const int TableNotFoundErrorNumber = 208;
private const int StoredProcedureNotFoundErrorNumber = 2812;
private const int DefaultPageSIze = 1000;
private const int BulkCopyBatchTimeout = 60;
private const int BulkCopyBatchSize = 1000;
private readonly string _connectionString;
private readonly EncryptionService _encryptionService;
public StorageProvider(string connectionString, EncryptionService encryptionService)
{
_connectionString = connectionString;
_encryptionService = encryptionService;
}
/// <summary>
/// Lists all active subscriptions of all types ordered by device Id.
/// </summary>
/// <param name="logger">Logger to be used.</param>
/// <returns>List of all subscriptions of all types ordered by device Id.</returns>
public async Task<List<DeviceSubscription>> ListAllSubscriptionsOrderedByDeviceId(Logger logger)
{
try
{
logger.Info("Getting all subscriptions in the DB");
using SqlConnection connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
var subscriptions = new List<DeviceSubscription>();
int lastPageSize;
int pageIndex = 0;
do
{
logger.Info("Fetching page {pageIndex} of device subscriptions", pageIndex);
using SqlCommand command = new SqlCommand("getDeviceSubscriptionsPaged", connection)
{
CommandType = CommandType.StoredProcedure,
};
command.Parameters.Add(new SqlParameter("@PageIndex", pageIndex++));
command.Parameters.Add(new SqlParameter("@RowsPerPage", DefaultPageSIze));
using SqlDataReader reader = await command.ExecuteReaderAsync();
var itemCountBeforePage = subscriptions.Count;
while (await reader.ReadAsync())
{
subscriptions.Add(new DeviceSubscription()
{
DeviceId = reader["DeviceId"].ToString(),
SubscriptionType = DeviceSubscriptionType.FromString(reader["SubscriptionType"].ToString()),
CallbackUrl = await _encryptionService.Decrypt(logger, reader["CallbackUrl"].ToString()),
CreatedAt = (DateTime)reader["CreatedAt"],
});
}
lastPageSize = subscriptions.Count - itemCountBeforePage;
}
while (lastPageSize > 0);
logger.Info("Found {subscriptionCount} subscriptions", subscriptions.Count);
return subscriptions;
}
catch (Exception e)
{
throw TranslateSqlException(e);
}
}
/// <summary>
/// Lists all active subscriptions of all types for a device.
/// </summary>
/// <param name="logger">Logger to be used.</param>
/// <param name="deviceId">Id of the device to get the subscriptions for.</param>
/// <returns>List of subscriptions for the given device.</returns>
public async Task<List<DeviceSubscription>> ListDeviceSubscriptions(Logger logger, string deviceId)
{
try
{
logger.Info("Getting all subscriptions for device {deviceId}", deviceId);
var sql = "SELECT * FROM DeviceSubscriptions WHERE DeviceId = @DeviceId";
using SqlConnection connection = new SqlConnection(_connectionString);
using SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.Add(new SqlParameter("DeviceId", deviceId));
await connection.OpenAsync();
using SqlDataReader reader = await command.ExecuteReaderAsync();
List<DeviceSubscription> subscriptions = new List<DeviceSubscription>();
while (await reader.ReadAsync())
{
subscriptions.Add(new DeviceSubscription()
{
DeviceId = reader["DeviceId"].ToString(),
SubscriptionType = DeviceSubscriptionType.FromString(reader["SubscriptionType"].ToString()),
CallbackUrl = await _encryptionService.Decrypt(logger, reader["CallbackUrl"].ToString()),
CreatedAt = (DateTime)reader["CreatedAt"],
});
}
logger.Info("Found {subscriptionCount} subscriptions for device {deviceId}", subscriptions.Count, deviceId);
return subscriptions;
}
catch (Exception e)
{
throw TranslateSqlException(e);
}
}
/// <summary>
/// Gets an active subscription of the specified type for a device, if one exists.
/// </summary>
/// <param name="logger">Logger to be used.</param>
/// <param name="deviceId">Id of the device to get the subscription for.</param>
/// <param name="subscriptionType">Type of the subscription to get.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The subscription, if exists. Null otherwise.</returns>
public async Task<DeviceSubscription> GetDeviceSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, CancellationToken cancellationToken)
{
try
{
logger.Info("Getting {subscriptionType} subscription for device {deviceId}", subscriptionType, deviceId);
var sql = "SELECT * FROM DeviceSubscriptions WHERE DeviceId = @DeviceId AND SubscriptionType = @SubscriptionType";
using SqlConnection connection = new SqlConnection(_connectionString);
using SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.Add(new SqlParameter("DeviceId", deviceId));
command.Parameters.Add(new SqlParameter("SubscriptionType", subscriptionType.ToString()));
await connection.OpenAsync(cancellationToken);
using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken);
if (await reader.ReadAsync(cancellationToken))
{
logger.Info("Got {subscriptionType} for device {deviceId}", subscriptionType, deviceId);
return new DeviceSubscription()
{
DeviceId = reader["DeviceId"].ToString(),
SubscriptionType = DeviceSubscriptionType.FromString(reader["SubscriptionType"].ToString()),
CallbackUrl = await _encryptionService.Decrypt(logger, reader["CallbackUrl"].ToString()),
CreatedAt = (DateTime)reader["CreatedAt"],
};
}
else
{
logger.Info("No {subscriptionType} subscription found for device {deviceId}", subscriptionType, deviceId);
return null;
}
}
catch (Exception e)
{
throw TranslateSqlException(e);
}
}
/// <summary>
/// Creates a subscription of the given type for the given device. If one already exists, it's updated with a new creation time and callback URL.
/// Returns the created or updated subscription.
/// </summary>
/// <param name="logger">Logger to be used.</param>
/// <param name="deviceId">Id of the device to create the subscription for.</param>
/// <param name="subscriptionType">Type of the subscription to be created.</param>
/// <param name="callbackUrl">Callback URL of the subscription.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The created subscription.</returns>
public async Task<DeviceSubscription> CreateOrUpdateDeviceSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, string callbackUrl, CancellationToken cancellationToken)
{
try
{
logger.Info("Creating or updating {subscriptionType} subscription for device {deviceId}", subscriptionType, deviceId);
using SqlConnection connection = new SqlConnection(_connectionString);
using SqlCommand command = new SqlCommand("upsertDeviceSubscription", connection)
{
CommandType = CommandType.StoredProcedure,
};
command.Parameters.Add(new SqlParameter("@DeviceId", deviceId));
command.Parameters.Add(new SqlParameter("@SubscriptionType", subscriptionType.ToString()));
command.Parameters.Add(new SqlParameter("@CallbackUrl", await _encryptionService.Encrypt(logger, callbackUrl)));
command.Parameters.Add(new SqlParameter("@CreatedAt", SqlDbType.DateTime)).Direction = ParameterDirection.Output;
await connection.OpenAsync(cancellationToken);
await command.ExecuteNonQueryAsync(cancellationToken);
logger.Info("Created or updated {subscriptionType} subscription for device {deviceId}", subscriptionType, deviceId);
return new DeviceSubscription()
{
DeviceId = deviceId,
SubscriptionType = subscriptionType,
CallbackUrl = callbackUrl,
CreatedAt = (DateTime)command.Parameters["@CreatedAt"].Value,
};
}
catch (Exception e)
{
throw TranslateSqlException(e);
}
}
/// <summary>
/// Deletes the subscription of the given type for a device, if one exists.
/// </summary>
/// <param name="logger">Logger to be used.</param>
/// <param name="deviceId">Id of the device to delete the subscription for.</param>
/// <param name="subscriptionType">Type of the subscription to be deleted.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task DeleteDeviceSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, CancellationToken cancellationToken)
{
try
{
logger.Info("Deleting {subscriptionType} subscription for device {deviceId}", subscriptionType, deviceId);
var sql = "DELETE FROM DeviceSubscriptions WHERE DeviceId = @DeviceId AND SubscriptionType = @SubscriptionType";
using SqlConnection connection = new SqlConnection(_connectionString);
using SqlCommand command = new SqlCommand(sql, connection);
command.Parameters.Add(new SqlParameter("DeviceId", deviceId));
command.Parameters.Add(new SqlParameter("SubscriptionType", subscriptionType.ToString()));
await connection.OpenAsync(cancellationToken);
await command.ExecuteNonQueryAsync(cancellationToken);
logger.Info("Deleted {subscriptionType} subscription for device {deviceId}", subscriptionType, deviceId);
}
catch (Exception e)
{
throw TranslateSqlException(e);
}
}
/// <summary>
/// Deletes from the hub cache any device that doesn't have a subscription and hasn't attempted to open a connection in the past week.
/// </summary>
/// <param name="logger">Logger to be used.</param>
public async Task GcHubCache(Logger logger)
{
try
{
logger.Info("Running Hub cache GC");
var sql = @"DELETE c FROM HubCache c
LEFT JOIN DeviceSubscriptions s ON s.DeviceId = c.DeviceId
WHERE (s.DeviceId IS NULL) AND (c.RenewedAt < DATEADD(day, -7, GETUTCDATE()))";
using SqlConnection connection = new SqlConnection(_connectionString);
using SqlCommand command = new SqlCommand(sql, connection);
await connection.OpenAsync();
var affectedRows = await command.ExecuteNonQueryAsync();
logger.Info("Successfully cleaned up {hubCount} Hubs during Hub cache GC", affectedRows);
}
catch (Exception e)
{
throw TranslateSqlException(e);
}
}
/// <summary>
/// Renews the Hub cache timestamp for a list of devices.
/// </summary>
/// <param name="logger">The logger instance to use.</param>
/// <param name="deviceIds">List of device Ids to renew.</param>
public async Task RenewHubCacheEntries(Logger logger, List<string> deviceIds)
{
try
{
logger.Info("Renewing Hub cache entries for {count} devices", deviceIds.Count);
// Add device Ids to a Data Table that we'll bulk copy to the DB.
var dt = new DataTable();
dt.Columns.Add("DeviceId");
foreach (var deviceId in deviceIds)
{
var row = dt.NewRow();
row["DeviceId"] = deviceId;
dt.Rows.Add(row);
}
using SqlConnection connection = new SqlConnection(_connectionString);
using SqlCommand command = new SqlCommand(string.Empty, connection);
await connection.OpenAsync();
// Create a target temp table.
command.CommandText = "CREATE TABLE #CacheEntriesToRenewTmpTable(DeviceId VARCHAR(255) NOT NULL PRIMARY KEY)";
await command.ExecuteNonQueryAsync();
// Bulk copy the device Ids to renew to the temp table, 1000 records at a time.
using SqlBulkCopy bulkcopy = new SqlBulkCopy(connection);
bulkcopy.BulkCopyTimeout = BulkCopyBatchTimeout;
bulkcopy.BatchSize = BulkCopyBatchSize;
bulkcopy.DestinationTableName = "#CacheEntriesToRenewTmpTable";
await bulkcopy.WriteToServerAsync(dt);
// Renew the Hub cache timestamp for every device Id in the temp table.
command.CommandTimeout = 300; // The operation should take no longer than 5 minutes
command.CommandText = @"UPDATE HubCache SET RenewedAt = GETUTCDATE()
FROM HubCache
INNER JOIN #CacheEntriesToRenewTmpTable Temp ON (Temp.DeviceId = HubCache.DeviceId)
DROP TABLE #CacheEntriesToRenewTmpTable";
await command.ExecuteNonQueryAsync();
}
catch (Exception e)
{
throw TranslateSqlException(e);
}
}
/// <summary>
/// Adds or updates a Hub cache entry for a device.
/// </summary>
/// <param name="logger">Logger to be used.</param>
/// <param name="deviceId">Id of the device for the new cache entry.</param>
/// <param name="hub">Hub to be added to the cache entry for the device.</param>
public async Task AddOrUpdateHubCacheEntry(Logger logger, string deviceId, string hub)
{
try
{
logger.Info("Adding or updating Hub cache entry for device {deviceId} ({hub})", deviceId, hub);
using SqlConnection connection = new SqlConnection(_connectionString);
using SqlCommand command = new SqlCommand("upsertHubCacheEntry", connection)
{
CommandType = CommandType.StoredProcedure,
};
command.Parameters.Add(new SqlParameter("@DeviceId", deviceId));
command.Parameters.Add(new SqlParameter("@Hub", hub));
await connection.OpenAsync();
await command.ExecuteNonQueryAsync();
logger.Info("Added or updated Hub cache entry for device {deviceId}", deviceId);
}
catch (Exception e)
{
throw TranslateSqlException(e);
}
}
/// <summary>
/// Lists all entries in the Hub cache.
/// </summary>
/// <param name="logger">Logger to be used.</param>
/// <returns>List of all entries in the DB hub cache.</returns>
public async Task<List<HubCacheEntry>> ListHubCacheEntries(Logger logger)
{
try
{
logger.Info("Getting all entries in the hub cache");
using SqlConnection connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
var allEntries = new List<HubCacheEntry>();
int lastPageSize;
int pageIndex = 0;
do
{
logger.Info("Fetching page {pageIndex} of Hub cache entries", pageIndex);
using SqlCommand command = new SqlCommand("getHubCacheEntriesPaged", connection)
{
CommandType = CommandType.StoredProcedure,
};
command.Parameters.Add(new SqlParameter("@PageIndex", pageIndex++));
command.Parameters.Add(new SqlParameter("@RowsPerPage", DefaultPageSIze));
using SqlDataReader reader = await command.ExecuteReaderAsync();
var itemCountBeforePage = allEntries.Count;
while (await reader.ReadAsync())
{
allEntries.Add(new HubCacheEntry()
{
DeviceId = reader["DeviceId"].ToString(),
Hub = reader["Hub"].ToString(),
});
}
lastPageSize = allEntries.Count - itemCountBeforePage;
}
while (lastPageSize > 0);
logger.Info("Found {hubCacheEntriesCount} Hub cache entries", allEntries.Count);
return allEntries;
}
catch (Exception e)
{
throw TranslateSqlException(e);
}
}
/// <summary>
/// Executes an arbitrary SQL command against the DB.
/// </summary>
/// <param name="logger">Logger instance to use.</param>
/// <param name="sql">SQL command to run.</param>
public async Task Exec(Logger logger, string sql)
{
try
{
logger.Info("Executing SQL command");
using SqlConnection connection = new SqlConnection(_connectionString);
using SqlCommand command = new SqlCommand(sql, connection);
await connection.OpenAsync();
await command.ExecuteNonQueryAsync();
logger.Info("SQL command executed successfully");
}
catch (Exception e)
{
throw TranslateSqlException(e);
}
}
/// <summary>
/// Translates SQL exceptions into service exceptions.
/// </summary>
/// <param name="e">Original SQL exception.</param>
/// <returns>The translated service exception.</returns>
private static BridgeException TranslateSqlException(Exception e)
{
if (e is SqlException sqlException && (sqlException.Number == StoredProcedureNotFoundErrorNumber || sqlException.Number == TableNotFoundErrorNumber))
{
return new StorageSetupIncompleteException(e);
}
return new UnknownStorageException(e);
}
}
}

Просмотреть файл

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Shared;
using NLog;
namespace DeviceBridge.Services
{
public class BridgeService : IBridgeService
{
private readonly ConnectionManager _connectionManager;
public BridgeService(ConnectionManager connectionManager)
{
_connectionManager = connectionManager;
}
public async Task SendTelemetry(Logger logger, string deviceId, IDictionary<string, object> payload, CancellationToken cancellationToken, IDictionary<string, string> properties = null, string componentName = null, DateTime? creationTimeUtc = null)
{
logger.Info("Sending telemetry for device {deviceId}", deviceId);
await _connectionManager.AssertDeviceConnectionOpenAsync(deviceId, true /* temporary */, false, cancellationToken);
await _connectionManager.SendEventAsync(logger, deviceId, payload, cancellationToken, properties, componentName, creationTimeUtc);
}
public async Task<Twin> GetTwin(Logger logger, string deviceId, CancellationToken cancellationToken)
{
logger.Info("Getting twin for device {deviceId}", deviceId);
await _connectionManager.AssertDeviceConnectionOpenAsync(deviceId, true /* temporary */, false, cancellationToken);
return await _connectionManager.GetTwinAsync(logger, deviceId, cancellationToken);
}
public async Task UpdateReportedProperties(Logger logger, string deviceId, IDictionary<string, object> patch, CancellationToken cancellationToken)
{
logger.Info("Updating reported properties for device {deviceId}", deviceId);
await _connectionManager.AssertDeviceConnectionOpenAsync(deviceId, true /* temporary */, false, cancellationToken);
await _connectionManager.UpdateReportedPropertiesAsync(logger, deviceId, patch, cancellationToken);
}
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Net.Sockets;
using Microsoft.Azure.Devices.Client;
namespace DeviceBridge.Services
{
/// <summary>
/// Extends the default SDK retry policy (ExponentialBackoff) to fail right away if the hub doesn't exist.
/// </summary>
public class CustomDeviceClientRetryPolicy : IRetryPolicy
{
private readonly ExponentialBackoff baseRetryPolicy;
public CustomDeviceClientRetryPolicy()
{
// Default retry setup of the device SDK.
this.baseRetryPolicy = new ExponentialBackoff(int.MaxValue, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(100));
}
/// <summary>
/// Returns true if, based on the parameters, the operation should be retried.
/// </summary>
/// <param name="currentRetryCount">How many times the operation has been retried.</param>
/// <param name="lastException">Operation exception.</param>
/// <param name="retryInterval">Next retry should be performed after this time interval.</param>
/// <returns>True if the operation should be retried, false otherwise.</returns>
public bool ShouldRetry(int currentRetryCount, Exception lastException, out TimeSpan retryInterval)
{
if ((lastException.InnerException as SocketException)?.SocketErrorCode == SocketError.HostNotFound)
{
retryInterval = TimeSpan.FromMilliseconds(1000);
return false;
}
// This seems weird, but overriding methods in .net only works when the parent is labeled as virtual. So we cant call base.ShouldRetry and just extend ExponentialBackoff.
return baseRetryPolicy.ShouldRetry(currentRetryCount, lastException, out retryInterval);
}
}
}

Просмотреть файл

@ -0,0 +1,129 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using DeviceBridge.Common.Exceptions;
using DeviceBridge.Providers;
using Microsoft.Azure.KeyVault.Models;
using NLog;
namespace DeviceBridge.Services
{
public class EncryptionService
{
private readonly ISecretsProvider _secretsProvider;
private IDictionary<string, SecretBundle> _encryptionKeys;
private string _latestKnownEncryptionKeyVersionId = null;
public EncryptionService(Logger logger, ISecretsProvider secretsProvider)
{
_secretsProvider = secretsProvider;
// Initialize secret cache
_encryptionKeys = _secretsProvider.GetEncryptionKeyVersions(logger).Result;
}
public async Task<string> Encrypt(Logger logger, string unencryptedString)
{
var keySecret = await GetEncryptionKey(logger);
var encryptionKey = keySecret.Value;
return $"{keySecret.SecretIdentifier.Version}-{EncryptString(unencryptedString, encryptionKey)}";
}
public async Task<string> Decrypt(Logger logger, string encryptedStringWithVersion)
{
var encryptedStringParts = encryptedStringWithVersion.Split('-');
string keyVersion = null;
if (encryptedStringParts.Length < 2)
{
throw new EncryptionException();
}
keyVersion = encryptedStringParts[0];
var encryptedString = encryptedStringParts[1];
var keySecret = await GetEncryptionKey(logger, keyVersion);
var encryptionKey = keySecret.Value;
return DecryptString(encryptedString, encryptionKey);
}
private static string EncryptString(string plainText, string stringKey)
{
var key = Encoding.ASCII.GetBytes(stringKey);
using var aes = Aes.Create();
aes.Key = key;
aes.GenerateIV();
var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
using var memoryStream = new MemoryStream();
using var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
using var streamWriter = new StreamWriter(cryptoStream);
streamWriter.Write(plainText);
streamWriter.Dispose();
return $"{Convert.ToBase64String(aes.IV)}:{Convert.ToBase64String(memoryStream.ToArray())}";
}
private static string DecryptString(string encryptedStringWithIv, string stringKey)
{
var key = Encoding.ASCII.GetBytes(stringKey);
using var aes = Aes.Create();
aes.Key = key;
var encryptedStringWithIvParts = encryptedStringWithIv.Split(':');
var iv = System.Convert.FromBase64String(encryptedStringWithIvParts[0]);
aes.IV = iv;
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
using var memoryStream = new MemoryStream(Convert.FromBase64String(encryptedStringWithIvParts[1]));
using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
using var streamReader = new StreamReader(cryptoStream);
return streamReader.ReadToEnd();
}
private async Task<SecretBundle> GetEncryptionKey(Logger logger, string version = null)
{
if (version == null && _latestKnownEncryptionKeyVersionId != null && _encryptionKeys.ContainsKey(_latestKnownEncryptionKeyVersionId))
{
// Use latest cached version
SecretBundle cachedValue;
_encryptionKeys.TryGetValue(_latestKnownEncryptionKeyVersionId, out cachedValue);
return cachedValue;
}
if (version != null && _encryptionKeys.ContainsKey(version))
{
// Used cached key
SecretBundle cachedValue;
_encryptionKeys.TryGetValue(version, out cachedValue);
return cachedValue;
}
// Get latest version from KV and cache
var foundKey = await _secretsProvider.GetEncryptionKey(logger, version);
if (!_encryptionKeys.ContainsKey(foundKey.SecretIdentifier.Version))
{
_encryptionKeys.Add(foundKey.SecretIdentifier.Version, foundKey);
}
if (version == null)
{
_latestKnownEncryptionKeyVersionId = foundKey.SecretIdentifier.Version;
}
return foundKey;
}
}
}

Просмотреть файл

@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using NLog;
namespace DeviceBridge.Services
{
/// <summary>
/// When the application starts, start the expired connection cleanup task.
/// </summary>
public class ExpiredConnectionCleanupHostedService : IHostedService
{
private readonly Logger _logger;
private readonly ConnectionManager _connectionManager;
public ExpiredConnectionCleanupHostedService(Logger logger, ConnectionManager connectionManager)
{
_logger = logger;
_connectionManager = connectionManager;
}
public Task StartAsync(CancellationToken cancellationToken)
{
var _ = _connectionManager.StartExpiredConnectionCleanupAsync().ContinueWith(t => _logger.Error(t.Exception, "Failed to start expired connection cleanup task"), TaskContinuationOptions.OnlyOnFaulted);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

Просмотреть файл

@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using DeviceBridge.Providers;
using Microsoft.Extensions.Hosting;
using NLog;
namespace DeviceBridge.Services
{
/// <summary>
/// Every 6 hours:
/// - renews the Hub cache entries for the devices that attempted to open a connection.
/// - runs the GC routine in the Hub cache, removing entries for any device that doesn't have a subscription and
/// hasn't connected in the last week.
/// </summary>
public class HubCacheGcHostedService : IHostedService, IDisposable
{
private const double HubCacheGcIntervalHours = 6; // How often to run the Hub cache GC task
private readonly Logger _logger;
private readonly IStorageProvider _storageProvider;
private readonly ConnectionManager _connectionManager;
private Timer _timer;
public HubCacheGcHostedService(Logger logger, IStorageProvider storageProvider, ConnectionManager connectionManager)
{
_logger = logger;
_storageProvider = storageProvider;
_connectionManager = connectionManager;
}
public Task StartAsync(CancellationToken stoppingToken)
{
_logger.Info("Initializing Hub cache GC hosted service");
_timer = new Timer(Run, null, TimeSpan.FromHours(HubCacheGcIntervalHours), TimeSpan.FromHours(HubCacheGcIntervalHours));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken stoppingToken)
{
_logger.Info("Hub cache GC hosted service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
private void Run(object state)
{
var _ = RunAsync().ContinueWith(t => _logger.Error(t.Exception, "Failed to run Hub cache GC"), TaskContinuationOptions.OnlyOnFaulted);
}
private async Task RunAsync()
{
// Get all devices that connected since the last period + 30min.
var devicesThatConnectedSinceLastRun = _connectionManager.GetDevicesThatConnectedSince(DateTime.Now.Subtract(TimeSpan.FromHours(HubCacheGcIntervalHours)).Subtract(TimeSpan.FromMinutes(30)));
await _storageProvider.RenewHubCacheEntries(_logger, devicesThatConnectedSinceLastRun);
await _storageProvider.GcHubCache(_logger);
}
}
}

Просмотреть файл

@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Shared;
using NLog;
namespace DeviceBridge.Services
{
public interface IBridgeService
{
Task<Twin> GetTwin(Logger logger, string deviceId, CancellationToken cancellationToken);
Task SendTelemetry(Logger logger, string deviceId, IDictionary<string, object> payload, CancellationToken cancellationToken, IDictionary<string, string> properties = null, string componentName = null, DateTime? creationTimeUtc = null);
Task UpdateReportedProperties(Logger logger, string deviceId, IDictionary<string, object> patch, CancellationToken cancellationToken);
}
}

Просмотреть файл

@ -0,0 +1,486 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using DeviceBridge.Models;
using DeviceBridge.Providers;
using Microsoft.Azure.Devices.Client;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
namespace DeviceBridge.Services
{
public class SubscriptionService
{
public const uint DefaultRampupBatchSize = 150; // How many devices to synchronize at a time when performing a full DB sync of all subscriptions
public const uint DefaultRampupBatchIntervalMs = 1000; // How long to wait between each batch when performing a full DB sync of all subscriptions
private const string SubscriptionStatusStarting = "Starting";
private const string SubscriptionStatusRunning = "Running";
private const string SubscriptionStatusStopped = "Stopped";
private readonly uint _rampupBatchSize;
private readonly uint _rampupBatchIntervalMs;
private readonly IStorageProvider _storageProvider;
private readonly ConnectionManager _connectionManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly Logger _logger;
private readonly ConcurrentDictionary<string, List<DeviceSubscription>> dataSubscriptionsToInitialize;
private ConcurrentDictionary<string, SemaphoreSlim> _dbAndConnectionStateSyncSemaphores = new ConcurrentDictionary<string, SemaphoreSlim>();
private ConcurrentDictionary<string, SemaphoreSlim> _connectionStatusSubscriptionSyncSemaphores = new ConcurrentDictionary<string, SemaphoreSlim>();
public SubscriptionService(Logger logger, ConnectionManager connectionManager, IStorageProvider storageProvider, IHttpClientFactory httpClientFactory, uint rampupBatchSize, uint rampupBatchIntervalMs)
{
_logger = logger;
_storageProvider = storageProvider;
_connectionManager = connectionManager;
_httpClientFactory = httpClientFactory;
_rampupBatchSize = rampupBatchSize;
_rampupBatchIntervalMs = rampupBatchIntervalMs;
// Fetch from DB all subscriptions to be initialized on service startup. This must be done synchronously before
// the service instance is fully constructed, so subsequent requests received by the service can prevent a device
// from being initialized with stale data by removing items from this list.
_logger.Info("Attempting to fetch all subscriptions to initialize from DB");
var allSubscriptions = _storageProvider.ListAllSubscriptionsOrderedByDeviceId(_logger).Result;
dataSubscriptionsToInitialize = new ConcurrentDictionary<string, List<DeviceSubscription>>();
string currentDeviceId = null;
List<DeviceSubscription> currentDeviceDataSubscriptions = null;
foreach (var subscription in allSubscriptions.FindAll(s => s.SubscriptionType.IsDataSubscription()))
{
var deviceId = subscription.DeviceId;
// Since results are grouped by device Id, we store the results of a device once we move to the next one.
if (deviceId != currentDeviceId)
{
if (currentDeviceId != null && currentDeviceDataSubscriptions != null)
{
dataSubscriptionsToInitialize.TryAdd(currentDeviceId, currentDeviceDataSubscriptions);
}
currentDeviceId = deviceId;
currentDeviceDataSubscriptions = new List<DeviceSubscription>();
}
currentDeviceDataSubscriptions.Add(subscription);
}
// Store the results for the last device.
if (currentDeviceId != null && currentDeviceDataSubscriptions != null)
{
dataSubscriptionsToInitialize.TryAdd(currentDeviceId, currentDeviceDataSubscriptions);
}
// Synchronously initialize all connection status subscriptions before the service is fully constructed. This ensures that
// subscriptions are in place before any connection can be established, so no status change events are missed.
// No lock is needed, since no other concurrent operation is received until the service starts.
_logger.Info("Attempting to initialize all connection status subscriptions");
foreach (var connectionStatusSubscription in allSubscriptions.FindAll(s => s.SubscriptionType == DeviceSubscriptionType.ConnectionStatus))
{
_connectionManager.SetConnectionStatusCallback(connectionStatusSubscription.DeviceId, GetConnectionStatusChangeCallback(connectionStatusSubscription.DeviceId, connectionStatusSubscription));
}
}
public async Task<DeviceSubscription> GetConnectionStatusSubscription(Logger logger, string deviceId, CancellationToken cancellationToken)
{
return await _storageProvider.GetDeviceSubscription(logger, deviceId, DeviceSubscriptionType.ConnectionStatus, cancellationToken);
}
public async Task<DeviceSubscription> CreateOrUpdateConnectionStatusSubscription(Logger logger, string deviceId, string callbackUrl, CancellationToken cancellationToken)
{
// We need to synchronize the DB and callback update to make sure that the registered callback always reflects the actual data in the DB.
// We use a different mutex from data subscriptions as they might take a long time to synchronize due to connection creation.
var mutex = _connectionStatusSubscriptionSyncSemaphores.GetOrAdd(deviceId, new SemaphoreSlim(1, 1));
await mutex.WaitAsync();
try
{
_logger.Info("Acquired connection status subscription sync lock for device {deviceId}", deviceId);
var subscription = await _storageProvider.CreateOrUpdateDeviceSubscription(logger, deviceId, DeviceSubscriptionType.ConnectionStatus, callbackUrl, cancellationToken);
_connectionManager.SetConnectionStatusCallback(deviceId, GetConnectionStatusChangeCallback(deviceId, subscription));
return subscription;
}
finally
{
mutex.Release();
}
}
public async Task DeleteConnectionStatusSubscription(Logger logger, string deviceId, CancellationToken cancellationToken)
{
// We need to synchronize the DB and callback update to make sure that the registered callback always reflects the actual data in the DB.
// We use a different mutex from data subscriptions as they might take a long time to synchronize due to connection creation.
var mutex = _connectionStatusSubscriptionSyncSemaphores.GetOrAdd(deviceId, new SemaphoreSlim(1, 1));
await mutex.WaitAsync();
try
{
_logger.Info("Acquired connection status subscription sync lock for device {deviceId}", deviceId);
await _storageProvider.DeleteDeviceSubscription(logger, deviceId, DeviceSubscriptionType.ConnectionStatus, cancellationToken);
_connectionManager.RemoveConnectionStatusCallback(deviceId);
}
finally
{
mutex.Release();
}
}
public async Task<DeviceSubscriptionWithStatus> GetDataSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, CancellationToken cancellationToken)
{
var subscription = await _storageProvider.GetDeviceSubscription(logger, deviceId, subscriptionType, cancellationToken);
return (subscription != null) ? new DeviceSubscriptionWithStatus(subscription)
{
Status = ComputeDataSubscriptionStatus(deviceId, subscription.SubscriptionType, subscription.CallbackUrl),
}
: null;
}
public async Task<DeviceSubscriptionWithStatus> CreateOrUpdateDataSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, string callbackUrl, CancellationToken cancellationToken)
{
var subscription = await _storageProvider.CreateOrUpdateDeviceSubscription(logger, deviceId, subscriptionType, callbackUrl, cancellationToken);
var _ = SynchronizeDeviceDbAndEngineDataSubscriptionsAsync(deviceId).ContinueWith(t => _logger.Error(t.Exception, "Failed to synchronize DB subscriptions and connection state for device {deviceId}", deviceId), TaskContinuationOptions.OnlyOnFaulted);
return new DeviceSubscriptionWithStatus(subscription)
{
Status = ComputeDataSubscriptionStatus(deviceId, subscription.SubscriptionType, subscription.CallbackUrl),
};
}
public async Task DeleteDataSubscription(Logger logger, string deviceId, DeviceSubscriptionType subscriptionType, CancellationToken cancellationToken)
{
await _storageProvider.DeleteDeviceSubscription(logger, deviceId, subscriptionType, cancellationToken);
var _ = SynchronizeDeviceDbAndEngineDataSubscriptionsAsync(deviceId).ContinueWith(t => _logger.Error(t.Exception, "Failed to synchronize DB subscriptions and connection state for device {deviceId}", deviceId), TaskContinuationOptions.OnlyOnFaulted);
}
/// <summary>
/// Starts the Initialization of data subscriptions for all devices based on the list fetched from the DB at service construction time.
/// For use during service startup.
/// </summary>
public async Task StartDataSubscriptionsInitializationAsync()
{
_logger.Info("Attempting to initialize subscriptions for all devices");
var deviceIds = dataSubscriptionsToInitialize.Keys.ToList();
// Synchronizes one batch of devices at a time.
for (int i = 0; i < deviceIds.Count; ++i)
{
var deviceId = deviceIds[i];
var _ = SynchronizeDeviceDbAndEngineDataSubscriptionsAsync(deviceId, true).ContinueWith(t => _logger.Error(t.Exception, "Failed to initialize DB subscriptions for device {deviceId}", deviceId), TaskContinuationOptions.OnlyOnFaulted);
if ((i + 1) % _rampupBatchSize == 0 && (i + 1) < deviceIds.Count)
{
_logger.Info("Waiting {subscriptionFullSyncBatchIntervalMs} ms before syncing subscriptions for next {subscriptionFullSyncBatchSize} devices", _rampupBatchIntervalMs, _rampupBatchSize);
await Task.Delay((int)_rampupBatchIntervalMs);
}
}
_logger.Info("Successfully initialized subscriptions from DB for {deviceCount} devices", deviceIds.Count);
}
/// <summary>
/// Asserts that the internal engine state (connection and callbacks) for a device reflects the subscriptions status and callbacks stored in the DB or in the initialization list.
/// Only applies to data subscriptions, i.e., not connection status subscriptions.
/// </summary>
/// <param name="deviceId">Id of the device to synchronize subscriptions for.</param>
/// <param name="useInitializationList">Whether subscriptions should be pulled from the initialization list or fetched from the DB.</param>
/// <param name="forceConnectionRetry">Whether to force a connection retry if the current connection is in a failed state.</param>
public async Task SynchronizeDeviceDbAndEngineDataSubscriptionsAsync(string deviceId, bool useInitializationList = false, bool forceConnectionRetry = false)
{
_logger.Info("Attempting to synchronize DB and engine subscriptions for device {deviceId}", deviceId);
// Synchronizing this code is the only way to guarantee that the current state of the runner will always reflect the latest state in the DB.
// Otherwise we could end up in an inconsistent state if a subscription is deleted and recreated too fast or if a subscription is modified
// while we're initializing the device with data fetched from the DB on startup.
var mutex = _dbAndConnectionStateSyncSemaphores.GetOrAdd(deviceId, new SemaphoreSlim(1, 1));
await mutex.WaitAsync();
try
{
_logger.Info("Acquired DB and connection state sync lock for device {deviceId}", deviceId);
List<DeviceSubscription> dataSubscriptions;
if (useInitializationList)
{
if (!dataSubscriptionsToInitialize.TryGetValue(deviceId, out dataSubscriptions))
{
_logger.Info("Subscriptions for Device {deviceId} not found in initialization list", deviceId);
return;
}
}
else
{
dataSubscriptions = (await _storageProvider.ListDeviceSubscriptions(_logger, deviceId)).FindAll(s => s.SubscriptionType.IsDataSubscription());
}
// Remove the device form the initialization list to mark it as already initialized.
dataSubscriptionsToInitialize.TryRemove(deviceId, out _);
// If a desired property subscription exists, register the callback. If not, ensure that the callback doesn't exist.
var desiredPropertySubscription = dataSubscriptions.Find(s => s.SubscriptionType == DeviceSubscriptionType.DesiredProperties);
if (desiredPropertySubscription != null)
{
await _connectionManager.SetDesiredPropertyUpdateCallbackAsync(deviceId, desiredPropertySubscription.CallbackUrl, GetDesiredPropertyUpdateCallback(deviceId, desiredPropertySubscription));
}
else
{
await _connectionManager.RemoveDesiredPropertyUpdateCallbackAsync(deviceId);
}
// If a method subscription exists, register the callback. If not, ensure that the callback doesn't exist.
var methodSubscription = dataSubscriptions.Find(s => s.SubscriptionType == DeviceSubscriptionType.Methods);
if (methodSubscription != null)
{
await _connectionManager.SetMethodCallbackAsync(deviceId, methodSubscription.CallbackUrl, GetMethodCallback(deviceId, methodSubscription));
}
else
{
await _connectionManager.RemoveMethodCallbackAsync(deviceId);
}
// If a C2D subscription exists, register the callback. If not, ensure that the callback doesn't exist.
var messageSubscription = dataSubscriptions.Find(s => s.SubscriptionType == DeviceSubscriptionType.C2DMessages);
if (messageSubscription != null)
{
await _connectionManager.SetMessageCallbackAsync(deviceId, messageSubscription.CallbackUrl, GetReceiveC2DMessageCallback(deviceId, messageSubscription));
}
else
{
await _connectionManager.RemoveMessageCallbackAsync(deviceId);
}
// The device needs a connection constantly open if at least one data subscription exists. If not, the connection can be closed.
if (dataSubscriptions.Count > 0)
{
await _connectionManager.AssertDeviceConnectionOpenAsync(deviceId, false /* permanent */, forceConnectionRetry);
}
else
{
await _connectionManager.AssertDeviceConnectionClosedAsync(deviceId);
}
}
finally
{
mutex.Release();
}
}
/// <summary>
/// Determines the status of a subscription based on the current state of the device client.
/// </summary>
/// <param name="deviceId">Id of the device for which to check the subscription status.</param>
/// <param name="subscriptionType">Type of the subscription that we want the status for.</param>
/// <param name="callbackUrl">URL for which we want to check the subscription status.</param>
/// <returns>Status of the subscription.</returns>
private string ComputeDataSubscriptionStatus(string deviceId, DeviceSubscriptionType subscriptionType, string callbackUrl)
{
// If the callback URL in storage does not match the one currently registered in the client, we can assume that the engine
// is still trying to synchronize this subscription (either it was just created or it's being initialized at startup).
if ((subscriptionType == DeviceSubscriptionType.DesiredProperties && _connectionManager.GetCurrentDesiredPropertyUpdateCallbackId(deviceId) != callbackUrl) ||
(subscriptionType == DeviceSubscriptionType.Methods && _connectionManager.GetCurrentMethodCallbackId(deviceId) != callbackUrl) ||
(subscriptionType == DeviceSubscriptionType.C2DMessages && _connectionManager.GetCurrentMessageCallbackId(deviceId) != callbackUrl))
{
return SubscriptionStatusStarting;
}
var deviceStatus = _connectionManager.GetDeviceStatus(deviceId);
if (deviceStatus?.status == ConnectionStatus.Connected)
{
// Device is connected and callback is registered, so the subscription is running.
return SubscriptionStatusRunning;
}
else if (deviceStatus?.status == ConnectionStatus.Disconnected || deviceStatus?.status == ConnectionStatus.Disabled)
{
// Callbacks match, but the device is disconnected and the SDK won't automatically retry, so the subscription is permanently stopped.
return SubscriptionStatusStopped;
}
else
{
// If the device is not explicitly connected or disconnected, we can assume that the SDK is retrying or a client is being created.
return SubscriptionStatusStarting;
}
}
private DesiredPropertyUpdateCallback GetDesiredPropertyUpdateCallback(string deviceId, DeviceSubscription desiredPropertySubscription)
{
return async (desiredPopertyUpdate, _) =>
{
_logger.Info("Got desired property update for device {deviceId}. Callback URL: {callbackUrl}. Payload: {desiredPopertyUpdate}", deviceId, desiredPropertySubscription.CallbackUrl, desiredPopertyUpdate.ToJson());
try
{
var body = new DesiredPropertyUpdateEventBody()
{
DeviceId = deviceId,
DeviceReceivedAt = DateTime.UtcNow,
DesiredProperties = new JRaw(desiredPopertyUpdate.ToJson()),
};
var payload = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
using var httpResponse = await _httpClientFactory.CreateClient("RetryClient").PostAsync(desiredPropertySubscription.CallbackUrl, payload);
httpResponse.EnsureSuccessStatusCode();
_logger.Info("Successfully executed desired property update callback for device {deviceId}. Callback status code {statusCode}", deviceId, httpResponse.StatusCode);
}
catch (Exception e)
{
_logger.Error(e, "Failed to execute desired property update callback for device {deviceId}", deviceId);
}
};
}
private Func<Message, Task<ReceiveMessageCallbackStatus>> GetReceiveC2DMessageCallback(string deviceId, DeviceSubscription messageSubscription)
{
_logger.Info("Creating C2D callback {deviceId}. Callback URL {callbackUrl}", deviceId, messageSubscription.CallbackUrl);
return async (receivedMessage) =>
{
try
{
using StreamReader reader = new StreamReader(receivedMessage.BodyStream);
var messageBody = reader.ReadToEnd();
_logger.Info("Got C2D message for device {deviceId}. Callback URL {callbackUrl}. Payload: {payload}", deviceId, messageSubscription.CallbackUrl, messageBody);
var body = new C2DMessageInvocationEventBody()
{
DeviceId = deviceId,
DeviceReceivedAt = DateTime.UtcNow,
MessageBody = new JRaw(messageBody),
Properties = receivedMessage.Properties,
MessageId = receivedMessage.MessageId,
ExpiryTimeUTC = receivedMessage.ExpiryTimeUtc,
};
// Send request to callback URL
var requestPayload = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
using var httpResponse = await _httpClientFactory.CreateClient("RetryClient").PostAsync(messageSubscription.CallbackUrl, requestPayload);
var statusCode = (int)httpResponse.StatusCode;
if (statusCode >= 200 && statusCode < 300)
{
_logger.Info("Received C2D message callback with status {statusCode}, request accepted.", statusCode);
return ReceiveMessageCallbackStatus.Accept;
}
if (statusCode >= 400 && statusCode < 500)
{
_logger.Info("Received C2D message callback with status {statusCode}, request rejected.", statusCode);
return ReceiveMessageCallbackStatus.Reject;
}
_logger.Info("Received C2D message callback with status {statusCode}, request abandoned.", statusCode);
return ReceiveMessageCallbackStatus.Abandon;
}
catch (Exception e)
{
_logger.Error(e, "Failed to execute message callback, device {deviceId}. Request abandoned.", deviceId);
return ReceiveMessageCallbackStatus.Abandon;
}
};
}
private MethodCallback GetMethodCallback(string deviceId, DeviceSubscription methodSubscription)
{
return async (methodRequest, _) =>
{
_logger.Info("Got method request for device {deviceId}. Callback URL {callbackUrl}. Method: {methodName}. Payload: {payload}", deviceId, methodSubscription.CallbackUrl, methodRequest.Name, methodRequest.DataAsJson);
try
{
var body = new MethodInvocationEventBody()
{
DeviceId = deviceId,
DeviceReceivedAt = DateTime.UtcNow,
MethodName = methodRequest.Name,
RequestData = new JRaw(methodRequest.DataAsJson),
};
// Send request to callback URL
var requestPayload = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
using var httpResponse = await _httpClientFactory.CreateClient("RetryClient").PostAsync(methodSubscription.CallbackUrl, requestPayload);
httpResponse.EnsureSuccessStatusCode();
// Read method response from callback response
using var responseStream = await httpResponse.Content.ReadAsStreamAsync();
MethodResponseBody responseBody = null;
try
{
responseBody = await System.Text.Json.JsonSerializer.DeserializeAsync<MethodResponseBody>(responseStream, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
});
}
catch (System.Text.Json.JsonException e)
{
_logger.Error(e, "Received malformed JSON response when executing method callback for device {deviceId}", deviceId);
}
MethodResponse methodResponse;
string serializedResponsePayload = null;
int status = 200;
// If we got a custom response, return the custom payload and status. If not, just respond with a 200.
if (responseBody != null && responseBody.Status != null)
{
status = responseBody.Status.Value;
}
if (responseBody != null && responseBody.Payload != null)
{
serializedResponsePayload = System.Text.Json.JsonSerializer.Serialize(responseBody.Payload);
methodResponse = new MethodResponse(Encoding.UTF8.GetBytes(serializedResponsePayload), status);
}
else
{
methodResponse = new MethodResponse(status);
}
_logger.Info("Successfully executed method callback for device {deviceId}. Response status: {responseStatus}. Response payload: {responsePayload}", deviceId, status, serializedResponsePayload);
return methodResponse;
}
catch (Exception e)
{
_logger.Error(e, "Failed to execute method callback for device {deviceId}", deviceId);
return new MethodResponse(500);
}
};
}
private Func<ConnectionStatus, ConnectionStatusChangeReason, Task> GetConnectionStatusChangeCallback(string deviceId, DeviceSubscription connectionStatusSubscription)
{
return async (status, reason) =>
{
_logger.Info("Got connection status change for device {deviceId}. Callback URL: {callbackUrl}. Status: {status}. Reason: {reason}", deviceId, connectionStatusSubscription.CallbackUrl, status, reason);
try
{
var body = new ConnectionStatusChangeEventBody()
{
DeviceId = deviceId,
DeviceReceivedAt = DateTime.UtcNow,
Status = status.ToString(),
Reason = reason.ToString(),
};
var payload = new StringContent(JsonConvert.SerializeObject(body), Encoding.UTF8, "application/json");
using var httpResponse = await _httpClientFactory.CreateClient("RetryClient").PostAsync(connectionStatusSubscription.CallbackUrl, payload);
httpResponse.EnsureSuccessStatusCode();
_logger.Info("Successfully executed connection status change callback for device {deviceId}. Callback status code {statusCode}", deviceId, httpResponse.StatusCode);
}
catch (Exception e)
{
_logger.Error(e, "Failed to execute connection status change callback for device {deviceId}", deviceId);
}
};
}
}
}

Просмотреть файл

@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using NLog;
namespace DeviceBridge.Services
{
/// <summary>
/// When the application starts, initialize all device subscriptions that we have in the DB.
/// </summary>
public class SubscriptionStartupHostedService : IHostedService
{
private readonly Logger _logger;
private readonly SubscriptionService _subscriptionService;
public SubscriptionStartupHostedService(Logger logger, SubscriptionService subscriptionService)
{
_logger = logger;
_subscriptionService = subscriptionService;
}
public Task StartAsync(CancellationToken cancellationToken)
{
var _ = _subscriptionService.StartDataSubscriptionsInitializationAsync().ContinueWith(t => _logger.Error(t.Exception, "Failed to start subscription initialization task"), TaskContinuationOptions.OnlyOnFaulted);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

170
DeviceBridge/Startup.cs Normal file
Просмотреть файл

@ -0,0 +1,170 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using DeviceBridge.Common;
using DeviceBridge.Common.Authentication;
using DeviceBridge.Models;
using DeviceBridge.Providers;
using DeviceBridge.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NLog;
using Polly;
using Polly.Extensions.Http;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace DeviceBridge
{
/// <summary>Class Startup.</summary>
public class Startup
{
private static Logger _logger = LogManager.GetCurrentClassLogger();
/// <summary>Initializes a new instance of the <see cref="Startup"/> class.</summary>
/// <param name="configuration">The configuration.</param>
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
/// <summary>This method gets called by the runtime. Use this method to add services to the container.</summary>
/// <param name="services">The services.</param>
public void ConfigureServices(IServiceCollection services)
{
_logger.Info("Configuring services");
string kvUrl = Environment.GetEnvironmentVariable("KV_URL");
// Build cache from Key Vault
var secretsService = new SecretsProvider(kvUrl);
var idScope = secretsService.GetIdScopeAsync(_logger).Result;
var sasKey = secretsService.GetIotcSasKeyAsync(_logger).Result;
var sqlConnectionString = Utils.GetSqlConnectionString(_logger, secretsService);
// Override defaults
var customMaxPoolSize = Environment.GetEnvironmentVariable("MAX_POOL_SIZE");
var customRampupBatchSize = Environment.GetEnvironmentVariable("DEVICE_RAMPUP_BATCH_SIZE");
var customRampupBatchIntervalMs = Environment.GetEnvironmentVariable("DEVICE_RAMPUP_BATCH_INTERVAL_MS");
uint maxPoolSize = (customMaxPoolSize != null && customMaxPoolSize != string.Empty) ? Convert.ToUInt32(customMaxPoolSize, 10) : ConnectionManager.DeafultMaxPoolSize;
uint rampupBatchSize = (customRampupBatchSize != null && customRampupBatchSize != string.Empty) ? Convert.ToUInt32(customRampupBatchSize, 10) : SubscriptionService.DefaultRampupBatchSize;
uint rampupBatchIntervalMs = (customRampupBatchIntervalMs != null && customRampupBatchIntervalMs != string.Empty) ? Convert.ToUInt32(customRampupBatchIntervalMs, 10) : SubscriptionService.DefaultRampupBatchIntervalMs;
_logger.SetProperty("idScope", idScope);
_logger.SetProperty("cv", Guid.NewGuid()); // CV for all background operations
services.AddHttpContextAccessor();
// Start services
services.AddSingleton<ISecretsProvider>(secretsService);
services.AddSingleton(_logger);
services.AddSingleton<EncryptionService>();
services.AddSingleton<IStorageProvider>(provider => new StorageProvider(sqlConnectionString, provider.GetRequiredService<EncryptionService>()));
services.AddSingleton(provider => new ConnectionManager(provider.GetRequiredService<Logger>(), idScope, sasKey, maxPoolSize, provider.GetRequiredService<IStorageProvider>()));
services.AddSingleton(provider => new SubscriptionService(provider.GetRequiredService<Logger>(), provider.GetRequiredService<ConnectionManager>(), provider.GetRequiredService<IStorageProvider>(), provider.GetRequiredService<IHttpClientFactory>(), rampupBatchSize, rampupBatchIntervalMs));
services.AddSingleton<IBridgeService, BridgeService>();
services.AddHttpClient("RetryClient").AddPolicyHandler(GetRetryPolicy());
services.AddHostedService<ExpiredConnectionCleanupHostedService>();
services.AddHostedService<SubscriptionStartupHostedService>();
services.AddHostedService<HubCacheGcHostedService>();
services.AddAuthentication(o =>
{
o.DefaultScheme = SchemesNamesConst.TokenAuthenticationDefaultScheme;
})
.AddScheme<TokenAuthenticationOptions, TokenAuthenticationHandler>(SchemesNamesConst.TokenAuthenticationDefaultScheme, o => { });
services.AddControllers(options =>
{
options.Filters.Add(new AuthorizeFilter());
});
services.AddSwaggerGen(options =>
{
// Set XML comments.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
options.CustomOperationIds(apiDesc => apiDesc.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null);
// Type mappers for custom serialization.
options.MapType(typeof(DeviceSubscriptionType), () => DeviceSubscriptionType.Schema);
options.MapType(typeof(DeviceTwin), () => DeviceTwin.Schema);
});
}
/// <summary>This method gets called by the runtime. Use this method to configure the HTTP request pipeline..</summary>
/// <param name="app">The application.</param>
/// <param name="env">The env.</param>
/// <param name="lifetime">The lifetime.</param>
/// <param name="connectionManager">The connection manager.</param>
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime, ConnectionManager connectionManager)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger(c =>
{
c.SerializeAsV2 = true;
});
app.UseMiddleware<RequestLoggingMiddleware>();
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
/// <summary>
/// <para>Gets the retry policy, used in HttpClient.</para>
/// </summary>
/// <returns>IAsyncPolicy&lt;HttpResponseMessage&gt;.</returns>
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
// Handles 5XX, 408 and 429 status codes.
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == (HttpStatusCode)429)
.WaitAndRetryAsync(
retryCount: Convert.ToInt32(Environment.GetEnvironmentVariable("HTTP_RETRY_LIMIT")),
sleepDurationProvider: (retryCount, response, context) =>
{
// Observe server Retry-After if applicable
IEnumerable<string> retryAfterValues;
if (response.Result.Headers.TryGetValues("Retry-After", out retryAfterValues))
{
return TimeSpan.FromSeconds(Convert.ToDouble(retryAfterValues.FirstOrDefault()));
}
return TimeSpan.FromSeconds(Math.Pow(2, retryCount));
},
onRetryAsync: async (response, timespan, retryCount, context) =>
{
await Task.CompletedTask;
});
}
}
}

Просмотреть файл

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

Просмотреть файл

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

Просмотреть файл

@ -0,0 +1,9 @@
{
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
"settings": {
"documentationRules": {
"companyName": "Microsoft Corporation",
"xmlHeader": false
}
}
}

Просмотреть файл

@ -0,0 +1,9 @@
namespace DeviceBridgeTests
{
public static class TestConstants
{
public const string KeyvaultUrl = "";
public const string IdScope = "";
public const string SasKey = "";
}
}

Просмотреть файл

@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using DeviceBridge.Models;
using DeviceBridge.Services;
using Microsoft.AspNetCore.Mvc;
using Moq;
using NLog;
using NUnit.Framework;
namespace DeviceBridge.Controllers.Tests
{
[TestFixture]
public class MessagesControllerTests
{
private const string MockDeviceId = "test-device";
private Mock<IBridgeService> _bridgeServiceMock;
private MessagesController _messagesController;
[SetUp]
public async Task Setup()
{
_bridgeServiceMock = new Mock<IBridgeService>();
_messagesController = new MessagesController(LogManager.GetCurrentClassLogger(), _bridgeServiceMock.Object);
}
[Test]
[Description("SendTelemetry should convert creation time to UTC, call BridgeService.SendTelemetry, and return a 200 Ok")]
public async Task SendMessage()
{
var mockBody = new MessageBody()
{
ComponentName = "MyComponent",
CreationTimeUtc = DateTime.Parse("02/10/2018 11:25:27 +08:00"),
Properties = new Dictionary<string, string>()
{
{ "prop", "val" },
},
Data = new Dictionary<string, object>()
{
{ "temperature", 4 },
},
};
var result = await _messagesController.SendMessage(MockDeviceId, mockBody);
Assert.That(result, Is.InstanceOf<OkResult>());
_bridgeServiceMock.Verify(p => p.SendTelemetry(It.IsAny<Logger>(), MockDeviceId, mockBody.Data, default, mockBody.Properties, mockBody.ComponentName, It.Is<DateTime>(d => d == mockBody.CreationTimeUtc && d.Kind == DateTimeKind.Utc)), Times.Once);
}
}
}

Просмотреть файл

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Threading.Tasks;
using NUnit.Framework;
namespace DeviceBridge.Controllers.Tests
{
[TestFixture]
public class SessionControllerTests
{
[Test]
public async Task Get()
{
// TODO
// Calls GetDeviceSession with the correct deviceId
// Returns 200 if session exists
// Returns 404 if session doesn't exist
}
[Test]
public async Task CreateOrUpdate()
{
// TODO
// Sets input expiry to UTC
// Calls CreateOrUpdateDeviceSession with correct deviceId and expiry
// Calls InitializeDeviceClientAsync with the correct deviceId
// Does not call InitializeDeviceClientAsync if CreateOrUpdateDeviceSession fails
// Returns 200 and the created/updated session
// Returns 400 if CreateOrUpdateDeviceSession throws ExpiresAtLessThanCurrentTimeException
}
[Test]
public async Task Delete()
{
// TODO
// Calls DeleteDeviceSession with the correct deviceId
// Calls TearDownDeviceClientAsync with correct deviceId, without awaiting the result
// Does no call TearDownDeviceClientAsync if DeleteDeviceSession fails
// Returns 204
}
}
}

Просмотреть файл

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>DeviceBridgeTests</RootNamespace>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" Version="4.15.2" />
<PackageReference Include="nunit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DeviceBridge\DeviceBridge.csproj" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="stylecop.json" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Not all elements must be documented")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1604:Element documentation should have summary", Justification = "Not all elements must be documented")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1615:Element return value should be documented", Justification = "Not all elements must be documented")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1611:Element parameters should be documented", Justification = "Not all elements must be documented")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1601:Partial elements should be documented", Justification = "Not all elements must be documented")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Ok to omit this prefix")]
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives should be placed correctly", Justification = "Conflicts with VS rule")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "Using underscore for private class fields")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1312:Variable names should begin with lower-case letter", Justification = "Prevents the use of underscore for unused local variable")]

Просмотреть файл

@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Threading.Tasks;
using DeviceBridgeTests;
using NLog.Web;
using NUnit.Framework;
namespace DeviceBridge.Providers.Tests
{
[TestFixture]
public class SecretsProviderTests
{
private const string TmpIdScope = "tmpIdScope";
private const string TmpSasKey = "tmpSasKey";
private NLog.Logger logger;
private SecretsProvider sp;
[SetUp]
public async Task Init()
{
logger = NLogBuilder.ConfigureNLog("NLog.config").GetCurrentClassLogger();
sp = new SecretsProvider(TestConstants.KeyvaultUrl);
await sp.PutSecretAsync(logger, SecretsProvider.IotcIdScope, TmpIdScope);
await sp.PutSecretAsync(logger, SecretsProvider.IotcSasKey, TmpSasKey);
}
[Test]
public async Task GetIdScopeAsyncTest()
{
var idScope = await sp.GetIdScopeAsync(logger);
Assert.AreEqual(TmpIdScope, idScope);
// TODO
// Calls GetSecretAsync with correct secret name
}
[Test]
public async Task GetIotcSasKeyAsyncTest()
{
var sasKey = await sp.GetIotcSasKeyAsync(logger);
Assert.AreEqual(TmpSasKey, sasKey);
// TODO
// Calls GetSecretAsync with correct secret name
}
}
}

Просмотреть файл

@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Threading.Tasks;
using NUnit.Framework;
namespace DeviceBridge.Providers.Tests
{
[TestFixture]
public class StorageProviderTests
{
[Test]
public async Task GetDeviceSession()
{
// TODO
// Passes the right query
// Adds deviceId as parameter
// Return all session fields, including expiry
// Correctly converts field types
// Returns null if session isn't found
}
[Test]
public async Task CreateOrUpdateDeviceSession()
{
// TODO
// Calls the right procedure
// Adds deviceId and expiry as parameter
// Returns updated session, including updatedAt from query output
// Throws custom exception if query fails with "ExpiresAt must be greater than current time"
}
[Test]
public async Task DeleteDeviceSession()
{
// TODO
// Passes the right query
// Adds deviceId as parameter
}
}
}

Просмотреть файл

@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Threading.Tasks;
using NUnit.Framework;
namespace DeviceBridge.Services.Tests
{
[TestFixture]
public class BridgeServiceTests
{
[Test]
public async Task SendTelemetry()
{
// TODO
// Calls SendEventAsync
// Passes correct payload
// Passes cancellation token matching HTTP timeout
// Fails if client wasn't found
// Fails if using an already-disposed client
}
}
}

Просмотреть файл

@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
using System.Threading.Tasks;
using NUnit.Framework;
namespace DeviceBridge.Services.Tests
{
[TestFixture]
public class ConnectionManagerServiceTests
{
[Test]
public async Task TryGetDeviceClient()
{
// TODO
// Returns device client if one exists
}
[Test]
public async Task InitializeDeviceClientAsync()
{
// TODO
// Multiple calls to InitializeDeviceClientAsync do not run in parallel (test mutual exclusion)
// Calls to InitializeDeviceClientAsync and TearDownDeviceClientAsync do not run in parallel (test mutual exclusion)
// Returns existing client if one already exists
// Tries to connect to cached device hub, if one exists, before attempting other known hubs
// Tries to connect to all known hubs before trying DPS registration
// Fails right away if OpenAsync throws an exception not in the list of expected errors
// Tries DPS registration with correct key if attempts to connect to all known hubs fail
// Adds new hub to local device -> hub cache
// Tries to add new hub to local cache of known hubs
// Tries to store new hub in Key Vault if it was not yet stored
// Sets pooling and correct pool size when building a client
// Sets custom retry policy when building a client
// Disposes temporary client if an error happens during client build
}
[Test]
public async Task TearDownDeviceClientAsync()
{
// TODO
// Multiple calls to TearDownDeviceClientAsync do not run in parallel (test mutual exclusion)
// Calls to InitializeDeviceClientAsync and TearDownDeviceClientAsync do not run in parallel (test mutual exclusion)
// Removes client from list before closing
// Calls CloseAsync before disposing
// Disposes client
}
[Test]
public async Task ConnectionStatusChange()
{
// TODO
// SDK connection status changes update device status
}
}
}

Просмотреть файл

@ -0,0 +1,9 @@
{
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
"settings": {
"documentationRules": {
"companyName": "Microsoft Corporation",
"xmlHeader": false
}
}
}

14
Dockerfile Normal file
Просмотреть файл

@ -0,0 +1,14 @@
# Use SDK to build release package
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-img
WORKDIR /app
COPY . ./
RUN dotnet --info
RUN dotnet publish -c Release -o out
# Use runtime for final image
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
WORKDIR /app
COPY --from=build-img /app/out ./
ENTRYPOINT ["dotnet", "DeviceBridge.dll"]
EXPOSE 5001

Двоичные данные
Docs/Assets/adapter-architecture.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 96 KiB

Двоичные данные
Docs/Assets/custom_deployment.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 22 KiB

Двоичные данные
Docs/Assets/edit_template.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 37 KiB

Двоичные данные
Docs/Assets/original-architecture.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 86 KiB

Двоичные данные
Docs/Assets/sas_enrollment_group.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 81 KiB

Двоичные данные
Docs/Assets/scope_id_and_key.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 60 KiB

683
Docs/swagger.json Normal file
Просмотреть файл

@ -0,0 +1,683 @@
{
"swagger": "2.0",
"info": {
"title": "DeviceBridge",
"version": "1.0"
},
"paths": {
"/devices/{deviceId}/ConnectionStatus": {
"get": {
"tags": [
"ConnectionStatus"
],
"summary": "Gets that latest connection status for a device.",
"description": "For a detailed description of each status, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.",
"operationId": "GetCurrentConnectionStatus",
"produces": [
"application/json"
],
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The latest connection status and reason.",
"schema": {
"$ref": "#/definitions/DeviceStatusResponseBody"
}
},
"404": {
"description": "If the connection status is not known (i.e., the device hasn't attempted to connect)."
}
}
}
},
"/devices/{deviceId}/ConnectionStatus/sub": {
"get": {
"tags": [
"ConnectionStatus"
],
"summary": "Gets the current connection status change subscription for a device.",
"operationId": "GetConnectionStatusSubscription",
"produces": [
"application/json"
],
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The current connection status subscription.",
"schema": {
"$ref": "#/definitions/DeviceSubscription"
}
},
"404": {
"description": "If a subscription doesn't exist."
}
}
},
"put": {
"tags": [
"ConnectionStatus"
],
"summary": "Creates or updates the current connection status change subscription for a device.",
"description": "When the internal connection status of a device changes, the service will send an event to the desired callback URL.\r\n \r\n Example event:\r\n {\r\n \"eventType\": \"string\",\r\n \"deviceId\": \"string\",\r\n \"deviceReceivedAt\": \"2020-12-04T01:06:14.251Z\",\r\n \"status\": \"string\",\r\n \"reason\": \"string\"\r\n }\r\n \r\nFor a detailed description of each status, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.",
"operationId": "CreateOrUpdateConnectionStatusSubscription",
"consumes": [
"application/json",
"text/json",
"application/*+json"
],
"produces": [
"application/json"
],
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "body",
"schema": {
"$ref": "#/definitions/SubscriptionCreateOrUpdateBody"
}
}
],
"responses": {
"200": {
"description": "The created or updated connection status subscription.",
"schema": {
"$ref": "#/definitions/DeviceSubscription"
}
}
}
},
"delete": {
"tags": [
"ConnectionStatus"
],
"summary": "Deletes the current connection status change subscription for a device.",
"operationId": "DeleteConnectionStatusSubscription",
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
}
],
"responses": {
"204": {
"description": "Subscription deleted successfully."
}
}
}
},
"/devices/{deviceId}/DeviceBound/sub": {
"get": {
"tags": [
"DeviceBound"
],
"summary": "Gets the current C2D message subscription for a device.",
"operationId": "GetC2DMessageSubscription",
"produces": [
"application/json"
],
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The current C2D message subscription.",
"schema": {
"$ref": "#/definitions/DeviceSubscriptionWithStatus"
}
},
"404": {
"description": "If a subscription doesn't exist."
}
}
},
"put": {
"tags": [
"DeviceBound"
],
"summary": "Creates or updates the current C2D message subscription for a device.",
"description": "When the device receives a new C2D message from IoTHub, the service will send an event to the desired callback URL.\r\n \r\n Example event:\r\n {\r\n \"eventType\": \"string\",\r\n \"deviceId\": \"string\",\r\n \"deviceReceivedAt\": \"2020-12-04T01:06:14.251Z\",\r\n \"messageBody\": {},\r\n \"properties\": {\r\n \"prop1\": \"string\",\r\n \"prop2\": \"string\",\r\n },\r\n \"messageId\": \"string\",\r\n \"expirtyTimeUtC\": \"2020-12-04T01:06:14.251Z\"\r\n }\r\n \r\nThe response status code of the callback URL will determine how the service will acknowledge a message:\r\n- Response code between 200 and 299: the service will complete the message.\r\n- Response code between 400 and 499: the service will reject the message.\r\n- Any other response status: the service will abandon the message, causing IotHub to redeliver it.\r\n \r\nFor a detailed overview of C2D messages, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d.",
"operationId": "CreateOrUpdateC2DMessageSubscription",
"consumes": [
"application/json",
"text/json",
"application/*+json"
],
"produces": [
"application/json"
],
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "body",
"schema": {
"$ref": "#/definitions/SubscriptionCreateOrUpdateBody"
}
}
],
"responses": {
"200": {
"description": "The created or updated C2D message subscription.",
"schema": {
"$ref": "#/definitions/DeviceSubscriptionWithStatus"
}
}
}
},
"delete": {
"tags": [
"DeviceBound"
],
"summary": "Deletes the current C2D message subscription for a device.",
"operationId": "DeleteC2DMessageSubscription",
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
}
],
"responses": {
"204": {
"description": "Subscription deleted successfully."
}
}
}
},
"/devices/{deviceId}/Messages/events": {
"post": {
"tags": [
"Messages"
],
"summary": "Sends a device message to IoTHub.",
"description": "Example request:\r\n \r\n POST /devices/{deviceId}/messages/events\r\n {\r\n \"data\": {\r\n \"temperature\": 4.8,\r\n \"humidity\": 31\r\n }\r\n }\r\n.",
"operationId": "SendMessage",
"consumes": [
"application/json",
"text/json",
"application/*+json"
],
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "body",
"schema": {
"$ref": "#/definitions/MessageBody"
}
}
],
"responses": {
"200": {
"description": "Message sent successfully."
}
}
}
},
"/devices/{deviceId}/Methods/sub": {
"get": {
"tags": [
"Methods"
],
"summary": "Gets the current direct methods subscription for a device.",
"operationId": "GetMethodsSubscription",
"produces": [
"application/json"
],
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The current direct methods subscription.",
"schema": {
"$ref": "#/definitions/DeviceSubscriptionWithStatus"
}
},
"404": {
"description": "If a subscription doesn't exist."
}
}
},
"put": {
"tags": [
"Methods"
],
"summary": "Creates or updates the current direct methods subscription for a device.",
"description": "When the device receives a direct method invocation from IoTHub, the service will send an event to the desired callback URL.\r\n \r\n Example event:\r\n {\r\n \"eventType\": \"string\",\r\n \"deviceId\": \"string\",\r\n \"deviceReceivedAt\": \"2020-12-04T01:06:14.251Z\",\r\n \"methodName\": \"string\",\r\n \"requestData\": {}\r\n }\r\n \r\nThe callback may return an optional response body, which will be sent to IoTHub as the method response:\r\n \r\n Example callback response:\r\n {\r\n \"status\": 200,\r\n \"payload\": {}\r\n }\r\n.",
"operationId": "CreateOrUpdateMethodsSubscription",
"consumes": [
"application/json",
"text/json",
"application/*+json"
],
"produces": [
"application/json"
],
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "body",
"schema": {
"$ref": "#/definitions/SubscriptionCreateOrUpdateBody"
}
}
],
"responses": {
"200": {
"description": "The created or updated C2D message subscription.",
"schema": {
"$ref": "#/definitions/DeviceSubscriptionWithStatus"
}
}
}
},
"delete": {
"tags": [
"Methods"
],
"summary": "Deletes the current direct methods subscription for a device.",
"operationId": "DeleteMethodsSubscription",
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
}
],
"responses": {
"204": {
"description": "Subscription deleted successfully."
}
}
}
},
"/devices/{deviceId}/Registration": {
"post": {
"tags": [
"Registration"
],
"summary": "Performs DPS registration for a device, optionally assigning it to a model.",
"description": "The registration result is internally cached to be used in future connections.\r\nThis route is only intended for ahead-of-time registration of devices with the bridge and assignment to a specific model. To access all DPS registration features,\r\nincluding sending custom registration payload and getting the assigned hub, please use the DPS REST API (https://docs.microsoft.com/en-us/rest/api/iot-dps/).\r\n \r\n<b>NOTE:</b> DPS registration is a long-running operation, so calls to this route may take a long time to return. If this is a concern, use the DPS REST API directly, which provides\r\nsupport for long-running operation status lookup.",
"operationId": "Register",
"consumes": [
"application/json",
"text/json",
"application/*+json"
],
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "body",
"schema": {
"$ref": "#/definitions/RegistrationBody"
}
}
],
"responses": {
"200": {
"description": "Registration successful."
}
}
}
},
"/devices/{deviceId}/Resync": {
"post": {
"tags": [
"Resync"
],
"summary": "Forces a full synchronization of all subscriptions for this device and attempts to restart any subscriptions in a stopped state.",
"description": "Internally it forces the reconnection of the device if it's in a permanent failure state, due for instance to:\r\n- Bad credentials.\r\n- Device was previously disabled in the cloud side.\r\n- Automatic retries expired (e.g., due to a long period without network connectivity).",
"operationId": "Resync",
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
}
],
"responses": {
"202": {
"description": "Resynchronization started."
}
}
}
},
"/devices/{deviceId}/Twin": {
"get": {
"tags": [
"Twin"
],
"summary": "Gets the device twin.",
"operationId": "GetTwin",
"produces": [
"application/json"
],
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The device twin.",
"schema": {
"type": "object",
"properties": {
"twin": {
"type": "object",
"properties": {
"properties": {
"type": "object",
"properties": {
"desired": {
"type": "object"
},
"reported": {
"type": "object"
}
}
}
}
}
}
}
}
}
}
},
"/devices/{deviceId}/Twin/properties/reported": {
"patch": {
"tags": [
"Twin"
],
"summary": "Updates reported properties in the device twin.",
"description": "Example request:\r\n \r\n PATCH /devices/{deviceId}/properties/reported\r\n {\r\n \"patch\": {\r\n \"fanSpeed\": 35,\r\n \"serial\": \"ABC\"\r\n }\r\n }\r\n.",
"operationId": "UpdateReportedProperties",
"consumes": [
"application/json",
"text/json",
"application/*+json"
],
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "body",
"schema": {
"$ref": "#/definitions/ReportedPropertiesPatch"
}
}
],
"responses": {
"204": {
"description": "Twin updated successfully."
}
}
}
},
"/devices/{deviceId}/Twin/properties/desired/sub": {
"get": {
"tags": [
"Twin"
],
"summary": "Gets the current desired property change subscription for a device.",
"operationId": "GetDesiredPropertiesSubscription",
"produces": [
"application/json"
],
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The current desired property change subscription.",
"schema": {
"$ref": "#/definitions/DeviceSubscriptionWithStatus"
}
},
"404": {
"description": "If a subscription doesn't exist."
}
}
},
"put": {
"tags": [
"Twin"
],
"summary": "Creates or updates the current desired property change subscription for a device.",
"description": "When the device receives a new desired property change from IoTHub, the service will send an event to the desired callback URL.\r\n \r\n Example event:\r\n {\r\n \"eventType\": \"string\",\r\n \"deviceId\": \"string\",\r\n \"deviceReceivedAt\": \"2020-12-04T01:06:14.251Z\",\r\n \"desiredProperties\": {\r\n \"prop1\": \"string\",\r\n \"prop2\": 12,\r\n \"prop3\": {},\r\n }\r\n }\r\n.",
"operationId": "CreateOrUpdateDesiredPropertiesSubscription",
"consumes": [
"application/json",
"text/json",
"application/*+json"
],
"produces": [
"application/json"
],
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "body",
"schema": {
"$ref": "#/definitions/SubscriptionCreateOrUpdateBody"
}
}
],
"responses": {
"200": {
"description": "The created or updated C2D message subscription.",
"schema": {
"$ref": "#/definitions/DeviceSubscriptionWithStatus"
}
}
}
},
"delete": {
"tags": [
"Twin"
],
"summary": "Deletes the current desired property change subscription for a device.",
"operationId": "DeleteDesiredPropertiesSubscription",
"parameters": [
{
"in": "path",
"name": "deviceId",
"required": true,
"type": "string"
}
],
"responses": {
"204": {
"description": "Subscription deleted successfully."
}
}
}
}
},
"definitions": {
"DeviceStatusResponseBody": {
"type": "object",
"properties": {
"status": {
"type": "string"
},
"reason": {
"type": "string"
}
}
},
"DeviceSubscription": {
"type": "object",
"properties": {
"deviceId": {
"type": "string"
},
"subscriptionType": {
"type": "string"
},
"callbackUrl": {
"type": "string"
},
"createdAt": {
"format": "date-time",
"type": "string"
}
}
},
"SubscriptionCreateOrUpdateBody": {
"required": [
"callbackUrl"
],
"type": "object",
"properties": {
"callbackUrl": {
"type": "string"
}
}
},
"DeviceSubscriptionWithStatus": {
"type": "object",
"properties": {
"deviceId": {
"type": "string"
},
"subscriptionType": {
"type": "string"
},
"callbackUrl": {
"type": "string"
},
"createdAt": {
"format": "date-time",
"type": "string"
},
"status": {
"type": "string"
}
}
},
"MessageBody": {
"required": [
"data"
],
"type": "object",
"properties": {
"data": {
"type": "object",
"additionalProperties": { }
},
"properties": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"componentName": {
"type": "string"
},
"creationTimeUtc": {
"format": "date-time",
"type": "string"
}
}
},
"RegistrationBody": {
"type": "object",
"properties": {
"modelId": {
"type": "string"
}
}
},
"ReportedPropertiesPatch": {
"required": [
"patch"
],
"type": "object",
"properties": {
"patch": {
"type": "object",
"additionalProperties": { }
}
}
}
}
}

379
README.md Normal file
Просмотреть файл

@ -0,0 +1,379 @@
# Azure IoT Central Device Bridge
The Device Bridge enables the connection of devices to Azure IoT Central in scenarios where it's not possible to use the Azure IoT Device SDKs.
The solution in this repository deploys a set of resources to your Azure Subscription. Once deployed, it exposes a simple, yet powerful, HTTP interface
that can be used for sending and receiving data between devices and Azure IoT. The solution can be used *as is* or customized with additional components,
such as data transformation and protocol adapter modules.
- [Deployment instructions](#deployment-instructions)
* [1 - Build and push the Docker image](#1---build-and-push-the-docker-image)
* [2 - Open the deployment template in Azure Portal](#2---open-the-deployment-template-in-azure-portal)
* [3 - Deployment parameters](#3---deployment-parameters)
+ [3.1 Bridge name](#3.1-bridge-name)
+ [3.2 IoTC SAS key and Id scope](#3.2-iotc-sas-key-and-id-scope)
+ [3.3 API key](#3.3-api-key)
+ [3.4 SQL credentials](#3.4-sql-credentials)
+ [3.5 Log Analytics workspace credentials](#3.5-log-analytics-workspace-credentials)
+ [3.6 Image and Azure Container Registry credentials](#3.6-image-and-azure-container-registry-credentials)
+ [4 - Create resources](#4---create-resources)
- [What is being provisioned](#what-is-being-provisioned)
* [Pricing](#pricing)
- [HTTP API](#http-api)
* [Device to cloud messages](#device-to-cloud-messages)
* [Get device twin](#get-device-twin)
* [Update reported properties](#update-reported-properties)
* [Subscribing to events](#subscribing-to-events)
+ [Methods](#methods)
+ [C2D messages](#c2d-messages)
+ [Desired property updates](#desired-property-updates)
* [Device connection status](#device-connection-status)
+ [Subscribing to connection status change events](#subscribing-to-connection-status-change-events)
+ [Restarting stopped subscriptions and forcing device reconnection](#restarting-stopped-subscriptions-and-forcing-device-reconnection)
* [Device provisioning](#device-provisioning)
* [Subscription callback retries](#subscription-callback-retries)
- [Load and performance](#load-and-performance)
* [SSL certificate limits](#ssl-certificate-limits)
* [Instance restarts and reconnection speed](#instance-restarts-and-reconnection-speed)
* [Multiplexing and connection pool](#multiplexing-and-connection-pool)
- [Monitoring](#monitoring)
- [Encryption key rotation](#encryption-key-rotation)
- [Custom adapters](#custom-adapters)
## Deployment instructions
To use the device bridge solution, you will need the following:
- an Azure account. You can create a free Azure account from [here](https://aka.ms/aft-iot)
- an Azure IoT Central application to connect the devices. Create a free app by following [these instructions](https://docs.microsoft.com/en-us/azure/iot-central/quick-deploy-iot-central)
### 1 - Build and push the Docker image
First, using the Docker CLI, run the `docker build .` command in the solution folder to build the Device Bridge image. Second, you'll need to
tag and push the image to a private container registry, such as Azure Container Registry (ACR). Instructions on how to build, tag, and push an
image to ACR can be found [here](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-get-started-docker-cli).
Note the image name and the ACR credentials, which are necessary in the next steps.
### 2 - Open the deployment template in Azure Portal
Open the [template deployment page in the Azure Portal](https://portal.azure.com/#create/Microsoft.Template), click `Build your own template in the editor`.
In the edit template page, paste the contents of the `azuredeploy.json` file in this repository in the editor window and click save.
![Custom deployment](Docs/Assets/custom_deployment.png "custom deployment")
![Edit template](Docs/Assets/edit_template.png "Edit template")
### 3 - Deployment parameters
#### 3.1 Bridge name
In the `Bridge-name` parameter, enter the name for the new Bridge instance. This name will be part of the HTTPS endpoint
for your instance, so it may only contain letters, numbers, and dashes.
#### 3.2 IoTC SAS key and Id scope
These parameters link your new Bridge instance to a specific Azure IoT Central application. Go to your IoT Central
application and navigate to the Administration > Device Connection area. Copy the `ID Scope` field and paste it into
the `iotc-id-scope` parameter in the template.
In the same page, under Enrollment groups, open the `SAS-IoT-Devices` group. In the group page, copy either the `Primary key` or
`Secondary key` and paste it in the `Iotc-dps-sas-key` parameter of the template (this key will be stored in a Key Vault provisioned with the solution).
![Scope Id and key](Docs/Assets/scope_id_and_key.png "scope id and key")
![Enrollment group](Docs/Assets/sas_enrollment_group.png "enrollment group")
#### 3.3 API key
In the `Api-key` parameter, define a strong (and preferably randomly generated) API key. This key will be used to protect the HTTP API and must be included in the
`x-api-key` header of every request to the Bridge.
#### 3.4 SQL credentials
In the `Sql-username` and `Sql-password` parameters, enter an admin login and password for the Azure SQL instance that will be provisioned
by the solution. The password must follow the [SQL Server password policy](https://docs.microsoft.com/en-us/sql/relational-databases/security/password-policy?view=sql-server-ver15#password-complexity).
#### 3.5 Log Analytics workspace credentials
By default, the Bridge will publish logs to a Log Analytics workspace of your choice. In the `Log-analytics-workspace-id` and `Log-analytics-workspace-key`
parameters, provide the credentials of your workspace (instructions to obtain the credentials of your Log Analytics workspace can be found [here](https://docs.microsoft.com/en-us/azure/container-instances/container-instances-log-analytics#get-log-analytics-credentials)).
#### 3.6 Image and Azure Container Registry credentials
In this step you need to grant the solution access to the Docker image that you built and pushed in step 1.
In the `Bridge-image` enter the name of the image that you pushed to your private ACR. In the `Acr-server`,
`Acr-username`, and `Acr-password` parameters provide the credentials of your ACR instance.
#### 4 - Create resources
After providing all necessary parameters, click the `Review+create` button. The deployment may take a few minutes. Once finished,
you can find the newly provisioned resources in the target resource group.
## What is being provisioned
The template in this solution will provision the following resources to your Azure subscription:
- Key Vault - to store all secrets
- Azure SQL - to store device and subscription data
- Azure container instance - main solution
- Azure container instance - setup - (only runs during the solution setup phase)
### Pricing
The main pricing components for the resources published by the ARM template in this repository are the Azure Container Instance that
hosts the core Bridge (1 CPU and 3 GB of memory - see [Container Instances pricing](https://azure.microsoft.com/en-us/pricing/details/container-instances/)
for an estimate) and the Azure SQL Database (Basic tier, 5 DTUs - see [Azure SQL pricing](https://azure.microsoft.com/en-us/pricing/details/sql-database/single/) for more details).
## HTTP API
Once deployed, the solution will expose an HTTPS endpoint at `https://<bridge-name>.<region>.azurecontainer.io`. This endpoint can also be found by navigating to
the Device Bridge container group deployed to your subscription > `Properties` > `FQDN`.
The HTTP endpoints are protected by the API key provided when the solution was provisioned. For every request, include an `x-api-key` header containing the key.
In what follows we give a quick overview of the functionalities available through the API. For a full description of the
endpoints and data types as well as sample requests and responses, refer to the API swagger under `Docs/swagger.json`.
### Device to cloud messages
The Bridge can send messages to Azure IoT on behalf of a device.
The main component of a message is the `data` field. Optionally, the request can include `componentName`, `properties`, and a `creationTimeUtc`.
Here's an example of a request to send a message:
```json
POST /devices/{deviceId}/messages/events
{
"data": {
"temperature": 4.8,
"humidity": 31
},
"properties": {
"prop1": "abc"
},
}
```
### Get device twin
The device twin will include the latest version of desired and reported properties of a device.
Below is an example of the response returned by the service when fetching the latest twin for a device:
```json
{
"twin": {
"properties": {
"desired": {
"fanSpeed": 30,
"$version": 2
},
"reported": {
"threshold": 4.8,
"LastReported": "2021-01-10T00:35:00.388Z",
"$version": 5
}
}
}
}
```
### Update reported properties
To update reported properties in a device twin, include a patch in the request body, as the example below:
```json
PATCH /devices/{deviceId}/properties/reported
{
"patch": {
"fanSpeed": 35,
"serial": "ABC"
}
}
```
### Subscribing to events
You can subscribe to get notified of cloud-to-device events (methods, C2D messages, and desired property updates). To create a subscription,
you need to provide the callback URL that the service will send a `POST` request to when a notification is available. Only one subscription of each type can exist at a time
(e.g., issuing a new subscription creation request of the same type will update the callback URL of the existing subscription).
#### Methods
Method subscriptions will emit an event whenever a new direct method invocation is issues by the cloud. The following is
an example of a method invocation event:
```json
{
"eventType": "DirectMethodInvocation",
"deviceId": "my-device",
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
"methodName": "increaseTemperature",
"requestData": {
"celsius" : 2
}
}
```
The callback may return an optional response body, which will be sent to IoTHub as the method response.
Below is an example of a response:
```json
{
"status": 200,
"payload": {
"newTemperature": 24
}
}
```
#### C2D messages
When the device receives a new C2D message from IoTHub, the service will send an event to the desired callback URL as in the example below:
```json
{
"eventType": "C2DMessage",
"deviceId": "my-device",
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
"messageBody": {
"someField": 20.2
},
"properties": {
"prop1": "val1",
"prop2": "val2",
},
"messageId": "abc",
"expirtyTimeUtC": "2020-12-04T01:06:14.251Z"
}
```
The response status code of the callback URL will determine how the service will acknowledge a message:
- Response code between 200 and 299: the service will `complete` the message.
- Response code between 400 and 499: the service will `reject` the message.
- Any other response status: the service will `abandon` the message, causing IotHub to redeliver it.
For a detailed overview of C2D messages, see https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d.
#### Desired property updates
When the device receives a new desired property change from IoT Hub, the service will send an event to the desired callback URL. Example event:
```json
{
"eventType": "DesiredPropertyUpdate",
"deviceId": "my-device",
"deviceReceivedAt": "2020-12-04T01:06:14.251Z",
"desiredProperties": {
"prop1": "string",
"prop2": 12,
"prop3": {},
}
}
```
> NOTE: desired property updates are only received while the device is connected to IoT Hub. For this reason, property updates
might not be received while, for instance, the device is internally reconnecting due to a transient network error. To mitigate scenarios like this,
you can listen to internal connection events, as described in the next section, and get the latest device twin whenever the `Connected` event
is received.
### Device connection status
Internally, the Device Bridge connects devices to Azure IoT through AMQP using connection multiplexing. It transparently manages
the life cycle of all connections (i.e., connecting, disconnecting, and retrying on transient errors). When you issue a command,
such as sending a message or getting the device twin, the Bridge opens
a temporary connection for the device that is set to live between 9 to 11 minutes and is renewed as requests come. For subscriptions,
the Bridge will open a permanent connection for the device. This connection will last until the last subscription is deleted.
Depending on the connection status of a device, it's subscriptions may have one of the following status:
- `Starting`: initial status after a subscription is created and before the device has been connected internally.
This status may also represent that the Bridge is reconnecting the device after the service restarts.
- `Running`: the device is connected and events are flowing.
- `Stopped`: the device is disconnected due to a permanent failure and events are not flowing. Permanent failure scenarios can include: the device has been disabled in the IoT Hub, credentials are no longer valid, the automatic connection retries
expired due to a long period without network connectivity, etc.
#### Subscribing to connection status change events
In many situations it may be useful to react to changes in the underlying device connection status. For instance,
you may want to fetch the latest device twin whenever the device reconnects internally. Alternatively, you may want to
be notified when a device experiences a permanent failure, such as credentials expired. For these scenarios you can subscribe
to connection status changes.
Whenever the device internally connects or reconnects, a `Connected` event will be sent. If the device fails to connect permanently,
a `Disconnected` event will be emitted, including an associated failure reason. Other possible statuses are `Disabled` (the connection has
been closed properly) and `Disconnected_Retrying` (the service is retrying to connect the device).
For a detailed description of all status and reasons, see https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
#### Restarting stopped subscriptions and forcing device reconnection
Issuing a call to the `resync` endpoint will cause the Bridge to attempt to reconnect any device previously in a permanent failure state.
If the device reconnection is successful, all stopped subscriptions will be back to a running state. You may want to issue this command, for instance,
after re-enabling a device in IoT Hub.
### Device provisioning
By default, issuing a command to the Bridge on behalf of a device will cause it to be automatically provisioned. The device
is provisioned using the SAS key provided during the solution deployment and will not be assigned to any particular model.
The Bridge provides an endpoint to optionally provision a device to a specific model ahead of time. The registration result is
internally cached to be used in future connections.
To access advanced registration features, including sending custom registration payload and getting the assigned hub,
please use the DPS REST API (https://docs.microsoft.com/en-us/rest/api/iot-dps/).
> NOTE: DPS registration is a long-running operation, so calls to this route may take a long time to return (in the order of seconds).
If this is a concern, use the DPS REST API directly, which provides support for long-running operation status lookup.
### Subscription callback retries
The solution will automatically retry if a transient error happens when calling a subscription callback endpoint. By default, it will
retry 5 times with an exponentially increasing interval (total 30 seconds). This number can be customized by overriding the `HTTP_RETRY_LIMIT`
environment variable in the provisioning template. For `HTTP 429s`, the service will respect the `Retry-After` header, if one is available.
> NOTE: if needed, the solution code can be modified to add HTTP circuit-breaking be following the instructions on https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/implement-circuit-breaker-pattern.
## Load and performance
The default setup provisioned by the ARM template in this repository was tested with up to 50,000 devices simultaneously connected.
In this scenario, we were able to send 10,000 device to cloud messages (telemetry) per minute through the Bridge. For cloud to device
events (including C2D messages, method invocations, and desired property updates combined), the Bridge was able to receive and forward 5,000
events per minute.
This test was performed in a constant-rate scenario, where all devices were already internally connected. Numbers may greatly vary depending
on different usage patterns, such as how often devices send or subscribe to events, which affects how often they are internally reconnected.
The specific setup being provisioned (e.g., different container sizes) will also directly influence the results. When deploying a new setup,
make sure you evaluate the performance considerations described in the following sections.
### SSL certificate limits
The Caddy webserver deployed in the solution uses [Let's Encrypt](https://letsencrypt.org/) to automatically obtain an HTTPS certificate.
This service is free, but has a current limit of SSL 5 certificates per week per endpoint. Because the containers used by this solution
deploy non-persistent volumes, if the container group is manually restarted more than 5 times in a week (for instance during development phase),
SSL certificate acquisition will fail and a new solution with a new endpoint will need to be deployed.
To avoid this limitation, you may choose to modify the solution to [use persistent storage](https://docs.microsoft.com/en-us/azure/container-instances/container-instances-volume-azure-files),
use a different [certificate authority and webserver configuration](https://docs.microsoft.com/en-us/azure/container-instances/container-instances-container-group-ssl),
or use Let's Encrypt staging environment during development (see https://letsencrypt.org/docs/rate-limits/ for more information).
### Instance restarts and reconnection speed
If the service instance restarts, the Bridge will automatically reconnect any devices with an active subscription. During this period,
all subscriptions will have a `Starting` status. The speed at which the Bridge reconnects devices is set by default to 150 connections per second.
This means that, in the event of a container restart, it would take around one minute to reconnect 10,000 devices.
> NOTE: this number can be customized through `DEVICE_RAMPUP_BATCH_SIZE` and `DEVICE_RAMPUP_BATCH_INTERVAL_MS` environment variables. However, before adjusting this
values it's important to perform a load test to make sure that the service can support the desired reconnection speed. Factors that may influence this speed is
the size of the hosting container, number of devices, and desired number of connections in the multiplexing pool.
### Multiplexing and connection pool
By default, the Bridge is configured to use a pool of 50 active AMQP connections. This amount allows around 50K devices to be connected
simultaneously. This number can be configured through the `MAX_POOL_SIZE` environment variable, however increasing this value may require
decreasing the default device reconnection speed, or else the Bridge may fail to reconnect devices in the event of a container restart.
For more details about connection pooling and multiplexing, see https://github.com/Azure/azure-iot-sdk-csharp/blob/master/iothub/device/devdoc/amqpstack.md.
> NOTE: this solution is meant to run as a single instance. Deploying more than one instance of the service pointing to the same
Azure IoT Central application will potentially cause duplicate device connections, as instances are not aware of each other. This will
result in connection drops, as each device can only have a single active AMQP link to IoT Hub at all times.
## Monitoring
Service logs are emitted to the Log Analytics Workspace provided during deployment. You can use the logs to query device events as well as
build monitors for specific scenarios. Below is an example query that shows all device events in the past hour:
```
ContainerInstanceLog_CL
| where TimeGenerated >= ago(1h)
| where ContainerGroup_s == "<container-group-name>"
| extend log = parse_json(Message)
| extend deviceId = tostring(log.deviceId)
| extend _message = tostring(log.message)
| where deviceId != ""
| distinct TimeGenerated, deviceId, _message
```
## Encryption key rotation
When a new subscription is created, the provided callback URL is stored in the database in an encrypted format. This provides an
extra layer of security, as these URLs may contain sensitive data, such as access keys or tokens in the query parameters. The
encryption key used in this scenario is automatically generated when the solution is deployed and stored in the Key Vault.
To rotate this encryption key, simply start the setup container provisioned to the same resource group (the container group name
starts with `iotc-container-groups-setup-`). Once started, the setup container will automatically generate a new encryption key and
use it to reencrypt all subscriptions stored in the database (you can follow the progress in the container logs `Container group > Containers > Logs`).
Once reencryption has finished, you can restart the main Bridge container (the container group name starts with `iotc-container-groups-`)
to make sure that it starts using the new key for new subscriptions.
## Custom adapters
One of the possible ways to extend the functionality of this Bridge is through a custom adapter deployed as a sidecar container.
With this type of adapter, which can be written in the language and runtime of your choice, you can for instance transform
the data before it reaches or leaves the Bridge and connect to different data sources. Under the `Samples/SampleTypeScriptAdapter`
folder we provide an example of a custom adapter written in TypeScript that forwards cloud-to-device messages to an EventHub.
This adapter uses a client automatically generated with [AutoRest](https://github.com/Azure/autorest) using the Bridge swagger available
under `Docs/swagger.json`. The example also contains the necessary ARM template to deploy it as a sidecar with the Bridge.
The diagrams below illustrate the original Bridge architecture and how a custom adapter fits into it:
![Original architecture](Docs/Assets/original-architecture.png "original architecture")
![Adapter architecture](Docs/Assets/adapter-architecture.png "adapter architecture")

Просмотреть файл

@ -0,0 +1 @@
node_modules/

3
Samples/SampleTypeScriptAdapter/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,3 @@
node_modules/
dist/
/package-lock.json

Просмотреть файл

@ -0,0 +1,9 @@
FROM node:12
CMD ["node", "dist/index.js"]
WORKDIR /app
COPY package.json ./
RUN npm i --quiet && npm cache clean --force
COPY ./ ./
RUN npm run build

Просмотреть файл

@ -0,0 +1,23 @@
# Sample adapter for the Device Bridge
This sample adapter written in TypeScript forwards D2C messages to an Event Hub.
The code uses a client automatically generated by autorest using the Device Bridge swagger.
Failures are logged in the same Log Analytics workspace used by the Bridge core module.
> NOTE: this code should only be used as a sample reference for how to build adapters for the Device
Bridge. It should not be used in a production setting without proper testing and error handling.
## APIs
### Subscribe
```
POST /subscribe/{deviceId}
```
Creates a C2D message subscription for a device and forwards all C2D messages to an EventHub.
### Unsubscribe
```
POST /unsubscribe/{deviceId}
```
Deletes the C2D message subscription for a device and stops publishing events.

Просмотреть файл

@ -0,0 +1,535 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"bridge-name": {
"type": "string",
"metadata": {
"description": "The name of the device bridge. Also will be used in the url."
}
},
"iotc-dps-sas-key": {
"type": "securestring",
"metadata": {
"description": "DPS sas key for provisioning devices and sending data. Retrieved from iot central."
}
},
"iotc-id-scope": {
"type": "string",
"metadata": {
"description": "ID Scope provisioning devices and sending data. Retrieved from iot central."
}
},
"event-hub-name": {
"type": "string",
"metadata": {
"description": "Name of the Event Hub to push events to."
}
},
"event-hub-connection-string": {
"type": "securestring",
"metadata": {
"description": "Connection string of the Event Hub to push events to."
}
},
"api-key": {
"type": "securestring",
"metadata": {
"description": "Api key used to validate requests to IoTC device bridge."
}
},
"sql-username": {
"type": "string",
"metadata": {
"description": "Username for the sql server provisioned."
}
},
"sql-password": {
"type": "securestring",
"metadata": {
"description": "Password for the sql server provisioned."
}
},
"log-analytics-workspace-id": {
"type": "string",
"metadata": {
"description": "Log Analytics workspace id for log storage."
}
},
"log-analytics-workspace-key": {
"type": "securestring",
"metadata": {
"description": "Log Analytics workspace key for log storage."
}
},
"bridge-image": {
"type": "string",
"metadata": {
"description": "Docker image to be deployed for the core and setup modules."
}
},
"adapter-image": {
"type": "string",
"metadata": {
"description": "Docker image to be deployed for the adapter module."
}
},
"acr-server": {
"type": "string",
"metadata": {
"description": "Private ACR server to pull the image from."
}
},
"acr-username": {
"type": "string",
"metadata": {
"description": "Username of the private ACR."
}
},
"acr-password": {
"type": "securestring",
"metadata": {
"description": "Password of the private ACR."
}
}
},
"variables": {
"setupContainerGroupsName": "[concat('iotc-container-groups-setup-', uniqueString(resourceGroup().id))]",
"containerGroupsName": "[concat('iotc-container-groups-', uniqueString(resourceGroup().id))]",
"bridgeContainerName": "[concat('iotc-bridge-container-', uniqueString(resourceGroup().id))]",
"keyvaultName": "[concat('iotc-kv-', uniqueString(resourceGroup().id))]",
"databaseName": "[concat('iotc-db-', uniqueString(resourceGroup().id))]",
"sqlServerName": "[concat('iotc-sql-', uniqueString(resourceGroup().id))]"
},
"resources": [
{
"type": "Microsoft.Sql/servers",
"apiVersion": "2019-06-01-preview",
"name": "[variables('sqlServerName')]",
"location": "[resourceGroup().location]",
"tags": {},
"kind": "v12.0",
"identity": {
"type": "SystemAssigned"
},
"properties": {
"administratorLogin": "[parameters('sql-username')]",
"administratorLoginPassword": "[parameters('sql-password')]",
"version": "12.0",
"publicNetworkAccess": "Enabled"
},
"resources": [
{
"type": "firewallrules",
"name": "AllowAllWindowsAzureIps",
"apiVersion": "2019-06-01-preview",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Sql/servers', concat(variables('sqlServerName')))]"
],
"properties": {
"endIpAddress": "0.0.0.0",
"startIpAddress": "0.0.0.0"
}
},
{
"type": "databases",
"apiVersion": "2019-06-01-preview",
"name": "[variables('databaseName')]",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Sql/servers', variables('sqlServerName'))]"
],
"sku": {
"name": "Basic",
"tier": "Basic",
"capacity": 5
},
"kind": "v12.0,user",
"properties": {
"collation": "SQL_Latin1_General_CP1_CI_AS",
"maxSizeBytes": 2147483648,
"catalogCollation": "SQL_Latin1_General_CP1_CI_AS",
"zoneRedundant": false,
"readScale": "Disabled",
"storageAccountType": "GRS"
}
}
]
},
{
"type": "Microsoft.ContainerInstance/containerGroups",
"apiVersion": "2019-12-01",
"name": "[variables('setupContainerGroupsName')]",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Sql/servers/databases', variables('sqlServerName'), variables('databaseName'))]"
],
"identity": {
"type": "SystemAssigned"
},
"properties": {
"sku": "Standard",
"containers": [
{
"name": "bridge-setup",
"properties": {
"image": "[parameters('bridge-image')]",
"environmentVariables": [
{
"name": "KV_URL",
"value": "[concat('https://', variables('keyvaultName'), '.vault.azure.net/')]"
}
],
"command": [
"dotnet",
"DeviceBridge.dll",
"--setup"
],
"resources": {
"requests": {
"memoryInGB": 1,
"cpu": 1
}
}
}
}
],
"restartPolicy": "OnFailure",
"imageRegistryCredentials": [
{
"server": "[parameters('acr-server')]",
"username": "[parameters('acr-username')]",
"password": "[parameters('acr-password')]"
}
],
"diagnostics" : {
"logAnalytics": {
"workspaceId": "[parameters('log-analytics-workspace-id')]",
"workspaceKey": "[parameters('log-analytics-workspace-key')]"
}
},
"osType": "Linux"
}
},
{
"type": "Microsoft.ContainerInstance/containerGroups",
"apiVersion": "2019-12-01",
"name": "[variables('containerGroupsName')]",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.Sql/servers/databases', variables('sqlServerName'), variables('databaseName'))]"
],
"identity": {
"type": "SystemAssigned"
},
"properties": {
"sku": "Standard",
"containers": [
{
"name": "adapter",
"properties": {
"image": "[parameters('adapter-image')]",
"ports": [
{
"port": 3000
},
{
"port": 3001
}
],
"environmentVariables": [
{
"name": "EVENTHUB_CONNECTION_STRING",
"value": "[parameters('event-hub-connection-string')]"
},
{
"name": "EVENTHUB_NAME",
"value": "[parameters('event-hub-name')]"
}
],
"resources": {
"requests": {
"memoryInGB": 0.5,
"cpu": 0.5
}
}
}
},
{
"name": "[variables('bridgeContainerName')]",
"properties": {
"image": "[parameters('bridge-image')]",
"ports": [
{
"port": 5001
}
],
"environmentVariables": [
{
"name": "MAX_POOL_SIZE",
"value": "50"
},
{
"name": "DEVICE_RAMPUP_BATCH_SIZE",
"value": "150"
},
{
"name": "DEVICE_RAMPUP_BATCH_INTERVAL_MS",
"value": "1000"
},
{
"name": "KV_URL",
"value": "[concat('https://', variables('keyvaultName'), '.vault.azure.net/')]"
},
{
"name": "PORT",
"value": "5001"
}
],
"resources": {
"requests": {
"memoryInGB": 1.5,
"cpu": 0.8
}
}
}
},
{
"name": "caddy-ssl-server",
"properties": {
"image": "caddy:latest",
"command": [
"caddy",
"reverse-proxy",
"--from",
"[concat(parameters('bridge-name'), '.', resourceGroup().location, '.azurecontainer.io')]",
"--to",
"localhost:3000"
],
"ports": [
{
"protocol": "TCP",
"port": 443
},
{
"protocol": "TCP",
"port": 80
}
],
"environmentVariables": [],
"resources": {
"requests": {
"memoryInGB": 0.5,
"cpu": 0.2
}
},
"volumeMounts": [
{
"name": "data",
"mountPath": "/data"
},
{
"name": "config",
"mountPath": "/config"
}
]
}
}
],
"volumes": [
{
"name": "data",
"emptyDir": {}
},
{
"name": "config",
"emptyDir": {}
}
],
"initContainers": [
],
"restartPolicy": "Always",
"imageRegistryCredentials": [
{
"server": "[parameters('acr-server')]",
"username": "[parameters('acr-username')]",
"password": "[parameters('acr-password')]"
}
],
"diagnostics" : {
"logAnalytics": {
"workspaceId": "[parameters('log-analytics-workspace-id')]",
"workspaceKey": "[parameters('log-analytics-workspace-key')]"
}
},
"ipAddress": {
"ports": [
{
"protocol": "TCP",
"port": 443
}
],
"type": "Public",
"dnsNameLabel": "[parameters('bridge-name')]"
},
"osType": "Linux"
}
},
{
"type": "Microsoft.KeyVault/vaults",
"apiVersion": "2016-10-01",
"name": "[variables('keyvaultName')]",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.ContainerInstance/containerGroups', variables('containerGroupsName'))]",
"[resourceId('Microsoft.ContainerInstance/containerGroups', variables('setupContainerGroupsName'))]"
],
"properties": {
"sku": {
"family": "A",
"name": "Standard"
},
"tenantId": "[subscription().tenantId]",
"enabledForDeployment": true,
"enabledForDiskEncryption": true,
"enabledForTemplateDeployment": true,
"accessPolicies": [
{
"tenantId": "[subscription().tenantid]",
"objectId": "[reference(resourceId('Microsoft.ContainerInstance/containerGroups', variables('containerGroupsName')),'2019-12-01', 'full').identity.principalId]",
"permissions": {
"keys": [],
"secrets": [
"get",
"list"
],
"certificates": []
}
},
{
"tenantId": "[subscription().tenantid]",
"objectId": "[reference(resourceId('Microsoft.ContainerInstance/containerGroups', variables('setupContainerGroupsName')),'2019-12-01', 'full').identity.principalId]",
"permissions": {
"keys": [],
"secrets": [
"get",
"list",
"set"
],
"certificates": []
}
}
]
},
"resources": [
{
"type": "secrets",
"apiVersion": "2016-10-01",
"name": "apiKey",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
],
"properties": {
"value": "[parameters('api-key')]",
"attributes": {
"enabled": true
}
}
},
{
"type": "secrets",
"apiVersion": "2016-10-01",
"name": "iotc-sas-key",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
],
"properties": {
"value": "[parameters('iotc-dps-sas-key')]",
"attributes": {
"enabled": true
}
}
},
{
"type": "secrets",
"apiVersion": "2016-10-01",
"name": "iotc-id-scope",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
],
"properties": {
"value": "[parameters('iotc-id-scope')]",
"attributes": {
"enabled": true
}
}
},
{
"type": "secrets",
"apiVersion": "2016-10-01",
"name": "sql-username",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
],
"properties": {
"value": "[parameters('sql-username')]",
"attributes": {
"enabled": true
}
}
},
{
"type": "secrets",
"apiVersion": "2016-10-01",
"name": "sql-password",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
],
"properties": {
"value": "[parameters('sql-password')]",
"attributes": {
"enabled": true
}
}
},
{
"type": "secrets",
"apiVersion": "2016-10-01",
"name": "sql-server",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]",
"[resourceId('Microsoft.Sql/servers', concat(variables('sqlServerName')))]"
],
"properties": {
"value": "[reference(variables('sqlServerName')).fullyQualifiedDomainName]",
"attributes": {
"enabled": true
}
}
},
{
"type": "secrets",
"apiVersion": "2016-10-01",
"name": "sql-database",
"location": "[resourceGroup().location]",
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]"
],
"properties": {
"value": "[variables('databaseName')]",
"attributes": {
"enabled": true
}
}
}
]
}
]
}

Просмотреть файл

@ -0,0 +1,23 @@
{
"name": "sample-adapter",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"dependencies": {
"@azure/event-hubs": "^5.3.1",
"@azure/ms-rest-js": "^2.1.0",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"typescript": "^3.9.7"
},
"devDependencies": {
"@types/body-parser": "^1.19.0",
"@types/express": "^4.17.9",
"@types/node": "^14.14.10"
}
}

Просмотреть файл

@ -0,0 +1,957 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* Code generated by Microsoft (R) AutoRest Code Generator.
* Changes may cause incorrect behavior and will be lost if the code is
* regenerated.
*/
import * as msRest from "@azure/ms-rest-js";
import * as Models from "./models";
import * as Mappers from "./models/mappers";
import * as Parameters from "./models/parameters";
import { DeviceBridgeContext } from "./deviceBridgeContext";
class DeviceBridge extends DeviceBridgeContext {
/**
* Initializes a new instance of the DeviceBridge class.
* @param [options] The parameter options
*/
constructor(options?: Models.DeviceBridgeOptions) {
super(options);
}
/**
* For a detailed description of each status, see
* https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
* @summary Gets that latest connection status for a device.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<Models.GetCurrentConnectionStatusResponse>
*/
getCurrentConnectionStatus(deviceId: string, options?: msRest.RequestOptionsBase): Promise<Models.GetCurrentConnectionStatusResponse>;
/**
* @param deviceId
* @param callback The callback
*/
getCurrentConnectionStatus(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceStatusResponseBody>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
getCurrentConnectionStatus(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<Models.DeviceStatusResponseBody>): void;
getCurrentConnectionStatus(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<Models.DeviceStatusResponseBody>, callback?: msRest.ServiceCallback<Models.DeviceStatusResponseBody>): Promise<Models.GetCurrentConnectionStatusResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
getCurrentConnectionStatusOperationSpec,
callback) as Promise<Models.GetCurrentConnectionStatusResponse>;
}
/**
* @summary Gets the current connection status change subscription for a device.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<Models.GetConnectionStatusSubscriptionResponse>
*/
getConnectionStatusSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<Models.GetConnectionStatusSubscriptionResponse>;
/**
* @param deviceId
* @param callback The callback
*/
getConnectionStatusSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscription>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
getConnectionStatusSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<Models.DeviceSubscription>): void;
getConnectionStatusSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<Models.DeviceSubscription>, callback?: msRest.ServiceCallback<Models.DeviceSubscription>): Promise<Models.GetConnectionStatusSubscriptionResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
getConnectionStatusSubscriptionOperationSpec,
callback) as Promise<Models.GetConnectionStatusSubscriptionResponse>;
}
/**
* When the internal connection status of a device changes, the service will send an event to the
* desired callback URL.
*
* Example event:
* {
* "eventType": "string",
* "deviceId": "string",
* "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
* "status": "string",
* "reason": "string"
* }
*
* For a detailed description of each status, see
* https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.devices.client.connectionstatus?view=azure-dotnet.
* @summary Creates or updates the current connection status change subscription for a device.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<Models.CreateOrUpdateConnectionStatusSubscriptionResponse>
*/
createOrUpdateConnectionStatusSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateConnectionStatusSubscriptionOptionalParams): Promise<Models.CreateOrUpdateConnectionStatusSubscriptionResponse>;
/**
* @param deviceId
* @param callback The callback
*/
createOrUpdateConnectionStatusSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscription>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
createOrUpdateConnectionStatusSubscription(deviceId: string, options: Models.DeviceBridgeCreateOrUpdateConnectionStatusSubscriptionOptionalParams, callback: msRest.ServiceCallback<Models.DeviceSubscription>): void;
createOrUpdateConnectionStatusSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateConnectionStatusSubscriptionOptionalParams | msRest.ServiceCallback<Models.DeviceSubscription>, callback?: msRest.ServiceCallback<Models.DeviceSubscription>): Promise<Models.CreateOrUpdateConnectionStatusSubscriptionResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
createOrUpdateConnectionStatusSubscriptionOperationSpec,
callback) as Promise<Models.CreateOrUpdateConnectionStatusSubscriptionResponse>;
}
/**
* @summary Deletes the current connection status change subscription for a device.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<msRest.RestResponse>
*/
deleteConnectionStatusSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<msRest.RestResponse>;
/**
* @param deviceId
* @param callback The callback
*/
deleteConnectionStatusSubscription(deviceId: string, callback: msRest.ServiceCallback<void>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
deleteConnectionStatusSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<void>): void;
deleteConnectionStatusSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
deleteConnectionStatusSubscriptionOperationSpec,
callback);
}
/**
* @summary Gets the current C2D message subscription for a device.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<Models.GetC2DMessageSubscriptionResponse>
*/
getC2DMessageSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<Models.GetC2DMessageSubscriptionResponse>;
/**
* @param deviceId
* @param callback The callback
*/
getC2DMessageSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
getC2DMessageSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
getC2DMessageSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>, callback?: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): Promise<Models.GetC2DMessageSubscriptionResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
getC2DMessageSubscriptionOperationSpec,
callback) as Promise<Models.GetC2DMessageSubscriptionResponse>;
}
/**
* When the device receives a new C2D message from IoTHub, the service will send an event to the
* desired callback URL.
*
* Example event:
* {
* "eventType": "string",
* "deviceId": "string",
* "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
* "messageBody": {},
* "properties": {
* "prop1": "string",
* "prop2": "string",
* },
* "messageId": "string",
* "expirtyTimeUtC": "2020-12-04T01:06:14.251Z"
* }
*
* The response status code of the callback URL will determine how the service will acknowledge a
* message:
* - Response code between 200 and 299: the service will complete the message.
* - Response code between 400 and 499: the service will reject the message.
* - Any other response status: the service will abandon the message, causing IotHub to redeliver
* it.
*
* For a detailed overview of C2D messages, see
* https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-messages-c2d.
* @summary Creates or updates the current C2D message subscription for a device.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<Models.CreateOrUpdateC2DMessageSubscriptionResponse>
*/
createOrUpdateC2DMessageSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateC2DMessageSubscriptionOptionalParams): Promise<Models.CreateOrUpdateC2DMessageSubscriptionResponse>;
/**
* @param deviceId
* @param callback The callback
*/
createOrUpdateC2DMessageSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
createOrUpdateC2DMessageSubscription(deviceId: string, options: Models.DeviceBridgeCreateOrUpdateC2DMessageSubscriptionOptionalParams, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
createOrUpdateC2DMessageSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateC2DMessageSubscriptionOptionalParams | msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>, callback?: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): Promise<Models.CreateOrUpdateC2DMessageSubscriptionResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
createOrUpdateC2DMessageSubscriptionOperationSpec,
callback) as Promise<Models.CreateOrUpdateC2DMessageSubscriptionResponse>;
}
/**
* @summary Deletes the current C2D message subscription for a device.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<msRest.RestResponse>
*/
deleteC2DMessageSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<msRest.RestResponse>;
/**
* @param deviceId
* @param callback The callback
*/
deleteC2DMessageSubscription(deviceId: string, callback: msRest.ServiceCallback<void>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
deleteC2DMessageSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<void>): void;
deleteC2DMessageSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
deleteC2DMessageSubscriptionOperationSpec,
callback);
}
/**
* Example request:
*
* POST /devices/{deviceId}/messages/events
* {
* "data": {
* "temperature": 4.8,
* "humidity": 31
* }
* }
* .
* @summary Sends a device message to IoTHub.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<msRest.RestResponse>
*/
sendMessage(deviceId: string, options?: Models.DeviceBridgeSendMessageOptionalParams): Promise<msRest.RestResponse>;
/**
* @param deviceId
* @param callback The callback
*/
sendMessage(deviceId: string, callback: msRest.ServiceCallback<void>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
sendMessage(deviceId: string, options: Models.DeviceBridgeSendMessageOptionalParams, callback: msRest.ServiceCallback<void>): void;
sendMessage(deviceId: string, options?: Models.DeviceBridgeSendMessageOptionalParams | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
sendMessageOperationSpec,
callback);
}
/**
* @summary Gets the current direct methods subscription for a device.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<Models.GetMethodsSubscriptionResponse>
*/
getMethodsSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<Models.GetMethodsSubscriptionResponse>;
/**
* @param deviceId
* @param callback The callback
*/
getMethodsSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
getMethodsSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
getMethodsSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>, callback?: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): Promise<Models.GetMethodsSubscriptionResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
getMethodsSubscriptionOperationSpec,
callback) as Promise<Models.GetMethodsSubscriptionResponse>;
}
/**
* When the device receives a direct method invocation from IoTHub, the service will send an event
* to the desired callback URL.
*
* Example event:
* {
* "eventType": "string",
* "deviceId": "string",
* "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
* "methodName": "string",
* "requestData": {}
* }
*
* The callback may return an optional response body, which will be sent to IoTHub as the method
* response:
*
* Example callback response:
* {
* "status": "string",
* "payload": {}
* }
* .
* @summary Creates or updates the current direct methods subscription for a device.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<Models.CreateOrUpdateMethodsSubscriptionResponse>
*/
createOrUpdateMethodsSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateMethodsSubscriptionOptionalParams): Promise<Models.CreateOrUpdateMethodsSubscriptionResponse>;
/**
* @param deviceId
* @param callback The callback
*/
createOrUpdateMethodsSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
createOrUpdateMethodsSubscription(deviceId: string, options: Models.DeviceBridgeCreateOrUpdateMethodsSubscriptionOptionalParams, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
createOrUpdateMethodsSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateMethodsSubscriptionOptionalParams | msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>, callback?: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): Promise<Models.CreateOrUpdateMethodsSubscriptionResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
createOrUpdateMethodsSubscriptionOperationSpec,
callback) as Promise<Models.CreateOrUpdateMethodsSubscriptionResponse>;
}
/**
* @summary Deletes the current direct methods subscription for a device.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<msRest.RestResponse>
*/
deleteMethodsSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<msRest.RestResponse>;
/**
* @param deviceId
* @param callback The callback
*/
deleteMethodsSubscription(deviceId: string, callback: msRest.ServiceCallback<void>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
deleteMethodsSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<void>): void;
deleteMethodsSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
deleteMethodsSubscriptionOperationSpec,
callback);
}
/**
* The registration result is internally cached to be used in future connections.
* This route is only intended for ahead-of-time registration of devices with the bridge and
* assignment to a specific model. To access all DPS registration features,
* including sending custom registration payload and getting the assigned hub, please use the DPS
* REST API (https://docs.microsoft.com/en-us/rest/api/iot-dps/).
*
* <b>NOTE:</b> DPS registration is a long-running operation, so calls to this route may take a
* long time to return. If this is a concern, use the DPS REST API directly, which provides
* support for long-running operation status lookup.
* @summary Performs DPS registration for a device, optionally assigning it to a model.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<msRest.RestResponse>
*/
register(deviceId: string, options?: Models.DeviceBridgeRegisterOptionalParams): Promise<msRest.RestResponse>;
/**
* @param deviceId
* @param callback The callback
*/
register(deviceId: string, callback: msRest.ServiceCallback<void>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
register(deviceId: string, options: Models.DeviceBridgeRegisterOptionalParams, callback: msRest.ServiceCallback<void>): void;
register(deviceId: string, options?: Models.DeviceBridgeRegisterOptionalParams | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
registerOperationSpec,
callback);
}
/**
* Internally it forces the reconnection of the device if it's in a permanent failure state, due
* for instance to:
* - Bad credentials.
* - Device was previously disabled in the cloud side.
* - Automatic retries expired (e.g., due to a long period without network connectivity).
* @summary Forces a full synchronization of all subscriptions for this device and attempts to
* restart any subscriptions in a stopped state.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<msRest.RestResponse>
*/
resync(deviceId: string, options?: msRest.RequestOptionsBase): Promise<msRest.RestResponse>;
/**
* @param deviceId
* @param callback The callback
*/
resync(deviceId: string, callback: msRest.ServiceCallback<void>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
resync(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<void>): void;
resync(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
resyncOperationSpec,
callback);
}
/**
* @summary Gets the device twin.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<Models.GetTwinResponse>
*/
getTwin(deviceId: string, options?: msRest.RequestOptionsBase): Promise<Models.GetTwinResponse>;
/**
* @param deviceId
* @param callback The callback
*/
getTwin(deviceId: string, callback: msRest.ServiceCallback<Models.GetTwinOKResponse>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
getTwin(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<Models.GetTwinOKResponse>): void;
getTwin(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<Models.GetTwinOKResponse>, callback?: msRest.ServiceCallback<Models.GetTwinOKResponse>): Promise<Models.GetTwinResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
getTwinOperationSpec,
callback) as Promise<Models.GetTwinResponse>;
}
/**
* Example request:
*
* PATCH /devices/{deviceId}/properties/reported
* {
* "patch": {
* "fanSpeed": 35,
* "serial": "ABC"
* }
* }
* .
* @summary Updates reported properties in the device twin.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<msRest.RestResponse>
*/
updateReportedProperties(deviceId: string, options?: Models.DeviceBridgeUpdateReportedPropertiesOptionalParams): Promise<msRest.RestResponse>;
/**
* @param deviceId
* @param callback The callback
*/
updateReportedProperties(deviceId: string, callback: msRest.ServiceCallback<void>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
updateReportedProperties(deviceId: string, options: Models.DeviceBridgeUpdateReportedPropertiesOptionalParams, callback: msRest.ServiceCallback<void>): void;
updateReportedProperties(deviceId: string, options?: Models.DeviceBridgeUpdateReportedPropertiesOptionalParams | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
updateReportedPropertiesOperationSpec,
callback);
}
/**
* @summary Gets the current desired property change subscription for a device.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<Models.GetDesiredPropertiesSubscriptionResponse>
*/
getDesiredPropertiesSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<Models.GetDesiredPropertiesSubscriptionResponse>;
/**
* @param deviceId
* @param callback The callback
*/
getDesiredPropertiesSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
getDesiredPropertiesSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
getDesiredPropertiesSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>, callback?: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): Promise<Models.GetDesiredPropertiesSubscriptionResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
getDesiredPropertiesSubscriptionOperationSpec,
callback) as Promise<Models.GetDesiredPropertiesSubscriptionResponse>;
}
/**
* When the device receives a new desired property change from IoTHub, the service will send an
* event to the desired callback URL.
*
* Example event:
* {
* "eventType": "string",
* "deviceId": "string",
* "deviceReceivedAt": "2020-12-04T01:06:14.251Z",
* "desiredProperties": {
* "prop1": "string",
* "prop2": 12,
* "prop3": {},
* }
* }
* .
* @summary Creates or updates the current desired property change subscription for a device.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<Models.CreateOrUpdateDesiredPropertiesSubscriptionResponse>
*/
createOrUpdateDesiredPropertiesSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateDesiredPropertiesSubscriptionOptionalParams): Promise<Models.CreateOrUpdateDesiredPropertiesSubscriptionResponse>;
/**
* @param deviceId
* @param callback The callback
*/
createOrUpdateDesiredPropertiesSubscription(deviceId: string, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
createOrUpdateDesiredPropertiesSubscription(deviceId: string, options: Models.DeviceBridgeCreateOrUpdateDesiredPropertiesSubscriptionOptionalParams, callback: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): void;
createOrUpdateDesiredPropertiesSubscription(deviceId: string, options?: Models.DeviceBridgeCreateOrUpdateDesiredPropertiesSubscriptionOptionalParams | msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>, callback?: msRest.ServiceCallback<Models.DeviceSubscriptionWithStatus>): Promise<Models.CreateOrUpdateDesiredPropertiesSubscriptionResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
createOrUpdateDesiredPropertiesSubscriptionOperationSpec,
callback) as Promise<Models.CreateOrUpdateDesiredPropertiesSubscriptionResponse>;
}
/**
* @summary Deletes the current desired property change subscription for a device.
* @param deviceId
* @param [options] The optional parameters
* @returns Promise<msRest.RestResponse>
*/
deleteDesiredPropertiesSubscription(deviceId: string, options?: msRest.RequestOptionsBase): Promise<msRest.RestResponse>;
/**
* @param deviceId
* @param callback The callback
*/
deleteDesiredPropertiesSubscription(deviceId: string, callback: msRest.ServiceCallback<void>): void;
/**
* @param deviceId
* @param options The optional parameters
* @param callback The callback
*/
deleteDesiredPropertiesSubscription(deviceId: string, options: msRest.RequestOptionsBase, callback: msRest.ServiceCallback<void>): void;
deleteDesiredPropertiesSubscription(deviceId: string, options?: msRest.RequestOptionsBase | msRest.ServiceCallback<void>, callback?: msRest.ServiceCallback<void>): Promise<msRest.RestResponse> {
return this.sendOperationRequest(
{
deviceId,
options
},
deleteDesiredPropertiesSubscriptionOperationSpec,
callback);
}
}
// Operation Specifications
const serializer = new msRest.Serializer(Mappers);
const getCurrentConnectionStatusOperationSpec: msRest.OperationSpec = {
httpMethod: "GET",
path: "devices/{deviceId}/ConnectionStatus",
urlParameters: [
Parameters.deviceId
],
responses: {
200: {
bodyMapper: Mappers.DeviceStatusResponseBody
},
404: {},
default: {}
},
serializer
};
const getConnectionStatusSubscriptionOperationSpec: msRest.OperationSpec = {
httpMethod: "GET",
path: "devices/{deviceId}/ConnectionStatus/sub",
urlParameters: [
Parameters.deviceId
],
responses: {
200: {
bodyMapper: Mappers.DeviceSubscription
},
404: {},
default: {}
},
serializer
};
const createOrUpdateConnectionStatusSubscriptionOperationSpec: msRest.OperationSpec = {
httpMethod: "PUT",
path: "devices/{deviceId}/ConnectionStatus/sub",
urlParameters: [
Parameters.deviceId
],
requestBody: {
parameterPath: [
"options",
"body"
],
mapper: Mappers.SubscriptionCreateOrUpdateBody
},
responses: {
200: {
bodyMapper: Mappers.DeviceSubscription
},
default: {}
},
serializer
};
const deleteConnectionStatusSubscriptionOperationSpec: msRest.OperationSpec = {
httpMethod: "DELETE",
path: "devices/{deviceId}/ConnectionStatus/sub",
urlParameters: [
Parameters.deviceId
],
responses: {
204: {},
default: {}
},
serializer
};
const getC2DMessageSubscriptionOperationSpec: msRest.OperationSpec = {
httpMethod: "GET",
path: "devices/{deviceId}/DeviceBound/sub",
urlParameters: [
Parameters.deviceId
],
responses: {
200: {
bodyMapper: Mappers.DeviceSubscriptionWithStatus
},
404: {},
default: {}
},
serializer
};
const createOrUpdateC2DMessageSubscriptionOperationSpec: msRest.OperationSpec = {
httpMethod: "PUT",
path: "devices/{deviceId}/DeviceBound/sub",
urlParameters: [
Parameters.deviceId
],
requestBody: {
parameterPath: [
"options",
"body"
],
mapper: Mappers.SubscriptionCreateOrUpdateBody
},
responses: {
200: {
bodyMapper: Mappers.DeviceSubscriptionWithStatus
},
default: {}
},
serializer
};
const deleteC2DMessageSubscriptionOperationSpec: msRest.OperationSpec = {
httpMethod: "DELETE",
path: "devices/{deviceId}/DeviceBound/sub",
urlParameters: [
Parameters.deviceId
],
responses: {
204: {},
default: {}
},
serializer
};
const sendMessageOperationSpec: msRest.OperationSpec = {
httpMethod: "POST",
path: "devices/{deviceId}/Messages/events",
urlParameters: [
Parameters.deviceId
],
requestBody: {
parameterPath: [
"options",
"body"
],
mapper: Mappers.MessageBody
},
responses: {
200: {},
default: {}
},
serializer
};
const getMethodsSubscriptionOperationSpec: msRest.OperationSpec = {
httpMethod: "GET",
path: "devices/{deviceId}/Methods/sub",
urlParameters: [
Parameters.deviceId
],
responses: {
200: {
bodyMapper: Mappers.DeviceSubscriptionWithStatus
},
404: {},
default: {}
},
serializer
};
const createOrUpdateMethodsSubscriptionOperationSpec: msRest.OperationSpec = {
httpMethod: "PUT",
path: "devices/{deviceId}/Methods/sub",
urlParameters: [
Parameters.deviceId
],
requestBody: {
parameterPath: [
"options",
"body"
],
mapper: Mappers.SubscriptionCreateOrUpdateBody
},
responses: {
200: {
bodyMapper: Mappers.DeviceSubscriptionWithStatus
},
default: {}
},
serializer
};
const deleteMethodsSubscriptionOperationSpec: msRest.OperationSpec = {
httpMethod: "DELETE",
path: "devices/{deviceId}/Methods/sub",
urlParameters: [
Parameters.deviceId
],
responses: {
204: {},
default: {}
},
serializer
};
const registerOperationSpec: msRest.OperationSpec = {
httpMethod: "POST",
path: "devices/{deviceId}/Registration",
urlParameters: [
Parameters.deviceId
],
requestBody: {
parameterPath: [
"options",
"body"
],
mapper: Mappers.RegistrationBody
},
responses: {
200: {},
default: {}
},
serializer
};
const resyncOperationSpec: msRest.OperationSpec = {
httpMethod: "POST",
path: "devices/{deviceId}/Resync",
urlParameters: [
Parameters.deviceId
],
responses: {
202: {},
default: {}
},
serializer
};
const getTwinOperationSpec: msRest.OperationSpec = {
httpMethod: "GET",
path: "devices/{deviceId}/Twin",
urlParameters: [
Parameters.deviceId
],
responses: {
200: {
bodyMapper: Mappers.GetTwinOKResponse
},
default: {}
},
serializer
};
const updateReportedPropertiesOperationSpec: msRest.OperationSpec = {
httpMethod: "PATCH",
path: "devices/{deviceId}/Twin/properties/reported",
urlParameters: [
Parameters.deviceId
],
requestBody: {
parameterPath: [
"options",
"body"
],
mapper: Mappers.ReportedPropertiesPatch
},
responses: {
204: {},
default: {}
},
serializer
};
const getDesiredPropertiesSubscriptionOperationSpec: msRest.OperationSpec = {
httpMethod: "GET",
path: "devices/{deviceId}/Twin/properties/desired/sub",
urlParameters: [
Parameters.deviceId
],
responses: {
200: {
bodyMapper: Mappers.DeviceSubscriptionWithStatus
},
404: {},
default: {}
},
serializer
};
const createOrUpdateDesiredPropertiesSubscriptionOperationSpec: msRest.OperationSpec = {
httpMethod: "PUT",
path: "devices/{deviceId}/Twin/properties/desired/sub",
urlParameters: [
Parameters.deviceId
],
requestBody: {
parameterPath: [
"options",
"body"
],
mapper: Mappers.SubscriptionCreateOrUpdateBody
},
responses: {
200: {
bodyMapper: Mappers.DeviceSubscriptionWithStatus
},
default: {}
},
serializer
};
const deleteDesiredPropertiesSubscriptionOperationSpec: msRest.OperationSpec = {
httpMethod: "DELETE",
path: "devices/{deviceId}/Twin/properties/desired/sub",
urlParameters: [
Parameters.deviceId
],
responses: {
204: {},
default: {}
},
serializer
};
export {
DeviceBridge,
DeviceBridgeContext,
Models as DeviceBridgeModels,
Mappers as DeviceBridgeMappers
};

Просмотреть файл

@ -0,0 +1,36 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* Code generated by Microsoft (R) AutoRest Code Generator.
* Changes may cause incorrect behavior and will be lost if the code is
* regenerated.
*/
import * as msRest from "@azure/ms-rest-js";
import * as Models from "./models";
const packageName = "";
const packageVersion = "";
export class DeviceBridgeContext extends msRest.ServiceClient {
/**
* Initializes a new instance of the DeviceBridgeContext class.
* @param [options] The parameter options
*/
constructor(options?: Models.DeviceBridgeOptions) {
if (!options) {
options = {};
}
if (!options.userAgent) {
const defaultUserAgent = msRest.getDefaultUserAgentValue();
options.userAgent = `${packageName}/${packageVersion} ${defaultUserAgent}`;
}
super(undefined, options);
this.baseUri = options.baseUri || this.baseUri || "http://localhost";
this.requestContentType = "application/json; charset=utf-8";
}
}

Просмотреть файл

@ -0,0 +1,347 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* Code generated by Microsoft (R) AutoRest Code Generator.
* Changes may cause incorrect behavior and will be lost if the code is regenerated.
*/
import { ServiceClientOptions } from "@azure/ms-rest-js";
import * as msRest from "@azure/ms-rest-js";
/**
* An interface representing DeviceStatusResponseBody.
*/
export interface DeviceStatusResponseBody {
status?: string;
reason?: string;
}
/**
* An interface representing DeviceSubscription.
*/
export interface DeviceSubscription {
deviceId?: string;
subscriptionType?: string;
callbackUrl?: string;
createdAt?: Date;
}
/**
* An interface representing SubscriptionCreateOrUpdateBody.
*/
export interface SubscriptionCreateOrUpdateBody {
callbackUrl: string;
}
/**
* An interface representing DeviceSubscriptionWithStatus.
*/
export interface DeviceSubscriptionWithStatus {
deviceId?: string;
subscriptionType?: string;
callbackUrl?: string;
createdAt?: Date;
status?: string;
}
/**
* An interface representing MessageBody.
*/
export interface MessageBody {
data: { [propertyName: string]: any };
properties?: { [propertyName: string]: string };
componentName?: string;
creationTimeUtc?: Date;
}
/**
* An interface representing RegistrationBody.
*/
export interface RegistrationBody {
modelId?: string;
}
/**
* An interface representing ReportedPropertiesPatch.
*/
export interface ReportedPropertiesPatch {
patch: { [propertyName: string]: any };
}
/**
* An interface representing GetTwinOKResponseTwinProperties.
*/
export interface GetTwinOKResponseTwinProperties {
desired?: any;
reported?: any;
}
/**
* An interface representing GetTwinOKResponseTwin.
*/
export interface GetTwinOKResponseTwin {
properties?: GetTwinOKResponseTwinProperties;
}
/**
* An interface representing GetTwinOKResponse.
*/
export interface GetTwinOKResponse {
twin?: GetTwinOKResponseTwin;
}
/**
* An interface representing DeviceBridgeOptions.
*/
export interface DeviceBridgeOptions extends ServiceClientOptions {
baseUri?: string;
}
/**
* Optional Parameters.
*/
export interface DeviceBridgeCreateOrUpdateConnectionStatusSubscriptionOptionalParams extends msRest.RequestOptionsBase {
body?: SubscriptionCreateOrUpdateBody;
}
/**
* Optional Parameters.
*/
export interface DeviceBridgeCreateOrUpdateC2DMessageSubscriptionOptionalParams extends msRest.RequestOptionsBase {
body?: SubscriptionCreateOrUpdateBody;
}
/**
* Optional Parameters.
*/
export interface DeviceBridgeSendMessageOptionalParams extends msRest.RequestOptionsBase {
body?: MessageBody;
}
/**
* Optional Parameters.
*/
export interface DeviceBridgeCreateOrUpdateMethodsSubscriptionOptionalParams extends msRest.RequestOptionsBase {
body?: SubscriptionCreateOrUpdateBody;
}
/**
* Optional Parameters.
*/
export interface DeviceBridgeRegisterOptionalParams extends msRest.RequestOptionsBase {
body?: RegistrationBody;
}
/**
* Optional Parameters.
*/
export interface DeviceBridgeUpdateReportedPropertiesOptionalParams extends msRest.RequestOptionsBase {
body?: ReportedPropertiesPatch;
}
/**
* Optional Parameters.
*/
export interface DeviceBridgeCreateOrUpdateDesiredPropertiesSubscriptionOptionalParams extends msRest.RequestOptionsBase {
body?: SubscriptionCreateOrUpdateBody;
}
/**
* Contains response data for the getCurrentConnectionStatus operation.
*/
export type GetCurrentConnectionStatusResponse = DeviceStatusResponseBody & {
/**
* The underlying HTTP response.
*/
_response: msRest.HttpResponse & {
/**
* The response body as text (string format)
*/
bodyAsText: string;
/**
* The response body as parsed JSON or XML
*/
parsedBody: DeviceStatusResponseBody;
};
};
/**
* Contains response data for the getConnectionStatusSubscription operation.
*/
export type GetConnectionStatusSubscriptionResponse = DeviceSubscription & {
/**
* The underlying HTTP response.
*/
_response: msRest.HttpResponse & {
/**
* The response body as text (string format)
*/
bodyAsText: string;
/**
* The response body as parsed JSON or XML
*/
parsedBody: DeviceSubscription;
};
};
/**
* Contains response data for the createOrUpdateConnectionStatusSubscription operation.
*/
export type CreateOrUpdateConnectionStatusSubscriptionResponse = DeviceSubscription & {
/**
* The underlying HTTP response.
*/
_response: msRest.HttpResponse & {
/**
* The response body as text (string format)
*/
bodyAsText: string;
/**
* The response body as parsed JSON or XML
*/
parsedBody: DeviceSubscription;
};
};
/**
* Contains response data for the getC2DMessageSubscription operation.
*/
export type GetC2DMessageSubscriptionResponse = DeviceSubscriptionWithStatus & {
/**
* The underlying HTTP response.
*/
_response: msRest.HttpResponse & {
/**
* The response body as text (string format)
*/
bodyAsText: string;
/**
* The response body as parsed JSON or XML
*/
parsedBody: DeviceSubscriptionWithStatus;
};
};
/**
* Contains response data for the createOrUpdateC2DMessageSubscription operation.
*/
export type CreateOrUpdateC2DMessageSubscriptionResponse = DeviceSubscriptionWithStatus & {
/**
* The underlying HTTP response.
*/
_response: msRest.HttpResponse & {
/**
* The response body as text (string format)
*/
bodyAsText: string;
/**
* The response body as parsed JSON or XML
*/
parsedBody: DeviceSubscriptionWithStatus;
};
};
/**
* Contains response data for the getMethodsSubscription operation.
*/
export type GetMethodsSubscriptionResponse = DeviceSubscriptionWithStatus & {
/**
* The underlying HTTP response.
*/
_response: msRest.HttpResponse & {
/**
* The response body as text (string format)
*/
bodyAsText: string;
/**
* The response body as parsed JSON or XML
*/
parsedBody: DeviceSubscriptionWithStatus;
};
};
/**
* Contains response data for the createOrUpdateMethodsSubscription operation.
*/
export type CreateOrUpdateMethodsSubscriptionResponse = DeviceSubscriptionWithStatus & {
/**
* The underlying HTTP response.
*/
_response: msRest.HttpResponse & {
/**
* The response body as text (string format)
*/
bodyAsText: string;
/**
* The response body as parsed JSON or XML
*/
parsedBody: DeviceSubscriptionWithStatus;
};
};
/**
* Contains response data for the getTwin operation.
*/
export type GetTwinResponse = GetTwinOKResponse & {
/**
* The underlying HTTP response.
*/
_response: msRest.HttpResponse & {
/**
* The response body as text (string format)
*/
bodyAsText: string;
/**
* The response body as parsed JSON or XML
*/
parsedBody: GetTwinOKResponse;
};
};
/**
* Contains response data for the getDesiredPropertiesSubscription operation.
*/
export type GetDesiredPropertiesSubscriptionResponse = DeviceSubscriptionWithStatus & {
/**
* The underlying HTTP response.
*/
_response: msRest.HttpResponse & {
/**
* The response body as text (string format)
*/
bodyAsText: string;
/**
* The response body as parsed JSON or XML
*/
parsedBody: DeviceSubscriptionWithStatus;
};
};
/**
* Contains response data for the createOrUpdateDesiredPropertiesSubscription operation.
*/
export type CreateOrUpdateDesiredPropertiesSubscriptionResponse = DeviceSubscriptionWithStatus & {
/**
* The underlying HTTP response.
*/
_response: msRest.HttpResponse & {
/**
* The response body as text (string format)
*/
bodyAsText: string;
/**
* The response body as parsed JSON or XML
*/
parsedBody: DeviceSubscriptionWithStatus;
};
};

Просмотреть файл

@ -0,0 +1,261 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* Code generated by Microsoft (R) AutoRest Code Generator.
* Changes may cause incorrect behavior and will be lost if the code is regenerated.
*/
import * as msRest from "@azure/ms-rest-js";
export const DeviceStatusResponseBody: msRest.CompositeMapper = {
serializedName: "DeviceStatusResponseBody",
type: {
name: "Composite",
className: "DeviceStatusResponseBody",
modelProperties: {
status: {
serializedName: "status",
type: {
name: "String"
}
},
reason: {
serializedName: "reason",
type: {
name: "String"
}
}
}
}
};
export const DeviceSubscription: msRest.CompositeMapper = {
serializedName: "DeviceSubscription",
type: {
name: "Composite",
className: "DeviceSubscription",
modelProperties: {
deviceId: {
serializedName: "deviceId",
type: {
name: "String"
}
},
subscriptionType: {
serializedName: "subscriptionType",
type: {
name: "String"
}
},
callbackUrl: {
serializedName: "callbackUrl",
type: {
name: "String"
}
},
createdAt: {
serializedName: "createdAt",
type: {
name: "DateTime"
}
}
}
}
};
export const SubscriptionCreateOrUpdateBody: msRest.CompositeMapper = {
serializedName: "SubscriptionCreateOrUpdateBody",
type: {
name: "Composite",
className: "SubscriptionCreateOrUpdateBody",
modelProperties: {
callbackUrl: {
required: true,
serializedName: "callbackUrl",
type: {
name: "String"
}
}
}
}
};
export const DeviceSubscriptionWithStatus: msRest.CompositeMapper = {
serializedName: "DeviceSubscriptionWithStatus",
type: {
name: "Composite",
className: "DeviceSubscriptionWithStatus",
modelProperties: {
deviceId: {
serializedName: "deviceId",
type: {
name: "String"
}
},
subscriptionType: {
serializedName: "subscriptionType",
type: {
name: "String"
}
},
callbackUrl: {
serializedName: "callbackUrl",
type: {
name: "String"
}
},
createdAt: {
serializedName: "createdAt",
type: {
name: "DateTime"
}
},
status: {
serializedName: "status",
type: {
name: "String"
}
}
}
}
};
export const MessageBody: msRest.CompositeMapper = {
serializedName: "MessageBody",
type: {
name: "Composite",
className: "MessageBody",
modelProperties: {
data: {
required: true,
serializedName: "data",
type: {
name: "Dictionary",
value: {
type: {
name: "Object"
}
}
}
},
properties: {
serializedName: "properties",
type: {
name: "Dictionary",
value: {
type: {
name: "String"
}
}
}
},
componentName: {
serializedName: "componentName",
type: {
name: "String"
}
},
creationTimeUtc: {
serializedName: "creationTimeUtc",
type: {
name: "DateTime"
}
}
}
}
};
export const RegistrationBody: msRest.CompositeMapper = {
serializedName: "RegistrationBody",
type: {
name: "Composite",
className: "RegistrationBody",
modelProperties: {
modelId: {
serializedName: "modelId",
type: {
name: "String"
}
}
}
}
};
export const ReportedPropertiesPatch: msRest.CompositeMapper = {
serializedName: "ReportedPropertiesPatch",
type: {
name: "Composite",
className: "ReportedPropertiesPatch",
modelProperties: {
patch: {
required: true,
serializedName: "patch",
type: {
name: "Dictionary",
value: {
type: {
name: "Object"
}
}
}
}
}
}
};
export const GetTwinOKResponseTwinProperties: msRest.CompositeMapper = {
serializedName: "GetTwinOKResponse_twin_properties",
type: {
name: "Composite",
className: "GetTwinOKResponseTwinProperties",
modelProperties: {
desired: {
serializedName: "desired",
type: {
name: "Object"
}
},
reported: {
serializedName: "reported",
type: {
name: "Object"
}
}
}
}
};
export const GetTwinOKResponseTwin: msRest.CompositeMapper = {
serializedName: "GetTwinOKResponse_twin",
type: {
name: "Composite",
className: "GetTwinOKResponseTwin",
modelProperties: {
properties: {
serializedName: "properties",
type: {
name: "Composite",
className: "GetTwinOKResponseTwinProperties"
}
}
}
}
};
export const GetTwinOKResponse: msRest.CompositeMapper = {
serializedName: "GetTwinOKResponse",
type: {
name: "Composite",
className: "GetTwinOKResponse",
modelProperties: {
twin: {
serializedName: "twin",
type: {
name: "Composite",
className: "GetTwinOKResponseTwin"
}
}
}
}
};

Просмотреть файл

@ -0,0 +1,20 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
*
* Code generated by Microsoft (R) AutoRest Code Generator.
* Changes may cause incorrect behavior and will be lost if the code is
* regenerated.
*/
import * as msRest from "@azure/ms-rest-js";
export const deviceId: msRest.OperationURLParameter = {
parameterPath: "deviceId",
mapper: {
required: true,
serializedName: "deviceId",
type: {
name: "String"
}
}
};

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше