An unofficial collection of APIs used in FreeJam games and mods
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

348 lines
12KB

  1. use serde::{Deserialize, Serialize};
  2. //use ureq::{Agent, Error, AgentBuilder};
  3. use reqwest::{Client, Error};
  4. //use cookie_store::CookieStore;
  5. //use url::{Url};
  6. use serde_json::from_slice;
  7. use chrono::{DateTime, naive::NaiveDateTime, Utc};
  8. const GAME_VERSION: &str = "100.0"; // currently, this accepts any version >= current public release
  9. const GAME_TARGET: &str = "Techblox";
  10. /// Token generator for authenticated API endpoints
  11. #[async_trait::async_trait]
  12. pub trait ITokenProvider {
  13. /// Retrieve the token to use
  14. async fn token(&mut self) -> Result<String, Error>;
  15. }
  16. /// Token provider for an existing Freejam account, authenticated through the web browser portal.
  17. ///
  18. /// Steam and Epic accounts are not supported.
  19. pub struct PortalTokenProvider {
  20. /// Login token
  21. token: ProgressionLoginResponse,
  22. /// User info token
  23. jwt: PortalCheckResponse,
  24. /// Ureq HTTP client
  25. client: Client,
  26. /// target game
  27. target: String,
  28. /// game version
  29. version: String,
  30. }
  31. impl PortalTokenProvider {
  32. /// Login through the web browser portal
  33. pub async fn portal() -> Result<Self, Error> {
  34. Self::target(GAME_TARGET.to_owned(), GAME_VERSION.to_owned()).await
  35. }
  36. /// Login through the portal with a custom target value
  37. pub async fn target(value: String, version: String) -> Result<Self, Error> {
  38. let client = Client::new();
  39. let payload = PortalStartPayload {
  40. target: value.clone(),
  41. };
  42. let start_response = client.post("https://account.freejamgames.com/api/authenticate/portal/start")
  43. .header("Content-Type", "application/json")
  44. .json(&payload)
  45. .send().await?;
  46. let start_res = start_response.json::<PortalStartResponse>().await?;
  47. println!("GO TO https://account.freejamgames.com/login?theme=rc2&redirect_url=portal?theme=rc2%26portalToken={}", start_res.token);
  48. let payload = PortalCheckPayload {
  49. token: start_res.token,
  50. };
  51. let mut check_response = client.post("https://account.freejamgames.com/api/authenticate/portal/check")
  52. .header("Content-Type", "application/json")
  53. .json(&payload)
  54. .send().await?;
  55. let mut auth_complete = check_response.status() == 200;
  56. while !auth_complete {
  57. check_response = client.post("https://account.freejamgames.com/api/authenticate/portal/check")
  58. .header("Content-Type", "application/json")
  59. .json(&payload)
  60. .send().await?;
  61. auth_complete = check_response.status() == 200;
  62. }
  63. let check_res = check_response.json::<PortalCheckResponse>().await?;
  64. // login with token we just got
  65. Self::login_internal(check_res, client, value, version).await
  66. }
  67. pub async fn with_email(email: &str, password: &str) -> Result<Self, Error> {
  68. let client = Client::new();
  69. let payload = AuthenticationEmailPayload {
  70. email_address: email.to_string(),
  71. password: password.to_string(),
  72. };
  73. let response = client.post("https://account.freejamgames.com/api/authenticate/email/web")
  74. .header("Content-Type", "application/json")
  75. .json(&payload)
  76. .send().await?;
  77. let json_res = response.json::<AuthenticationResponseInfo>().await?;
  78. Self::auto_portal(client, GAME_TARGET.to_owned(), json_res.token, GAME_VERSION.to_owned()).await
  79. }
  80. pub async fn with_username(username: &str, password: &str) -> Result<Self, Error> {
  81. let client = Client::new();
  82. let payload = AuthenticationUsernamePayload {
  83. username: username.to_string(),
  84. password: password.to_string(),
  85. };
  86. let response = client.post("https://account.freejamgames.com/api/authenticate/displayname/web")
  87. .header("Content-Type", "application/json")
  88. .json(&payload)
  89. .send().await?;
  90. let json_res = response.json::<AuthenticationResponseInfo>().await?;
  91. Self::auto_portal(client, GAME_TARGET.to_owned(), json_res.token, GAME_VERSION.to_owned()).await
  92. }
  93. /// Automatically validate portal
  94. async fn auto_portal(client: Client, value: String, token: String, version: String) -> Result<Self, Error> {
  95. let payload = PortalStartPayload {
  96. target: value.clone(),
  97. };
  98. let start_response = client.post("https://account.freejamgames.com/api/authenticate/portal/start")
  99. .header("Content-Type", "application/json")
  100. .json(&payload)
  101. .send().await?;
  102. let start_res = start_response.json::<PortalStartResponse>().await?;
  103. let payload = PortalCheckPayload {
  104. token: start_res.token,
  105. };
  106. let _assign_response = client.post("https://account.freejamgames.com/api/authenticate/portal/assign")
  107. .header("Content-Type", "application/json")
  108. .header("Authorization", "Web ".to_owned() + &token)
  109. .json(&payload)
  110. .send().await?;
  111. let check_response = client.post("https://account.freejamgames.com/api/authenticate/portal/check")
  112. .header("Content-Type", "application/json")
  113. .json(&payload)
  114. .send().await?;
  115. let check_res = check_response.json::<PortalCheckResponse>().await?;
  116. // login with token we just got
  117. Self::login_internal(check_res, client, value, version).await
  118. }
  119. async fn login_internal(token_data: PortalCheckResponse, client: Client, target: String, version: String) -> Result<Self, Error> {
  120. let progress_res = Self::login_step(&token_data, &client, version.clone()).await?;
  121. Ok(Self {
  122. token: progress_res,
  123. jwt: token_data,
  124. client: client,
  125. target: target,
  126. version: version,
  127. })
  128. }
  129. async fn login_step(token_data: &PortalCheckResponse, client: &Client, version: String) -> Result<ProgressionLoginResponse, Error> {
  130. let payload = ProgressionLoginPayload {
  131. token: token_data.token.clone(),
  132. client_version: version,
  133. };
  134. let progress_response = client.post("https://progression.production.robocraft2.com/login/fj")
  135. .header("Content-Type", "application/json")
  136. .json(&payload)
  137. .send().await?;
  138. progress_response.json::<ProgressionLoginResponse>().await
  139. }
  140. /// Login using the portal token data from a previous portal authentication
  141. pub async fn login(token_data: PortalCheckResponse, target: String, version: String) -> Result<Self, Error> {
  142. Self::login_internal(token_data, Client::new(), target, version).await
  143. }
  144. pub fn get_account_info(&self) -> Result<AccountInfo, Error> {
  145. Ok(self.jwt.decode_jwt_data())
  146. }
  147. pub fn token_data(&self) -> &'_ PortalCheckResponse {
  148. &self.jwt
  149. }
  150. }
  151. #[async_trait::async_trait]
  152. impl ITokenProvider for PortalTokenProvider {
  153. async fn token(&mut self) -> Result<String, Error> {
  154. let decoded_jwt = self.jwt.decode_jwt_data();
  155. let expiry = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(decoded_jwt.exp as i64, 0), Utc);
  156. let now = Utc::now();
  157. if now >= expiry || self.token.token.is_none() {
  158. // refresh token when expired
  159. // TODO make sure refresh token isn't also expired
  160. // (it would be a bit concerning if you decide to run libfj for 1+ month, though)
  161. let payload = RefreshTokenPayload {
  162. target: self.target.clone(),
  163. refresh_token: self.jwt.refresh_token.clone(),
  164. public_id: decoded_jwt.public_id,
  165. };
  166. let refresh_response = self.client.post("https://account.freejamgames.com/api/authenticate/token/refresh")
  167. .header("Content-Type", "application/json")
  168. .json(&payload)
  169. .send().await?;
  170. self.jwt = refresh_response.json::<PortalCheckResponse>().await?;
  171. self.token = Self::login_step(&self.jwt, &self.client, self.version.clone()).await?;
  172. }
  173. Ok(self.token.token.clone().unwrap())
  174. //Ok(self.jwt.token.clone())
  175. }
  176. }
  177. #[derive(Deserialize, Serialize, Clone)]
  178. pub(crate) struct AuthenticationEmailPayload {
  179. #[serde(rename = "EmailAddress")]
  180. pub email_address: String,
  181. #[serde(rename = "Password")]
  182. pub password: String,
  183. }
  184. #[derive(Deserialize, Serialize, Clone)]
  185. pub(crate) struct AuthenticationUsernamePayload {
  186. #[serde(rename = "DisplayName")]
  187. pub username: String,
  188. #[serde(rename = "Password")]
  189. pub password: String,
  190. }
  191. #[derive(Deserialize, Serialize, Clone, Debug)]
  192. pub(crate) struct AuthenticationResponseInfo {
  193. #[serde(rename = "Token")]
  194. pub token: String,
  195. #[serde(rename = "RefreshToken")]
  196. pub refresh_token: String,
  197. #[serde(rename = "RefreshTokenExpiry")]
  198. pub refresh_token_expiry: String,
  199. }
  200. #[derive(Deserialize, Serialize, Clone)]
  201. pub(crate) struct PortalStartPayload {
  202. #[serde(rename = "Target")]
  203. pub target: String,
  204. }
  205. #[derive(Deserialize, Serialize, Clone, Debug)]
  206. pub(crate) struct PortalStartResponse {
  207. #[serde(rename = "Token")]
  208. pub token: String,
  209. }
  210. #[derive(Deserialize, Serialize, Clone)]
  211. pub(crate) struct PortalCheckPayload {
  212. #[serde(rename = "Token")]
  213. pub token: String,
  214. }
  215. #[derive(Deserialize, Serialize, Clone, Debug)]
  216. pub struct PortalCheckResponse {
  217. #[serde(rename = "Token")]
  218. pub token: String,
  219. #[serde(rename = "RefreshToken")]
  220. pub refresh_token: String,
  221. #[serde(rename = "RefreshTokenExpiry")]
  222. pub refresh_token_expiry: String,
  223. }
  224. impl PortalCheckResponse {
  225. pub fn decode_jwt_data(&self) -> AccountInfo {
  226. // Refer to https://jwt.io/
  227. // header is before dot, signature is after dot.
  228. // data is sandwiched in the middle, and it's all we care about
  229. let data = self.token.split(".").collect::<Vec<&str>>()[1];
  230. let data_vec = base64::decode(data).unwrap();
  231. from_slice::<AccountInfo>(&data_vec).unwrap()
  232. }
  233. }
  234. #[derive(Deserialize, Serialize, Clone)]
  235. pub(crate) struct ProgressionLoginPayload {
  236. #[serde(rename = "Token")]
  237. pub token: String,
  238. #[serde(rename = "ClientVersion")]
  239. pub client_version: String,
  240. }
  241. #[derive(Deserialize, Serialize, Clone, Debug)]
  242. pub(crate) struct ProgressionLoginResponse {
  243. #[serde(rename = "success")]
  244. pub success: bool,
  245. #[serde(rename = "error")]
  246. pub error: Option<String>,
  247. #[serde(rename = "token")]
  248. pub token: Option<String>,
  249. #[serde(rename = "serverToken")]
  250. pub server_token: Option<String>,
  251. }
  252. #[derive(Deserialize, Serialize, Clone, Debug)]
  253. pub(crate) struct RefreshTokenPayload {
  254. #[serde(rename = "Target")]
  255. pub target: String, // "Techblox"
  256. #[serde(rename = "RefreshToken")]
  257. pub refresh_token: String,
  258. #[serde(rename = "PublicId")]
  259. pub public_id: String,
  260. }
  261. /// Robocraft2 account information.
  262. #[derive(Deserialize, Serialize, Clone, Debug)]
  263. pub struct AccountInfo {
  264. /// User's public ID
  265. #[serde(rename = "PublicId")]
  266. pub public_id: String,
  267. /// Account display name
  268. #[serde(rename = "DisplayName")]
  269. pub display_name: String,
  270. /// Account GUID, or display name for older accounts
  271. #[serde(rename = "RobocraftName")]
  272. pub robocraft_name: String,
  273. /// ??? is confirmed?
  274. #[serde(rename = "Confirmed")]
  275. pub confirmed: bool,
  276. /// Freejam support code
  277. #[serde(rename = "SupportCode")]
  278. pub support_code: String,
  279. /// User's email address
  280. #[serde(rename = "EmailAddress")]
  281. pub email_address: String,
  282. /// Email address is verified?
  283. #[serde(rename = "EmailVerified")]
  284. pub email_verified: bool,
  285. /// Account creation date
  286. #[serde(rename = "CreatedDate")]
  287. pub created_date: String,
  288. /// Owned products (?)
  289. #[serde(rename = "Products")]
  290. pub products: Vec<String>,
  291. /// Account flags
  292. #[serde(rename = "Flags")]
  293. pub flags: Vec<String>,
  294. /// Account has a password?
  295. #[serde(rename = "HasPassword")]
  296. pub has_password: bool,
  297. /// Mailing lists that the account is signed up for
  298. #[serde(rename = "MailingLists")]
  299. pub mailing_lists: Vec<String>,
  300. /// Is Steam account? (always false)
  301. #[serde(rename = "HasSteam")]
  302. pub has_steam: bool,
  303. /// iss (?)
  304. #[serde(rename = "iss")]
  305. pub iss: String,
  306. /// sub (?)
  307. #[serde(rename = "sub")]
  308. pub sub: String,
  309. /// Token created at (unix time) (?)
  310. #[serde(rename = "iat")]
  311. pub iat: u64,
  312. /// Token expiry (unix time) (?)
  313. #[serde(rename = "exp")]
  314. pub exp: u64,
  315. }