Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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