Code cleanup:

- folders in api project
- changing json model object property names to be .NET style
- etc
Implemented wrapper HttpClient methods that handle token refreshing and first pass at transient error retries / throwing non-transient errors
This commit is contained in:
Anonymous
2019-08-09 19:17:26 -04:00
parent 50a6e2e16d
commit e9ffa5c13b
17 changed files with 530 additions and 458 deletions

View File

@@ -1,166 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
namespace GM.Api
{
public class DiagnosticReader
{
IEnumerable<Diagnosticresponse> _dr;
public DiagnosticReader(IEnumerable<Diagnosticresponse> elements)
{
_dr = elements;
}
public float AmbientAirTempCelcius => float.Parse((from f in _dr
where f.name == "AMBIENT AIR TEMPERATURE"
from r in f.diagnosticElement
where r.name == "AMBIENT AIR TEMPERATURE"
select r.value).FirstOrDefault());
public string ChargerPowerLevel => (from f in _dr
where f.name == "CHARGER POWER LEVEL"
from r in f.diagnosticElement
where r.name == "CHARGER POWER LEVEL"
select r.value).FirstOrDefault();
public float EvBatteryLevelPercent => float.Parse((from f in _dr
where f.name == "EV BATTERY LEVEL"
from r in f.diagnosticElement
where r.name == "EV BATTERY LEVEL"
select r.value).FirstOrDefault());
}
public static class DiagnosticHelper
{
public static float GetElectricEconomyKwh(this IEnumerable<Diagnosticresponse> elements)
{
var itm = float.Parse((from f in elements
where f.name == "ENERGY EFFICIENCY"
from r in f.diagnosticElement
where r.name == "ELECTRIC ECONOMY"
select r.value).FirstOrDefault());
return itm;
}
public static float GetLifetimeEfficiencyKwh(this IEnumerable<Diagnosticresponse> elements)
{
var itm = float.Parse((from f in elements
where f.name == "ENERGY EFFICIENCY"
from r in f.diagnosticElement
where r.name == "LIFETIME EFFICIENCY"
select r.value).FirstOrDefault());
return itm;
}
public static float GetLifetimeMpgE(this IEnumerable<Diagnosticresponse> elements)
{
var itm = float.Parse((from f in elements
where f.name == "ENERGY EFFICIENCY"
from r in f.diagnosticElement
where r.name == "LIFETIME MPGE"
select r.value).FirstOrDefault());
return itm;
}
public static float GetOdometerKm(this IEnumerable<Diagnosticresponse> elements)
{
var itm = float.Parse((from f in elements
where f.name == "ENERGY EFFICIENCY"
from r in f.diagnosticElement
where r.name == "ODOMETER"
select r.value).FirstOrDefault());
return itm;
}
public static float GetEvBatteryLevelPercent(this IEnumerable<Diagnosticresponse> elements)
{
var itm = float.Parse((from f in elements
where f.name == "EV BATTERY LEVEL"
from r in f.diagnosticElement
where r.name == "EV BATTERY LEVEL"
select r.value).FirstOrDefault());
return itm;
}
}
public class DiagnosticRequestRoot
{
public static readonly string[] DefaultItems = new string[]
{
"ENGINE COOLANT TEMP",
"ENGINE RPM",
"HV BATTERY ESTIMATED CAPACITY",
"LAST TRIP FUEL ECONOMY",
"ENERGY EFFICIENCY",
"HYBRID BATTERY MINIMUM TEMPERATURE",
"EV ESTIMATED CHARGE END",
"EV BATTERY LEVEL",
"EV PLUG VOLTAGE",
"ODOMETER",
"CHARGER POWER LEVEL",
"LIFETIME EV ODOMETER",
"EV PLUG STATE",
"EV CHARGE STATE",
"TIRE PRESSURE",
"AMBIENT AIR TEMPERATURE",
"LAST TRIP DISTANCE",
"INTERM VOLT BATT VOLT",
"GET COMMUTE SCHEDULE",
"GET CHARGE MODE",
"EV SCHEDULED CHARGE START",
"VEHICLE RANGE"
};
//public Diagnosticsrequest diagnosticsRequest { get; set; }
}
//public class Diagnosticsrequest
//{
// public string[] diagnosticItem { get; set; }
//}
public class ResponseBody
{
public Diagnosticresponse[] diagnosticResponse { get; set; }
}
public class Diagnosticresponse
{
public string name { get; set; }
public Diagnosticelement[] diagnosticElement { get; set; }
}
public class Diagnosticelement
{
public string name { get; set; }
public string status { get; set; }
public string message { get; set; }
public string value { get; set; }
public string unit { get; set; }
}
}

