Simple CAPI client to retrieve player profile from the Elite Dangerous Companion API using OAuth2
-
-
Save klightspeed/357cddf4e9e0669305d713e02eac1496 to your computer and use it in GitHub Desktop.
<?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> |
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); | |
} | |
} | |
} |
<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> |
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; | |
} | |
} | |
} |
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)); | |
} | |
} | |
} |
Hello, this is a long shot because this repo is a few years old but here it goes. I'm trying to use a modified version of your code in a DSharp+ Discord bot running in a Docker container on an Ubuntu VM in the GCP. The listener is created and I set the redirect to the IP of the Host machine.
The basic adjustments are: -modified the AuthURL to allow for getting authenticating credentials for any platform (pc or consoles) -changed the RedirectURI to the host IP port 80
This way the Discord user receives the AuthURL as a button in a DM. Click the button which opens a browser. Here's where it goes kind of sideways. Once the user enters their credentials it redirects them rather than showing them the approve/deny page. What I'm surprised about is that it worked fine when I ran it on the localhost but once I moved it to the VM it started skipping the approve/deny page.
The other problem I have is getting the network sorted out but I don't think that is related to your code.
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.
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?
Hello, this is a long shot because this repo is a few years old but here it goes. I'm trying to use a modified version of your code in a DSharp+ Discord bot running in a Docker container on an Ubuntu VM in the GCP. The listener is created and I set the redirect to the IP of the Host machine.
The basic adjustments are:
-modified the AuthURL to allow for getting authenticating credentials for any platform (pc or consoles)
-changed the RedirectURI to the host IP port 80
This way the Discord user receives the AuthURL as a button in a DM. Click the button which opens a browser. Here's where it goes kind of sideways. Once the user enters their credentials it redirects them rather than showing them the approve/deny page. What I'm surprised about is that it worked fine when I ran it on the localhost but once I moved it to the VM it started skipping the approve/deny page.
The other problem I have is getting the network sorted out but I don't think that is related to your code.