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.

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