Skip to content

Instantly share code, notes, and snippets.

@klightspeed
Last active June 10, 2023 03:30
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save klightspeed/357cddf4e9e0669305d713e02eac1496 to your computer and use it in GitHub Desktop.
Save klightspeed/357cddf4e9e0669305d713e02eac1496 to your computer and use it in GitHub Desktop.
Simple C# Elite Dangerous Companion API client

Simple CAPI client to retrieve player profile from the Elite Dangerous Companion API using OAuth2

<?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));
}
}
}
@pseudokris
Copy link

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.

@klightspeed
Copy link
Author

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.

@pseudokris
Copy link

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.

@ROMVoid95
Copy link

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