Skip to content

Commit

Permalink
Initial Version 0.2
Browse files Browse the repository at this point in the history
  • Loading branch information
Frankenslag committed Aug 17, 2021
1 parent 68d01e6 commit 88479db
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 113 deletions.
30 changes: 23 additions & 7 deletions MazdaApiLib/MazdaApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
// SOFTWARE.
//

using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
Expand All @@ -44,25 +45,40 @@ public partial class MazdaApiClient
/// <param name="emailAddress">The email address you use to log into the MyMazda mobile app</param>
/// <param name="password">The password you use to log into the MyMazda mobile app</param>
/// <param name="region">The code for the region in which your account was registered (MNAO - North America, MME - Europe, MJO - Japan)</param>
/// <param name="httpClient">HttpClient used to communicate with MyMazda</param>
/// <param name="useCachedVehicleList">A flag that when set to true caches calls to methods that return vehicles. (Optional, defaults to false)</param>
/// <param name="logger">An ILogger that can be used for debugging and tracing purposes. (Optional, defaults to null)</param>
public MazdaApiClient(string emailAddress, string password, string region, bool useCachedVehicleList = false, ILogger<MazdaApiClient> logger = null)
public MazdaApiClient(string emailAddress, string password, string region, HttpClient httpClient, bool useCachedVehicleList = false, ILogger<MazdaApiClient> logger = null)
{
if (!string.IsNullOrWhiteSpace(emailAddress))
if (!(httpClient is null))
{
if (!string.IsNullOrWhiteSpace(password))
if (!string.IsNullOrWhiteSpace(emailAddress))
{
_controller = new MazdaApiController(emailAddress, password, region, logger);
_useCachedVehicleList = useCachedVehicleList;
if (!string.IsNullOrWhiteSpace(password))
{
if (!string.IsNullOrWhiteSpace(region))
{
_controller = new MazdaApiController(emailAddress, password, region, httpClient, logger);
_useCachedVehicleList = useCachedVehicleList;
}
else
{
throw new MazdaApiConfigException("Invalid or missing region parameter");
}
}
else
{
throw new MazdaApiConfigException("Invalid or missing password parameter");
}
}
else
{
throw new MazdaApiConfigException("Invalid or missing password");
throw new MazdaApiConfigException("Invalid or missing email address parameter");
}
}
else
{
throw new MazdaApiConfigException("Invalid or missing email address");
throw new MazdaApiConfigException("Invalid or missing httpClient parameter");
}
}

Expand Down
60 changes: 31 additions & 29 deletions MazdaApiLib/MazdaApiConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ namespace WingandPrayer.MazdaApi
internal class MazdaApiConnection
{
private readonly ILogger<MazdaApiClient> _logger;
private readonly HttpClient _httpClient;

private const string AppOs = "Android";
private const string Iv = "0102030405060708";
Expand All @@ -66,24 +67,26 @@ internal class MazdaApiConnection
{ "MJO", new RegionConfig { ApplicationCode = "202009170613074283422", BaseUrl = new Uri("https://wcs9p6wj.mazda.com/prod"), UsherUrl = new Uri("https://c5ulfwxr.mazda.com/appapi/v1") } }
};

private readonly string _baseApiDeviceId;
private readonly string _emailAddress;

private readonly RegionConfig _regionConfig;
private readonly string _emailAddress;
private readonly string _usherApiPassword;

private readonly SensorDataBuilder _sensorDataBuilder;
private readonly string _baseApiDeviceId;
private readonly string _usherApiDeviceId;
private readonly string _usherApiPassword;
private string _accessToken;
private DateTime _accessTokenExpirationTs;
private string _encKey;
private string _signKey;

