|
|
@@ -1,37 +1,46 @@ |
|
|
|
use std::sync::RwLock; |
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize}; |
|
|
|
use ureq::{Agent, Error}; |
|
|
|
use serde_json::to_string; |
|
|
|
//use ureq::{Agent, Error, AgentBuilder}; |
|
|
|
use reqwest::{Client, Error}; |
|
|
|
//use cookie_store::CookieStore; |
|
|
|
//use url::{Url}; |
|
|
|
use serde_json::from_slice; |
|
|
|
|
|
|
|
use crate::robocraft::{ITokenProvider, account::AuthenticationResponseInfo, AccountInfo}; |
|
|
|
/// Token generator for authenticated API endpoints |
|
|
|
#[async_trait::async_trait] |
|
|
|
pub trait ITokenProvider { |
|
|
|
/// Retrieve the token to use |
|
|
|
async fn token(&mut self) -> Result<String, ()>; |
|
|
|
} |
|
|
|
|
|
|
|
/// Token provider for an existing Freejam account, authenticated through the web browser portal. |
|
|
|
/// |
|
|
|
/// Steam and Epic accounts are not supported. |
|
|
|
pub struct PortalTokenProvider { |
|
|
|
/// Login token |
|
|
|
token: RwLock<ProgressionLoginResponse>, |
|
|
|
token: ProgressionLoginResponse, |
|
|
|
/// User info token |
|
|
|
jwt: AuthenticationResponseInfo, |
|
|
|
jwt: PortalCheckResponse, |
|
|
|
/// Ureq HTTP client |
|
|
|
client: Agent, |
|
|
|
client: Client, |
|
|
|
} |
|
|
|
|
|
|
|
impl PortalTokenProvider { |
|
|
|
pub fn portal() -> Result<Self, Error> { |
|
|
|
Self::target("Techblox".to_owned()) |
|
|
|
/// Login through the web browser portal |
|
|
|
pub async fn portal() -> Result<Self, Error> { |
|
|
|
Self::target("Techblox".to_owned()).await |
|
|
|
} |
|
|
|
|
|
|
|
pub fn target(value: String) -> Result<Self, Error> { |
|
|
|
let client = Agent::new(); |
|
|
|
/// Login through the portal with a custom target value |
|
|
|
pub async fn target(value: String) -> Result<Self, Error> { |
|
|
|
let client = Client::new(); |
|
|
|
let payload = PortalStartPayload { |
|
|
|
target: value, |
|
|
|
}; |
|
|
|
let start_response = client.post("https://account.freejamgames.com/api/authenticate/portal/start") |
|
|
|
.set("Content-Type", "application/json") |
|
|
|
.send_string(&to_string(&payload).unwrap())?; |
|
|
|
let start_res = start_response.into_json::<PortalStartResponse>()?; |
|
|
|
.header("Content-Type", "application/json") |
|
|
|
.json(&payload) |
|
|
|
.send().await?; |
|
|
|
let start_res = start_response.json::<PortalStartResponse>().await?; |
|
|
|
|
|
|
|
println!("GO TO https://account.freejamgames.com/login?theme=rc2&redirect_url=portal?theme=rc2%26portalToken={}", start_res.token); |
|
|
|
|
|
|
@@ -39,42 +48,117 @@ impl PortalTokenProvider { |
|
|
|
token: start_res.token, |
|
|
|
}; |
|
|
|
let mut check_response = client.post("https://account.freejamgames.com/api/authenticate/portal/check") |
|
|
|
.set("Content-Type", "application/json") |
|
|
|
.send_json(&payload)?; |
|
|
|
//.send_string(&to_string(&payload).unwrap())?; |
|
|
|
.header("Content-Type", "application/json") |
|
|
|
.json(&payload) |
|
|
|
.send().await?; |
|
|
|
let mut auth_complete = check_response.status() == 200; |
|
|
|
while !auth_complete { |
|
|
|
check_response = client.post("https://account.freejamgames.com/api/authenticate/portal/check") |
|
|
|
.set("Content-Type", "application/json") |
|
|
|
.send_json(&payload)?; |
|
|
|
.header("Content-Type", "application/json") |
|
|
|
.json(&payload) |
|
|
|
.send().await?; |
|
|
|
auth_complete = check_response.status() == 200; |
|
|
|
} |
|
|
|
let check_res = check_response.into_json::<AuthenticationResponseInfo>()?; |
|
|
|
let check_res = check_response.json::<PortalCheckResponse>().await?; |
|
|
|
|
|
|
|
// login with token we just got |
|
|
|
Self::login_internal(check_res, client).await |
|
|
|
} |
|
|
|
|
|
|
|
pub async fn with_email(email: &str, password: &str) -> Result<Self, Error> { |
|
|
|
let client = Client::new(); |
|
|
|
let payload = AuthenticationEmailPayload { |
|
|
|
email_address: email.to_string(), |
|
|
|
password: password.to_string(), |
|
|
|
}; |
|
|
|
let response = client.post("https://account.freejamgames.com/api/authenticate/email/web") |
|
|
|
.header("Content-Type", "application/json") |
|
|
|
.json(&payload) |
|
|
|
.send().await?; |
|
|
|
let json_res = response.json::<AuthenticationResponseInfo>().await?; |
|
|
|
Self::auto_portal(client, "Techblox".to_owned(), json_res.token).await |
|
|
|
} |
|
|
|
|
|
|
|
pub async fn with_username(username: &str, password: &str) -> Result<Self, Error> { |
|
|
|
let client = Client::new(); |
|
|
|
let payload = AuthenticationUsernamePayload { |
|
|
|
username: username.to_string(), |
|
|
|
password: password.to_string(), |
|
|
|
}; |
|
|
|
let response = client.post("https://account.freejamgames.com/api/authenticate/displayname/web") |
|
|
|
.header("Content-Type", "application/json") |
|
|
|
.json(&payload) |
|
|
|
.send().await?; |
|
|
|
let json_res = response.json::<AuthenticationResponseInfo>().await?; |
|
|
|
Self::auto_portal(client, "Techblox".to_owned(), json_res.token).await |
|
|
|
} |
|
|
|
|
|
|
|
/// Automatically validate portal |
|
|
|
async fn auto_portal(client: Client, value: String, token: String) -> Result<Self, Error> { |
|
|
|
let payload = PortalStartPayload { |
|
|
|
target: value, |
|
|
|
}; |
|
|
|
let start_response = client.post("https://account.freejamgames.com/api/authenticate/portal/start") |
|
|
|
.header("Content-Type", "application/json") |
|
|
|
.json(&payload) |
|
|
|
.send().await?; |
|
|
|
let start_res = start_response.json::<PortalStartResponse>().await?; |
|
|
|
|
|
|
|
let payload = PortalCheckPayload { |
|
|
|
token: start_res.token, |
|
|
|
}; |
|
|
|
|
|
|
|
let _assign_response = client.post("https://account.freejamgames.com/api/authenticate/portal/assign") |
|
|
|
.header("Content-Type", "application/json") |
|
|
|
.header("Authorization", "Web ".to_owned() + &token) |
|
|
|
.json(&payload) |
|
|
|
.send().await?; |
|
|
|
|
|
|
|
let check_response = client.post("https://account.freejamgames.com/api/authenticate/portal/check") |
|
|
|
.header("Content-Type", "application/json") |
|
|
|
.json(&payload) |
|
|
|
.send().await?; |
|
|
|
let check_res = check_response.json::<PortalCheckResponse>().await?; |
|
|
|
|
|
|
|
// login with token we just got |
|
|
|
Self::login_internal(check_res, client).await |
|
|
|
} |
|
|
|
|
|
|
|
async fn login_internal(token_data: PortalCheckResponse, client: Client) -> Result<Self, Error> { |
|
|
|
let payload = ProgressionLoginPayload { |
|
|
|
token: check_res.token.clone(), |
|
|
|
token: token_data.token.clone(), |
|
|
|
}; |
|
|
|
let progress_response = client.post("https://progression.production.robocraft2.com/login/fj") |
|
|
|
.set("Content-Type", "application/json") |
|
|
|
.send_json(&payload)?; |
|
|
|
let progress_res = progress_response.into_json::<ProgressionLoginResponse>()?; |
|
|
|
.header("Content-Type", "application/json") |
|
|
|
.json(&payload) |
|
|
|
.send().await?; |
|
|
|
let progress_res = progress_response.json::<ProgressionLoginResponse>().await?; |
|
|
|
Ok(Self { |
|
|
|
token: RwLock::new(progress_res), |
|
|
|
jwt: check_res, |
|
|
|
token: progress_res, |
|
|
|
jwt: token_data, |
|
|
|
client: client, |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
/// Login using the portal token data from a previous portal authentication |
|
|
|
pub async fn login(token_data: PortalCheckResponse) -> Result<Self, Error> { |
|
|
|
Self::login_internal(token_data, Client::new()).await |
|
|
|
} |
|
|
|
|
|
|
|
pub fn get_account_info(&self) -> Result<AccountInfo, Error> { |
|
|
|
Ok(self.jwt.decode_jwt_data()) |
|
|
|
} |
|
|
|
|
|
|
|
pub fn token_data(&self) -> &'_ PortalCheckResponse { |
|
|
|
&self.jwt |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
#[async_trait::async_trait] |
|
|
|
impl ITokenProvider for PortalTokenProvider { |
|
|
|
fn token(&self) -> Result<String, ()> { |
|
|
|
async fn token(&mut self) -> Result<String, ()> { |
|
|
|
// TODO re-authenticate when expired |
|
|
|
if let Some(token) = self.token.read().map_err(|_| ())?.token.clone() { |
|
|
|
if let Some(token) = self.token.token.clone() { |
|
|
|
Ok(token) |
|
|
|
} else { |
|
|
|
Err(()) |
|
|
@@ -82,6 +166,32 @@ impl ITokenProvider for PortalTokenProvider { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Clone)] |
|
|
|
pub(crate) struct AuthenticationEmailPayload { |
|
|
|
#[serde(rename = "EmailAddress")] |
|
|
|
pub email_address: String, |
|
|
|
#[serde(rename = "Password")] |
|
|
|
pub password: String, |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Clone)] |
|
|
|
pub(crate) struct AuthenticationUsernamePayload { |
|
|
|
#[serde(rename = "DisplayName")] |
|
|
|
pub username: String, |
|
|
|
#[serde(rename = "Password")] |
|
|
|
pub password: String, |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug)] |
|
|
|
pub(crate) struct AuthenticationResponseInfo { |
|
|
|
#[serde(rename = "Token")] |
|
|
|
pub token: String, |
|
|
|
#[serde(rename = "RefreshToken")] |
|
|
|
pub refresh_token: String, |
|
|
|
#[serde(rename = "RefreshTokenExpiry")] |
|
|
|
pub refresh_token_expiry: String, |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Clone)] |
|
|
|
pub(crate) struct PortalStartPayload { |
|
|
|
#[serde(rename = "Target")] |
|
|
@@ -100,6 +210,27 @@ pub(crate) struct PortalCheckPayload { |
|
|
|
pub token: String, |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Debug)] |
|
|
|
pub struct PortalCheckResponse { |
|
|
|
#[serde(rename = "Token")] |
|
|
|
pub token: String, |
|
|
|
#[serde(rename = "RefreshToken")] |
|
|
|
pub refresh_token: String, |
|
|
|
#[serde(rename = "RefreshTokenExpiry")] |
|
|
|
pub refresh_token_expiry: String, |
|
|
|
} |
|
|
|
|
|
|
|
impl PortalCheckResponse { |
|
|
|
pub fn decode_jwt_data(&self) -> AccountInfo { |
|
|
|
// Refer to https://jwt.io/ |
|
|
|
// header is before dot, signature is after dot. |
|
|
|
// data is sandwiched in the middle, and it's all we care about |
|
|
|
let data = self.token.split(".").collect::<Vec<&str>>()[1]; |
|
|
|
let data_vec = base64::decode(data).unwrap(); |
|
|
|
from_slice::<AccountInfo>(&data_vec).unwrap() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Clone)] |
|
|
|
pub(crate) struct ProgressionLoginPayload { |
|
|
|
#[serde(rename = "token")] |
|
|
@@ -117,3 +248,59 @@ pub(crate) struct ProgressionLoginResponse { |
|
|
|
#[serde(rename = "serverToken")] |
|
|
|
pub server_token: Option<String>, |
|
|
|
} |
|
|
|
|
|
|
|
/// Robocraft2 account information. |
|
|
|
#[derive(Deserialize, Serialize, Clone)] |
|
|
|
pub struct AccountInfo { |
|
|
|
/// User's public ID |
|
|
|
#[serde(rename = "PublicId")] |
|
|
|
pub public_id: String, |
|
|
|
/// Account display name |
|
|
|
#[serde(rename = "DisplayName")] |
|
|
|
pub display_name: String, |
|
|
|
/// Account GUID, or display name for older accounts |
|
|
|
#[serde(rename = "RobocraftName")] |
|
|
|
pub robocraft_name: String, |
|
|
|
/// ??? is confirmed? |
|
|
|
#[serde(rename = "Confirmed")] |
|
|
|
pub confirmed: bool, |
|
|
|
/// Freejam support code |
|
|
|
#[serde(rename = "SupportCode")] |
|
|
|
pub support_code: String, |
|
|
|
/// User's email address |
|
|
|
#[serde(rename = "EmailAddress")] |
|
|
|
pub email_address: String, |
|
|
|
/// Email address is verified? |
|
|
|
#[serde(rename = "EmailVerified")] |
|
|
|
pub email_verified: bool, |
|
|
|
/// Account creation date |
|
|
|
#[serde(rename = "CreatedDate")] |
|
|
|
pub created_date: String, |
|
|
|
/// Owned products (?) |
|
|
|
#[serde(rename = "Products")] |
|
|
|
pub products: Vec<String>, |
|
|
|
/// Account flags |
|
|
|
#[serde(rename = "Flags")] |
|
|
|
pub flags: Vec<String>, |
|
|
|
/// Account has a password? |
|
|
|
#[serde(rename = "HasPassword")] |
|
|
|
pub has_password: bool, |
|
|
|
/// Mailing lists that the account is signed up for |
|
|
|
#[serde(rename = "MailingLists")] |
|
|
|
pub mailing_lists: Vec<String>, |
|
|
|
/// Is Steam account? (always false) |
|
|
|
#[serde(rename = "HasSteam")] |
|
|
|
pub has_steam: bool, |
|
|
|
/// iss (?) |
|
|
|
#[serde(rename = "iss")] |
|
|
|
pub iss: String, |
|
|
|
/// sub (?) |
|
|
|
#[serde(rename = "sub")] |
|
|
|
pub sub: String, |
|
|
|
/// Token created at (unix time) (?) |
|
|
|
#[serde(rename = "iat")] |
|
|
|
pub iat: u64, |
|
|
|
/// Token expiry (unix time) (?) |
|
|
|
#[serde(rename = "exp")] |
|
|
|
pub exp: u64, |
|
|
|
} |