Skip to content

Commit 25662d1

Browse files
authored
Auth retrying, std logs (#879)
1 parent 01ab507 commit 25662d1

File tree

20 files changed

+378
-113
lines changed

20 files changed

+378
-113
lines changed

theseus/src/api/hydra/init.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
55

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

8-
use super::MICROSOFT_CLIENT_ID;
8+
use super::{stages::auth_retry, MICROSOFT_CLIENT_ID};
99

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

3030
// urlencoding::encode("XboxLive.signin offline_access"));
31-
let req = REQWEST_CLIENT.post("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
32-
.header("Content-Type", "application/x-www-form-urlencoded").form(&params).send().await?;
31+
let resp = auth_retry(|| REQWEST_CLIENT.post("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
32+
.header("Content-Type", "application/x-www-form-urlencoded").form(&params).send()).await?;
3333

34-
match req.status() {
35-
reqwest::StatusCode::OK => Ok(req.json().await?),
34+
match resp.status() {
35+
reqwest::StatusCode::OK => Ok(resp.json().await?),
3636
_ => {
37-
let microsoft_error = req.json::<MicrosoftError>().await?;
37+
let microsoft_error = resp.json::<MicrosoftError>().await?;
3838
Err(crate::ErrorKind::HydraError(format!(
3939
"Error from Microsoft: {:?}",
4040
microsoft_error.error_description

theseus/src/api/hydra/refresh.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use crate::{
88
util::fetch::REQWEST_CLIENT,
99
};
1010

11+
use super::stages::auth_retry;
12+
1113
#[derive(Debug, Deserialize)]
1214
pub struct OauthSuccess {
1315
pub token_type: String,
@@ -25,11 +27,14 @@ pub async fn refresh(refresh_token: String) -> crate::Result<OauthSuccess> {
2527

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

3540
match resp.status() {

theseus/src/api/hydra/stages/bearer_token.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
use serde_json::json;
22

3+
use super::auth_retry;
4+
35
const MCSERVICES_AUTH_URL: &str =
46
"https://api.minecraftservices.com/launcher/login";
57

8+
#[tracing::instrument]
69
pub async fn fetch_bearer(token: &str, uhs: &str) -> crate::Result<String> {
7-
let client = reqwest::Client::new();
8-
let body = client
9-
.post(MCSERVICES_AUTH_URL)
10-
.json(&json!({
11-
"xtoken": format!("XBL3.0 x={};{}", uhs, token),
12-
"platform": "PC_LAUNCHER"
13-
}))
14-
.send()
15-
.await?
16-
.text()
17-
.await?;
10+
let body = auth_retry(|| {
11+
let client = reqwest::Client::new();
12+
client
13+
.post(MCSERVICES_AUTH_URL)
14+
.json(&json!({
15+
"xtoken": format!("XBL3.0 x={};{}", uhs, token),
16+
"platform": "PC_LAUNCHER"
17+
}))
18+
.send()
19+
})
20+
.await?
21+
.text()
22+
.await?;
1823

1924
serde_json::from_str::<serde_json::Value>(&body)?
2025
.get("access_token")
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,37 @@
11
//! MSA authentication stages
22
3+
use futures::Future;
4+
use reqwest::Response;
5+
6+
const RETRY_COUNT: usize = 2; // Does command 3 times
7+
const RETRY_WAIT: std::time::Duration = std::time::Duration::from_secs(2);
8+
39
pub mod bearer_token;
410
pub mod player_info;
511
pub mod poll_response;
612
pub mod xbl_signin;
713
pub mod xsts_token;
14+
15+
#[tracing::instrument(skip(reqwest_request))]
16+
pub async fn auth_retry<F>(
17+
reqwest_request: impl Fn() -> F,
18+
) -> crate::Result<reqwest::Response>
19+
where
20+
F: Future<Output = Result<Response, reqwest::Error>>,
21+
{
22+
let mut resp = reqwest_request().await?;
23+
for i in 0..RETRY_COUNT {
24+
if resp.status().is_success() {
25+
break;
26+
}
27+
tracing::debug!(
28+
"Request failed with status code {}, retrying...",
29+
resp.status()
30+
);
31+
if i < RETRY_COUNT - 1 {
32+
tokio::time::sleep(RETRY_WAIT).await;
33+
}
34+
resp = reqwest_request().await?;
35+
}
36+
Ok(resp)
37+
}

theseus/src/api/hydra/stages/player_info.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
//! Fetch player info for display
22
use serde::Deserialize;
33

4+
use crate::util::fetch::REQWEST_CLIENT;
5+
6+
use super::auth_retry;
7+
48
const PROFILE_URL: &str = "https://api.minecraftservices.com/minecraft/profile";
59

610
#[derive(Deserialize)]
@@ -18,16 +22,17 @@ impl Default for PlayerInfo {
1822
}
1923
}
2024

25+
#[tracing::instrument]
2126
pub async fn fetch_info(token: &str) -> crate::Result<PlayerInfo> {
22-
let client = reqwest::Client::new();
23-
let resp = client
24-
.get(PROFILE_URL)
25-
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
26-
.send()
27-
.await?
28-
.error_for_status()?
29-
.json()
30-
.await?;
27+
let response = auth_retry(|| {
28+
REQWEST_CLIENT
29+
.get(PROFILE_URL)
30+
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
31+
.send()
32+
})
33+
.await?;
34+
35+
let resp = response.error_for_status()?.json().await?;
3136

3237
Ok(resp)
3338
}

theseus/src/api/hydra/stages/poll_response.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use crate::{
88
util::fetch::REQWEST_CLIENT,
99
};
1010

11+
use super::auth_retry;
12+
1113
#[derive(Debug, Deserialize)]
1214
pub struct OauthSuccess {
1315
pub token_type: String,
@@ -17,6 +19,7 @@ pub struct OauthSuccess {
1719
pub refresh_token: String,
1820
}
1921

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

3843
match resp.status() {
3944
StatusCode::OK => {

theseus/src/api/hydra/stages/xbl_signin.rs

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
use serde_json::json;
22

3+
use crate::util::fetch::REQWEST_CLIENT;
4+
5+
use super::auth_retry;
6+
37
const XBL_AUTH_URL: &str = "https://user.auth.xboxlive.com/user/authenticate";
48

59
// Deserialization
@@ -9,25 +13,26 @@ pub struct XBLLogin {
913
}
1014

1115
// Impl
16+
#[tracing::instrument]
1217
pub async fn login_xbl(token: &str) -> crate::Result<XBLLogin> {
13-
let client = reqwest::Client::new();
14-
let body = client
15-
.post(XBL_AUTH_URL)
16-
.header(reqwest::header::ACCEPT, "application/json")
17-
.header("x-xbl-contract-version", "1")
18-
.json(&json!({
19-
"Properties": {
20-
"AuthMethod": "RPS",
21-
"SiteName": "user.auth.xboxlive.com",
22-
"RpsTicket": format!("d={token}")
23-
},
24-
"RelyingParty": "http://auth.xboxlive.com",
25-
"TokenType": "JWT"
26-
}))
27-
.send()
28-
.await?
29-
.text()
30-
.await?;
18+
let response = auth_retry(|| {
19+
REQWEST_CLIENT
20+
.post(XBL_AUTH_URL)
21+
.header(reqwest::header::ACCEPT, "application/json")
22+
.header("x-xbl-contract-version", "1")
23+
.json(&json!({
24+
"Properties": {
25+
"AuthMethod": "RPS",
26+
"SiteName": "user.auth.xboxlive.com",
27+
"RpsTicket": format!("d={token}")
28+
},
29+
"RelyingParty": "http://auth.xboxlive.com",
30+
"TokenType": "JWT"
31+
}))
32+
.send()
33+
})
34+
.await?;
35+
let body = response.text().await?;
3136

3237
let json = serde_json::from_str::<serde_json::Value>(&body)?;
3338
let token = Some(&json)

theseus/src/api/hydra/stages/xsts_token.rs

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,35 @@
11
use serde_json::json;
22

3+
use crate::util::fetch::REQWEST_CLIENT;
4+
5+
use super::auth_retry;
6+
37
const XSTS_AUTH_URL: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";
48

59
pub enum XSTSResponse {
610
Unauthorized(String),
711
Success { token: String },
812
}
913

14+
#[tracing::instrument]
1015
pub async fn fetch_token(token: &str) -> crate::Result<XSTSResponse> {
11-
let client = reqwest::Client::new();
12-
let resp = client
13-
.post(XSTS_AUTH_URL)
14-
.header(reqwest::header::ACCEPT, "application/json")
15-
.json(&json!({
16-
"Properties": {
17-
"SandboxId": "RETAIL",
18-
"UserTokens": [
19-
token
20-
]
21-
},
22-
"RelyingParty": "rp://api.minecraftservices.com/",
23-
"TokenType": "JWT"
24-
}))
25-
.send()
26-
.await?;
16+
let resp = auth_retry(|| {
17+
REQWEST_CLIENT
18+
.post(XSTS_AUTH_URL)
19+
.header(reqwest::header::ACCEPT, "application/json")
20+
.json(&json!({
21+
"Properties": {
22+
"SandboxId": "RETAIL",
23+
"UserTokens": [
24+
token
25+
]
26+
},
27+
"RelyingParty": "rp://api.minecraftservices.com/",
28+
"TokenType": "JWT"
29+
}))
30+
.send()
31+
})
32+
.await?;
2733
let status = resp.status();
2834

2935
let body = resp.text().await?;

theseus/src/api/logs.rs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::io::{Read, SeekFrom};
22

33
use crate::{
4-
prelude::Credentials,
4+
prelude::{Credentials, DirectoryInfo},
55
util::io::{self, IOError},
66
{state::ProfilePathId, State},
77
};
@@ -74,7 +74,6 @@ pub async fn get_logs(
7474
profile_path: ProfilePathId,
7575
clear_contents: Option<bool>,
7676
) -> crate::Result<Vec<Logs>> {
77-
let state = State::get().await?;
7877
let profile_path =
7978
if let Some(p) = crate::profile::get(&profile_path, None).await? {
8079
p.profile_id()
@@ -85,7 +84,7 @@ pub async fn get_logs(
8584
.into());
8685
};
8786

88-
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
87+
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
8988
let mut logs = Vec::new();
9089
if logs_folder.exists() {
9190
for entry in std::fs::read_dir(&logs_folder)
@@ -138,8 +137,7 @@ pub async fn get_output_by_filename(
138137
file_name: &str,
139138
) -> crate::Result<CensoredString> {
140139
let state = State::get().await?;
141-
let logs_folder =
142-
state.directories.profile_logs_dir(profile_subpath).await?;
140+
let logs_folder = DirectoryInfo::profile_logs_dir(profile_subpath).await?;
143141
let path = logs_folder.join(file_name);
144142

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

204-
let state = State::get().await?;
205-
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
202+
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
206203
for entry in std::fs::read_dir(&logs_folder)
207204
.map_err(|e| IOError::with_path(e, &logs_folder))?
208205
{
@@ -230,8 +227,7 @@ pub async fn delete_logs_by_filename(
230227
.into());
231228
};
232229

233-
let state = State::get().await?;
234-
let logs_folder = state.directories.profile_logs_dir(&profile_path).await?;
230+
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
235231
let path = logs_folder.join(filename);
236232
io::remove_dir_all(&path).await?;
237233
Ok(())
@@ -240,6 +236,23 @@ pub async fn delete_logs_by_filename(
240236
#[tracing::instrument]
241237
pub async fn get_latest_log_cursor(
242238
profile_path: ProfilePathId,
239+
cursor: u64, // 0 to start at beginning of file
240+
) -> crate::Result<LatestLogCursor> {
241+
get_generic_live_log_cursor(profile_path, "latest.log", cursor).await
242+
}
243+
244+
#[tracing::instrument]
245+
pub async fn get_std_log_cursor(
246+
profile_path: ProfilePathId,
247+
cursor: u64, // 0 to start at beginning of file
248+
) -> crate::Result<LatestLogCursor> {
249+
get_generic_live_log_cursor(profile_path, "latest_stdout.log", cursor).await
250+
}
251+
252+
#[tracing::instrument]
253+
pub async fn get_generic_live_log_cursor(
254+
profile_path: ProfilePathId,
255+
log_file_name: &str,
243256
mut cursor: u64, // 0 to start at beginning of file
244257
) -> crate::Result<LatestLogCursor> {
245258
let profile_path =
@@ -253,8 +266,8 @@ pub async fn get_latest_log_cursor(
253266
};
254267

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

0 commit comments

Comments
 (0)