public MazdaApiConnection(string emailAddress, string password, string region, ILogger<MazdaApiClient> logger)
public MazdaApiConnection(string emailAddress, string password, string region, HttpClient httpClient, ILogger<MazdaApiClient> logger)
{
_logger = logger;

if (RegionsConfigs.ContainsKey(region))
{
_httpClient = httpClient;
_emailAddress = emailAddress;
_usherApiPassword = password;
_accessToken = null;
Expand Down Expand Up @@ -132,9 +135,9 @@ private async Task<string> SendApiRequestAsync(HttpMethod method, string uri, st
{
if (needsKeys && (string.IsNullOrEmpty(_encKey) || string.IsNullOrEmpty(_signKey)))
{
_logger.LogDebug("Retrieving encryption keys");
_logger?.LogDebug("Retrieving encryption keys");
CheckVersionResponse checkVersionResponse = JsonSerializer.Deserialize<CheckVersionResponse>(await SendApiRequestAsync(HttpMethod.Post, "/service/checkVersion", null, false, false), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
_logger.LogDebug("Successfully retrieved encryption keys");
_logger?.LogDebug("Successfully retrieved encryption keys");
_encKey = checkVersionResponse?.EncKey;
_signKey = checkVersionResponse?.SignKey;
}
Expand All @@ -143,53 +146,53 @@ private async Task<string> SendApiRequestAsync(HttpMethod method, string uri, st
{
if (!string.IsNullOrWhiteSpace(_accessToken) && DateTime.Now - new TimeSpan(0, 0, 1, 0) > _accessTokenExpirationTs)
{
_logger.LogDebug("Access token is expired.Fetching a new one.");
_logger?.LogDebug("Access token is expired.Fetching a new one.");
_accessToken = null;
_accessTokenExpirationTs = DateTime.MinValue;
}

if (string.IsNullOrWhiteSpace(_accessToken))
{
_logger.LogDebug("No access token present. Logging in.");
_logger?.LogDebug("No access token present. Logging in.");
await LoginAsync();
}
}

_logger.LogTrace($"Sending {method} request to {uri}{(numRetries == 0 ? string.Empty : $" - attempt {numRetries + 1}")}{(string.IsNullOrWhiteSpace(body) ? string.Empty : $"\n\rBody: {body}")}");
_logger?.LogTrace($"Sending {method} request to {uri}{(numRetries == 0 ? string.Empty : $" - attempt {numRetries + 1}")}{(string.IsNullOrWhiteSpace(body) ? string.Empty : $"\n\rBody: {body}")}");

try
{
return await SendRawApiRequestAsync(method, uri, body, needsAuth);
}
catch (MazdaApiEncryptionException)
{
_logger.LogWarning("Server reports request was not encrypted properly. Retrieving new encryption keys.");
_logger?.LogWarning("Server reports request was not encrypted properly. Retrieving new encryption keys.");
CheckVersionResponse checkVersionResponse = JsonSerializer.Deserialize<CheckVersionResponse>(await SendApiRequestAsync(HttpMethod.Post, "/service/checkVersion", null, false, false), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
_encKey = checkVersionResponse?.EncKey;
_signKey = checkVersionResponse?.SignKey;
return await SendApiRequestAsync(method, uri, body, needsKeys, needsAuth, numRetries + 1);
}
catch (MazdaApiTokenExpiredException)
{
_logger.LogDebug("Server reports access token was expired. Retrieving new access token.");
_logger?.LogDebug("Server reports access token was expired. Retrieving new access token.");
await LoginAsync();
return await SendApiRequestAsync(method, uri, body, needsKeys, needsAuth, numRetries + 1);
}
catch (MazdaApiLoginFailedException)
{
_logger.LogDebug("Login failed for an unknown reason. Trying again.");
_logger?.LogDebug("Login failed for an unknown reason. Trying again.");
await LoginAsync();
return await SendApiRequestAsync(method, uri, body, needsKeys, needsAuth, numRetries + 1);
}
catch (MazdaApiRequestInProgressException)
{
_logger.LogDebug("Request failed because another request was already in progress. Waiting 30 seconds and trying again.");
_logger?.LogDebug("Request failed because another request was already in progress. Waiting 30 seconds and trying again.");
await Task.Delay(30 * 1000);
return await SendApiRequestAsync(method, uri, body, needsKeys, needsAuth, numRetries + 1);
}
}

_logger.LogDebug("Request exceeded max number of retries");
_logger?.LogDebug("Request exceeded max number of retries");
throw new MazdaApiException("Request exceeded max number of retries");
}

Expand All @@ -201,9 +204,8 @@ private async Task<string> SendRawApiRequestAsync(HttpMethod method, string uri,

if (!string.IsNullOrWhiteSpace(body)) encryptedBody = CryptoUtils.EncryptPayloadUsingKey(body, _encKey, Iv);

using HttpClient httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Clear();

httpClient.DefaultRequestHeaders.Clear();
using (HttpRequestMessage request = new HttpRequestMessage { RequestUri = new Uri(_regionConfig.BaseUrl + uri), Method = method, Content = new StringContent(encryptedBody) })
{
request.Headers.Add("device-id", _baseApiDeviceId);
Expand All @@ -223,7 +225,7 @@ private async Task<string> SendRawApiRequestAsync(HttpMethod method, string uri,
request.Headers.Add("sign", GetSignFromPayloadAndTimestamp("", timestamp));
else if (method == HttpMethod.Post) request.Headers.Add("sign", GetSignFromPayloadAndTimestamp(body, timestamp));

HttpResponseMessage apiResponseMessage = await httpClient.SendAsync(request);
HttpResponseMessage apiResponseMessage = await _httpClient.SendAsync(request);

apiResponse = JsonSerializer.Deserialize(await apiResponseMessage.Content.ReadAsByteArrayAsync(), typeof(ApiResponse), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) as ApiResponse;
}
Expand All @@ -244,7 +246,7 @@ private async Task<string> SendRawApiRequestAsync(HttpMethod method, string uri,

string payload = CryptoUtils.DecryptPayloadUsingKey(apiResponse.Payload, key, Iv);

_logger.LogTrace($"Payload received: {payload}");
_logger?.LogTrace($"Payload received: {payload}");

return payload;
}
Expand All @@ -271,8 +273,8 @@ private async Task<string> SendRawApiRequestAsync(HttpMethod method, string uri,

private async Task LoginAsync()
{
using HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgentUsherApi);
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgentUsherApi);
UriBuilder builder = new UriBuilder(_regionConfig.UsherUrl);
builder.Path += @"/system/encryptionKey";
NameValueCollection query = HttpUtility.ParseQueryString(builder.Query);
Expand All @@ -281,11 +283,11 @@ private async Task LoginAsync()
query["deviceId"] = _usherApiDeviceId;
query["sdkVersion"] = UsherSdkVersion;
builder.Query = query.ToString()!;
HttpResponseMessage response = await httpClient.GetAsync(builder.ToString());
HttpResponseMessage response = await _httpClient.GetAsync(builder.ToString());
EncryptionKeyResponse encryptionKey = JsonSerializer.Deserialize(await response.Content.ReadAsByteArrayAsync(), typeof(EncryptionKeyResponse), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) as EncryptionKeyResponse;

_logger.LogDebug($"Logging in as {_emailAddress}");
_logger.LogDebug("Retrieving public key to encrypt password");
_logger?.LogDebug($"Logging in as {_emailAddress}");
_logger?.LogDebug("Retrieving public key to encrypt password");

using (RSA rsa = CryptoUtils.CreateRsaFromDerData(Convert.FromBase64String(encryptionKey!.Data.PublicKey)))
{
Expand All @@ -301,31 +303,31 @@ private async Task LoginAsync()
UserId = _emailAddress,
UserIdType = "email"
}), Encoding.UTF8, "application/json");
response = await httpClient.PostAsync(builder.ToString(), content);
response = await _httpClient.PostAsync(builder.ToString(), content);
}

_logger.LogDebug("Sending login request");
_logger?.LogDebug("Sending login request");

LoginResponse loginResponse = JsonSerializer.Deserialize(await response.Content.ReadAsByteArrayAsync(), typeof(LoginResponse), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) as LoginResponse;

switch (loginResponse?.Status ?? string.Empty)
{
case "OK":
_logger.LogDebug($"Successfully logged in as {_emailAddress}");
_logger?.LogDebug($"Successfully logged in as {_emailAddress}");
_accessToken = loginResponse!.Data.AccessToken;
_accessTokenExpirationTs = DateTimeOffset.FromUnixTimeSeconds(loginResponse!.Data.AccessTokenExpirationTs).DateTime;
break;

case "INVALID_CREDENTIAL":
_logger.LogDebug("Login failed due to invalid email or password");
_logger?.LogDebug("Login failed due to invalid email or password");
throw new MazdaApiAuthenticationException("Invalid email or password");

case "USER_LOCKED":
_logger.LogDebug("Login failed to account being locked");
_logger?.LogDebug("Login failed to account being locked");
throw new MazdaApiAccountLockedException("Login failed to account being locked");

default:
_logger.LogDebug($"Login failed{(string.IsNullOrWhiteSpace(loginResponse?.Status) ? string.Empty : $":{loginResponse.Status}")}");
_logger?.LogDebug($"Login failed{(string.IsNullOrWhiteSpace(loginResponse?.Status) ? string.Empty : $":{loginResponse.Status}")}");
throw new MazdaApiLoginFailedException("Login failed");
}
}
Expand Down
2 changes: 1 addition & 1 deletion MazdaApiLib/MazdaApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ internal class MazdaApiController
{
private readonly MazdaApiConnection _connection;

public MazdaApiController(string emailAddress, string password, string region, ILogger<MazdaApiClient> logger) => _connection = new MazdaApiConnection(emailAddress, password, region, logger);
public MazdaApiController(string emailAddress, string password, string region, HttpClient httpClient, ILogger<MazdaApiClient> logger) => _connection = new MazdaApiConnection(emailAddress, password, region, httpClient, logger);

private static bool CheckResult(string json)
{
Expand Down
6 changes: 3 additions & 3 deletions MazdaApiLib/MazdaApiLib.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
<PackageLicenseExpression></PackageLicenseExpression>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<AssemblyVersion>1.0.1.0</AssemblyVersion>
<FileVersion>1.0.1.0</FileVersion>
<Version>1.0.1</Version>
<AssemblyVersion>0.2.0.0</AssemblyVersion>
<FileVersion>0.2.0.0</FileVersion>
<Version>0.2.0</Version>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
Expand Down
3 changes: 2 additions & 1 deletion MazdaApiLib/MazdaApiLib.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 0 additions & 36 deletions MazdaApiLib/README.Designer.md

This file was deleted.

Loading

0 comments on commit 88479db

Please sign in to comment.