diff --git a/GM.Api/GMClientBase.cs b/GM.Api/GMClientBase.cs index a70e812..f9adfb4 100644 --- a/GM.Api/GMClientBase.cs +++ b/GM.Api/GMClientBase.cs @@ -191,7 +191,7 @@ namespace GM.Api /// /// /// - async Task VehicleConnect() + async Task 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(respString); - if (respObj == null || respObj.commandResponse == null) return null; - return respObj.commandResponse; + var respObj = JsonConvert.DeserializeObject(respString); + if (respObj == null || respObj.CommandResponse == null) return null; + return respObj.CommandResponse; } else { @@ -384,7 +384,7 @@ namespace GM.Api /// OnStar PIN /// command name /// - async Task InitiateCommand(string command, JObject requestParameters) + async Task 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(); + var commandResult = await response.Content.ReadAsAsync(); - return commandResult.commandResponse; + return commandResult.CommandResponse; } } @@ -439,7 +439,7 @@ namespace GM.Api /// /// statusUrl returned when the command was initiated /// Response from final poll - async Task WaitForCommandCompletion(string statusUrl) + async Task 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 InitiateCommandAndWait(string command, JObject requestParameters) + protected async Task 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 PollCommandStatus(string statusUrl) + async Task PollCommandStatus(string statusUrl) { var response = await GetAsync($"{statusUrl}?units=METRIC"); if (response.IsSuccessStatusCode) { - var result = await response.Content.ReadAsAsync(); - return result.commandResponse; + var result = await response.Content.ReadAsAsync(); + return result.CommandResponse; } else { diff --git a/GM.Api/GenericGMClient.cs b/GM.Api/GenericGMClient.cs index b189cc6..aa465b8 100644 --- a/GM.Api/GenericGMClient.cs +++ b/GM.Api/GenericGMClient.cs @@ -17,7 +17,7 @@ namespace GM.Api } - public async Task GetDiagnostics() + public async Task 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 IssueCommand(string commandName, JObject parameters = null) + public async Task IssueCommand(string commandName, JObject parameters = null) { return await InitiateCommandAndWait(commandName, parameters); } diff --git a/GM.Api/Models/CommandResponse.cs b/GM.Api/Models/CommandResponse.cs index e9f54b2..fe7f8e7 100644 --- a/GM.Api/Models/CommandResponse.cs +++ b/GM.Api/Models/CommandResponse.cs @@ -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 + /// + /// Root object returned by a command request, or a call to a status url + /// + public class CommandRequestResponse { - public Commandresponse commandResponse { get; set; } + /// + /// Inner response + /// + [JsonProperty("commandResponse")] + public CommandResponse CommandResponse { get; set; } } - - public class Commandresponse + /// + /// Command Response Object + /// + 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; } + /// + /// Timestamp the request was received by the server + /// + [JsonProperty("requestTime")] + public DateTime RequestTime { get; set; } + + /// + /// Timestamp the server completed the request + /// + [JsonProperty("completionTime")] + public DateTime CompletionTime { get; set; } + + /// + /// Status URL to be polled for updates (commands are async) + /// + [JsonProperty("url")] + public string Url { get; set; } + + /// + /// Current status of the command request + /// (e.g. "inProgress", "success") + /// + [JsonProperty("status")] + public string Status { get; set; } //inProgress, success + + /// + /// Probably refers to the type of the response body + /// + [JsonProperty("type")] + public string Type { get; set; } + + /// + /// Response boldy for commands that include a response (e.g. diagnostics, location) + /// + [JsonProperty("body")] + public ResponseBody Body { get; set; } } diff --git a/GM.Api/Models/Configuration.cs b/GM.Api/Models/Configuration.cs index 20cfb33..fe75ffb 100644 --- a/GM.Api/Models/Configuration.cs +++ b/GM.Api/Models/Configuration.cs @@ -5,55 +5,168 @@ using System.Text; namespace GM.Api.Models { + /// + /// Model of the encrypted Andorid configuration file + /// public class GmConfiguration { - public Dictionary 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 certs { get; set; } + /// + /// Client Credentials by GM Brand + /// + [JsonProperty("brand_client_info")] + public Dictionary BrandClientInfo { get; set; } + + /// + /// Endpoint Configuration collection + /// + [JsonProperty("configs")] + public ApiConfig[] Configs { get; set; } + + /// + /// Presumably configuration used for navigation + /// + [JsonProperty("telenav_config")] + public TelenavConfig TelenavConfig { get; set; } + + /// + /// Unknown + /// + [JsonProperty("equip_key")] + public string EquipKey { get; set; } + + /// + /// Probably the key used to encrypt the saved OnStar PINs + /// + [JsonProperty("key_store_password")] + public string KeyStorePassword { get; set; } + + /// + /// Unknown + /// + [JsonProperty("key_password")] + public string KeyPassword { get; set; } + + /// + /// Certificate pinning information used to prevent SSL spoofing + /// + [JsonProperty("certs")] + public Dictionary Certs { get; set; } } + /// + /// Client Credentials for a given GM brand + /// 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; } + /// + /// OAuth Client ID + /// + [JsonProperty("client_id")] + public string ClientId { get; set; } + + /// + /// OAuth Client Secret + /// + [JsonProperty("client_secret")] + public string ClientSecret { get; set; } + + /// + /// Debug environment Oauth Client ID + /// + [JsonProperty("debug_client_id")] + public string DebugClientId { get; set; } + + /// + /// Debug environment Oauth Client Secret + /// + [JsonProperty("debug_client_secret")] + public string DebugClientSecret { get; set; } } - + /// + /// API configuration for a given GM brand + /// 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; } /// - /// do not use this + /// GM Brand name /// - public string client_id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + /// - /// do not use this + /// Base API endpoint URL (eg "https://api.gm.com/api") /// - public string client_secret { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + + /// + /// Space separated scopes required for login + /// + [JsonProperty("required_client_scope")] + public string RequiredClientScope { get; set; } + + /// + /// Space separated scopes optional for login + /// + [JsonProperty("optional_client_scope")] + public string OptionalClientScope { get; set; } + + /// + /// Use the Brand config Client ID instead + /// + [JsonProperty("client_id")] + public string ClientId { get; set; } + + /// + /// Use the Brand config Client Secret instead + /// + [JsonProperty("client_secret")] + public string ClientSecret { get; set; } } - - public class Telenav_Config + /// + /// Client credentials for Telenav system + /// + public class TelenavConfig { - public string name { get; set; } - public string client_id { get; set; } - public string client_secret { get; set; } + /// + /// Name + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// OAuth Client ID + /// + [JsonProperty("client_id")] + public string ClientId { get; set; } + + /// + /// OAuth Client Secret + /// + [JsonProperty("client_secret")] + public string ClientSecret { get; set; } } + /// + /// Container for certificate pinning info + /// public class RegionCert { - public string pattern { get; set; } - public string[] certificate_pins { get; set; } + /// + /// Pattern used by the expected certificate. Should match the CN I'm guessing + /// + [JsonProperty("pattern")] + public string Pattern { get; set; } + + /// + /// 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 + /// + [JsonProperty("certificate_pins")] + public string[] CertificatePins { get; set; } } } diff --git a/GM.Api/Models/Diagnostic.cs b/GM.Api/Models/Diagnostic.cs index d875dab..e61edf8 100644 --- a/GM.Api/Models/Diagnostic.cs +++ b/GM.Api/Models/Diagnostic.cs @@ -2,27 +2,45 @@ using System.Collections.Generic; using System.Text; using System.Linq; +using Newtonsoft.Json; namespace GM.Api.Models { + /// + /// Response Body + /// Note: this only contains a diagnostic response. there are likely others. + /// 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; } } } diff --git a/GM.Api/Models/Vehicle.cs b/GM.Api/Models/Vehicle.cs index c3d1924..f9d8dd4 100644 --- a/GM.Api/Models/Vehicle.cs +++ b/GM.Api/Models/Vehicle.cs @@ -16,33 +16,64 @@ namespace GM.Api.Models public class Vehicles { + /// + /// Size of the Vehicle array, or full size. One would need to have more than 10 cars to find out... + /// [JsonProperty("size")] public string Size { get; set; } + /// + /// List of vehicles associated with the account + /// Note that there is paging and by default the page size is 10 + /// [JsonProperty("vehicle")] public Vehicle[] Vehicle { get; set; } } + /// + /// Vehicle description + /// public class Vehicle { + /// + /// Vehicle VIN + /// [JsonProperty("vin")] public string Vin { get; set; } + /// + /// Vehicle Make + /// [JsonProperty("make")] public string Make { get; set; } + /// + /// Vehicle Model + /// [JsonProperty("model")] public string Model { get; set; } + /// + /// Vehicle Year + /// [JsonProperty("year")] public string Year { get; set; } + /// + /// Vehicle Manufacturer - not sure why this is required... + /// [JsonProperty("manufacturer")] public string Manufacturer { get; set; } + /// + /// (e.g. car, maybe truck) + /// [JsonProperty("bodyStyle")] public string BodyStyle { get; set; } + /// + /// Vehicle cellular / OnStar phone number + /// [JsonProperty("phone")] public string Phone { get; set; } @@ -52,6 +83,9 @@ namespace GM.Api.Models [JsonProperty("onstarStatus")] public string OnStarStatus { get; set; } + /// + /// Base URL for API calls regarding this vehicle + /// [JsonProperty("url")] public string Url { get; set; } @@ -64,12 +98,21 @@ namespace GM.Api.Models [JsonProperty("enrolledInContinuousCoverage")] public bool? EnrolledInContinuousCoverage { get; set; } + /// + /// Details on supported commands + /// [JsonProperty("commands")] public Commands Commands { get; set; } + /// + /// Details on available modules + /// [JsonProperty("modules")] public Modules Modules { get; set; } + /// + /// Details on available entitlements + /// [JsonProperty("entitlements")] public Entitlements Entitlements { get; set; } @@ -102,24 +145,46 @@ namespace GM.Api.Models public class Commands { + /// + /// List of commands supported by the vehicle + /// [JsonProperty("command")] public Command[] Command { get; set; } } + /// + /// Details about a supported command + /// public class Command { + /// + /// Command name + /// [JsonProperty("name")] public string Name { get; set; } + /// + /// Description of what the command does + /// [JsonProperty("description")] public string Description { get; set; } + /// + /// API URL to be used for issuing the command + /// This SDK uses this url rather than constructing it + /// [JsonProperty("url")] public string Url { get; set; } + /// + /// True or False if the command requires the token to be upgraded with an OnStar PIN + /// [JsonProperty("isPrivSessionRequired")] public bool? IsPrivSessionRequired { get; set; } + /// + /// For commands with additional data such as diagnostics + /// [JsonProperty("commandData")] public CommandData CommandData { get; set; } } @@ -132,12 +197,18 @@ namespace GM.Api.Models public class SupportedDiagnostics { + /// + /// List of the diagnostic elements that may be requsted for the vehicle + /// [JsonProperty("supportedDiagnostic")] public string[] SupportedDiagnostic { get; set; } } public class Modules { + /// + /// List of modules - not much here + /// [JsonProperty("module")] public Module[] Module { get; set; } } @@ -153,23 +224,43 @@ namespace GM.Api.Models public class Entitlements { + /// + /// 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 + /// [JsonProperty("entitlement")] public Entitlement[] Entitlement { get; set; } } + /// + /// Details about an Entitlement + /// public class Entitlement { + /// + /// ID or name of entitlement + /// [JsonProperty("id")] public string Id { get; set; } + /// + /// True or false if the entitlement is available on this vehicle or account + /// [JsonProperty("eligible")] public bool? Eligible { get; set; } + /// + /// Reason for inelligibility (whether the car is incapable or the owner isn't subscribed) + /// [JsonProperty("ineligibleReasonCode")] public string IneligibleReasonCode { get; set; } + /// + /// True or false if the entitlement can send notifications + /// [JsonProperty("notificationCapable")] - public string NotificationCapable { get; set; } + public bool? NotificationCapable { get; set; } } diff --git a/GM.WindowsUI/BrandWindow.xaml.cs b/GM.WindowsUI/BrandWindow.xaml.cs index 2a54f78..98df45a 100644 --- a/GM.WindowsUI/BrandWindow.xaml.cs +++ b/GM.WindowsUI/BrandWindow.xaml.cs @@ -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)); } diff --git a/GM.WindowsUI/MainWindow.xaml.cs b/GM.WindowsUI/MainWindow.xaml.cs index 7d2bc3b..de4a7d2 100644 --- a/GM.WindowsUI/MainWindow.xaml.cs +++ b/GM.WindowsUI/MainWindow.xaml.cs @@ -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; } diff --git a/README.md b/README.md index c24bcc7..e719aa1 100644 --- a/README.md +++ b/README.md @@ -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