From e9ffa5c13bc4096d5489701f96e13ada1a338c98 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Fri, 9 Aug 2019 19:17:26 -0400 Subject: [PATCH] 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 --- GM.Api/Diagnostic.cs | 166 ------------------ GM.Api/GMClient.cs | 226 +++++++++++++------------ GM.Api/{ => Models}/CommandResponse.cs | 2 +- GM.Api/{ => Models}/Configuration.cs | 2 +- GM.Api/Models/Diagnostic.cs | 166 ++++++++++++++++++ GM.Api/{ => Models}/Vehicle.cs | 2 +- GM.Api/Token.cs | 101 ----------- GM.Api/{ => Tokens}/Base32.cs | 2 +- GM.Api/{ => Tokens}/JwtTool.cs | 18 +- GM.Api/Tokens/LoginData.cs | 73 ++++++++ GM.Api/Tokens/LoginRequest.cs | 72 ++++++++ GM.Api/Tokens/SortedJsonSerializer.cs | 65 +++++++ GM.Api/firstLoginToken.cs | 44 ----- GM.Api/helpers.cs | 30 +--- GM.WindowsUI/BrandWindow.xaml.cs | 7 +- GM.WindowsUI/MainWindow.xaml.cs | 10 +- README.md | 2 + 17 files changed, 530 insertions(+), 458 deletions(-) delete mode 100644 GM.Api/Diagnostic.cs rename GM.Api/{ => Models}/CommandResponse.cs (95%) rename GM.Api/{ => Models}/Configuration.cs (98%) create mode 100644 GM.Api/Models/Diagnostic.cs rename GM.Api/{ => Models}/Vehicle.cs (99%) delete mode 100644 GM.Api/Token.cs rename GM.Api/{ => Tokens}/Base32.cs (99%) rename GM.Api/{ => Tokens}/JwtTool.cs (89%) create mode 100644 GM.Api/Tokens/LoginData.cs create mode 100644 GM.Api/Tokens/LoginRequest.cs create mode 100644 GM.Api/Tokens/SortedJsonSerializer.cs delete mode 100644 GM.Api/firstLoginToken.cs diff --git a/GM.Api/Diagnostic.cs b/GM.Api/Diagnostic.cs deleted file mode 100644 index 75ea320..0000000 --- a/GM.Api/Diagnostic.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Linq; - -namespace GM.Api -{ - - - - public class DiagnosticReader - { - - IEnumerable _dr; - - public DiagnosticReader(IEnumerable 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 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 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 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 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 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; } - } - -} diff --git a/GM.Api/GMClient.cs b/GM.Api/GMClient.cs index 3991d93..4308a28 100644 --- a/GM.Api/GMClient.cs +++ b/GM.Api/GMClient.cs @@ -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 VehicleConnect(string vin) + async Task 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 PostAsync(string requestUri, HttpContent content, bool noAuth = false) + { + return await SendAsync(new HttpRequestMessage(HttpMethod.Post, requestUri) { Content = content }, noAuth); + } + + async Task GetAsync(string requestUri, bool noAuth = false) + { + return await SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), noAuth); + } + + + + + async Task 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 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 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(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(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 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 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 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> 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 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)) diff --git a/GM.Api/CommandResponse.cs b/GM.Api/Models/CommandResponse.cs similarity index 95% rename from GM.Api/CommandResponse.cs rename to GM.Api/Models/CommandResponse.cs index ab7d3fd..e9f54b2 100644 --- a/GM.Api/CommandResponse.cs +++ b/GM.Api/Models/CommandResponse.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace GM.Api +namespace GM.Api.Models { public class CommandResponseRoot { diff --git a/GM.Api/Configuration.cs b/GM.Api/Models/Configuration.cs similarity index 98% rename from GM.Api/Configuration.cs rename to GM.Api/Models/Configuration.cs index afc902f..20cfb33 100644 --- a/GM.Api/Configuration.cs +++ b/GM.Api/Models/Configuration.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Text; -namespace GM.Api +namespace GM.Api.Models { public class GmConfiguration diff --git a/GM.Api/Models/Diagnostic.cs b/GM.Api/Models/Diagnostic.cs new file mode 100644 index 0000000..b9b46e8 --- /dev/null +++ b/GM.Api/Models/Diagnostic.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; + +namespace GM.Api.Models +{ + + + + //public class DiagnosticReader + //{ + + // IEnumerable _dr; + + // public DiagnosticReader(IEnumerable 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 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 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 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 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 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; } + } + +} diff --git a/GM.Api/Vehicle.cs b/GM.Api/Models/Vehicle.cs similarity index 99% rename from GM.Api/Vehicle.cs rename to GM.Api/Models/Vehicle.cs index 338c850..9052c81 100644 --- a/GM.Api/Vehicle.cs +++ b/GM.Api/Models/Vehicle.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace GM.Api +namespace GM.Api.Models { diff --git a/GM.Api/Token.cs b/GM.Api/Token.cs deleted file mode 100644 index f534655..0000000 --- a/GM.Api/Token.cs +++ /dev/null @@ -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(string json) - { - return JsonConvert.DeserializeObject(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; } - } - - -} diff --git a/GM.Api/Base32.cs b/GM.Api/Tokens/Base32.cs similarity index 99% rename from GM.Api/Base32.cs rename to GM.Api/Tokens/Base32.cs index a2c775b..424760a 100644 --- a/GM.Api/Base32.cs +++ b/GM.Api/Tokens/Base32.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace GM.Api +namespace GM.Api.Tokens { /// /// Base32 Implementation diff --git a/GM.Api/JwtTool.cs b/GM.Api/Tokens/JwtTool.cs similarity index 89% rename from GM.Api/JwtTool.cs rename to GM.Api/Tokens/JwtTool.cs index 2872c8d..cfdf856 100644 --- a/GM.Api/JwtTool.cs +++ b/GM.Api/Tokens/JwtTool.cs @@ -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(token); } - - - - - - - } + + + + + } diff --git a/GM.Api/Tokens/LoginData.cs b/GM.Api/Tokens/LoginData.cs new file mode 100644 index 0000000..1dfd11b --- /dev/null +++ b/GM.Api/Tokens/LoginData.cs @@ -0,0 +1,73 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace GM.Api.Tokens +{ + /// + /// Data contained within the login response JWT or as updated when refreshed + /// + 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; } + + /// + /// Timestamp the token was recieved + /// + [JsonIgnore] + public DateTime IssuedAtUtc { get; set; } = DateTime.UtcNow; + + /// + /// Approximate timestamp the token expires + /// + [JsonIgnore] + public DateTime ExpiresAtUtc => (IssuedAtUtc + TimeSpan.FromSeconds(ExpiresIn - 2)); + + /// + /// Check if the token is expired based on timestamp + /// + [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; } + } + +} diff --git a/GM.Api/Tokens/LoginRequest.cs b/GM.Api/Tokens/LoginRequest.cs new file mode 100644 index 0000000..27b8b3f --- /dev/null +++ b/GM.Api/Tokens/LoginRequest.cs @@ -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; } + + /// + /// Scope + /// ex: onstar gmoc commerce user_trailer msso + /// + [JsonProperty("scope", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Scope { get; set; } + + /// + /// Current timestamp in UTC using "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK" format string + /// + [JsonProperty("timestamp", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Timestamp { get; set; } + + [JsonProperty("username", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Username { get; set; } + + + /// + /// OnStar PIN used to upgrade token + /// + [JsonProperty("credential", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Credential { get; set; } + + /// + /// "PIN" for onstanr pin + /// + [JsonProperty("credential_type", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string CredentialType { get; set; } + + /// + /// IdToken from login payload - used for refreshing + /// + [JsonProperty("assertion", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Assertion { get; set; } + + + } + + + + + +} diff --git a/GM.Api/Tokens/SortedJsonSerializer.cs b/GM.Api/Tokens/SortedJsonSerializer.cs new file mode 100644 index 0000000..b4f7702 --- /dev/null +++ b/GM.Api/Tokens/SortedJsonSerializer.cs @@ -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 +{ + /// + /// Custom JSON serialized used with JWT.net + /// JWT.net's JWT header is not alphebetized by default... + /// + public class SortedJsonSerializer : IJsonSerializer + { + public string Serialize(object obj) + { + return NormalizeJsonString(Newtonsoft.Json.JsonConvert.SerializeObject(obj)); + } + + public T Deserialize(string json) + { + return JsonConvert.DeserializeObject(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; + } + + + } + +} diff --git a/GM.Api/firstLoginToken.cs b/GM.Api/firstLoginToken.cs deleted file mode 100644 index 1d58ac8..0000000 --- a/GM.Api/firstLoginToken.cs +++ /dev/null @@ -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; } - } - -} diff --git a/GM.Api/helpers.cs b/GM.Api/helpers.cs index 72aa55a..a2118da 100644 --- a/GM.Api/helpers.cs +++ b/GM.Api/helpers.cs @@ -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); - } - - - + /// + /// Set an HTTP header to a single value, clearing any existing values + /// + /// + /// + /// public static void SetValue(this HttpHeaderValueCollection headerValue, string value) where T: class { headerValue.Clear(); diff --git a/GM.WindowsUI/BrandWindow.xaml.cs b/GM.WindowsUI/BrandWindow.xaml.cs index 90492f7..2a54f78 100644 --- a/GM.WindowsUI/BrandWindow.xaml.cs +++ b/GM.WindowsUI/BrandWindow.xaml.cs @@ -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(); diff --git a/GM.WindowsUI/MainWindow.xaml.cs b/GM.WindowsUI/MainWindow.xaml.cs index cfdf37d..83bdc57 100644 --- a/GM.WindowsUI/MainWindow.xaml.cs +++ b/GM.WindowsUI/MainWindow.xaml.cs @@ -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(File.ReadAllText("Config\\configuration.json", Encoding.UTF8)); + _globalConfig = JsonConvert.DeserializeObject(File.ReadAllText("Config\\configuration.json", Encoding.UTF8)); } catch (Exception ex) { diff --git a/README.md b/README.md index 1702c83..02795ba 100644 --- a/README.md +++ b/README.md @@ -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