View File

@@ -1,4 +1,6 @@
using JWT;
using GM.Api.Models;
using GM.Api.Tokens;
using JWT;
using JWT.Algorithms;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -13,9 +15,9 @@ namespace GM.Api
{
public class GMClient
{
//TODO: maybe throw exceptions?
//TODO: all calls need to catch a 401, attempt refresh, try one more time and fail on second 401
//TODO: all calls need to catch transient exceptions and retry a certain number of times
public static int RetryCount { get; set; } = 3;
//TODO: consistent exception throwing
string _clientId;
string _deviceId;
@@ -70,23 +72,96 @@ namespace GM.Api
}
async Task<Commandresponse> VehicleConnect(string vin)
async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool noAuth = false)
{
if (LoginData == null) throw new InvalidOperationException("Login required");
if (LoginData.IsExpired)
if (!noAuth)
{
if (!await RefreshToken())
if (LoginData == null)
{
throw new InvalidOperationException("Token refresh failed");
throw new InvalidOperationException("Not Logged in");
}
if (LoginData.IsExpired)
{
var result = await RefreshToken();
if (!result)
{
throw new InvalidOperationException("Token refresh failed");
}
}
}
else
{
request.Headers.Authorization = null;
}
var req = new HttpRequestMessage(HttpMethod.Post, $"{_apiUrl}/v1/account/vehicles/{vin}/commands/connect");
req.Content = new StringContent("{}", Encoding.UTF8, "application/json");
int attempt = 0;
while (attempt < RetryCount)
{
attempt++;
HttpResponseMessage resp = null;
try
{
resp = await _client.SendAsync(request);
}
catch (Exception ex)
{
//todo: only catch transient errors
//todo: log this
continue;
}
var response = await _client.SendAsync(req);
if (!resp.IsSuccessStatusCode)
{
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized || resp.StatusCode == System.Net.HttpStatusCode.Forbidden)
{
var result = await RefreshToken();
if (!result)
{
throw new InvalidOperationException("Token refresh failed");
}
continue;
}
else if (resp.StatusCode == System.Net.HttpStatusCode.BadGateway || resp.StatusCode == System.Net.HttpStatusCode.Conflict || resp.StatusCode == System.Net.HttpStatusCode.GatewayTimeout || resp.StatusCode == System.Net.HttpStatusCode.InternalServerError || resp.StatusCode == System.Net.HttpStatusCode.RequestTimeout || resp.StatusCode == System.Net.HttpStatusCode.ResetContent || resp.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
{
//possible transient errors
//todo: log this
await Task.Delay(500);
continue;
}
else
{
var respMessage = (await resp.Content.ReadAsStringAsync())??"";
throw new InvalidOperationException("Request error. StatusCode: " + resp.StatusCode.ToString() + ", msg: " + respMessage);
}
}
else
{
return resp;
}
}
//todo: include more info
throw new InvalidOperationException("Request failed too many times");
}
async Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content, bool noAuth = false)
{
return await SendAsync(new HttpRequestMessage(HttpMethod.Post, requestUri) { Content = content }, noAuth);
}
async Task<HttpResponseMessage> GetAsync(string requestUri, bool noAuth = false)
{
return await SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), noAuth);
}
async Task<Commandresponse> VehicleConnect(string vin)
{
var response = await PostAsync($"{_apiUrl}/v1/account/vehicles/{vin}/commands/connect", new StringContent("{}", Encoding.UTF8, "application/json"));
if (response.IsSuccessStatusCode)
{
@@ -106,22 +181,19 @@ namespace GM.Api
async Task<bool> UpgradeToken(string pin)
{
var payload = new UpgradeTokenPayload()
var payload = new LoginRequest()
{
client_id = _clientId,
device_id = _deviceId,
credential = pin,
credential_type = "PIN",
nonce = helpers.GenerateNonce(),
timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK")
ClientId = _clientId,
DeviceId = _deviceId,
Credential = pin,
CredentialType = "PIN",
Nonce = helpers.GenerateNonce(),
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK")
};
var token = _jwtTool.EncodeToken(payload);
var req = new HttpRequestMessage(HttpMethod.Post, $"{_apiUrl}/v1/oauth/token/upgrade");
req.Content = new StringContent(token, Encoding.UTF8, "text/plain");
var response = await _client.SendAsync(req);
var response = await PostAsync($"{_apiUrl}/v1/oauth/token/upgrade", new StringContent(token, Encoding.UTF8, "text/plain"));
if (response.IsSuccessStatusCode)
{
@@ -138,26 +210,21 @@ namespace GM.Api
public async Task<bool> Login(string username, string password)
{
var payload = new LoginPayload()
var payload = new LoginRequest()
{
client_id = _clientId,
device_id = _deviceId,
grant_type = "password",
nonce = helpers.GenerateNonce(),
password = password,
scope = "onstar gmoc commerce user_trailer msso",
timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK"),
username = username
ClientId = _clientId,
DeviceId = _deviceId,
GrantType = "password",
Nonce = helpers.GenerateNonce(),
Password = password,
Scope = "onstar gmoc commerce user_trailer msso",
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK"),
Username = username
};
var token = _jwtTool.EncodeToken(payload);
var req = new HttpRequestMessage(HttpMethod.Post, $"{_apiUrl}/v1/oauth/token");
req.Headers.Authorization = null;
req.Content = new StringContent(token, Encoding.UTF8, "text/plain");
var response = await _client.SendAsync(req);
var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true);
string rawResponseToken = null;
@@ -179,7 +246,7 @@ namespace GM.Api
var loginTokenData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
LoginData = loginTokenData;
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.access_token);
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.AccessToken);
//todo: should this be a copy rather than a reference?
await TokenUpdateCallback?.Invoke(LoginData);
@@ -190,26 +257,20 @@ namespace GM.Api
{
if (LoginData == null) return false;
var payload = new RefreshTokenPayload()
var payload = new LoginRequest()
{
client_id = _clientId,
device_id = _deviceId,
grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer",
nonce = helpers.GenerateNonce(),
scope = "onstar gmoc commerce user_trailer",
timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK"),
assertion = LoginData.id_token
ClientId = _clientId,
DeviceId = _deviceId,
GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer",
Nonce = helpers.GenerateNonce(),
Scope = "onstar gmoc commerce user_trailer",
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK"),
Assertion = LoginData.IdToken
};
var token = _jwtTool.EncodeToken(payload);
var req = new HttpRequestMessage(HttpMethod.Post, $"{_apiUrl}/v1/oauth/token");
req.Headers.Authorization = null;
req.Content = new StringContent(token, Encoding.UTF8, "text/plain");
var response = await _client.SendAsync(req);
var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true);
string rawResponseToken = null;
@@ -241,14 +302,14 @@ namespace GM.Api
var refreshData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
LoginData.access_token = refreshData.access_token;
LoginData.IssuedUtc = refreshData.IssuedUtc;
LoginData.expires_in = refreshData.expires_in;
LoginData.AccessToken = refreshData.AccessToken;
LoginData.IssuedAtUtc = refreshData.IssuedAtUtc;
LoginData.ExpiresIn = refreshData.ExpiresIn;
//should we assume the upgrade status is broken?
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.access_token);
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.AccessToken);
//todo: should this be a copy rather than a reference?
await TokenUpdateCallback?.Invoke(LoginData);
@@ -265,15 +326,6 @@ namespace GM.Api
public async Task<Commandresponse> InitiateCommand(string vin, string pin, string command)
{
if (LoginData == null) throw new InvalidOperationException("Login required");
if (LoginData.IsExpired)
{
if (!await RefreshToken())
{
throw new InvalidOperationException("Token refresh failed");
}
}
if (!_isConnected)
{
await VehicleConnect(vin);
@@ -332,15 +384,9 @@ namespace GM.Api
reqObj = new JObject();
}
var response = await PostAsync($"{_apiUrl}/v1/account/vehicles/{vin}/commands/{command}", new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(reqObj), Encoding.UTF8, "application/json"));
var req = new HttpRequestMessage(HttpMethod.Post, $"{_apiUrl}/v1/account/vehicles/{vin}/commands/{command}");
req.Content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(reqObj), Encoding.UTF8, "application/json");
var response = await _client.SendAsync(req);
if (!response.IsSuccessStatusCode)
{
var error = await response.Content.ReadAsStringAsync();
@@ -355,14 +401,6 @@ namespace GM.Api
public async Task<Commandresponse> WaitForCommandCompletion(string statusUrl)
{
if (LoginData == null) throw new InvalidOperationException("Login required");
if (LoginData.IsExpired)
{
if (!await RefreshToken())
{
throw new InvalidOperationException("Token refresh failed");
}
}
int nullResponseCount = 0;
while (true)
@@ -404,9 +442,7 @@ namespace GM.Api
async Task<Commandresponse> PollCommandStatus(string statusUrl)
{
var req = new HttpRequestMessage(HttpMethod.Get, $"{statusUrl}?units=METRIC");
var response = await _client.SendAsync(req);
var response = await GetAsync($"{statusUrl}?units=METRIC");
if (response.IsSuccessStatusCode)
{
@@ -422,17 +458,8 @@ namespace GM.Api
public async Task<IEnumerable<Vehicle>> GetVehicles()
{
if (LoginData == null) throw new InvalidOperationException("Login required");
if (LoginData.IsExpired)
{
if (!await RefreshToken())
{
throw new InvalidOperationException("Token refresh failed");
}
}
//these could be parameterized, but we better stick with what the app does
var resp = await _client.GetAsync($"{_apiUrl}/v1/account/vehicles?offset=0&limit=10&includeCommands=true&includeEntitlements=true&includeModules=true");
var resp = await GetAsync($"{_apiUrl}/v1/account/vehicles?offset=0&limit=10&includeCommands=true&includeEntitlements=true&includeModules=true");
if (resp.IsSuccessStatusCode)
{
@@ -450,15 +477,6 @@ namespace GM.Api
public async Task<Diagnosticresponse[]> GetDiagnostics(string vin, string pin)
{
if (LoginData == null) throw new InvalidOperationException("Login required");
if (LoginData.IsExpired)
{
if (!await RefreshToken())
{
throw new InvalidOperationException("Token refresh failed");
}
}
var result = await InitiateCommandAndWait(vin, pin, "diagnostics");
if (result == null) return null;
if ("success".Equals(result.status, StringComparison.OrdinalIgnoreCase))

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Text;
namespace GM.Api
namespace GM.Api.Models
{
public class CommandResponseRoot
{

View File

@@ -3,7 +3,7 @@ using System;
using System.Collections.Generic;
using System.Text;
namespace GM.Api
namespace GM.Api.Models
{
public class GmConfiguration

166
GM.Api/Models/Diagnostic.cs Normal file
View File

@@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
namespace GM.Api.Models
{
//public class DiagnosticReader
//{
// IEnumerable<Diagnosticresponse> _dr;
// public DiagnosticReader(IEnumerable<Diagnosticresponse> elements)
// {
// _dr = elements;
// }
// public float AmbientAirTempCelcius => float.Parse((from f in _dr
// where f.name == "AMBIENT AIR TEMPERATURE"
// from r in f.diagnosticElement
// where r.name == "AMBIENT AIR TEMPERATURE"
// select r.value).FirstOrDefault());
// public string ChargerPowerLevel => (from f in _dr
// where f.name == "CHARGER POWER LEVEL"
// from r in f.diagnosticElement
// where r.name == "CHARGER POWER LEVEL"
// select r.value).FirstOrDefault();
// public float EvBatteryLevelPercent => float.Parse((from f in _dr
// where f.name == "EV BATTERY LEVEL"
// from r in f.diagnosticElement
// where r.name == "EV BATTERY LEVEL"
// select r.value).FirstOrDefault());
//}
//public static class DiagnosticHelper
//{
// public static float GetElectricEconomyKwh(this IEnumerable<Diagnosticresponse> elements)
// {
// var itm = float.Parse((from f in elements
// where f.name == "ENERGY EFFICIENCY"
// from r in f.diagnosticElement
// where r.name == "ELECTRIC ECONOMY"
// select r.value).FirstOrDefault());
// return itm;
// }
// public static float GetLifetimeEfficiencyKwh(this IEnumerable<Diagnosticresponse> elements)
// {
// var itm = float.Parse((from f in elements
// where f.name == "ENERGY EFFICIENCY"
// from r in f.diagnosticElement
// where r.name == "LIFETIME EFFICIENCY"
// select r.value).FirstOrDefault());
// return itm;
// }
// public static float GetLifetimeMpgE(this IEnumerable<Diagnosticresponse> elements)
// {
// var itm = float.Parse((from f in elements
// where f.name == "ENERGY EFFICIENCY"
// from r in f.diagnosticElement
// where r.name == "LIFETIME MPGE"
// select r.value).FirstOrDefault());
// return itm;
// }
// public static float GetOdometerKm(this IEnumerable<Diagnosticresponse> elements)
// {
// var itm = float.Parse((from f in elements
// where f.name == "ENERGY EFFICIENCY"
// from r in f.diagnosticElement
// where r.name == "ODOMETER"
// select r.value).FirstOrDefault());
// return itm;
// }
// public static float GetEvBatteryLevelPercent(this IEnumerable<Diagnosticresponse> elements)
// {
// var itm = float.Parse((from f in elements
// where f.name == "EV BATTERY LEVEL"
// from r in f.diagnosticElement
// where r.name == "EV BATTERY LEVEL"
// select r.value).FirstOrDefault());
// return itm;
// }
//}
public class DiagnosticRequestRoot
{
public static readonly string[] DefaultItems = new string[]
{
"ENGINE COOLANT TEMP",
"ENGINE RPM",
"HV BATTERY ESTIMATED CAPACITY",
"LAST TRIP FUEL ECONOMY",
"ENERGY EFFICIENCY",
"HYBRID BATTERY MINIMUM TEMPERATURE",
"EV ESTIMATED CHARGE END",
"EV BATTERY LEVEL",
"EV PLUG VOLTAGE",
"ODOMETER",
"CHARGER POWER LEVEL",
"LIFETIME EV ODOMETER",
"EV PLUG STATE",
"EV CHARGE STATE",
"TIRE PRESSURE",
"AMBIENT AIR TEMPERATURE",
"LAST TRIP DISTANCE",
"INTERM VOLT BATT VOLT",
"GET COMMUTE SCHEDULE",
"GET CHARGE MODE",
"EV SCHEDULED CHARGE START",
"VEHICLE RANGE"
};
//public Diagnosticsrequest diagnosticsRequest { get; set; }
}
//public class Diagnosticsrequest
//{
// public string[] diagnosticItem { get; set; }
//}
public class ResponseBody
{
public Diagnosticresponse[] diagnosticResponse { get; set; }
}
public class Diagnosticresponse
{
public string name { get; set; }
public Diagnosticelement[] diagnosticElement { get; set; }
}
public class Diagnosticelement
{
public string name { get; set; }
public string status { get; set; }
public string message { get; set; }
public string value { get; set; }
public string unit { get; set; }
}
}

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Text;
namespace GM.Api
namespace GM.Api.Models
{

View File

@@ -1,101 +0,0 @@
using JWT;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace GM.Api
{
public class CustomJsonSerializer : IJsonSerializer
{
public string Serialize(object obj)
{
return JsonUtility.NormalizeJsonString(Newtonsoft.Json.JsonConvert.SerializeObject(obj));
}
public T Deserialize<T>(string json)
{
return JsonConvert.DeserializeObject<T>(json);
}
}
public class JsonUtility
{
public static string NormalizeJsonString(string json)
{
// Parse json string into JObject.
var parsedObject = JObject.Parse(json);
// Sort properties of JObject.
var normalizedObject = SortPropertiesAlphabetically(parsedObject);
// Serialize JObject .
return JsonConvert.SerializeObject(normalizedObject);
}
private static JObject SortPropertiesAlphabetically(JObject original)
{
var result = new JObject();
foreach (var property in original.Properties().ToList().OrderBy(p => p.Name))
{
var value = property.Value as JObject;
if (value != null)
{
value = SortPropertiesAlphabetically(value);
result.Add(property.Name, value);
}
else
{
result.Add(property.Name, property.Value);
}
}
return result;
}
}
public class LoginPayload
{
public string client_id { get; set; } //from config
public string device_id { get; set; } //random guid
public string grant_type { get; set; } //"password"
public string nonce { get; set; } //return new BigInteger(130, new SecureRandom()).toString(32);
public string password { get; set; }
public string scope { get; set; } // onstar gmoc commerce user_trailer msso
public string timestamp { get; set; } //ISO_8601_DATE_FORMAT.format(new date());
public string username { get; set; }
}
public class UpgradeTokenPayload
{
public string client_id { get; set; }
public string credential { get; set; }
public string credential_type { get; set; }
public string device_id { get; set; }
public string nonce { get; set; }
public string timestamp { get; set; }
}
public class RefreshTokenPayload
{
public string assertion { get; set; }
public string client_id { get; set; }
public string device_id { get; set; }
public string grant_type { get; set; }
public string nonce { get; set; }
public string scope { get; set; }
public string timestamp { get; set; }
}
}

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Text;
namespace GM.Api
namespace GM.Api.Tokens
{
/// <summary>
/// Base32 Implementation

View File

@@ -1,10 +1,12 @@
using JWT;
using JWT.Algorithms;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Text;
namespace GM.Api
namespace GM.Api.Tokens
{
class JwtTool
{
@@ -19,7 +21,7 @@ namespace GM.Api
_key = Encoding.ASCII.GetBytes(key);
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
IJsonSerializer serializer = new CustomJsonSerializer();
IJsonSerializer serializer = new SortedJsonSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
Encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
@@ -44,12 +46,10 @@ namespace GM.Api
{
return Decoder.DecodeToObject<T>(token);
}
}
}

View File

@@ -0,0 +1,73 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace GM.Api.Tokens
{
/// <summary>
/// Data contained within the login response JWT or as updated when refreshed
/// </summary>
public class LoginData
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
[JsonProperty("scope")]
public string Scope { get; set; }
[JsonProperty("onstar_account_info")]
public Onstar_Account_Info OnStarAccountInfo { get; set; }
[JsonProperty("user_info")]
public User_Info UserInfo { get; set; }
[JsonProperty("id_token")]
public string IdToken { get; set; }
/// <summary>
/// Timestamp the token was recieved
/// </summary>
[JsonIgnore]
public DateTime IssuedAtUtc { get; set; } = DateTime.UtcNow;
/// <summary>
/// Approximate timestamp the token expires
/// </summary>
[JsonIgnore]
public DateTime ExpiresAtUtc => (IssuedAtUtc + TimeSpan.FromSeconds(ExpiresIn - 2));
/// <summary>
/// Check if the token is expired based on timestamp
/// </summary>
[JsonIgnore]
public bool IsExpired => (DateTime.UtcNow >= ExpiresAtUtc);
}
public class Onstar_Account_Info
{
[JsonProperty("country_code")]
public string CountryCode { get; set; }
[JsonProperty("account_no")]
public string AccountNo { get; set; }
}
public class User_Info
{
[JsonProperty("RemoteUserId")]
public string RemoteUserId { get; set; }
[JsonProperty("country")]
public string Country { get; set; }
}
}

View File

@@ -0,0 +1,72 @@
using JWT;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace GM.Api.Tokens
{
public class LoginRequest
{
[JsonProperty("client_id", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string ClientId { get; set; }
[JsonProperty("device_id", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string DeviceId { get; set; }
[JsonProperty("grant_type", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string GrantType { get; set; }
[JsonProperty("nonce", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Nonce { get; set; }
[JsonProperty("password", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Password { get; set; }
/// <summary>
/// Scope
/// ex: onstar gmoc commerce user_trailer msso
/// </summary>
[JsonProperty("scope", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Scope { get; set; }
/// <summary>
/// Current timestamp in UTC using "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK" format string
/// </summary>
[JsonProperty("timestamp", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Timestamp { get; set; }
[JsonProperty("username", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Username { get; set; }
/// <summary>
/// OnStar PIN used to upgrade token
/// </summary>
[JsonProperty("credential", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Credential { get; set; }
/// <summary>
/// "PIN" for onstanr pin
/// </summary>
[JsonProperty("credential_type", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string CredentialType { get; set; }
/// <summary>
/// IdToken from login payload - used for refreshing
/// </summary>
[JsonProperty("assertion", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string Assertion { get; set; }
}
}

View File

@@ -0,0 +1,65 @@
using JWT;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace GM.Api.Tokens
{
/// <summary>
/// Custom JSON serialized used with JWT.net
/// JWT.net's JWT header is not alphebetized by default...
/// </summary>
public class SortedJsonSerializer : IJsonSerializer
{
public string Serialize(object obj)
{
return NormalizeJsonString(Newtonsoft.Json.JsonConvert.SerializeObject(obj));
}
public T Deserialize<T>(string json)
{
return JsonConvert.DeserializeObject<T>(json);
}
public static string NormalizeJsonString(string json)
{
// Parse json string into JObject.
var parsedObject = JObject.Parse(json);
// Sort properties of JObject.
var normalizedObject = SortPropertiesAlphabetically(parsedObject);
// Serialize JObject .
return JsonConvert.SerializeObject(normalizedObject);
}
private static JObject SortPropertiesAlphabetically(JObject original)
{
var result = new JObject();
foreach (var property in original.Properties().ToList().OrderBy(p => p.Name))
{
var value = property.Value as JObject;
if (value != null)
{
value = SortPropertiesAlphabetically(value);
result.Add(property.Name, value);
}
else
{
result.Add(property.Name, property.Value);
}
}
return result;
}
}
}

View File

@@ -1,44 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace GM.Api
{
public class LoginData
{
public string access_token { get; set; }
public string token_type { get; set; }
public int expires_in { get; set; }
public string scope { get; set; }
public Onstar_Account_Info onstar_account_info { get; set; }
public User_Info user_info { get; set; }
public string id_token { get; set; }
[JsonIgnore]
public DateTime IssuedUtc { get; set; } = DateTime.UtcNow;
// subtracting 2 seconds for safety
[JsonIgnore]
public DateTime ExpiresAtUtc => (IssuedUtc + TimeSpan.FromSeconds(expires_in - 2));
public bool IsExpired => (DateTime.UtcNow >= ExpiresAtUtc);
}
public class Onstar_Account_Info
{
public string country_code { get; set; }
public string account_no { get; set; }
}
public class User_Info
{
public string RemoteUserId { get; set; }
public string country { get; set; }
}
}

View File

@@ -10,8 +10,6 @@ namespace GM.Api
{
static class helpers
{
public static string GenerateNonce()
{
//17.25 bytes = 130 bits
@@ -21,30 +19,16 @@ namespace GM.Api
var byteArray = new byte[17];
provider.GetBytes(byteArray);
var nonce = Base32.ToBase32String(byteArray);
var nonce = Tokens.Base32.ToBase32String(byteArray);
return nonce.ToLower().Substring(0, 26);
}
public static IJwtEncoder GetJwtEncoder()
{
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
IJsonSerializer serializer = new CustomJsonSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
return new JwtEncoder(algorithm, serializer, urlEncoder);
}
public static IJwtDecoder GetJwtDecoder()
{
IJsonSerializer serializer = new CustomJsonSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IDateTimeProvider dateTimeProvider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, dateTimeProvider);
return new JwtDecoder(serializer, validator, urlEncoder);
}
/// <summary>
/// Set an HTTP header to a single value, clearing any existing values
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="headerValue"></param>
/// <param name="value"></param>
public static void SetValue<T>(this HttpHeaderValueCollection<T> headerValue, string value) where T: class
{
headerValue.Clear();

View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using GM.Api.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
@@ -22,12 +23,12 @@ namespace GM.WindowsUI
public partial class BrandWindow : Window
{
GM.Api.GmConfiguration _config;
GmConfiguration _config;
public string SelectedBrand { get; set; } = null;
public BrandWindow(GM.Api.GmConfiguration configuration)
public BrandWindow(GmConfiguration configuration)
{
_config = configuration;
InitializeComponent();

View File

@@ -1,4 +1,6 @@
using GM.Api;
using GM.Api.Models;
using GM.Api.Tokens;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
@@ -26,10 +28,10 @@ namespace GM.WindowsUI
GMClient _client;
GM.Api.GmConfiguration _globalConfig;
GmConfiguration _globalConfig;
GM.Api.ApiConfig _apiConfig;
GM.Api.BrandClientInfo _clientCredentials;
ApiConfig _apiConfig;
BrandClientInfo _clientCredentials;
string _brand;
string _brandDisplay;
@@ -128,7 +130,7 @@ namespace GM.WindowsUI
try
{
_globalConfig = JsonConvert.DeserializeObject<GM.Api.GmConfiguration>(File.ReadAllText("Config\\configuration.json", Encoding.UTF8));
_globalConfig = JsonConvert.DeserializeObject<GmConfiguration>(File.ReadAllText("Config\\configuration.json", Encoding.UTF8));
}
catch (Exception ex)
{

View File

@@ -28,3 +28,5 @@ TODO: determine how app elevates creds when using fingerprint - does the app sav
TODO: there is a means of refreshing a token using a pin...
TODO: determine how long elevation lasts, keep track and re-elevate when required
TODO: consider using MS JWT implementation