Simple CAPI client to retrieve player profile from the Elite Dangerous Companion API using OAuth2
Last active
June 10, 2023 03:30
-
-
Save klightspeed/357cddf4e9e0669305d713e02eac1496 to your computer and use it in GitHub Desktop.
Simple C# Elite Dangerous Companion API client
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8" ?> | |
<configuration> | |
<appSettings> | |
<add key="AppName" value="EDCD-Your-App-Name/0.1"/> | |
<!-- Register your client at https://user.frontierstore.net/developer to get a Client ID --> | |
<add key="ClientID" value="Your-App-Client-ID"/> | |
</appSettings> | |
</configuration> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Text; | |
using System.IO; | |
using Newtonsoft.Json; | |
using Newtonsoft.Json.Linq; | |
namespace EliteDangerousCompanionAPI | |
{ | |
public class CAPI | |
{ | |
private const string ProfileURL = "https://companion.orerve.net/profile"; | |
private const string MarketURL = "https://companion.orerve.net/market"; | |
private const string ShipyardURL = "https://companion.orerve.net/shipyard"; | |
private const string JournalURL = "https://companion.orerve.net/journal"; | |
public OAuth2 OAuth { get; private set; } | |
public CAPI(OAuth2 auth) | |
{ | |
OAuth = auth; | |
} | |
private JObject Get(string url) | |
{ | |
var req = OAuth.CreateRequest(url); | |
req.Method = "GET"; | |
using (var response = req.GetResponse()) | |
{ | |
using (var stream = response.GetResponseStream()) | |
{ | |
using (var textreader = new StreamReader(stream, Encoding.UTF8)) | |
{ | |
using (var jsonreader = new JsonTextReader(textreader)) | |
{ | |
return JObject.Load(jsonreader); | |
} | |
} | |
} | |
} | |
} | |
public JObject GetProfile() | |
{ | |
return Get(ProfileURL); | |
} | |
public JObject GetMarket() | |
{ | |
return Get(MarketURL); | |
} | |
public JObject GetShipyard() | |
{ | |
return Get(ShipyardURL); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<OutputType>Exe</OutputType> | |
<TargetFramework>netcoreapp2.2</TargetFramework> | |
</PropertyGroup> | |
<ItemGroup> | |
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" /> | |
<PackageReference Include="System.Configuration.ConfigurationManager" Version="4.5.0" /> | |
</ItemGroup> | |
</Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Security.Cryptography; | |
using System.Text; | |
using System.Net; | |
using System.Threading; | |
using System.IO; | |
using Newtonsoft.Json; | |
using Newtonsoft.Json.Linq; | |
using System.Configuration; | |
namespace EliteDangerousCompanionAPI | |
{ | |
public class OAuth2 | |
{ | |
private static readonly string ClientID = ConfigurationManager.AppSettings["ClientID"]; | |
private static readonly string AppName = ConfigurationManager.AppSettings["AppName"]; | |
private const string Scope = "capi"; | |
private const string AuthServerAuthURL = "https://auth.frontierstore.net/auth"; | |
private const string AuthServerTokenURL = "https://auth.frontierstore.net/token"; | |
private const string AuthServerDecodeURL = "https://auth.frontierstore.net/decode"; | |
private string AccessToken; | |
private string RefreshToken; | |
private string TokenType; | |
public interface IOAuth2Request : IDisposable | |
{ | |
string AuthURL { get; } | |
OAuth2 GetAuth(); | |
} | |
public class OAuth2Request : IOAuth2Request, IDisposable | |
{ | |
private string CodeVerifier { get; } = GetBase64Random(32); | |
private string State { get; } = GetBase64Random(8); | |
public string AuthURL { get; private set; } | |
private HttpListener Listener { get; } | |
private ManualResetEventSlim Waithandle { get; } = new ManualResetEventSlim(false); | |
private string RedirectURI { get; } | |
private OAuth2 OAuth { get; set; } | |
public OAuth2Request() | |
{ | |
try | |
{ | |
Listener = CreateListener(out int port); | |
CodeVerifier = GetBase64Random(32); | |
State = GetBase64Random(8); | |
string challenge = Base64URLEncode(SHA256(Encoding.ASCII.GetBytes(CodeVerifier))); | |
RedirectURI = $"http://localhost:{port}/"; | |
AuthURL = AuthServerAuthURL + | |
"?scope=" + Uri.EscapeDataString(Scope) + | |
"&audience=frontier" + | |
"&response_type=code" + | |
"&client_id=" + Uri.EscapeDataString(ClientID) + | |
"&code_challenge=" + Uri.EscapeDataString(challenge) + | |
"&code_challenge_method=S256" + | |
"&state=" + Uri.EscapeDataString(State) + | |
"&redirect_uri=" + Uri.EscapeDataString(RedirectURI); | |
Listener.BeginGetContext(EndGetContext, null); | |
var psi = new System.Diagnostics.ProcessStartInfo | |
{ | |
FileName = AuthURL, | |
UseShellExecute = true | |
}; | |
System.Diagnostics.Process.Start(psi); | |
} | |
catch | |
{ | |
Listener.Stop(); | |
} | |
} | |
private void EndGetContext(IAsyncResult target) | |
{ | |
var ctx = Listener.EndGetContext(target); | |
var req = ctx.Request; | |
var code = req.QueryString["code"]; | |
string tokenurl = AuthServerTokenURL; | |
string postdata = | |
"grant_type=authorization_code" + | |
"&client_id=" + Uri.EscapeDataString(ClientID) + | |
"&code_verifier=" + Uri.EscapeDataString(CodeVerifier) + | |
"&code=" + Uri.EscapeDataString(code) + | |
"&redirect_uri=" + RedirectURI; | |
var httpreq = HttpWebRequest.Create(tokenurl); | |
httpreq.Headers[HttpRequestHeader.UserAgent] = AppName; | |
httpreq.Headers[HttpRequestHeader.Accept] = "application/json"; | |
httpreq.ContentType = "application/x-www-form-urlencoded"; | |
httpreq.Method = "POST"; | |
using (var stream = httpreq.GetRequestStream()) | |
{ | |
using (var textwriter = new StreamWriter(stream)) | |
{ | |
textwriter.Write(postdata); | |
} | |
} | |
JObject jo; | |
using (var httpresp = httpreq.GetResponse()) | |
{ | |
using (var respstream = httpresp.GetResponseStream()) | |
{ | |
using (var textreader = new StreamReader(respstream)) | |
{ | |
using (var jsonreader = new JsonTextReader(textreader)) | |
{ | |
jo = JObject.Load(jsonreader); | |
} | |
} | |
} | |
} | |
var oauth = new OAuth2(); | |
oauth.AccessToken = jo.Value<string>("access_token"); | |
oauth.RefreshToken = jo.Value<string>("refresh_token"); | |
oauth.TokenType = jo.Value<string>("token_type"); | |
this.OAuth = oauth; | |
var resp = ctx.Response; | |
resp.StatusCode = 200; | |
resp.StatusDescription = "OK"; | |
resp.ContentType = "text/plain"; | |
resp.OutputStream.Write(Encoding.ASCII.GetBytes("OK")); | |
resp.Close(); | |
Waithandle.Set(); | |
} | |
public OAuth2 GetAuth() | |
{ | |
Waithandle.Wait(); | |
return OAuth; | |
} | |
public void Dispose() | |
{ | |
Listener.Stop(); | |
} | |
} | |
private OAuth2() | |
{ | |
} | |
private static string Base64URLEncode(byte[] bytes) | |
{ | |
return Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_').TrimEnd('='); | |
} | |
private static string GetBase64Random(int len) | |
{ | |
var rng = RandomNumberGenerator.Create(); | |
var bytes = new byte[len]; | |
rng.GetBytes(bytes); | |
return Base64URLEncode(bytes); | |
} | |
private static byte[] SHA256(byte[] data) | |
{ | |
var sha = System.Security.Cryptography.SHA256.Create(); | |
return sha.ComputeHash(data); | |
} | |
private static HttpListener CreateListener(out int port) | |
{ | |
HttpListener listener; | |
HashSet<int> usedports = new HashSet<int>(); | |
Random rnd = new Random(); | |
while (true) | |
{ | |
port = rnd.Next(49152, 65534); | |
if (usedports.Contains(port)) | |
{ | |
continue; | |
} | |
listener = new HttpListener(); | |
try | |
{ | |
listener.Prefixes.Add($"http://127.0.0.1:{port}/"); | |
listener.Start(); | |
return listener; | |
} | |
catch | |
{ | |
listener.Stop(); | |
((IDisposable)listener).Dispose(); | |
usedports.Add(port); | |
} | |
} | |
} | |
public static IOAuth2Request Authorize() | |
{ | |
return new OAuth2Request(); | |
} | |
public static OAuth2 Load() | |
{ | |
try | |
{ | |
var jo = JObject.Parse(File.ReadAllText("access-token.json")); | |
return new OAuth2 | |
{ | |
AccessToken = jo.Value<string>("access_token"), | |
RefreshToken = jo.Value<string>("refresh_token"), | |
TokenType = jo.Value<string>("token_type") | |
}; | |
} | |
catch | |
{ | |
return null; | |
} | |
} | |
public void Save() | |
{ | |
var jo = new JObject | |
{ | |
["access_token"] = AccessToken, | |
["refresh_token"] = RefreshToken, | |
["token_type"] = TokenType | |
}; | |
File.WriteAllText("access-token.json", jo.ToString()); | |
} | |
public bool Refresh() | |
{ | |
// TODO: check and refresh token | |
return true; | |
} | |
public HttpWebRequest CreateRequest(string url) | |
{ | |
var request = (HttpWebRequest)WebRequest.Create(url); | |
request.Headers[HttpRequestHeader.Authorization] = TokenType + " " + AccessToken; | |
request.Headers[HttpRequestHeader.UserAgent] = AppName; | |
return request; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace EliteDangerousCompanionAPI | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
OAuth2 auth = OAuth2.Load(); | |
if (auth == null || !auth.Refresh()) | |
{ | |
var req = OAuth2.Authorize(); | |
Console.WriteLine(req.AuthURL); | |
auth = req.GetAuth(); | |
} | |
auth.Save(); | |
var capi = new CAPI(auth); | |
var profile = capi.GetProfile(); | |
System.Diagnostics.Trace.WriteLine(profile.ToString(Newtonsoft.Json.Formatting.Indented)); | |
} | |
} | |
} |
Wow, thanks for the quick response. It turns out in all my hasty changes I left off the final / at the end of the redirect URL. Once I added that back it went to the Approve/Deny page again correctly. Now I just need to sort out why I'm getting a 404 error when Frontier sends the bot the confirmation. Something's not connecting between the bot and the host I guess. Anyway, thanks again.
By chance are there any Java implementations for CAPI?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It's possible that the Frontier auth server is special-casing localhost as a redirect URL, and checks the redirect URL against that configured in the developer zone in user.frontierstore.net when it's not localhost.