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.

307 lines
10KB

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