Completed renaming of JSON model properties and added some helpful code docs. Fixed a bug in the UI

This commit is contained in:
Anonymous
2019-08-11 17:48:54 -04:00
parent db4e4f6a83
commit 19191fc52d
9 changed files with 341 additions and 84 deletions

View File

@@ -191,7 +191,7 @@ namespace GM.Api
/// </summary>
/// <param name="vin"></param>
/// <returns></returns>
async Task<Commandresponse> VehicleConnect()
async Task<CommandResponse> VehicleConnect()
{
if (ActiveVehicle == null) throw new InvalidOperationException("ActiveVehicle must be populated");
using (var response = await PostAsync(ActiveVehicle.GetCommand("connect").Url, new StringContent("{}", Encoding.UTF8, "application/json")))
@@ -199,9 +199,9 @@ namespace GM.Api
if (response.IsSuccessStatusCode)
{
var respString = await response.Content.ReadAsStringAsync();
var respObj = JsonConvert.DeserializeObject<CommandResponseRoot>(respString);
if (respObj == null || respObj.commandResponse == null) return null;
return respObj.commandResponse;
var respObj = JsonConvert.DeserializeObject<CommandRequestResponse>(respString);
if (respObj == null || respObj.CommandResponse == null) return null;
return respObj.CommandResponse;
}
else
{
@@ -384,7 +384,7 @@ namespace GM.Api
/// <param name="pin">OnStar PIN</param>
/// <param name="command">command name</param>
/// <returns></returns>
async Task<Commandresponse> InitiateCommand(string command, JObject requestParameters)
async Task<CommandResponse> InitiateCommand(string command, JObject requestParameters)
{
if (ActiveVehicle == null) throw new InvalidOperationException("ActiveVehicle must be populated");
@@ -427,9 +427,9 @@ namespace GM.Api
return null;
}
var commandResult = await response.Content.ReadAsAsync<CommandResponseRoot>();
var commandResult = await response.Content.ReadAsAsync<CommandRequestResponse>();
return commandResult.commandResponse;
return commandResult.CommandResponse;
}
}
@@ -439,7 +439,7 @@ namespace GM.Api
/// </summary>
/// <param name="statusUrl">statusUrl returned when the command was initiated</param>
/// <returns>Response from final poll</returns>
async Task<Commandresponse> WaitForCommandCompletion(string statusUrl)
async Task<CommandResponse> WaitForCommandCompletion(string statusUrl)
{
int nullResponseCount = 0;
@@ -452,16 +452,16 @@ namespace GM.Api
nullResponseCount++;
if (nullResponseCount > 5) return null;
}
if ("inProgress".Equals(result.status, StringComparison.OrdinalIgnoreCase)) continue;
if ("inProgress".Equals(result.Status, StringComparison.OrdinalIgnoreCase)) continue;
return result;
}
}
protected async Task<Commandresponse> InitiateCommandAndWait(string command, JObject requestParameters)
protected async Task<CommandResponse> InitiateCommandAndWait(string command, JObject requestParameters)
{
var result = await InitiateCommand(command, requestParameters);
var endStatus = await WaitForCommandCompletion(result.url);
var endStatus = await WaitForCommandCompletion(result.Url);
return endStatus;
}
@@ -469,7 +469,7 @@ namespace GM.Api
{
var result = await InitiateCommandAndWait(command, requestParameters);
if (result == null) return false;
if ("success".Equals(result.status, StringComparison.OrdinalIgnoreCase))
if ("success".Equals(result.Status, StringComparison.OrdinalIgnoreCase))
{
return true;
}
@@ -480,14 +480,14 @@ namespace GM.Api
}
async Task<Commandresponse> PollCommandStatus(string statusUrl)
async Task<CommandResponse> PollCommandStatus(string statusUrl)
{
var response = await GetAsync($"{statusUrl}?units=METRIC");
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadAsAsync<CommandResponseRoot>();
return result.commandResponse;
var result = await response.Content.ReadAsAsync<CommandRequestResponse>();
return result.CommandResponse;
}
else
{

View File

@@ -17,7 +17,7 @@ namespace GM.Api
}
public async Task<Diagnosticresponse[]> GetDiagnostics()
public async Task<DiagnosticResponse[]> GetDiagnostics()
{
var cmdInfo = ActiveVehicle.GetCommand("diagnostics");
@@ -28,9 +28,9 @@ namespace GM.Api
var result = await InitiateCommandAndWait("diagnostics", reqObj);
if (result == null) return null;
if ("success".Equals(result.status, StringComparison.OrdinalIgnoreCase))
if ("success".Equals(result.Status, StringComparison.OrdinalIgnoreCase))
{
return result.body.diagnosticResponse;
return result.Body.DiagnosticResponse;
}
else
{
@@ -40,7 +40,7 @@ namespace GM.Api
public async Task<Commandresponse> IssueCommand(string commandName, JObject parameters = null)
public async Task<CommandResponse> IssueCommand(string commandName, JObject parameters = null)
{
return await InitiateCommandAndWait(commandName, parameters);
}

View File

@@ -1,23 +1,63 @@
using System;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace GM.Api.Models
{
public class CommandResponseRoot
/// <summary>
/// Root object returned by a command request, or a call to a status url
/// </summary>
public class CommandRequestResponse
{
public Commandresponse commandResponse { get; set; }
/// <summary>
/// Inner response
/// </summary>
[JsonProperty("commandResponse")]
public CommandResponse CommandResponse { get; set; }
}
public class Commandresponse
/// <summary>
/// Command Response Object
/// </summary>
public class CommandResponse
{
public DateTime requestTime { get; set; }
public DateTime completionTime { get; set; }
public string url { get; set; }
public string status { get; set; } //inProgress, success
public string type { get; set; }
public ResponseBody body { get; set; }
/// <summary>
/// Timestamp the request was received by the server
/// </summary>
[JsonProperty("requestTime")]
public DateTime RequestTime { get; set; }
/// <summary>
/// Timestamp the server completed the request
/// </summary>
[JsonProperty("completionTime")]
public DateTime CompletionTime { get; set; }
/// <summary>
/// Status URL to be polled for updates (commands are async)
/// </summary>
[JsonProperty("url")]
public string Url { get; set; }
/// <summary>
/// Current status of the command request
/// (e.g. "inProgress", "success")
/// </summary>
[JsonProperty("status")]
public string Status { get; set; } //inProgress, success
/// <summary>
/// Probably refers to the type of the response body
/// </summary>
[JsonProperty("type")]
public string Type { get; set; }
/// <summary>
/// Response boldy for commands that include a response (e.g. diagnostics, location)
/// </summary>
[JsonProperty("body")]
public ResponseBody Body { get; set; }
}

View File

@@ -5,55 +5,168 @@ using System.Text;
namespace GM.Api.Models
{
/// <summary>
/// Model of the encrypted Andorid configuration file
/// </summary>
public class GmConfiguration
{
public Dictionary<string, BrandClientInfo> brand_client_info { get; set; }
public ApiConfig[] configs { get; set; }
public Telenav_Config telenav_config { get; set; }
public string equip_key { get; set; }
public string key_store_password { get; set; }
public string key_password { get; set; }
public Dictionary<string, RegionCert> certs { get; set; }
/// <summary>
/// Client Credentials by GM Brand
/// </summary>
[JsonProperty("brand_client_info")]
public Dictionary<string, BrandClientInfo> BrandClientInfo { get; set; }
/// <summary>
/// Endpoint Configuration collection
/// </summary>
[JsonProperty("configs")]
public ApiConfig[] Configs { get; set; }
/// <summary>
/// Presumably configuration used for navigation
/// </summary>
[JsonProperty("telenav_config")]
public TelenavConfig TelenavConfig { get; set; }
/// <summary>
/// Unknown
/// </summary>
[JsonProperty("equip_key")]
public string EquipKey { get; set; }
/// <summary>
/// Probably the key used to encrypt the saved OnStar PINs
/// </summary>
[JsonProperty("key_store_password")]
public string KeyStorePassword { get; set; }
/// <summary>
/// Unknown
/// </summary>
[JsonProperty("key_password")]
public string KeyPassword { get; set; }
/// <summary>
/// Certificate pinning information used to prevent SSL spoofing
/// </summary>
[JsonProperty("certs")]
public Dictionary<string, RegionCert> Certs { get; set; }
}
/// <summary>
/// Client Credentials for a given GM brand
/// </summary>
public class BrandClientInfo
{
public string client_id { get; set; }
public string client_secret { get; set; }
public string debug_client_id { get; set; }
public string debug_client_secret { get; set; }
/// <summary>
/// OAuth Client ID
/// </summary>
[JsonProperty("client_id")]
public string ClientId { get; set; }
/// <summary>
/// OAuth Client Secret
/// </summary>
[JsonProperty("client_secret")]
public string ClientSecret { get; set; }
/// <summary>
/// Debug environment Oauth Client ID
/// </summary>
[JsonProperty("debug_client_id")]
public string DebugClientId { get; set; }
/// <summary>
/// Debug environment Oauth Client Secret
/// </summary>
[JsonProperty("debug_client_secret")]
public string DebugClientSecret { get; set; }
}
/// <summary>
/// API configuration for a given GM brand
/// </summary>
public class ApiConfig
{
public string name { get; set; }
public string url { get; set; }
public string required_client_scope { get; set; }
public string optional_client_scope { get; set; }
/// <summary>
/// do not use this
/// GM Brand name
/// </summary>
public string client_id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// do not use this
/// Base API endpoint URL (eg "https://api.gm.com/api")
/// </summary>
public string client_secret { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
/// <summary>
/// Space separated scopes required for login
/// </summary>
[JsonProperty("required_client_scope")]
public string RequiredClientScope { get; set; }
/// <summary>
/// Space separated scopes optional for login
/// </summary>
[JsonProperty("optional_client_scope")]
public string OptionalClientScope { get; set; }
/// <summary>
/// Use the Brand config Client ID instead
/// </summary>
[JsonProperty("client_id")]
public string ClientId { get; set; }
/// <summary>
/// Use the Brand config Client Secret instead
/// </summary>
[JsonProperty("client_secret")]
public string ClientSecret { get; set; }
}
public class Telenav_Config
/// <summary>
/// Client credentials for Telenav system
/// </summary>
public class TelenavConfig
{
public string name { get; set; }
public string client_id { get; set; }
public string client_secret { get; set; }
/// <summary>
/// Name
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// OAuth Client ID
/// </summary>
[JsonProperty("client_id")]
public string ClientId { get; set; }
/// <summary>
/// OAuth Client Secret
/// </summary>
[JsonProperty("client_secret")]
public string ClientSecret { get; set; }
}
/// <summary>
/// Container for certificate pinning info
/// </summary>
public class RegionCert
{
public string pattern { get; set; }
public string[] certificate_pins { get; set; }
/// <summary>
/// Pattern used by the expected certificate. Should match the CN I'm guessing
/// </summary>
[JsonProperty("pattern")]
public string Pattern { get; set; }
/// <summary>
/// A list of certificate pins. There are usually 3 and only one matches the actual SSL cert of the server
/// The other two do not match the intermediate / root certs - not sure what they are
/// </summary>
[JsonProperty("certificate_pins")]
public string[] CertificatePins { get; set; }
}
}

View File

@@ -2,27 +2,45 @@
using System.Collections.Generic;
using System.Text;
using System.Linq;
using Newtonsoft.Json;
namespace GM.Api.Models
{
/// <summary>
/// Response Body
/// Note: this only contains a diagnostic response. there are likely others.
/// </summary>
public class ResponseBody
{
public Diagnosticresponse[] diagnosticResponse { get; set; }
[JsonProperty("diagnosticResponse")]
public DiagnosticResponse[] DiagnosticResponse { get; set; }
}
public class Diagnosticresponse
public class DiagnosticResponse
{
public string name { get; set; }
public Diagnosticelement[] diagnosticElement { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("diagnosticElement")]
public DiagnosticElement[] DiagnosticElement { get; set; }
}
public class Diagnosticelement
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; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("value")]
public string Value { get; set; }
[JsonProperty("unit")]
public string Unit { get; set; }
}
}

View File

@@ -16,33 +16,64 @@ namespace GM.Api.Models
public class Vehicles
{
/// <summary>
/// Size of the Vehicle array, or full size. One would need to have more than 10 cars to find out...
/// </summary>
[JsonProperty("size")]
public string Size { get; set; }
/// <summary>
/// List of vehicles associated with the account
/// Note that there is paging and by default the page size is 10
/// </summary>
[JsonProperty("vehicle")]
public Vehicle[] Vehicle { get; set; }
}
/// <summary>
/// Vehicle description
/// </summary>
public class Vehicle
{
/// <summary>
/// Vehicle VIN
/// </summary>
[JsonProperty("vin")]
public string Vin { get; set; }
/// <summary>
/// Vehicle Make
/// </summary>
[JsonProperty("make")]
public string Make { get; set; }
/// <summary>
/// Vehicle Model
/// </summary>
[JsonProperty("model")]
public string Model { get; set; }
/// <summary>
/// Vehicle Year
/// </summary>
[JsonProperty("year")]
public string Year { get; set; }
/// <summary>
/// Vehicle Manufacturer - not sure why this is required...
/// </summary>
[JsonProperty("manufacturer")]
public string Manufacturer { get; set; }
/// <summary>
/// (e.g. car, maybe truck)
/// </summary>
[JsonProperty("bodyStyle")]
public string BodyStyle { get; set; }
/// <summary>
/// Vehicle cellular / OnStar phone number
/// </summary>
[JsonProperty("phone")]
public string Phone { get; set; }
@@ -52,6 +83,9 @@ namespace GM.Api.Models
[JsonProperty("onstarStatus")]
public string OnStarStatus { get; set; }
/// <summary>
/// Base URL for API calls regarding this vehicle
/// </summary>
[JsonProperty("url")]
public string Url { get; set; }
@@ -64,12 +98,21 @@ namespace GM.Api.Models
[JsonProperty("enrolledInContinuousCoverage")]
public bool? EnrolledInContinuousCoverage { get; set; }
/// <summary>
/// Details on supported commands
/// </summary>
[JsonProperty("commands")]
public Commands Commands { get; set; }
/// <summary>
/// Details on available modules
/// </summary>
[JsonProperty("modules")]
public Modules Modules { get; set; }
/// <summary>
/// Details on available entitlements
/// </summary>
[JsonProperty("entitlements")]
public Entitlements Entitlements { get; set; }
@@ -102,24 +145,46 @@ namespace GM.Api.Models
public class Commands
{
/// <summary>
/// List of commands supported by the vehicle
/// </summary>
[JsonProperty("command")]
public Command[] Command { get; set; }
}
/// <summary>
/// Details about a supported command
/// </summary>
public class Command
{
/// <summary>
/// Command name
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// Description of what the command does
/// </summary>
[JsonProperty("description")]
public string Description { get; set; }
/// <summary>
/// API URL to be used for issuing the command
/// This SDK uses this url rather than constructing it
/// </summary>
[JsonProperty("url")]
public string Url { get; set; }
/// <summary>
/// True or False if the command requires the token to be upgraded with an OnStar PIN
/// </summary>
[JsonProperty("isPrivSessionRequired")]
public bool? IsPrivSessionRequired { get; set; }
/// <summary>
/// For commands with additional data such as diagnostics
/// </summary>
[JsonProperty("commandData")]
public CommandData CommandData { get; set; }
}
@@ -132,12 +197,18 @@ namespace GM.Api.Models
public class SupportedDiagnostics
{
/// <summary>
/// List of the diagnostic elements that may be requsted for the vehicle
/// </summary>
[JsonProperty("supportedDiagnostic")]
public string[] SupportedDiagnostic { get; set; }
}
public class Modules
{
/// <summary>
/// List of modules - not much here
/// </summary>
[JsonProperty("module")]
public Module[] Module { get; set; }
}
@@ -153,23 +224,43 @@ namespace GM.Api.Models
public class Entitlements
{
/// <summary>
/// List of entitlements - features and activities vehicles are capable of
/// List contains things the vehicle or account may or may not support
/// Check the Elligible flag
/// </summary>
[JsonProperty("entitlement")]
public Entitlement[] Entitlement { get; set; }
}
/// <summary>
/// Details about an Entitlement
/// </summary>
public class Entitlement
{
/// <summary>
/// ID or name of entitlement
/// </summary>
[JsonProperty("id")]
public string Id { get; set; }
/// <summary>
/// True or false if the entitlement is available on this vehicle or account
/// </summary>
[JsonProperty("eligible")]
public bool? Eligible { get; set; }
/// <summary>
/// Reason for inelligibility (whether the car is incapable or the owner isn't subscribed)
/// </summary>
[JsonProperty("ineligibleReasonCode")]
public string IneligibleReasonCode { get; set; }
/// <summary>
/// True or false if the entitlement can send notifications
/// </summary>
[JsonProperty("notificationCapable")]
public string NotificationCapable { get; set; }
public bool? NotificationCapable { get; set; }
}

View File

@@ -33,7 +33,7 @@ namespace GM.WindowsUI
_config = configuration;
InitializeComponent();
foreach (var brandName in _config.brand_client_info.Keys.OrderBy((val) => val, StringComparer.OrdinalIgnoreCase))
foreach (var brandName in _config.BrandClientInfo.Keys.OrderBy((val) => val, StringComparer.OrdinalIgnoreCase))
{
lstBrands.Items.Add(brandName.Substring(0, 1).ToUpperInvariant() + brandName.Substring(1));
}

View File

@@ -60,7 +60,7 @@ namespace GM.WindowsUI
}
//todo: maybe the client reads the config and takes the brand and device id as param?
_client = new GenericGMClient(_clientCredentials.client_id, Properties.Settings.Default.DeviceId, _clientCredentials.client_secret, _apiConfig.url);
_client = new GenericGMClient(_clientCredentials.ClientId, Properties.Settings.Default.DeviceId, _clientCredentials.ClientSecret, _apiConfig.Url);
_client.TokenUpdateCallback = TokenUpdateHandler;
if (!string.IsNullOrEmpty(Properties.Settings.Default.LoginData))
@@ -119,8 +119,8 @@ namespace GM.WindowsUI
Title = _brandDisplay + " Vehicle Control";
_clientCredentials = _globalConfig.brand_client_info[_brand];
_apiConfig = (from f in _globalConfig.configs where f.name.Equals(_brand, StringComparison.OrdinalIgnoreCase) select f).FirstOrDefault();
_clientCredentials = _globalConfig.BrandClientInfo[_brand];
_apiConfig = (from f in _globalConfig.Configs where f.Name.Equals(_brand, StringComparison.OrdinalIgnoreCase) select f).FirstOrDefault();
}
void LoadConfiguration()
@@ -393,7 +393,7 @@ namespace GM.WindowsUI
lblStatus.Content = "Getting Diagnostics (Please Wait)...";
var details = await _client.GetDiagnostics();
txtOutput.Text = JsonConvert.SerializeObject(details, Formatting.Indented);
lblStatus.Content = "Getting Diagnostics Complete";
grpActions.IsEnabled = true;
btnLogin.IsEnabled = true;
}

View File

@@ -25,16 +25,11 @@ VERY IMPORTANT: Unless you want an international incident on your hands DO NOT S
# TODO
This is very early, unpolished, incomplete code. No judgement please.
* Implement more commands
* Analyze and implement vehicle location capability
* consider using MS JWT implementation
* Implement secure means of saving onstar pin. If possible.
* recognize response from calling priv'd command without upgrade and trigger upgrade using saved pin.
Notes: The android app saves the onstar pin using biometrics to unlock - no difference in the api calls. It does not use a different token refresh mechanism after elevating permissions, but the elevation persists across a refresh. The upgrade request does not specify an expiration. Testing will be required to determine the lifespan of token upgrades.
TODO: implement lots more actions
TODO: Complete updating JSON model property names
Note: the android app saves the onstar pin using biometrics to unlock - no difference in the api calls
Note: the android app does not use a different token refresh mechanism after elevating permissions, but the elevation persists across a refresh. The upgrade request does not specify an expiration. Testing will be required to determine the lifespan of token upgrades.
TODO: Implement secure means of saving onstar pin. If possible.
TODO: recognize response from calling priv'd command without upgrade and trigger upgrade using saved pin.
TODO: consider using MS JWT implementation