mirror of
https://github.com/q39JzrRa/GM-Vehicle-API.git
synced 2025-12-23 23:38:45 -05:00
Removed key extraction code.
Moved command implementations to base class Removed references to config file structure Turned brand into an Enum (Note: this may change so urls can be modified in app.config) Base class virtualizes key encoding Implemented a client that uses external token signing when dev doesn't have keys Fixed diagnostics request
This commit is contained in:
100
GM.Api/Brands.cs
Normal file
100
GM.Api/Brands.cs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace GM.Api
|
||||||
|
{
|
||||||
|
|
||||||
|
public enum Brand
|
||||||
|
{
|
||||||
|
Opel,
|
||||||
|
Vauxhall,
|
||||||
|
Chevrolet,
|
||||||
|
OnStar,
|
||||||
|
Cadillac,
|
||||||
|
Buick,
|
||||||
|
Gmc
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class BrandHelpers
|
||||||
|
{
|
||||||
|
|
||||||
|
public static string GetDisplayName(this Brand brand)
|
||||||
|
{
|
||||||
|
return brand.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetName(this Brand brand)
|
||||||
|
{
|
||||||
|
switch (brand)
|
||||||
|
{
|
||||||
|
case Brand.Opel:
|
||||||
|
return "opel";
|
||||||
|
case Brand.Vauxhall:
|
||||||
|
return "vauxhall";
|
||||||
|
case Brand.Chevrolet:
|
||||||
|
return "chevrolet";
|
||||||
|
case Brand.OnStar:
|
||||||
|
return "onstar";
|
||||||
|
case Brand.Cadillac:
|
||||||
|
return "cadillac";
|
||||||
|
case Brand.Buick:
|
||||||
|
return "buick";
|
||||||
|
case Brand.Gmc:
|
||||||
|
return "gmc";
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException("Unknown Brand");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetUrl(this Brand brand)
|
||||||
|
{
|
||||||
|
switch (brand)
|
||||||
|
{
|
||||||
|
case Brand.Opel:
|
||||||
|
return "https://api.eur.onstar.com/api";
|
||||||
|
case Brand.Vauxhall:
|
||||||
|
return "https://api.eur.onstar.com/api";
|
||||||
|
case Brand.Chevrolet:
|
||||||
|
return "https://api.gm.com/api";
|
||||||
|
case Brand.OnStar:
|
||||||
|
return "https://api.gm.com/api";
|
||||||
|
case Brand.Cadillac:
|
||||||
|
return "https://api.gm.com/api";
|
||||||
|
case Brand.Buick:
|
||||||
|
return "https://api.gm.com/api";
|
||||||
|
case Brand.Gmc:
|
||||||
|
return "https://api.gm.com/api";
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException("Unknown Brand");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Brand GetBrand(string brandName)
|
||||||
|
{
|
||||||
|
var cleanName = brandName.ToLowerInvariant();
|
||||||
|
|
||||||
|
switch (cleanName)
|
||||||
|
{
|
||||||
|
case "opel":
|
||||||
|
return Brand.Opel;
|
||||||
|
case "vauxhall":
|
||||||
|
return Brand.Vauxhall;
|
||||||
|
case "chevrolet":
|
||||||
|
return Brand.Chevrolet;
|
||||||
|
case "onstar":
|
||||||
|
return Brand.OnStar;
|
||||||
|
case "cadillac":
|
||||||
|
return Brand.Cadillac;
|
||||||
|
case "buick":
|
||||||
|
return Brand.Buick;
|
||||||
|
case "gmc":
|
||||||
|
return Brand.Gmc;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException("Unknown Brand");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
GM.Api/GMClient.cs
Normal file
30
GM.Api/GMClient.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GM.Api.Tokens;
|
||||||
|
|
||||||
|
namespace GM.Api
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// GM Client implementation to be used when you have local access to the Client ID and Client Secret
|
||||||
|
/// </summary>
|
||||||
|
public class GMClient : GMClientBase
|
||||||
|
{
|
||||||
|
string _clientId;
|
||||||
|
JwtTool _jwtTool;
|
||||||
|
|
||||||
|
|
||||||
|
public GMClient(string deviceId, Brand brand, string clientId, string clientSecret) : base(deviceId, brand)
|
||||||
|
{
|
||||||
|
_clientId = clientId;
|
||||||
|
_jwtTool = new JwtTool(clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<string> EncodeLoginRequest(LoginRequest request)
|
||||||
|
{
|
||||||
|
request.ClientId = _clientId;
|
||||||
|
return _jwtTool.EncodeToken(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,12 +21,9 @@ namespace GM.Api
|
|||||||
public static int RetryCount { get; set; } = 3;
|
public static int RetryCount { get; set; } = 3;
|
||||||
|
|
||||||
//TODO: consistent exception throwing
|
//TODO: consistent exception throwing
|
||||||
|
protected Brand _brand;
|
||||||
string _clientId;
|
protected string _deviceId;
|
||||||
string _deviceId;
|
protected string _apiUrl;
|
||||||
JwtTool _jwtTool;
|
|
||||||
string _apiUrl;
|
|
||||||
string _host;
|
|
||||||
|
|
||||||
HttpClient _client;
|
HttpClient _client;
|
||||||
|
|
||||||
@@ -62,24 +59,20 @@ namespace GM.Api
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new GMClient
|
/// Create a new GMClient
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="clientId">Client ID for authentication</param>
|
|
||||||
/// <param name="deviceId">Device ID (should be in the format of a GUID)</param>
|
/// <param name="deviceId">Device ID (should be in the format of a GUID)</param>
|
||||||
/// <param name="clientSecret">Client Secret for authentication</param>
|
/// <param name="brand">One of the supported brands from </param>
|
||||||
/// <param name="apiUrl">Base url for the API. Usually https://api.gm.com/api </param>
|
public GMClientBase(string deviceId, Brand brand)
|
||||||
public GMClientBase(string clientId, string deviceId, string clientSecret, string apiUrl)
|
|
||||||
{
|
{
|
||||||
Setup(clientId, deviceId, clientSecret, apiUrl);
|
Setup(deviceId, brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Setup(string clientId, string deviceId, string clientSecret, string apiUrl)
|
void Setup(string deviceId, Brand brand)
|
||||||
{
|
{
|
||||||
_clientId = clientId;
|
_brand = brand;
|
||||||
_deviceId = deviceId;
|
_deviceId = deviceId;
|
||||||
_jwtTool = new JwtTool(clientSecret);
|
_apiUrl = brand.GetUrl();
|
||||||
_apiUrl = apiUrl;
|
|
||||||
var uri = new Uri(_apiUrl);
|
var uri = new Uri(_apiUrl);
|
||||||
_host = uri.Host;
|
_client = CreateClient(uri.Host);
|
||||||
_client = CreateClient(_host);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -98,6 +91,19 @@ namespace GM.Api
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected abstract Task<string> EncodeLoginRequest(LoginRequest request);
|
||||||
|
|
||||||
|
LoginData DecodeLoginData(string token)
|
||||||
|
{
|
||||||
|
IJsonSerializer serializer = new SortedJsonSerializer();
|
||||||
|
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
|
||||||
|
IDateTimeProvider dateTimeProvider = new UtcDateTimeProvider();
|
||||||
|
IJwtValidator validator = new JwtValidator(serializer, dateTimeProvider);
|
||||||
|
var decoder = new JwtDecoder(serializer, validator, urlEncoder);
|
||||||
|
return decoder.DecodeToObject<LoginData>(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#region Client Helpers
|
#region Client Helpers
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -157,6 +163,9 @@ namespace GM.Api
|
|||||||
}
|
}
|
||||||
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)
|
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)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
var respMessage = (await resp.Content.ReadAsStringAsync()) ?? "";
|
||||||
|
int f = 5;
|
||||||
//possible transient errors
|
//possible transient errors
|
||||||
//todo: log this
|
//todo: log this
|
||||||
await Task.Delay(500);
|
await Task.Delay(500);
|
||||||
@@ -226,7 +235,7 @@ namespace GM.Api
|
|||||||
{
|
{
|
||||||
var payload = new LoginRequest()
|
var payload = new LoginRequest()
|
||||||
{
|
{
|
||||||
ClientId = _clientId,
|
//ClientId = _clientId,
|
||||||
DeviceId = _deviceId,
|
DeviceId = _deviceId,
|
||||||
Credential = onStarPin,
|
Credential = onStarPin,
|
||||||
CredentialType = "PIN",
|
CredentialType = "PIN",
|
||||||
@@ -234,7 +243,8 @@ namespace GM.Api
|
|||||||
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK")
|
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK")
|
||||||
};
|
};
|
||||||
|
|
||||||
var token = _jwtTool.EncodeToken(payload);
|
//var token = _jwtTool.EncodeToken(payload);
|
||||||
|
var token = await EncodeLoginRequest(payload);
|
||||||
|
|
||||||
using (var response = await PostAsync($"{_apiUrl}/v1/oauth/token/upgrade", new StringContent(token, Encoding.UTF8, "text/plain")))
|
using (var response = await PostAsync($"{_apiUrl}/v1/oauth/token/upgrade", new StringContent(token, Encoding.UTF8, "text/plain")))
|
||||||
{
|
{
|
||||||
@@ -264,7 +274,7 @@ namespace GM.Api
|
|||||||
{
|
{
|
||||||
var payload = new LoginRequest()
|
var payload = new LoginRequest()
|
||||||
{
|
{
|
||||||
ClientId = _clientId,
|
//ClientId = _clientId,
|
||||||
DeviceId = _deviceId,
|
DeviceId = _deviceId,
|
||||||
GrantType = "password",
|
GrantType = "password",
|
||||||
Nonce = helpers.GenerateNonce(),
|
Nonce = helpers.GenerateNonce(),
|
||||||
@@ -274,7 +284,8 @@ namespace GM.Api
|
|||||||
Username = username
|
Username = username
|
||||||
};
|
};
|
||||||
|
|
||||||
var token = _jwtTool.EncodeToken(payload);
|
//var token = _jwtTool.EncodeToken(payload);
|
||||||
|
var token = await EncodeLoginRequest(payload);
|
||||||
|
|
||||||
using (var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true))
|
using (var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true))
|
||||||
{
|
{
|
||||||
@@ -294,7 +305,8 @@ namespace GM.Api
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginTokenData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
|
//var loginTokenData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
|
||||||
|
var loginTokenData = DecodeLoginData(rawResponseToken);
|
||||||
|
|
||||||
LoginData = loginTokenData;
|
LoginData = loginTokenData;
|
||||||
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.AccessToken);
|
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.AccessToken);
|
||||||
@@ -315,7 +327,6 @@ namespace GM.Api
|
|||||||
|
|
||||||
var payload = new LoginRequest()
|
var payload = new LoginRequest()
|
||||||
{
|
{
|
||||||
ClientId = _clientId,
|
|
||||||
DeviceId = _deviceId,
|
DeviceId = _deviceId,
|
||||||
GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||||
Nonce = helpers.GenerateNonce(),
|
Nonce = helpers.GenerateNonce(),
|
||||||
@@ -324,7 +335,7 @@ namespace GM.Api
|
|||||||
Assertion = LoginData.IdToken
|
Assertion = LoginData.IdToken
|
||||||
};
|
};
|
||||||
|
|
||||||
var token = _jwtTool.EncodeToken(payload);
|
var token = await EncodeLoginRequest(payload);
|
||||||
|
|
||||||
using (var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true))
|
using (var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true))
|
||||||
{
|
{
|
||||||
@@ -358,7 +369,7 @@ namespace GM.Api
|
|||||||
}*/
|
}*/
|
||||||
// Not sure if the scope needs to be updated, as msso has been removed in the refresh request
|
// Not sure if the scope needs to be updated, as msso has been removed in the refresh request
|
||||||
|
|
||||||
var refreshData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
|
var refreshData = DecodeLoginData(rawResponseToken);
|
||||||
|
|
||||||
LoginData.AccessToken = refreshData.AccessToken;
|
LoginData.AccessToken = refreshData.AccessToken;
|
||||||
LoginData.IssuedAtUtc = refreshData.IssuedAtUtc;
|
LoginData.IssuedAtUtc = refreshData.IssuedAtUtc;
|
||||||
@@ -544,5 +555,173 @@ namespace GM.Api
|
|||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Command Implementations
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve Diagnostic data for the active vehicle
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<DiagnosticResponse[]> GetDiagnostics()
|
||||||
|
{
|
||||||
|
var cmdInfo = ActiveVehicle.GetCommand("diagnostics");
|
||||||
|
|
||||||
|
var reqObj = new JObject()
|
||||||
|
{
|
||||||
|
["diagnosticsRequest"] = new JObject()
|
||||||
|
{
|
||||||
|
["diagnosticItem"] = new JArray(cmdInfo.CommandData.SupportedDiagnostics.SupportedDiagnostic)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await InitiateCommandAndWait("diagnostics", reqObj);
|
||||||
|
if (result == null) return null;
|
||||||
|
if ("success".Equals(result.Status, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return result.Body.DiagnosticResponse;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Issue an arbitrary command
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commandName">Name of the command. Must exists in the vehicle's configuration</param>
|
||||||
|
/// <param name="parameters">JSON parameters for the command</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<CommandResponse> IssueCommand(string commandName, JObject parameters = null)
|
||||||
|
{
|
||||||
|
return await InitiateCommandAndWait(commandName, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lock the active vehicles's doors and wait for completion
|
||||||
|
/// Privileged Command
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True or false for success</returns>
|
||||||
|
public async Task<bool> LockDoor()
|
||||||
|
{
|
||||||
|
|
||||||
|
var reqObj = new JObject()
|
||||||
|
{
|
||||||
|
["lockDoorRequest"] = new JObject()
|
||||||
|
{
|
||||||
|
["delay"] = 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return await InitiateCommandAndWaitForSuccess("lockDoor", reqObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fails when the hotspot is off...
|
||||||
|
/// Note: the app uses diagnotics that also fail when the hotpot is off
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<HotspotInfo> GetHotspotInfo()
|
||||||
|
{
|
||||||
|
var resp = await InitiateCommandAndWait("getHotspotInfo", null);
|
||||||
|
return resp.Body.HotspotInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a turn-by-turn destination to the vehicle
|
||||||
|
/// Requires both coordinates and address info
|
||||||
|
/// Vehicle may not respond if turned off or may take a very long time to respond
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="destination"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> SendTBTRoute(TbtDestination destination)
|
||||||
|
{
|
||||||
|
var reqObj = new JObject()
|
||||||
|
{
|
||||||
|
["tbtDestination"] = new JObject(destination)
|
||||||
|
};
|
||||||
|
|
||||||
|
return await InitiateCommandAndWaitForSuccess("sendTBTRoute", reqObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unlock the active vehicles's doors and wait for completion
|
||||||
|
/// Privileged Command
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True or false for success</returns>
|
||||||
|
public async Task<bool> UnlockDoor()
|
||||||
|
{
|
||||||
|
var reqObj = new JObject()
|
||||||
|
{
|
||||||
|
["unlockDoorRequest"] = new JObject()
|
||||||
|
{
|
||||||
|
["delay"] = 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return await InitiateCommandAndWaitForSuccess("unlockDoor", reqObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remote start the active vehicle and wait for completion
|
||||||
|
/// Privileged Command
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True or false for success</returns>
|
||||||
|
public async Task<bool> Start()
|
||||||
|
{
|
||||||
|
return await InitiateCommandAndWaitForSuccess("start", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remote stop the active vehicle and wait for completion
|
||||||
|
/// Privileged Command
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True or false for success</returns>
|
||||||
|
public async Task<bool> CancelStart()
|
||||||
|
{
|
||||||
|
return await InitiateCommandAndWaitForSuccess("cancelStart", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set off remote alarm on the active vehicle and wait for completion
|
||||||
|
/// Privileged Command
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True or false for success</returns>
|
||||||
|
public async Task<bool> Alert()
|
||||||
|
{
|
||||||
|
var reqObj = new JObject()
|
||||||
|
{
|
||||||
|
["alertRequest"] = new JObject()
|
||||||
|
{
|
||||||
|
["action"] = new JArray() { "Honk", "Flash" },
|
||||||
|
["delay"] = 0,
|
||||||
|
["duration"] = 1,
|
||||||
|
["override"] = new JArray() { "DoorOpen", "IgnitionOn" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return await InitiateCommandAndWaitForSuccess("alert", reqObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop remote alarm on the active vehicle and wait for completion
|
||||||
|
/// Privileged Command
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True or false for success</returns>
|
||||||
|
public async Task<bool> CancelAlert()
|
||||||
|
{
|
||||||
|
return await InitiateCommandAndWaitForSuccess("cancelAlert", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
GM.Api/GMClientNoKey.cs
Normal file
45
GM.Api/GMClientNoKey.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GM.Api.Tokens;
|
||||||
|
|
||||||
|
namespace GM.Api
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// GM Client implementation that uses a web service to sign the JWT tokens required for authentication
|
||||||
|
/// Use this if you do not have access to the Client ID and Client Secret
|
||||||
|
/// </summary>
|
||||||
|
public class GMClientNoKey : GMClientBase
|
||||||
|
{
|
||||||
|
string _tokenSignUrl;
|
||||||
|
HttpClient _tokenClient = new HttpClient();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create new GM Client
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deviceId">deviceId = string representation of a GUID</param>
|
||||||
|
/// <param name="brand">API is segmented by brand</param>
|
||||||
|
/// <param name="tokenSignUrl">URL for webservice that will sign JWT tokens (e.g. "https://gmsigner.herokuapp.com/")</param>
|
||||||
|
public GMClientNoKey(string deviceId, Brand brand, string tokenSignUrl) : base(deviceId, brand)
|
||||||
|
{
|
||||||
|
_tokenSignUrl = tokenSignUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<string> EncodeLoginRequest(LoginRequest request)
|
||||||
|
{
|
||||||
|
var resp = await _tokenClient.PostAsJsonAsync($"{_tokenSignUrl}?brand={_brand.GetName()}", request);
|
||||||
|
|
||||||
|
if (resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return await resp.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string errorText = await resp.Content.ReadAsStringAsync();
|
||||||
|
throw new InvalidOperationException("Token sign failure: " + errorText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
using GM.Api.Models;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace GM.Api
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Generic implementation of GM Client supporting a limited set of commands
|
|
||||||
/// </summary>
|
|
||||||
public class GenericGMClient : GMClientBase
|
|
||||||
{
|
|
||||||
public GenericGMClient(string clientId, string deviceId, string clientSecret, string apiUrl) : base(clientId, deviceId, clientSecret, apiUrl)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieve Diagnostic data for the active vehicle
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<DiagnosticResponse[]> GetDiagnostics()
|
|
||||||
{
|
|
||||||
var cmdInfo = ActiveVehicle.GetCommand("diagnostics");
|
|
||||||
|
|
||||||
var reqObj = new JObject()
|
|
||||||
{
|
|
||||||
["diagnosticItem"] = new JArray(cmdInfo.CommandData.SupportedDiagnostics.SupportedDiagnostic)
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = await InitiateCommandAndWait("diagnostics", reqObj);
|
|
||||||
if (result == null) return null;
|
|
||||||
if ("success".Equals(result.Status, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return result.Body.DiagnosticResponse;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Issue an arbitrary command
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="commandName">Name of the command. Must exists in the vehicle's configuration</param>
|
|
||||||
/// <param name="parameters">JSON parameters for the command</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<CommandResponse> IssueCommand(string commandName, JObject parameters = null)
|
|
||||||
{
|
|
||||||
return await InitiateCommandAndWait(commandName, parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Lock the active vehicles's doors and wait for completion
|
|
||||||
/// Privileged Command
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True or false for success</returns>
|
|
||||||
public async Task<bool> LockDoor()
|
|
||||||
{
|
|
||||||
|
|
||||||
var reqObj = new JObject()
|
|
||||||
{
|
|
||||||
["lockDoorRequest"] = new JObject()
|
|
||||||
{
|
|
||||||
["delay"] = 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return await InitiateCommandAndWaitForSuccess("lockDoor", reqObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fails when the hotspot is off...
|
|
||||||
/// Note: the app uses diagnotics that also fail when the hotpot is off
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<HotspotInfo> GetHotspotInfo()
|
|
||||||
{
|
|
||||||
var resp = await InitiateCommandAndWait("getHotspotInfo", null);
|
|
||||||
return resp.Body.HotspotInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Send a turn-by-turn destination to the vehicle
|
|
||||||
/// Requires both coordinates and address info
|
|
||||||
/// Vehicle may not respond if turned off or may take a very long time to respond
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="destination"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<bool> SendTBTRoute(TbtDestination destination)
|
|
||||||
{
|
|
||||||
var reqObj = new JObject()
|
|
||||||
{
|
|
||||||
["tbtDestination"] = new JObject(destination)
|
|
||||||
};
|
|
||||||
|
|
||||||
return await InitiateCommandAndWaitForSuccess("sendTBTRoute", reqObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unlock the active vehicles's doors and wait for completion
|
|
||||||
/// Privileged Command
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True or false for success</returns>
|
|
||||||
public async Task<bool> UnlockDoor()
|
|
||||||
{
|
|
||||||
var reqObj = new JObject()
|
|
||||||
{
|
|
||||||
["unlockDoorRequest"] = new JObject()
|
|
||||||
{
|
|
||||||
["delay"] = 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return await InitiateCommandAndWaitForSuccess("unlockDoor", reqObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remote start the active vehicle and wait for completion
|
|
||||||
/// Privileged Command
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True or false for success</returns>
|
|
||||||
public async Task<bool> Start()
|
|
||||||
{
|
|
||||||
return await InitiateCommandAndWaitForSuccess("start", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remote stop the active vehicle and wait for completion
|
|
||||||
/// Privileged Command
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True or false for success</returns>
|
|
||||||
public async Task<bool> CancelStart()
|
|
||||||
{
|
|
||||||
return await InitiateCommandAndWaitForSuccess("cancelStart", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set off remote alarm on the active vehicle and wait for completion
|
|
||||||
/// Privileged Command
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True or false for success</returns>
|
|
||||||
public async Task<bool> Alert()
|
|
||||||
{
|
|
||||||
|
|
||||||
|
|
||||||
var reqObj = new JObject()
|
|
||||||
{
|
|
||||||
["alertRequest"] = new JObject()
|
|
||||||
{
|
|
||||||
["action"] = new JArray() { "Honk", "Flash" },
|
|
||||||
["delay"] = 0,
|
|
||||||
["duration"] = 1,
|
|
||||||
["override"] = new JArray() { "DoorOpen", "IgnitionOn" }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return await InitiateCommandAndWaitForSuccess("alert", reqObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stop remote alarm on the active vehicle and wait for completion
|
|
||||||
/// Privileged Command
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True or false for success</returns>
|
|
||||||
public async Task<bool> CancelAlert()
|
|
||||||
{
|
|
||||||
return await InitiateCommandAndWaitForSuccess("cancelAlert", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace GM.Api.Models
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Model of the encrypted Andorid configuration file
|
|
||||||
/// </summary>
|
|
||||||
|
|
||||||
public class GmConfiguration
|
|
||||||
{
|
|
||||||
/// <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
|
|
||||||
{
|
|
||||||
/// <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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// GM Brand name
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base API endpoint URL (eg "https://api.gm.com/api")
|
|
||||||
/// </summary>
|
|
||||||
[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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Client credentials for Telenav system
|
|
||||||
/// </summary>
|
|
||||||
public class TelenavConfig
|
|
||||||
{
|
|
||||||
/// <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
|
|
||||||
{
|
|
||||||
/// <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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ using System.Text;
|
|||||||
|
|
||||||
namespace GM.Api.Tokens
|
namespace GM.Api.Tokens
|
||||||
{
|
{
|
||||||
class JwtTool
|
public class JwtTool
|
||||||
{
|
{
|
||||||
public IJwtEncoder Encoder { get; private set; }
|
public IJwtEncoder Encoder { get; private set; }
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ namespace GM.Api.Tokens
|
|||||||
IDateTimeProvider dateTimeProvider = new UtcDateTimeProvider();
|
IDateTimeProvider dateTimeProvider = new UtcDateTimeProvider();
|
||||||
IJwtValidator validator = new JwtValidator(serializer, dateTimeProvider);
|
IJwtValidator validator = new JwtValidator(serializer, dateTimeProvider);
|
||||||
Decoder = new JwtDecoder(serializer, validator, urlEncoder);
|
Decoder = new JwtDecoder(serializer, validator, urlEncoder);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||||
<section name="GM.WindowsUI.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
<section name="GM.WindowsUI.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
||||||
</sectionGroup>
|
</sectionGroup>
|
||||||
|
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||||
|
<section name="GM.WindowsUI.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
|
||||||
|
</sectionGroup>
|
||||||
</configSections>
|
</configSections>
|
||||||
<startup>
|
<startup>
|
||||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
||||||
@@ -35,4 +38,11 @@
|
|||||||
</dependentAssembly>
|
</dependentAssembly>
|
||||||
</assemblyBinding>
|
</assemblyBinding>
|
||||||
</runtime>
|
</runtime>
|
||||||
|
<applicationSettings>
|
||||||
|
<GM.WindowsUI.Properties.Settings>
|
||||||
|
<setting name="TokenSignerUrl" serializeAs="String">
|
||||||
|
<value>https://gmsigner.herokuapp.com/</value>
|
||||||
|
</setting>
|
||||||
|
</GM.WindowsUI.Properties.Settings>
|
||||||
|
</applicationSettings>
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using GM.Api.Models;
|
using GM.Api;
|
||||||
|
using GM.Api.Models;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -22,27 +23,24 @@ namespace GM.WindowsUI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class BrandWindow : Window
|
public partial class BrandWindow : Window
|
||||||
{
|
{
|
||||||
|
public Brand? SelectedBrand { get; set; } = null;
|
||||||
|
|
||||||
GmConfiguration _config;
|
public BrandWindow()
|
||||||
|
|
||||||
public string SelectedBrand { get; set; } = null;
|
|
||||||
|
|
||||||
|
|
||||||
public BrandWindow(GmConfiguration configuration)
|
|
||||||
{
|
{
|
||||||
_config = configuration;
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
foreach (var brandName in _config.BrandClientInfo.Keys.OrderBy((val) => val, StringComparer.OrdinalIgnoreCase))
|
var brandNames = Enum.GetNames(typeof(Brand));
|
||||||
|
|
||||||
|
foreach (var brandName in brandNames.OrderBy((val) => val, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
lstBrands.Items.Add(brandName.Substring(0, 1).ToUpperInvariant() + brandName.Substring(1));
|
lstBrands.Items.Add(brandName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BtnOk_Click(object sender, RoutedEventArgs e)
|
private void BtnOk_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (lstBrands.SelectedItem == null) return;
|
if (lstBrands.SelectedItem == null) return;
|
||||||
SelectedBrand = ((string)lstBrands.SelectedItem).ToLowerInvariant();
|
SelectedBrand = BrandHelpers.GetBrand((string)lstBrands.SelectedItem);
|
||||||
this.Close();
|
this.Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,17 +25,9 @@ namespace GM.WindowsUI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
GenericGMClient _client;
|
GMClientBase _client;
|
||||||
|
|
||||||
|
Brand _brand;
|
||||||
GmConfiguration _globalConfig;
|
|
||||||
|
|
||||||
ApiConfig _apiConfig;
|
|
||||||
BrandClientInfo _clientCredentials;
|
|
||||||
|
|
||||||
string _brand;
|
|
||||||
string _brandDisplay;
|
|
||||||
|
|
||||||
|
|
||||||
Vehicle[] _vehicles = null;
|
Vehicle[] _vehicles = null;
|
||||||
|
|
||||||
@@ -44,7 +36,6 @@ namespace GM.WindowsUI
|
|||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
LoadConfiguration();
|
|
||||||
LoadBrand();
|
LoadBrand();
|
||||||
CreateClient();
|
CreateClient();
|
||||||
grpActions.IsEnabled = false;
|
grpActions.IsEnabled = false;
|
||||||
@@ -60,7 +51,7 @@ namespace GM.WindowsUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
//todo: maybe the client reads the config and takes the brand and device id as param?
|
//todo: maybe the client reads the config and takes the brand and device id as param?
|
||||||
_client = new GenericGMClient(_clientCredentials.ClientId, Properties.Settings.Default.DeviceId, _clientCredentials.ClientSecret, _apiConfig.Url);
|
_client = new GMClientNoKey(Properties.Settings.Default.DeviceId, _brand, Properties.Settings.Default.TokenSignerUrl);
|
||||||
_client.TokenUpdateCallback = TokenUpdateHandler;
|
_client.TokenUpdateCallback = TokenUpdateHandler;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Properties.Settings.Default.LoginData))
|
if (!string.IsNullOrEmpty(Properties.Settings.Default.LoginData))
|
||||||
@@ -100,52 +91,23 @@ namespace GM.WindowsUI
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(Properties.Settings.Default.Brand))
|
if (string.IsNullOrEmpty(Properties.Settings.Default.Brand))
|
||||||
{
|
{
|
||||||
var bw = new BrandWindow(_globalConfig);
|
var bw = new BrandWindow();
|
||||||
bw.ShowDialog();
|
bw.ShowDialog();
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(bw.SelectedBrand))
|
if (!bw.SelectedBrand.HasValue)
|
||||||
{
|
{
|
||||||
MessageBox.Show("You must select a brand!");
|
MessageBox.Show("You must select a brand!");
|
||||||
Environment.Exit(100);
|
Environment.Exit(100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Properties.Settings.Default.Brand = bw.SelectedBrand;
|
Properties.Settings.Default.Brand = bw.SelectedBrand.Value.GetName();
|
||||||
Properties.Settings.Default.Save();
|
Properties.Settings.Default.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
_brand = Properties.Settings.Default.Brand;
|
_brand = BrandHelpers.GetBrand(Properties.Settings.Default.Brand);
|
||||||
_brandDisplay = _brand.Substring(0, 1).ToUpperInvariant() + _brand.Substring(1);
|
|
||||||
|
|
||||||
Title = _brandDisplay + " Vehicle Control";
|
Title = _brand.GetDisplayName() + " Vehicle Control";
|
||||||
|
|
||||||
_clientCredentials = _globalConfig.BrandClientInfo[_brand];
|
|
||||||
_apiConfig = (from f in _globalConfig.Configs where f.Name.Equals(_brand, StringComparison.OrdinalIgnoreCase) select f).FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
void LoadConfiguration()
|
|
||||||
{
|
|
||||||
if (!Directory.Exists("apk")) Directory.CreateDirectory("apk");
|
|
||||||
|
|
||||||
var fn = (from f in Directory.EnumerateFiles("apk") where System.IO.Path.GetExtension(f).Equals(".apk", StringComparison.OrdinalIgnoreCase) select f).FirstOrDefault();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(fn))
|
|
||||||
{
|
|
||||||
MessageBox.Show("You must copy the Android app's .apk file to the apk folder first.", "Missing apk");
|
|
||||||
Environment.Exit(100);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_globalConfig = JsonConvert.DeserializeObject<GmConfiguration>(GM.SettingsReader.ReadUtility.Read(Properties.Resources.a, Properties.Resources.gm, File.OpenRead(fn)));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBox.Show("Error reading config file: " + ex.ToString(), "Config read error");
|
|
||||||
Environment.Exit(100);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
9
GM.WindowsUI/Properties/Settings.Designer.cs
generated
9
GM.WindowsUI/Properties/Settings.Designer.cs
generated
@@ -82,5 +82,14 @@ namespace GM.WindowsUI.Properties {
|
|||||||
this["Vin"] = value;
|
this["Vin"] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[global::System.Configuration.ApplicationScopedSettingAttribute()]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Configuration.DefaultSettingValueAttribute("https://gmsigner.herokuapp.com/")]
|
||||||
|
public string TokenSignerUrl {
|
||||||
|
get {
|
||||||
|
return ((string)(this["TokenSignerUrl"]));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,5 +17,8 @@
|
|||||||
<Setting Name="Vin" Type="System.String" Scope="User">
|
<Setting Name="Vin" Type="System.String" Scope="User">
|
||||||
<Value Profile="(Default)" />
|
<Value Profile="(Default)" />
|
||||||
</Setting>
|
</Setting>
|
||||||
|
<Setting Name="TokenSignerUrl" Type="System.String" Scope="Application">
|
||||||
|
<Value Profile="(Default)">https://gmsigner.herokuapp.com/</Value>
|
||||||
|
</Setting>
|
||||||
</Settings>
|
</Settings>
|
||||||
</SettingsFile>
|
</SettingsFile>
|
||||||
3
GM.sln
3
GM.sln
@@ -9,8 +9,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GM.WindowsUI", "GM.WindowsU
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8DADF6E3-2511-4EE4-849C-CC71C89CBD7E}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8DADF6E3-2511-4EE4-849C-CC71C89CBD7E}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
Assemblies\GM.SettingsReader.dll = Assemblies\GM.SettingsReader.dll
|
README.md = README.md
|
||||||
readme.md = readme.md
|
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -15,20 +15,20 @@ You are accepting all responsibility and liability for the use of this content.
|
|||||||
# Client Credentials
|
# Client Credentials
|
||||||
To use this API you will require a valid client id and client secret. The correct approach would be to request access from GM at https://developer.gm.com/ or by emailing them at developer.gm.com.
|
To use this API you will require a valid client id and client secret. The correct approach would be to request access from GM at https://developer.gm.com/ or by emailing them at developer.gm.com.
|
||||||
|
|
||||||
Alternatively (and because GM refuses to respond to developer requests) you can extract the credentials from the Android app's .apk file.
|
I have a utility that is capable of extracting the client credentials from the Android APK but I am _very_ hesitant to share. The client secrets get out and GM will freak out. So...
|
||||||
I am _NOT_ including the source code for this process, but I have included the capability. GM.SettingsReader.dll can do this. I have obfuscated the process.
|
|
||||||
|
|
||||||
IMPORTANT: The demo app requires a copy of the Android app's .apk file to be copied to the "apk" folder. It has been tested with the myChevrolet app, version 3.21.0.
|
I have implemented a very small, very simple web service hosted with heroku (https://gmsigner.herokuapp.com/) that will sign token requests for you,
|
||||||
VERY IMPORTANT: Unless you want an international incident on your hands DO NOT SHARE ANY OF THE CONTENTS OF THE SETTINGS FILE ANYWHERE _EVER_!!!!
|
and I have implemented a version of the client that uses this service.
|
||||||
|
(Please note that you will be sending your login credentials to this service. I would highly advise reviewing the service source code here: https://github.com/q39JzrRa/GM-Vehicle-API-AuthUtil . It is deployed to Heroku using CD from the master branch so the code you see is the code that runs)
|
||||||
|
|
||||||
|
|
||||||
# Quick Start
|
# Quick Start
|
||||||
If you prefer not to use the Windows UI or examine how it works, here is how you might start your car if you only have one.
|
If you prefer not to use the Windows UI or examine how it works, here is how you might start your car if you only have one.
|
||||||
|
|
||||||
```
|
```
|
||||||
// Obtain Client ID, generate Device ID (GUID formatted as a string), Obtain Client Secret
|
// generate Device ID (GUID formatted as a string)
|
||||||
|
|
||||||
var client = new GenericGMClient("{client id}", "{deviceId}", "{client secret}", "https:\\api.gm.com\api");
|
var client = new GMClientNoKey("{deviceId}", Brand.Chevrolet, "https://gmsigner.herokuapp.com/");
|
||||||
if (!await _client.Login("{ your username}", "{your password}")) { throw new InvalidOperationException("Login Failed"); }
|
if (!await _client.Login("{ your username}", "{your password}")) { throw new InvalidOperationException("Login Failed"); }
|
||||||
var vehicles = await _client.GetVehicles();
|
var vehicles = await _client.GetVehicles();
|
||||||
if (vehicles == null || !vehicles.Any()) { throw new InvalidOperationException("No Vehicles on acccount"); }
|
if (vehicles == null || !vehicles.Any()) { throw new InvalidOperationException("No Vehicles on acccount"); }
|
||||||
|
|||||||
Reference in New Issue
Block a user