Skip to content

Commit

Permalink
Auth retrying, std logs (#879)
Browse files Browse the repository at this point in the history
  • Loading branch information
thesuzerain committed Nov 18, 2023
1 parent 01ab507 commit 25662d1
Show file tree
Hide file tree
Showing 20 changed files with 378 additions and 113 deletions.
12 changes: 6 additions & 6 deletions theseus/src/api/hydra/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};

use crate::{hydra::MicrosoftError, util::fetch::REQWEST_CLIENT};

use super::MICROSOFT_CLIENT_ID;
use super::{stages::auth_retry, MICROSOFT_CLIENT_ID};

#[derive(Serialize, Deserialize, Debug)]
pub struct DeviceLoginSuccess {
Expand All @@ -28,13 +28,13 @@ pub async fn init() -> crate::Result<DeviceLoginSuccess> {
params.insert("scope", "XboxLive.signin offline_access");

// urlencoding::encode("XboxLive.signin offline_access"));
let req = REQWEST_CLIENT.post("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
.header("Content-Type", "application/x-www-form-urlencoded").form(&params).send().await?;
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(&params).send()).await?;

match req.status() {
reqwest::StatusCode::OK => Ok(req.json().await?),
match resp.status() {
reqwest::StatusCode::OK => Ok(resp.json().await?),
_ => {
let microsoft_error = req.json::<MicrosoftError>().await?;
let microsoft_error = resp.json::<MicrosoftError>().await?;
Err(crate::ErrorKind::HydraError(format!(
"Error from Microsoft: {:?}",
microsoft_error.error_description
Expand Down
7 changes: 6 additions & 1 deletion theseus/src/api/hydra/refresh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use crate::{
util::fetch::REQWEST_CLIENT,
};

use super::stages::auth_retry;

#[derive(Debug, Deserialize)]
pub struct OauthSuccess {
pub token_type: String,
Expand All @@ -25,11 +27,14 @@ pub async fn refresh(refresh_token: String) -> crate::Result<OauthSuccess> {

// Poll the URL in a loop until we are successful.
// On an authorization_pending response, wait 5 seconds and try again.
let resp = REQWEST_CLIENT
let resp =
auth_retry(|| {
REQWEST_CLIENT
.post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
.header("Content-Type", "application/x-www-form-urlencoded")
.form(&params)
.send()
})
.await?;

match resp.status() {
Expand Down
27 changes: 16 additions & 11 deletions theseus/src/api/hydra/stages/bearer_token.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
use serde_json::json;

use super::auth_retry;

const MCSERVICES_AUTH_URL: &str =
"https://api.minecraftservices.com/launcher/login";

#[tracing::instrument]
pub async fn fetch_bearer(token: &str, uhs: &str) -> crate::Result<String> {
let client = reqwest::Client::new();
let body = client
.post(MCSERVICES_AUTH_URL)
.json(&json!({
"xtoken": format!("XBL3.0 x={};{}", uhs, token),
"platform": "PC_LAUNCHER"
}))
.send()
.await?
.text()
.await?;
let body = auth_retry(|| {
let client = reqwest::Client::new();
client
.post(MCSERVICES_AUTH_URL)
.json(&json!({
"xtoken": format!("XBL3.0 x={};{}", uhs, token),
"platform": "PC_LAUNCHER"
}))
.send()
})
.await?
.text()
.await?;

serde_json::from_str::<serde_json::Value>(&body)?
.get("access_token")
Expand Down
30 changes: 30 additions & 0 deletions theseus/src/api/hydra/stages/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
//! MSA authentication stages

use futures::Future;
use reqwest::Response;

const RETRY_COUNT: usize = 2; // Does command 3 times
const RETRY_WAIT: std::time::Duration = std::time::Duration::from_secs(2);

pub mod bearer_token;
pub mod player_info;
pub mod poll_response;
pub mod xbl_signin;
pub mod xsts_token;

#[tracing::instrument(skip(reqwest_request))]
pub async fn auth_retry<F>(
reqwest_request: impl Fn() -> F,
) -> crate::Result<reqwest::Response>
where
F: Future<Output = Result<Response, reqwest::Error>>,
{
let mut resp = reqwest_request().await?;
for i in 0..RETRY_COUNT {
if resp.status().is_success() {
break;
}
tracing::debug!(
"Request failed with status code {}, retrying...",
resp.status()
);
if i < RETRY_COUNT - 1 {
tokio::time::sleep(RETRY_WAIT).await;
}
resp = reqwest_request().await?;
}
Ok(resp)
}
23 changes: 14 additions & 9 deletions theseus/src/api/hydra/stages/player_info.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
//! Fetch player info for display
use serde::Deserialize;

use crate::util::fetch::REQWEST_CLIENT;

use super::auth_retry;

const PROFILE_URL: &str = "https://api.minecraftservices.com/minecraft/profile";

#[derive(Deserialize)]
Expand All @@ -18,16 +22,17 @@ impl Default for PlayerInfo {
}
}

#[tracing::instrument]
pub async fn fetch_info(token: &str) -> crate::Result<PlayerInfo> {
let client = reqwest::Client::new();
let resp = client
.get(PROFILE_URL)
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
.send()
.await?
.error_for_status()?
.json()
.await?;
let response = auth_retry(|| {
REQWEST_CLIENT
.get(PROFILE_URL)
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
.send()
})
.await?;

let resp = response.error_for_status()?.json().await?;

Ok(resp)
}
9 changes: 7 additions & 2 deletions theseus/src/api/hydra/stages/poll_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use crate::{
util::fetch::REQWEST_CLIENT,
};

use super::auth_retry;

#[derive(Debug, Deserialize)]
pub struct OauthSuccess {
pub token_type: String,
Expand All @@ -17,6 +19,7 @@ pub struct OauthSuccess {
pub refresh_token: String,
}

#[tracing::instrument]
pub async fn poll_response(device_code: String) -> crate::Result<OauthSuccess> {
let mut params = HashMap::new();
params.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
Expand All @@ -26,14 +29,16 @@ pub async fn poll_response(device_code: String) -> crate::Result<OauthSuccess> {
// Poll the URL in a loop until we are successful.
// On an authorization_pending response, wait 5 seconds and try again.
loop {
let resp = REQWEST_CLIENT
let resp = auth_retry(|| {
REQWEST_CLIENT
.post(
"https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
)
.header("Content-Type", "application/x-www-form-urlencoded")
.form(&params)
.send()
.await?;
})
.await?;

match resp.status() {
StatusCode::OK => {
Expand Down
41 changes: 23 additions & 18 deletions theseus/src/api/hydra/stages/xbl_signin.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use serde_json::json;

use crate::util::fetch::REQWEST_CLIENT;

use super::auth_retry;

const XBL_AUTH_URL: &str = "https://user.auth.xboxlive.com/user/authenticate";

// Deserialization
Expand All @@ -9,25 +13,26 @@ pub struct XBLLogin {
}

// Impl
#[tracing::instrument]
pub async fn login_xbl(token: &str) -> crate::Result<XBLLogin> {
let client = reqwest::Client::new();
let body = client
.post(XBL_AUTH_URL)
.header(reqwest::header::ACCEPT, "application/json")
.header("x-xbl-contract-version", "1")
.json(&json!({
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": format!("d={token}")
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}))
.send()
.await?
.text()
.await?;
let response = auth_retry(|| {
REQWEST_CLIENT
.post(XBL_AUTH_URL)
.header(reqwest::header::ACCEPT, "application/json")
.header("x-xbl-contract-version", "1")
.json(&json!({
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": format!("d={token}")
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}))
.send()
})
.await?;
let body = response.text().await?;

let json = serde_json::from_str::<serde_json::Value>(&body)?;
let token = Some(&json)
Expand Down
38 changes: 22 additions & 16 deletions theseus/src/api/hydra/stages/xsts_token.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
use serde_json::json;

use crate::util::fetch::REQWEST_CLIENT;

use super::auth_retry;

const XSTS_AUTH_URL: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";

pub enum XSTSResponse {
Unauthorized(String),
Success { token: String },
}

#[tracing::instrument]
pub async fn fetch_token(token: &str) -> crate::Result<XSTSResponse> {
let client = reqwest::Client::new();
let resp = client
.post(XSTS_AUTH_URL)
.header(reqwest::header::ACCEPT, "application/json")
.json(&json!({
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
token
]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}))
.send()
.await?;
let resp = auth_retry(|| {
REQWEST_CLIENT
.post(XSTS_AUTH_URL)
.header(reqwest::header::ACCEPT, "application/json")
.json(&json!({
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
token
]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}))
.send()
})
.await?;
let status = resp.status();

let body = resp.text().await?;
Expand Down
35 changes: 24 additions & 11 deletions theseus/src/api/logs.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::io::{Read, SeekFrom};

use crate::{
prelude::Credentials,
prelude::{Credentials, DirectoryInfo},
util::io::{self, IOError},
{state::ProfilePathId, State},
};
Expand Down Expand Up @@ -74,7 +74,6 @@ pub async fn get_logs(
profile_path: ProfilePathId,
clear_contents: Option<bool>,
) -> crate::Result<Vec<Logs>> {
let state = State::get().await?;
let profile_path =
if let Some(p) = crate::profile::get(&profile_path, None).await? {
p.profile_id()
Expand All @@ -85,7 +84,7 @@ pub async fn get_logs(
.into());
};

let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
let mut logs = Vec::new();
if logs_folder.exists() {
for entry in std::fs::read_dir(&logs_folder)
Expand Down Expand Up @@ -138,8 +137,7 @@ pub async fn get_output_by_filename(
file_name: &str,
) -> crate::Result<CensoredString> {
let state = State::get().await?;
let logs_folder =
state.directories.profile_logs_dir(profile_subpath).await?;
let logs_folder = DirectoryInfo::profile_logs_dir(profile_subpath).await?;
let path = logs_folder.join(file_name);

let credentials: Vec<Credentials> =
Expand Down Expand Up @@ -201,8 +199,7 @@ pub async fn delete_logs(profile_path: ProfilePathId) -> crate::Result<()> {
.into());
};

let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
for entry in std::fs::read_dir(&logs_folder)
.map_err(|e| IOError::with_path(e, &logs_folder))?
{
Expand Down Expand Up @@ -230,8 +227,7 @@ pub async fn delete_logs_by_filename(
.into());
};

let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
let path = logs_folder.join(filename);
io::remove_dir_all(&path).await?;
Ok(())
Expand All @@ -240,6 +236,23 @@ pub async fn delete_logs_by_filename(
#[tracing::instrument]
pub async fn get_latest_log_cursor(
profile_path: ProfilePathId,
cursor: u64, // 0 to start at beginning of file
) -> crate::Result<LatestLogCursor> {
get_generic_live_log_cursor(profile_path, "latest.log", cursor).await
}

#[tracing::instrument]
pub async fn get_std_log_cursor(
profile_path: ProfilePathId,
cursor: u64, // 0 to start at beginning of file
) -> crate::Result<LatestLogCursor> {
get_generic_live_log_cursor(profile_path, "latest_stdout.log", cursor).await
}

#[tracing::instrument]
pub async fn get_generic_live_log_cursor(
profile_path: ProfilePathId,
log_file_name: &str,
mut cursor: u64, // 0 to start at beginning of file
) -> crate::Result<LatestLogCursor> {
let profile_path =
Expand All @@ -253,8 +266,8 @@ pub async fn get_latest_log_cursor(
};

let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
let path = logs_folder.join("latest.log");
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
let path = logs_folder.join(log_file_name);
if !path.exists() {
// Allow silent failure if latest.log doesn't exist (as the instance may have been launched, but not yet created the file)
return Ok(LatestLogCursor {
Expand Down
Loading

0 comments on commit 25662d1

Please sign in to comment.