diff --git a/Cargo.lock b/Cargo.lock index ad79357d7..47156347d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4685,7 +4685,7 @@ dependencies = [ [[package]] name = "theseus" -version = "0.6.2" +version = "0.6.3" dependencies = [ "async-recursion", "async-tungstenite", @@ -4733,7 +4733,7 @@ dependencies = [ [[package]] name = "theseus_cli" -version = "0.6.2" +version = "0.6.3" dependencies = [ "argh", "color-eyre", @@ -4760,7 +4760,7 @@ dependencies = [ [[package]] name = "theseus_gui" -version = "0.6.2" +version = "0.6.3" dependencies = [ "chrono", "cocoa", diff --git a/theseus/Cargo.toml b/theseus/Cargo.toml index aa29389d6..c2c35d954 100644 --- a/theseus/Cargo.toml +++ b/theseus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "theseus" -version = "0.6.2" +version = "0.6.3" authors = ["Jai A "] edition = "2018" diff --git a/theseus/src/api/hydra/complete.rs b/theseus/src/api/hydra/complete.rs index 7b60a6e89..e42d1f375 100644 --- a/theseus/src/api/hydra/complete.rs +++ b/theseus/src/api/hydra/complete.rs @@ -41,7 +41,7 @@ pub async fn wait_finish(device_code: String) -> crate::Result { } xsts_token::XSTSResponse::Success { token: xsts_token } => { // Get xsts bearer token from xsts token - let bearer_token = + let (bearer_token, expires_in) = bearer_token::fetch_bearer(&xsts_token, &xbl_token.uhs) .await .map_err(|err| { @@ -63,8 +63,7 @@ pub async fn wait_finish(device_code: String) -> crate::Result { player_info.name, bearer_token, oauth.refresh_token, - chrono::Utc::now() - + chrono::Duration::seconds(oauth.expires_in), + chrono::Utc::now() + chrono::Duration::seconds(expires_in), ); // Put credentials into state diff --git a/theseus/src/api/hydra/init.rs b/theseus/src/api/hydra/init.rs index 324bb727a..e0f34d7ad 100644 --- a/theseus/src/api/hydra/init.rs +++ b/theseus/src/api/hydra/init.rs @@ -1,6 +1,4 @@ //! Login route for Hydra, redirects to the Microsoft login page before going to the redirect route -use std::collections::HashMap; - use serde::{Deserialize, Serialize}; use crate::{hydra::MicrosoftError, util::fetch::REQWEST_CLIENT}; @@ -19,17 +17,21 @@ pub struct DeviceLoginSuccess { pub async fn init() -> crate::Result { // Get the initial URL - let client_id = MICROSOFT_CLIENT_ID; - // Get device code // Define the parameters - let mut params = HashMap::new(); - params.insert("client_id", client_id); - params.insert("scope", "XboxLive.signin offline_access"); // urlencoding::encode("XboxLive.signin offline_access")); - let resp = auth_retry(|| REQWEST_CLIENT.post("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode") - .header("Content-Type", "application/x-www-form-urlencoded").form(¶ms).send()).await?; + let resp = auth_retry(|| REQWEST_CLIENT.get("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode") + .header("Content-Length", "0") + .query(&[ + ("client_id", MICROSOFT_CLIENT_ID), + ( + "scope", + "XboxLive.signin XboxLive.offline_access profile openid email", + ), + ]) + .send() + ).await?; match resp.status() { reqwest::StatusCode::OK => Ok(resp.json().await?), diff --git a/theseus/src/api/hydra/refresh.rs b/theseus/src/api/hydra/refresh.rs index 9fb3c8fc4..b8834ecc5 100644 --- a/theseus/src/api/hydra/refresh.rs +++ b/theseus/src/api/hydra/refresh.rs @@ -24,6 +24,10 @@ pub async fn refresh(refresh_token: String) -> crate::Result { params.insert("grant_type", "refresh_token"); params.insert("client_id", MICROSOFT_CLIENT_ID); params.insert("refresh_token", &refresh_token); + params.insert( + "redirect_uri", + "https://login.microsoftonline.com/common/oauth2/nativeclient", + ); // Poll the URL in a loop until we are successful. // On an authorization_pending response, wait 5 seconds and try again. diff --git a/theseus/src/api/hydra/stages/bearer_token.rs b/theseus/src/api/hydra/stages/bearer_token.rs index 68ef855c4..4c6600189 100644 --- a/theseus/src/api/hydra/stages/bearer_token.rs +++ b/theseus/src/api/hydra/stages/bearer_token.rs @@ -1,19 +1,29 @@ +use serde::Deserialize; use serde_json::json; use super::auth_retry; const MCSERVICES_AUTH_URL: &str = - "https://api.minecraftservices.com/launcher/login"; + "https://api.minecraftservices.com/authentication/login_with_xbox"; + +#[derive(Deserialize)] +pub struct BearerTokenResponse { + access_token: String, + expires_in: i64, +} #[tracing::instrument] -pub async fn fetch_bearer(token: &str, uhs: &str) -> crate::Result { +pub async fn fetch_bearer( + token: &str, + uhs: &str, +) -> crate::Result<(String, i64)> { let body = auth_retry(|| { let client = reqwest::Client::new(); client .post(MCSERVICES_AUTH_URL) + .header("Accept", "application/json") .json(&json!({ - "xtoken": format!("XBL3.0 x={};{}", uhs, token), - "platform": "PC_LAUNCHER" + "identityToken": format!("XBL3.0 x={};{}", uhs, token), })) .send() }) @@ -21,14 +31,12 @@ pub async fn fetch_bearer(token: &str, uhs: &str) -> crate::Result { .text() .await?; - serde_json::from_str::(&body)? - .get("access_token") - .and_then(serde_json::Value::as_str) - .map(String::from) - .ok_or( + serde_json::from_str::(&body) + .map(|x| (x.access_token, x.expires_in)) + .map_err(|_| { crate::ErrorKind::HydraError(format!( "Response didn't contain valid bearer token. body: {body}" )) - .into(), - ) + .into() + }) } diff --git a/theseus/src/api/hydra/stages/mod.rs b/theseus/src/api/hydra/stages/mod.rs index 5bb58dcc6..b24421707 100644 --- a/theseus/src/api/hydra/stages/mod.rs +++ b/theseus/src/api/hydra/stages/mod.rs @@ -3,7 +3,7 @@ use futures::Future; use reqwest::Response; -const RETRY_COUNT: usize = 2; // Does command 3 times +const RETRY_COUNT: usize = 9; // Does command 3 times const RETRY_WAIT: std::time::Duration = std::time::Duration::from_secs(2); pub mod bearer_token; diff --git a/theseus/src/api/hydra/stages/player_info.rs b/theseus/src/api/hydra/stages/player_info.rs index 1383cd25c..a78ddcdb9 100644 --- a/theseus/src/api/hydra/stages/player_info.rs +++ b/theseus/src/api/hydra/stages/player_info.rs @@ -24,6 +24,14 @@ impl Default for PlayerInfo { #[tracing::instrument] pub async fn fetch_info(token: &str) -> crate::Result { + auth_retry(|| { + REQWEST_CLIENT + .get("https://api.minecraftservices.com/entitlements/mcstore") + .bearer_auth(token) + .send() + }) + .await?; + let response = auth_retry(|| { REQWEST_CLIENT .get(PROFILE_URL) diff --git a/theseus/src/api/hydra/stages/poll_response.rs b/theseus/src/api/hydra/stages/poll_response.rs index 84c59d57e..102d098b0 100644 --- a/theseus/src/api/hydra/stages/poll_response.rs +++ b/theseus/src/api/hydra/stages/poll_response.rs @@ -25,6 +25,10 @@ pub async fn poll_response(device_code: String) -> crate::Result { params.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); params.insert("client_id", MICROSOFT_CLIENT_ID); params.insert("device_code", &device_code); + params.insert( + "scope", + "XboxLive.signin XboxLive.offline_access profile openid email", + ); // Poll the URL in a loop until we are successful. // On an authorization_pending response, wait 5 seconds and try again. @@ -34,7 +38,6 @@ pub async fn poll_response(device_code: String) -> crate::Result { .post( "https://login.microsoftonline.com/consumers/oauth2/v2.0/token", ) - .header("Content-Type", "application/x-www-form-urlencoded") .form(¶ms) .send() }) diff --git a/theseus/src/api/hydra/stages/xbl_signin.rs b/theseus/src/api/hydra/stages/xbl_signin.rs index 7f4c048e6..6f2a7c025 100644 --- a/theseus/src/api/hydra/stages/xbl_signin.rs +++ b/theseus/src/api/hydra/stages/xbl_signin.rs @@ -19,7 +19,6 @@ pub async fn login_xbl(token: &str) -> crate::Result { REQWEST_CLIENT .post(XBL_AUTH_URL) .header(reqwest::header::ACCEPT, "application/json") - .header("x-xbl-contract-version", "1") .json(&json!({ "Properties": { "AuthMethod": "RPS", diff --git a/theseus/src/api/profile/create.rs b/theseus/src/api/profile/create.rs index 803df3e47..770da0523 100644 --- a/theseus/src/api/profile/create.rs +++ b/theseus/src/api/profile/create.rs @@ -272,8 +272,8 @@ pub(crate) async fn get_loader_version_from_loader( let loader_version = loaders .iter() + .find(|&x| filter(x)) .cloned() - .find(filter) .or( // If stable was searched for but not found, return latest by default if version == "stable" { diff --git a/theseus/src/launcher/auth.rs b/theseus/src/launcher/auth.rs index cc9304d91..8f6062649 100644 --- a/theseus/src/launcher/auth.rs +++ b/theseus/src/launcher/auth.rs @@ -64,7 +64,7 @@ pub async fn refresh_credentials( .as_error()) } xsts_token::XSTSResponse::Success { token: xsts_token } => { - let bearer_token = + let (bearer_token, expires_in) = bearer_token::fetch_bearer(&xsts_token, &xbl_token.uhs) .await .map_err(|err| { @@ -76,8 +76,7 @@ pub async fn refresh_credentials( credentials.access_token = bearer_token; credentials.refresh_token = oauth.refresh_token; - credentials.expires = - Utc::now() + Duration::seconds(oauth.expires_in); + credentials.expires = Utc::now() + Duration::seconds(expires_in); } } diff --git a/theseus/src/launcher/mod.rs b/theseus/src/launcher/mod.rs index 08ab2e877..103caa845 100644 --- a/theseus/src/launcher/mod.rs +++ b/theseus/src/launcher/mod.rs @@ -177,7 +177,7 @@ pub async fn install_minecraft( let state = State::get().await?; let instance_path = - &io::canonicalize(&profile.get_profile_full_path().await?)?; + &io::canonicalize(profile.get_profile_full_path().await?)?; let metadata = state.metadata.read().await; let version_index = metadata diff --git a/theseus_cli/Cargo.toml b/theseus_cli/Cargo.toml index 2ba2f838b..8f3eb0b05 100644 --- a/theseus_cli/Cargo.toml +++ b/theseus_cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "theseus_cli" -version = "0.6.2" +version = "0.6.3" authors = ["Jai A "] edition = "2018" diff --git a/theseus_cli/src/subcommands/profile.rs b/theseus_cli/src/subcommands/profile.rs index 0d046aefd..3aebe1b23 100644 --- a/theseus_cli/src/subcommands/profile.rs +++ b/theseus_cli/src/subcommands/profile.rs @@ -175,12 +175,13 @@ impl ProfileInit { .ok_or_else(|| eyre::eyre!("Modloader {loader} unsupported for Minecraft version {game_version}"))? .loaders; - let loader_version = - loaders.iter().cloned().find(filter).ok_or_else(|| { - eyre::eyre!( - "Invalid version {version} for modloader {loader}" - ) - })?; + let loader_version = loaders + .iter() + .find(|&x| filter(x)) + .cloned() + .ok_or_else(|| { + eyre::eyre!("Invalid version {version} for modloader {loader}") + })?; Some((loader_version, loader)) } else { diff --git a/theseus_gui/package.json b/theseus_gui/package.json index 35bea5b06..7104f500a 100644 --- a/theseus_gui/package.json +++ b/theseus_gui/package.json @@ -1,7 +1,7 @@ { "name": "theseus_gui", "private": true, - "version": "0.6.2", + "version": "0.6.3", "type": "module", "scripts": { "dev": "vite", diff --git a/theseus_gui/src-tauri/Cargo.toml b/theseus_gui/src-tauri/Cargo.toml index 3ad490c75..faff9fb42 100644 --- a/theseus_gui/src-tauri/Cargo.toml +++ b/theseus_gui/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "theseus_gui" -version = "0.6.2" +version = "0.6.3" description = "A Tauri App" authors = ["you"] license = "" diff --git a/theseus_gui/src-tauri/tauri.conf.json b/theseus_gui/src-tauri/tauri.conf.json index 489458909..cee7f8641 100644 --- a/theseus_gui/src-tauri/tauri.conf.json +++ b/theseus_gui/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "Modrinth App", - "version": "0.6.2" + "version": "0.6.3" }, "tauri": { "allowlist": { diff --git a/theseus_gui/src/components/ui/AccountsCard.vue b/theseus_gui/src/components/ui/AccountsCard.vue index 7c2860056..a5443b1e2 100644 --- a/theseus_gui/src/components/ui/AccountsCard.vue +++ b/theseus_gui/src/components/ui/AccountsCard.vue @@ -42,7 +42,7 @@