An unofficial collection of APIs used in FreeJam games and mods
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

348 linhas
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. }