provisioning/service/src/Contract/ContractApiHttp.cs (175 lines of code) (raw):

// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Devices.Common; using Microsoft.Azure.Devices.Common.Service.Auth; using Microsoft.Azure.Devices.Shared; namespace Microsoft.Azure.Devices.Provisioning.Service { internal class ContractApiHttp : IContractApiHttp { private const string MediaTypeForDeviceManagementApis = "application/json"; private readonly Uri _baseAddress; private readonly IAuthorizationHeaderProvider _authenticationHeaderProvider; private HttpClientHandler _httpClientHandler; private HttpClient _httpClientObj; private static readonly TimeSpan s_defaultOperationTimeout = TimeSpan.FromSeconds(100); public ContractApiHttp( Uri baseAddress, IAuthorizationHeaderProvider authenticationHeaderProvider, HttpTransportSettings httpTransportSettings) { _baseAddress = baseAddress; _authenticationHeaderProvider = authenticationHeaderProvider; _httpClientHandler = new HttpClientHandler { // Cannot specify a specific protocol here, as desired due to an error: // ProvisioningDeviceClient_ValidRegistrationId_AmqpWithProxy_SymmetricKey_RegisterOk_GroupEnrollment failing for me with System.PlatformNotSupportedException: Operation is not supported on this platform. // When revisiting TLS12 work for DPS, we should figure out why. Perhaps the service needs to support it. //SslProtocols = TlsVersions.Preferred, CheckCertificateRevocationList = TlsVersions.Instance.CertificateRevocationCheck }; IWebProxy webProxy = httpTransportSettings.Proxy; if (webProxy != DefaultWebProxySettings.Instance) { _httpClientHandler.UseProxy = webProxy != null; _httpClientHandler.Proxy = webProxy; } _httpClientObj = new HttpClient(_httpClientHandler, false) { BaseAddress = _baseAddress, Timeout = s_defaultOperationTimeout, }; _httpClientObj.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeForDeviceManagementApis)); _httpClientObj.DefaultRequestHeaders.ExpectContinue = false; } /// <summary> /// Unified HTTP request API /// </summary> /// <param name="httpMethod">the <see cref="HttpMethod"/> with the HTTP verb.</param> /// <param name="requestUri">the rest API <see cref="Uri"/> with for the requested service.</param> /// <param name="customHeaders">the optional <c>Dictionary</c> with additional header fields. It can be <c>null</c>.</param> /// <param name="body">the <c>string</c> with the message body. It can be <c>null</c> or empty.</param> /// <param name="ifMatch">the optional <c>string</c> with the match condition, normally an eTag. It can be <c>null</c>.</param> /// <param name="cancellationToken">the task cancellation Token.</param> /// <returns>The <see cref="ContractApiResponse"/> with the HTTP response.</returns> /// <exception cref="ProvisioningServiceClientException">if the cancellation was requested.</exception> /// <exception cref="ProvisioningServiceClientTransportException">if there is a error in the HTTP communication /// between client and service.</exception> /// <exception cref="ProvisioningServiceClientHttpException">if the service answer the request with error status.</exception> public async Task<ContractApiResponse> RequestAsync( HttpMethod httpMethod, Uri requestUri, IDictionary<string, string> customHeaders, string body, string ifMatch, CancellationToken cancellationToken) { ContractApiResponse response; using (var msg = new HttpRequestMessage(httpMethod, requestUri)) { if (!string.IsNullOrEmpty(body)) { msg.Content = new StringContent(body, Encoding.UTF8, MediaTypeForDeviceManagementApis); } msg.Headers.Add(HttpRequestHeader.Authorization.ToString(), _authenticationHeaderProvider.GetAuthorizationHeader()); msg.Headers.Add(HttpRequestHeader.UserAgent.ToString(), Utils.GetClientVersion()); if (customHeaders != null) { foreach (KeyValuePair<string, string> header in customHeaders) { msg.Headers.Add(header.Key, header.Value); } } InsertIfMatch(msg, ifMatch); try { using HttpResponseMessage httpResponse = await _httpClientObj.SendAsync(msg, cancellationToken).ConfigureAwait(false); if (httpResponse == null) { throw new ProvisioningServiceClientTransportException( $"The response message was null when executing operation {httpMethod}."); } response = new ContractApiResponse( await httpResponse.Content.ReadHttpContentAsStringAsync(cancellationToken).ConfigureAwait(false), httpResponse.StatusCode, httpResponse.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault()), httpResponse.ReasonPhrase); } catch (AggregateException ex) { ReadOnlyCollection<Exception> innerExceptions = ex.Flatten().InnerExceptions; if (innerExceptions.Any(e => e is TimeoutException)) { throw new ProvisioningServiceClientTransportException(ex.Message, ex); } throw; } catch (TimeoutException ex) { throw new ProvisioningServiceClientTransportException(ex.Message, ex); } catch (IOException ex) { throw new ProvisioningServiceClientTransportException(ex.Message, ex); } catch (HttpRequestException ex) { throw new ProvisioningServiceClientTransportException(ex.Message, ex); } catch (TaskCanceledException ex) { // Unfortunately TaskCanceledException is thrown when HttpClient times out. if (cancellationToken.IsCancellationRequested) { throw new ProvisioningServiceClientException(ex.Message, ex); } throw new ProvisioningServiceClientTransportException($"The {httpMethod} operation timed out.", ex); } } ValidateHttpResponse(response); return response; } private static void ValidateHttpResponse(ContractApiResponse response) { if (response.StatusCode >= HttpStatusCode.InternalServerError || (int)response.StatusCode == 429) { throw new ProvisioningServiceClientHttpException(response, isTransient: true); } else if (response.StatusCode >= HttpStatusCode.Ambiguous) { throw new ProvisioningServiceClientHttpException(response, isTransient: false); } } private static void InsertIfMatch(HttpRequestMessage requestMessage, string ifMatch) { if (string.IsNullOrWhiteSpace(ifMatch)) { return; } var quotedIfMatch = new StringBuilder(); if (!ifMatch.StartsWith("\"", StringComparison.OrdinalIgnoreCase)) { quotedIfMatch.Append('"'); } quotedIfMatch.Append(ifMatch); if (!ifMatch.EndsWith("\"", StringComparison.OrdinalIgnoreCase)) { quotedIfMatch.Append('"'); } requestMessage.Headers.IfMatch.Add(new EntityTagHeaderValue(quotedIfMatch.ToString())); } /// <summary> /// Release all HTTP resources. /// </summary> public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { if (_httpClientObj != null) { _httpClientObj.Dispose(); _httpClientObj = null; } if (_httpClientHandler != null) { _httpClientHandler.Dispose(); _httpClientHandler = null; } } } } }