Fix TelemetryPublisherFirestore.cs bugs not re-authenticating properly.

This commit is contained in:
Denis-Cosmin Nutiu 2020-11-25 20:49:15 +02:00
parent 61bccfbdf1
commit 9ad323e755
5 changed files with 57 additions and 47 deletions

View file

@ -5,8 +5,8 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using sNetHttp = System.Net.Http;
using sNetHttpHeaders = System.Net.Http.Headers;
using System.Net.Http;
using System.Net.Http.Headers;
namespace NucuCar.Domain.Http
{
@ -14,7 +14,7 @@ namespace NucuCar.Domain.Http
/// A simple HttpClient wrapper designed to make it easier to work with web requests with media type application/json.
/// It implements a simple retry mechanism.
/// </summary>
public class HttpClient : IDisposable
public class MinimalHttpClient : IDisposable
{
#region Fields
@ -54,26 +54,26 @@ namespace NucuCar.Domain.Http
protected int timeout;
// ReSharper restore InconsistentNaming
private readonly sNetHttp.HttpClient _httpClient;
private readonly HttpClient _httpClient;
#endregion
#region Constructors
public HttpClient()
public MinimalHttpClient()
{
_httpClient = new sNetHttp.HttpClient();
_httpClient = new HttpClient();
maxRetries = 3;
timeout = 10000;
Logger = null;
}
public HttpClient(string baseAddress) : this()
public MinimalHttpClient(string baseAddress) : this()
{
_httpClient.BaseAddress = new Uri(baseAddress);
}
public HttpClient(string baseAddress, int maxRetries) : this(baseAddress)
public MinimalHttpClient(string baseAddress, int maxRetries) : this(baseAddress)
{
MaxRetries = maxRetries;
}
@ -82,10 +82,15 @@ namespace NucuCar.Domain.Http
#region Public Methods
public void ClearAuthorizationHeader()
{
_httpClient.DefaultRequestHeaders.Authorization = null;
}
public void Authorization(string scheme, string token)
{
_httpClient.DefaultRequestHeaders.Authorization =
new sNetHttpHeaders.AuthenticationHeaderValue(scheme, token);
new AuthenticationHeaderValue(scheme, token);
}
public void Authorization(string token)
@ -93,29 +98,29 @@ namespace NucuCar.Domain.Http
Authorization("Bearer", token);
}
public async Task<sNetHttp.HttpResponseMessage> GetAsync(string path)
public async Task<HttpResponseMessage> GetAsync(string path)
{
var request = _makeRequest(sNetHttp.HttpMethod.Get, path);
var request = _makeRequest(HttpMethod.Get, path);
return await SendAsync(request);
}
public async Task<sNetHttp.HttpResponseMessage> PostAsync(string path, Dictionary<string, object> data)
public async Task<HttpResponseMessage> PostAsync(string path, Dictionary<string, object> data)
{
var request = _makeRequest(sNetHttp.HttpMethod.Post, path);
var request = _makeRequest(HttpMethod.Post, path);
request.Content = _makeContent(data);
return await SendAsync(request);
}
public async Task<sNetHttp.HttpResponseMessage> PutAsync(string path, Dictionary<string, object> data)
public async Task<HttpResponseMessage> PutAsync(string path, Dictionary<string, object> data)
{
var request = _makeRequest(sNetHttp.HttpMethod.Put, path);
var request = _makeRequest(HttpMethod.Put, path);
request.Content = _makeContent(data);
return await SendAsync(request);
}
public async Task<sNetHttp.HttpResponseMessage> DeleteAsync(string path, Dictionary<string, object> data)
public async Task<HttpResponseMessage> DeleteAsync(string path, Dictionary<string, object> data)
{
var request = _makeRequest(sNetHttp.HttpMethod.Delete, path);
var request = _makeRequest(HttpMethod.Delete, path);
request.Content = _makeContent(data);
return await SendAsync(request);
}
@ -125,23 +130,28 @@ namespace NucuCar.Domain.Http
/// </summary>
/// <param name="requestMessage">The request to make.</param>
/// <returns></returns>
public virtual async Task<sNetHttp.HttpResponseMessage> SendAsync(sNetHttp.HttpRequestMessage requestMessage)
public virtual async Task<HttpResponseMessage> SendAsync(HttpRequestMessage requestMessage)
{
var currentRetry = 0;
sNetHttp.HttpResponseMessage responseMessage = null;
HttpResponseMessage responseMessage = null;
while (currentRetry < maxRetries)
{
try
{
responseMessage = await _sendAsync(requestMessage);
// We need a request copy because we can't send the same request multiple times.
var requestCopy = new HttpRequestMessage(requestMessage.Method, requestMessage.RequestUri);
requestCopy.Headers.Authorization = requestMessage.Headers.Authorization;
requestCopy.Content = requestMessage.Content;
responseMessage = await _sendAsync(requestCopy);
break;
}
catch (TaskCanceledException)
{
Logger?.LogError($"Request timeout for {requestMessage.RequestUri}!");
}
catch (sNetHttp.HttpRequestException e)
catch (HttpRequestException e)
{
// The request failed due to an underlying issue such as network connectivity, DNS failure,
// server certificate validation or timeout.
@ -172,9 +182,9 @@ namespace NucuCar.Domain.Http
/// </summary>
/// <param name="data">A dictionary representing JSON data.</param>
/// <returns></returns>
private sNetHttp.StringContent _makeContent(Dictionary<string, object> data)
private StringContent _makeContent(Dictionary<string, object> data)
{
return new sNetHttp.StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json");
return new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json");
}
/// <summary>
@ -183,11 +193,11 @@ namespace NucuCar.Domain.Http
/// <param name="method">The HttpMethod to use</param>
/// <param name="path">The path, whether it is relative to the base or a new one.</param>
/// <returns></returns>
private sNetHttp.HttpRequestMessage _makeRequest(sNetHttp.HttpMethod method, string path)
private HttpRequestMessage _makeRequest(HttpMethod method, string path)
{
var uri = _httpClient.BaseAddress == null ? new Uri(path) : new Uri(_httpClient.BaseAddress, path);
var requestMessage = new sNetHttp.HttpRequestMessage
var requestMessage = new HttpRequestMessage
{
Method = method,
RequestUri = uri
@ -202,17 +212,17 @@ namespace NucuCar.Domain.Http
/// </summary>
/// <param name="requestMessage"></param>
/// <returns></returns>
private async Task<sNetHttp.HttpResponseMessage> _sendAsync(sNetHttp.HttpRequestMessage requestMessage)
private async Task<HttpResponseMessage> _sendAsync(HttpRequestMessage requestMessage)
{
var cts = new CancellationTokenSource();
sNetHttp.HttpResponseMessage response;
HttpResponseMessage response;
// Make sure we cancel after a certain timeout.
cts.CancelAfter(timeout);
try
{
response = await _httpClient.SendAsync(requestMessage,
sNetHttp.HttpCompletionOption.ResponseContentRead,
HttpCompletionOption.ResponseContentRead,
cts.Token);
}
finally
@ -242,9 +252,9 @@ namespace NucuCar.Domain.Http
/// <summary>
/// Extension used to deserialize the body of a HttpResponseMessage into Json.
/// </summary>
/// <param name="responseMessage">The HttpResponseMessage message. <see cref="sNetHttp.HttpResponseMessage"/></param>
/// <param name="responseMessage">The HttpResponseMessage message. <see cref="HttpResponseMessage"/></param>
/// <returns>A JsonElement. <see cref="JsonElement"/></returns>
public static async Task<JsonElement> GetJson(this sNetHttp.HttpResponseMessage responseMessage)
public static async Task<JsonElement> GetJson(this HttpResponseMessage responseMessage)
{
return JsonSerializer.Deserialize<JsonElement>(await responseMessage.Content.ReadAsStringAsync());
}

View file

@ -4,14 +4,14 @@ using System.Threading.Tasks;
namespace NucuCar.Domain.Http
{
public class MockHttpClient : Domain.Http.HttpClient
public class MockMinimalHttpClient : MinimalHttpClient
{
public List<HttpRequestMessage> SendAsyncArgCalls;
public List<HttpResponseMessage> SendAsyncResponses;
private int _sendAsyncCallCounter;
public MockHttpClient(string baseAddress) : base(baseAddress)
public MockMinimalHttpClient(string baseAddress) : base(baseAddress)
{
_sendAsyncCallCounter = 0;
SendAsyncArgCalls = new List<HttpRequestMessage>();

View file

@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging;
using NucuCar.Domain.Http;
using NucuCar.Domain.Utilities;
using NucuCar.Telemetry.Abstractions;
using HttpClient = NucuCar.Domain.Http.HttpClient;
namespace NucuCar.Telemetry.Publishers
{
@ -29,7 +28,7 @@ namespace NucuCar.Telemetry.Publishers
/// </summary>
public class TelemetryPublisherFirestore : TelemetryPublisher
{
protected HttpClient HttpClient;
protected MinimalHttpClient HttpClient;
private string _idToken;
private DateTime _authorizationExpiryTime;
@ -67,13 +66,15 @@ namespace NucuCar.Telemetry.Publishers
// Setup HttpClient
var requestUrl = $"https://firestore.googleapis.com/v1/projects/{firestoreProjectId}/" +
$"databases/(default)/documents/{firestoreCollection}/";
HttpClient = new HttpClient(requestUrl) {Timeout = timeout, Logger = Logger};
HttpClient = new MinimalHttpClient(requestUrl) {Timeout = timeout, Logger = Logger};
Logger?.LogInformation($"Initialized {nameof(TelemetryPublisherFirestore)}");
Logger?.LogInformation($"ProjectId: {firestoreProjectId}; CollectionName: {firestoreCollection}.");
}
private async Task SetupAuthorization()
{
HttpClient.ClearAuthorizationHeader();
// https://cloud.google.com/identity-platform/docs/use-rest-api#section-sign-in-email-password
var requestUrl = $"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={_webApiKey}";
var data = new Dictionary<string, object>()
@ -82,7 +83,7 @@ namespace NucuCar.Telemetry.Publishers
["password"] = _webPassword,
["returnSecureToken"] = true
};
var response = await HttpClient.PostAsync(requestUrl, data);
if (response?.StatusCode == HttpStatusCode.OK)

View file

@ -15,7 +15,7 @@ namespace NucuCar.Telemetry
/// Creates an instance of <see cref="TelemetryPublisher"/>. See <see cref="TelemetryPublisherType"/>
/// </summary>
/// <param name="type">The type of the publisher. <see cref="TelemetryPublisherType"/> </param>
/// <param name="connectionString">Device connection string for Microsoft Azure IoT hub device.</param>
/// <param name="connectionString">Device connection string for the telemetry publisher.</param>
/// <param name="telemetrySource">String that is used to identify the source of the telemetry data.</param>
/// <param name="logger">An <see cref="ILogger"/> logger instance. </param>
/// <returns>A <see cref="TelemetryPublisher"/> instance.</returns>
@ -25,7 +25,7 @@ namespace NucuCar.Telemetry
Guard.ArgumentNotNullOrWhiteSpace(nameof(connectionString), connectionString);
Guard.ArgumentNotNullOrWhiteSpace(nameof(telemetrySource), telemetrySource);
Guard.ArgumentNotNull(nameof(logger), logger);
var opts = new TelemetryPublisherOptions()
var opts = new TelemetryPublisherOptions
{ConnectionString = connectionString, TelemetrySource = telemetrySource, Logger = logger};
return SpawnPublisher(type, opts);
}
@ -40,7 +40,7 @@ namespace NucuCar.Telemetry
{
Guard.ArgumentNotNullOrWhiteSpace(nameof(connectionString), connectionString);
var opts = new TelemetryPublisherOptions()
{ConnectionString = connectionString, TelemetrySource = "TelemetryPublisherAzure"};
{ConnectionString = connectionString, TelemetrySource = "NucuCar.Sensors"};
return SpawnPublisher(type, opts);
}

View file

@ -9,7 +9,6 @@ using NucuCar.Domain.Http;
using NucuCar.Telemetry;
using NucuCar.Telemetry.Publishers;
using Xunit;
using HttpClient = NucuCar.Domain.Http.HttpClient;
namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
{
@ -25,7 +24,7 @@ namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
_mockData = new Dictionary<string, object>();
}
public void SetHttpClient(HttpClient client)
public void SetHttpClient(MinimalHttpClient client)
{
HttpClient = client;
}
@ -57,7 +56,7 @@ namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
}
[Fact]
private void Test_Construct_BadCollectiontName()
private void Test_Construct_BadCollectionName()
{
// Setup
var opts = new TelemetryPublisherOptions()
@ -78,7 +77,7 @@ namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
ConnectionString = "ProjectId=test;CollectionName=test;WebApiKey=TAPIKEY;WebApiEmail=t@emai.com;WebApiPassword=tpass"
};
var publisher = new MockTelemetryPublisherFirestore(opts);
var mockHttpClient = new MockHttpClient("http://testing.com");
var mockHttpClient = new MockMinimalHttpClient("http://testing.com");
var authResponse = new HttpResponseMessage(HttpStatusCode.OK)
{Content = new StringContent("{\"idToken\": \"1\",\"expiresIn\": \"3600\"}")};
mockHttpClient.SendAsyncResponses.Add(authResponse);
@ -106,7 +105,7 @@ namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
ConnectionString = "ProjectId=test;CollectionName=test;WebApiKey=TAPIKEY;WebApiEmail=t@emai.com;WebApiPassword=tpass"
};
var publisher = new MockTelemetryPublisherFirestore(opts);
var mockHttpClient = new MockHttpClient("http://testing.com");
var mockHttpClient = new MockMinimalHttpClient("http://testing.com");
var authResponse = new HttpResponseMessage(HttpStatusCode.OK)
{Content = new StringContent("{\"idToken\": \"1\",\"expiresIn\": \"3600\"}")};
mockHttpClient.SendAsyncResponses.Add(authResponse);
@ -130,7 +129,7 @@ namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
ConnectionString = "ProjectId=test;CollectionName=test"
};
var publisher = new MockTelemetryPublisherFirestore(opts);
var mockHttpClient = new MockHttpClient("http://testing.com");
var mockHttpClient = new MockMinimalHttpClient("http://testing.com");
mockHttpClient.SendAsyncResponses.Add(new HttpResponseMessage(HttpStatusCode.OK));
publisher.SetHttpClient(mockHttpClient);
@ -155,7 +154,7 @@ namespace NucuCar.UnitTests.NucuCar.Domain.Telemetry.Tests
"ProjectId=test;CollectionName=test;WebApiKey=TAPIKEY;WebApiEmail=t@emai.com;WebApiPassword=tpass"
};
var publisher = new MockTelemetryPublisherFirestore(opts);
var mockHttpClient = new MockHttpClient("http://testing.com");
var mockHttpClient = new MockMinimalHttpClient("http://testing.com");
mockHttpClient.SendAsyncResponses.Add(new HttpResponseMessage(HttpStatusCode.OK)
{Content = new StringContent("{\"idToken\": \"1\",\"expiresIn\": \"0\"}")});