✨ feature(core): improve logging system, fix minecraft auth timestamps
This commit is contained in:
parent
7b3a89e055
commit
a67796bd6d
5 changed files with 344 additions and 147 deletions
|
|
@ -4,93 +4,116 @@ use crate::data::{Credentials, Directories, MinecraftCredentials};
|
|||
use crate::prelude::ClusterPath;
|
||||
use crate::utils::io::{self, IOError};
|
||||
use futures::TryFutureExt;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::Read;
|
||||
use std::time::SystemTime;
|
||||
use tokio::io::{AsyncReadExt, AsyncSeekExt};
|
||||
|
||||
// TODO: put this in the global store
|
||||
/// Core logging state and reader for OneLauncher.
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct LogManager {
|
||||
/// The log file to read as a [`String`]
|
||||
/// Type log type associated with this log file.
|
||||
pub log_type: LogType,
|
||||
/// The age of this log as a [`u64`] in seconds.
|
||||
pub age: u64,
|
||||
/// The log file to read as a [`String`].
|
||||
pub log_file: String,
|
||||
/// The parsed and censored output of the logfile.
|
||||
pub output: Option<LogOutput>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum LogType {
|
||||
Info,
|
||||
Crash,
|
||||
}
|
||||
|
||||
impl LogManager {
|
||||
/// Initialize a new [`LogManager`].
|
||||
async fn initialize(
|
||||
log_type: LogType,
|
||||
age: SystemTime,
|
||||
cluster: &ClusterPath,
|
||||
log_file: String,
|
||||
clear: Option<bool>,
|
||||
) -> crate::Result<Self> {
|
||||
Ok(Self {
|
||||
log_type,
|
||||
age: age
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
|
||||
.as_secs(),
|
||||
output: if clear.unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
Some(get_output_by_file(cluster, &log_file).await?)
|
||||
Some(get_output_by_file(cluster, log_type, &log_file).await?)
|
||||
},
|
||||
log_file,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a [`Vec<LogManager>`] for a specific [`ClusterPath`].
|
||||
/// Verify all [`LogManager`]s of a certain [`LogType`]
|
||||
#[tracing::instrument]
|
||||
pub async fn get_logs(
|
||||
cluster_path: ClusterPath,
|
||||
pub async fn get_logs_by_type(
|
||||
cluster_path: &ClusterPath,
|
||||
log_type: LogType,
|
||||
clear: Option<bool>,
|
||||
) -> crate::Result<Vec<LogManager>> {
|
||||
let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
|
||||
c.cluster_path()
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"cluster {} isn't managed by onelauncher!",
|
||||
cluster_path.to_string()
|
||||
)
|
||||
.into());
|
||||
logs: &mut Vec<crate::Result<LogManager>>,
|
||||
) -> crate::Result<()> {
|
||||
let logs_folder = match log_type {
|
||||
LogType::Info => Directories::cluster_logs_dir(cluster_path).await?,
|
||||
LogType::Crash => Directories::crash_reports_dir(cluster_path).await?,
|
||||
};
|
||||
|
||||
let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
|
||||
let mut logs = Vec::new();
|
||||
if logs_folder.exists() {
|
||||
for entry in
|
||||
std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))?
|
||||
{
|
||||
let entry: std::fs::DirEntry =
|
||||
entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
|
||||
let age = entry
|
||||
.metadata()?
|
||||
.created()
|
||||
.unwrap_or_else(|_| SystemTime::UNIX_EPOCH);
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
if let Some(name) = path.file_name() {
|
||||
let name = name.to_string_lossy().to_string();
|
||||
logs.push(LogManager::initialize(&cluster_path, name, clear).await);
|
||||
|
||||
logs.push(LogManager::initialize(log_type, age, &cluster_path, name, clear).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all [`LogManager`]s in the global [`State`].
|
||||
pub async fn get_logs(
|
||||
cluster_path: ClusterPath,
|
||||
clear: Option<bool>,
|
||||
) -> crate::Result<Vec<LogManager>> {
|
||||
let cluster_path = cluster_path.cluster_path().await?;
|
||||
|
||||
let mut logs = Vec::new();
|
||||
get_logs_by_type(&cluster_path, LogType::Info, clear, &mut logs).await?;
|
||||
get_logs_by_type(&cluster_path, LogType::Crash, clear, &mut logs).await?;
|
||||
|
||||
let mut logs = logs
|
||||
.into_iter()
|
||||
.collect::<crate::Result<Vec<LogManager>>>()?;
|
||||
logs.sort_by_key(|x| x.log_file.clone());
|
||||
logs.sort_by(|a, b| b.age.cmp(&a.age).then(b.log_file.cmp(&a.log_file)));
|
||||
Ok(logs)
|
||||
}
|
||||
|
||||
/// Delete all stored logs from a specific [`ClusterPath`].
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs(cluster_path: ClusterPath) -> crate::Result<()> {
|
||||
let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
|
||||
c.cluster_path()
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"cluster {} isn't managed by onelauncher!",
|
||||
cluster_path.to_string()
|
||||
)
|
||||
.into());
|
||||
};
|
||||
|
||||
let cluster_path = cluster_path.cluster_path().await?;
|
||||
let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
|
||||
for entry in std::fs::read_dir(&logs_folder).map_err(|e| IOError::with_path(e, &logs_folder))? {
|
||||
let entry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
|
||||
|
|
@ -107,22 +130,23 @@ pub async fn delete_logs(cluster_path: ClusterPath) -> crate::Result<()> {
|
|||
#[tracing::instrument]
|
||||
pub async fn get_logs_by_file(
|
||||
cluster_path: ClusterPath,
|
||||
log_type: LogType,
|
||||
log_file: String,
|
||||
) -> crate::Result<LogManager> {
|
||||
let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
|
||||
c.cluster_path()
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"cluster {} isn't managed by onelauncher!",
|
||||
cluster_path.to_string()
|
||||
)
|
||||
.into());
|
||||
};
|
||||
let cluster_path = cluster_path.cluster_path().await?;
|
||||
|
||||
Ok(LogManager {
|
||||
output: Some(get_output_by_file(&cluster_path, &log_file).await?),
|
||||
log_file,
|
||||
})
|
||||
let path = match log_type {
|
||||
LogType::Info => Directories::cluster_logs_dir(&cluster_path).await,
|
||||
LogType::Crash => Directories::crash_reports_dir(&cluster_path).await,
|
||||
}?
|
||||
.join(&log_file);
|
||||
|
||||
let metadata = std::fs::metadata(&path)?;
|
||||
let age = metadata
|
||||
.created()
|
||||
.unwrap_or_else(|_| SystemTime::UNIX_EPOCH);
|
||||
|
||||
LogManager::initialize(log_type, age, &cluster_path, log_file, Some(true)).await
|
||||
}
|
||||
|
||||
/// Get the default [`LogCursor`] for a [`ClusterPath`].
|
||||
|
|
@ -138,16 +162,7 @@ pub async fn get_live_log_cursor(
|
|||
log_file: &str,
|
||||
mut cursor: u64,
|
||||
) -> crate::Result<LogCursor> {
|
||||
let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
|
||||
c.cluster_path()
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"cluster {} isn't managed by onelauncher!",
|
||||
cluster_path.to_string()
|
||||
)
|
||||
.into());
|
||||
};
|
||||
|
||||
let cluster_path = cluster_path.cluster_path().await?;
|
||||
let state = crate::State::get().await?;
|
||||
let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
|
||||
let path = logs_folder.join(log_file);
|
||||
|
|
@ -201,18 +216,17 @@ pub async fn get_live_log_cursor(
|
|||
|
||||
/// Delete a specific minecraft log file.
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_logs_by_file(cluster_path: ClusterPath, log_file: &str) -> crate::Result<()> {
|
||||
let cluster_path = if let Some(c) = crate::cluster::get(&cluster_path, None).await? {
|
||||
c.cluster_path()
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"cluster {} isn't managed by onelauncher!",
|
||||
cluster_path.to_string()
|
||||
)
|
||||
.into());
|
||||
pub async fn delete_logs_by_file(
|
||||
cluster_path: ClusterPath,
|
||||
log_type: LogType,
|
||||
log_file: &str,
|
||||
) -> crate::Result<()> {
|
||||
let cluster_path = cluster_path.cluster_path().await?;
|
||||
let logs_folder = match log_type {
|
||||
LogType::Info => Directories::cluster_logs_dir(&cluster_path).await?,
|
||||
LogType::Crash => Directories::crash_reports_dir(&cluster_path).await?,
|
||||
};
|
||||
|
||||
let logs_folder = Directories::cluster_logs_dir(&cluster_path).await?;
|
||||
let path = logs_folder.join(log_file);
|
||||
io::remove_dir_all(&path).await?;
|
||||
Ok(())
|
||||
|
|
@ -222,10 +236,14 @@ pub async fn delete_logs_by_file(cluster_path: ClusterPath, log_file: &str) -> c
|
|||
#[tracing::instrument]
|
||||
pub async fn get_output_by_file(
|
||||
cluster_path: &ClusterPath,
|
||||
log_type: LogType,
|
||||
log_file: &str,
|
||||
) -> crate::Result<LogOutput> {
|
||||
let state = crate::State::get().await?;
|
||||
let logs_folder = Directories::cluster_logs_dir(cluster_path).await?;
|
||||
let logs_folder = match log_type {
|
||||
LogType::Info => Directories::cluster_logs_dir(cluster_path).await?,
|
||||
LogType::Crash => Directories::crash_reports_dir(cluster_path).await?,
|
||||
};
|
||||
let path = logs_folder.join(log_file);
|
||||
let credentials: Vec<MinecraftCredentials> = state
|
||||
.users
|
||||
|
|
@ -255,7 +273,7 @@ pub async fn get_output_by_file(
|
|||
}
|
||||
|
||||
return Ok(LogOutput::censor_secrets(result, &credentials, None));
|
||||
} else if ext == "log" {
|
||||
} else if ext == "log" || ext == "txt" {
|
||||
while file
|
||||
.read(&mut contents)
|
||||
.map_err(|e| IOError::with_path(e, &path))?
|
||||
|
|
|
|||
|
|
@ -123,7 +123,8 @@ pub enum ErrorKind {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
source: tracing_error::TracedError<ErrorKind>,
|
||||
pub raw: std::sync::Arc<ErrorKind>,
|
||||
pub source: tracing_error::TracedError<std::sync::Arc<ErrorKind>>,
|
||||
}
|
||||
|
||||
impl From<Error> for String {
|
||||
|
|
@ -146,8 +147,12 @@ impl std::fmt::Display for Error {
|
|||
|
||||
impl<E: Into<ErrorKind>> From<E> for Error {
|
||||
fn from(source: E) -> Self {
|
||||
let error = Into::<ErrorKind>::into(source);
|
||||
let boxed_error = std::sync::Arc::new(error);
|
||||
|
||||
Self {
|
||||
source: Into::<ErrorKind>::into(source).in_current_span(),
|
||||
raw: boxed_error.clone(),
|
||||
source: boxed_error.in_current_span(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,19 @@ impl ClusterPath {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
/// Validate the cluster and clone the current [`ClusterPath`].
|
||||
pub async fn cluster_path(&self) -> crate::Result<ClusterPath> {
|
||||
if let Some(c) = crate::cluster::get(&self, None).await? {
|
||||
Ok(c.cluster_path())
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"failed to get path of unmanaged or corrupted cluster {}",
|
||||
self.to_string()
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`ClusterPath`] from a full [`PathBuf`].
|
||||
pub async fn from_path(path: PathBuf) -> crate::Result<Self> {
|
||||
let path: PathBuf = io::canonicalize(path)?;
|
||||
|
|
|
|||
|
|
@ -140,6 +140,12 @@ impl Directories {
|
|||
Ok(cluster_path.full_path().await?.join("logs"))
|
||||
}
|
||||
|
||||
/// Get the `{cluster_path}/crash_reports` directory.
|
||||
#[inline]
|
||||
pub async fn crash_reports_dir(cluster_path: &super::ClusterPath) -> crate::Result<PathBuf> {
|
||||
Ok(cluster_path.full_path().await?.join("crash-reports"))
|
||||
}
|
||||
|
||||
/// Get the `config_dir/metadata/resources` directory.
|
||||
#[inline]
|
||||
pub async fn legacy_assets_dir(&self) -> PathBuf {
|
||||
|
|
|
|||
|
|
@ -59,11 +59,15 @@ impl MinecraftState {
|
|||
|
||||
/// Refresh all tokens in the global state.
|
||||
#[tracing::instrument(skip(self))]
|
||||
async fn refresh(&mut self) -> crate::Result<(DeviceKey, DeviceToken)> {
|
||||
async fn refresh(
|
||||
&mut self,
|
||||
current_date: DateTime<Utc>,
|
||||
force: bool,
|
||||
) -> crate::Result<(DeviceKey, DeviceToken, DateTime<Utc>, bool)> {
|
||||
macro_rules! device_key {
|
||||
($self:ident, $device_key:expr, $device_token:expr, $SaveDeviceToken:path) => {{
|
||||
let key = device_key()?;
|
||||
let token = device_token(&key).await?;
|
||||
let res = device_token(&key, current_date).await?;
|
||||
|
||||
self.token = Some(SaveDeviceToken {
|
||||
id: key.id.clone(),
|
||||
|
|
@ -74,18 +78,17 @@ impl MinecraftState {
|
|||
.to_string(),
|
||||
x: key.x.clone(),
|
||||
y: key.y.clone(),
|
||||
token: token.clone(),
|
||||
modern: true,
|
||||
token: res.value.clone(),
|
||||
});
|
||||
self.save().await?;
|
||||
|
||||
(key, token)
|
||||
(key, res.value, res.date, true)
|
||||
}};
|
||||
}
|
||||
|
||||
let (key, token) = if let Some(ref token) = self.token {
|
||||
if token.token.not_after > Utc::now() {
|
||||
if let Ok(private_key) = SigningKey::from_pkcs8_pem(&token.private_key) {
|
||||
let (key, token, date, valid_date) = if let Some(ref token) = self.token {
|
||||
if let Ok(private_key) = SigningKey::from_pkcs8_pem(&token.private_key) {
|
||||
if token.token.not_after > Utc::now() && !force {
|
||||
(
|
||||
DeviceKey {
|
||||
id: token.id.clone(),
|
||||
|
|
@ -94,9 +97,20 @@ impl MinecraftState {
|
|||
y: token.y.clone(),
|
||||
},
|
||||
token.token.clone(),
|
||||
current_date,
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
device_key!(self, device_key, device_token, SaveDeviceToken)
|
||||
let key = DeviceKey {
|
||||
id: token.id.clone(),
|
||||
key: private_key,
|
||||
x: token.x.clone(),
|
||||
y: token.y.clone(),
|
||||
};
|
||||
|
||||
let res = device_token(&key, current_date).await?;
|
||||
|
||||
(key, res.value, res.date, true)
|
||||
}
|
||||
} else {
|
||||
device_key!(self, device_key, device_token, SaveDeviceToken)
|
||||
|
|
@ -105,25 +119,48 @@ impl MinecraftState {
|
|||
device_key!(self, device_key, device_token, SaveDeviceToken)
|
||||
};
|
||||
|
||||
Ok((key, token))
|
||||
Ok((key, token, date, valid_date))
|
||||
}
|
||||
|
||||
/// Begin a Microsoft authentication flow.
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn begin(&mut self) -> crate::Result<MinecraftLogin> {
|
||||
let (key, token) = self.refresh().await?;
|
||||
let (key, token, current_date, valid_date) = self.refresh(Utc::now(), false).await?;
|
||||
let verify = generate_oauth_challenge();
|
||||
let mut hash = sha2::Sha256::new();
|
||||
hash.update(&verify);
|
||||
let hash_result = hash.finalize();
|
||||
let challenge = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(&hash_result);
|
||||
let (sid, ruri) = sisu_authenticate(&token.token, &challenge, &key).await?;
|
||||
Ok(MinecraftLogin {
|
||||
verify,
|
||||
challenge,
|
||||
session_id: sid,
|
||||
redirect_uri: ruri.msa_oauth_redirect,
|
||||
})
|
||||
let challenge = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(hash_result);
|
||||
|
||||
match sisu_authenticate(&token.token, &challenge, &key, current_date).await {
|
||||
Ok((session_id, redirect_uri)) => Ok(MinecraftLogin {
|
||||
verify,
|
||||
challenge,
|
||||
session_id,
|
||||
redirect_uri: redirect_uri.value.msa_oauth_redirect,
|
||||
}),
|
||||
Err(err) => {
|
||||
if !valid_date {
|
||||
let (key, token, current_date, _) = self.refresh(Utc::now(), false).await?;
|
||||
let verify = generate_oauth_challenge();
|
||||
let mut hash = sha2::Sha256::new();
|
||||
hash.update(&verify);
|
||||
let hash_result = hash.finalize();
|
||||
let challenge = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(hash_result);
|
||||
let (session_id, redirect_uri) =
|
||||
sisu_authenticate(&token.token, &challenge, &key, current_date).await?;
|
||||
|
||||
Ok(MinecraftLogin {
|
||||
verify,
|
||||
challenge,
|
||||
session_id,
|
||||
redirect_uri: redirect_uri.value.msa_oauth_redirect,
|
||||
})
|
||||
} else {
|
||||
Err(crate::ErrorKind::from(err).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish a Microsoft authentication flow.
|
||||
|
|
@ -133,17 +170,24 @@ impl MinecraftState {
|
|||
code: &str,
|
||||
flow: MinecraftLogin,
|
||||
) -> crate::Result<MinecraftCredentials> {
|
||||
let (key, token) = self.refresh().await?;
|
||||
let (key, token, _, _) = self.refresh(Utc::now(), false).await?;
|
||||
let oauth_token = oauth_token(code, &flow.verify).await?;
|
||||
let sisu_authorize = sisu_authorize(
|
||||
Some(&flow.session_id),
|
||||
&oauth_token.access_token,
|
||||
&oauth_token.value.access_token,
|
||||
&token.token,
|
||||
&key,
|
||||
oauth_token.date,
|
||||
)
|
||||
.await?;
|
||||
let xbox_token = xsts_authorize(sisu_authorize, &token.token, &key).await?;
|
||||
let minecraft_token = minecraft_token(xbox_token).await?;
|
||||
let xbox_token = xsts_authorize(
|
||||
sisu_authorize.value,
|
||||
&token.token,
|
||||
&key,
|
||||
sisu_authorize.date,
|
||||
)
|
||||
.await?;
|
||||
let minecraft_token = minecraft_token(xbox_token.value).await?;
|
||||
minecraft_entitlements(&minecraft_token.access_token).await?;
|
||||
let profile = minecraft_profile(&minecraft_token.access_token).await?;
|
||||
let profile_id = profile.id.unwrap_or_default();
|
||||
|
|
@ -151,9 +195,10 @@ impl MinecraftState {
|
|||
id: profile_id,
|
||||
username: profile.name,
|
||||
access_token: minecraft_token.access_token,
|
||||
refresh_token: oauth_token.refresh_token,
|
||||
refresh_token: oauth_token.value.refresh_token,
|
||||
#[allow(deprecated)]
|
||||
expires: Utc::now() + chrono::TimeDelta::seconds(oauth_token.expires_in as i64),
|
||||
expires: oauth_token.date
|
||||
+ chrono::TimeDelta::seconds(oauth_token.value.expires_in as i64),
|
||||
};
|
||||
|
||||
self.users.insert(profile_id, credentials.clone());
|
||||
|
|
@ -167,6 +212,47 @@ impl MinecraftState {
|
|||
Ok(credentials)
|
||||
}
|
||||
|
||||
/// Refresh the current stored [`MinecraftCredentials`].
|
||||
async fn refresh_token(
|
||||
&mut self,
|
||||
creds: &MinecraftCredentials,
|
||||
) -> crate::Result<Option<MinecraftCredentials>> {
|
||||
let cred_id = creds.id;
|
||||
let cred_name = creds.username.clone();
|
||||
let oauth_token = oauth_refresh(&creds.refresh_token).await?;
|
||||
let (key, token, current_date, _) = self.refresh(oauth_token.date, false).await?;
|
||||
let sisu_authorize = sisu_authorize(
|
||||
None,
|
||||
&oauth_token.value.access_token,
|
||||
&token.token,
|
||||
&key,
|
||||
current_date,
|
||||
)
|
||||
.await?;
|
||||
let xbox_token = xsts_authorize(
|
||||
sisu_authorize.value,
|
||||
&token.token,
|
||||
&key,
|
||||
sisu_authorize.date,
|
||||
)
|
||||
.await?;
|
||||
let minecraft_token = minecraft_token(xbox_token.value).await?;
|
||||
let val = MinecraftCredentials {
|
||||
id: cred_id,
|
||||
username: cred_name,
|
||||
access_token: minecraft_token.access_token,
|
||||
refresh_token: oauth_token.value.refresh_token,
|
||||
#[allow(deprecated)]
|
||||
expires: oauth_token.date
|
||||
+ chrono::TimeDelta::seconds(oauth_token.value.expires_in as i64),
|
||||
};
|
||||
|
||||
self.users.insert(val.id, val.clone());
|
||||
self.save().await?;
|
||||
|
||||
Ok(Some(val))
|
||||
}
|
||||
|
||||
/// Get the default user account as a set of optional [`MinecraftCredentials`].
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get_default(&mut self) -> crate::Result<Option<MinecraftCredentials>> {
|
||||
|
|
@ -187,29 +273,25 @@ impl MinecraftState {
|
|||
}
|
||||
|
||||
if creds.expires < Utc::now() {
|
||||
let cred_id = creds.id;
|
||||
let profile_name = creds.username.clone();
|
||||
let old_creds = creds.clone();
|
||||
let res = self.refresh_token(&old_creds).await;
|
||||
|
||||
let oauth_token = oauth_refresh(&creds.refresh_token).await?;
|
||||
let (key, token) = self.refresh().await?;
|
||||
let sisu_authorize =
|
||||
sisu_authorize(None, &oauth_token.access_token, &token.token, &key).await?;
|
||||
match res {
|
||||
Ok(val) => Ok(val),
|
||||
Err(err) => {
|
||||
if let crate::ErrorKind::AuthError(MinecraftAuthError::RequestError {
|
||||
ref source,
|
||||
..
|
||||
}) = *err.raw
|
||||
{
|
||||
if source.is_connect() || source.is_timeout() {
|
||||
return Ok(Some(old_creds));
|
||||
}
|
||||
}
|
||||
|
||||
let xbox_token = xsts_authorize(sisu_authorize, &token.token, &key).await?;
|
||||
let minecraft_token = minecraft_token(xbox_token).await?;
|
||||
let val = MinecraftCredentials {
|
||||
id: cred_id,
|
||||
username: profile_name,
|
||||
access_token: minecraft_token.access_token,
|
||||
refresh_token: oauth_token.refresh_token,
|
||||
#[allow(deprecated)]
|
||||
expires: Utc::now() + chrono::TimeDelta::seconds(oauth_token.expires_in as i64),
|
||||
};
|
||||
|
||||
self.users.insert(val.id, val.clone());
|
||||
self.save().await?;
|
||||
|
||||
Ok(Some(val))
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(Some(creds.clone()))
|
||||
}
|
||||
|
|
@ -235,6 +317,12 @@ const REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
|||
/// Microsoft login xboxlive scopes to get tokens.
|
||||
const SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
||||
|
||||
/// A [`reqwest::Request`] with a [`DateTime<Utc>`] attached to it.
|
||||
struct RequestWithDate<T> {
|
||||
pub date: DateTime<Utc>,
|
||||
pub value: T,
|
||||
}
|
||||
|
||||
/// A device token used for Microsoft authentication.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
|
|
@ -275,9 +363,6 @@ pub struct SaveDeviceToken {
|
|||
pub y: String,
|
||||
/// The assocated [`DeviceToken`].
|
||||
pub token: DeviceToken,
|
||||
/// Whether or not we should patch the auth device token to use a modern auth flow
|
||||
#[serde(default)]
|
||||
modern: bool,
|
||||
}
|
||||
|
||||
/// Core variables passed throughout the Minecraft login flow.
|
||||
|
|
@ -308,8 +393,11 @@ pub struct MinecraftCredentials {
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip(key))]
|
||||
pub async fn device_token(key: &DeviceKey) -> Result<DeviceToken, MinecraftAuthError> {
|
||||
Ok(send_signed_request(
|
||||
pub async fn device_token(
|
||||
key: &DeviceKey,
|
||||
current_date: DateTime<Utc>,
|
||||
) -> Result<RequestWithDate<DeviceToken>, MinecraftAuthError> {
|
||||
let res = send_signed_request(
|
||||
None,
|
||||
"https://device.auth.xboxlive.com/device/authenticate",
|
||||
"/device/authenticate",
|
||||
|
|
@ -333,9 +421,14 @@ pub async fn device_token(key: &DeviceKey) -> Result<DeviceToken, MinecraftAuthE
|
|||
}),
|
||||
key,
|
||||
MinecraftAuthStep::DeviceToken,
|
||||
current_date,
|
||||
)
|
||||
.await?
|
||||
.1)
|
||||
.await?;
|
||||
|
||||
Ok(RequestWithDate {
|
||||
date: res.current_date,
|
||||
value: res.body,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -349,9 +442,10 @@ async fn sisu_authenticate(
|
|||
token: &str,
|
||||
challenge: &str,
|
||||
key: &DeviceKey,
|
||||
) -> Result<(String, RedirectUri), MinecraftAuthError> {
|
||||
current_date: DateTime<Utc>,
|
||||
) -> Result<(String, RequestWithDate<RedirectUri>), MinecraftAuthError> {
|
||||
// what was microsoft on when writing this??????????
|
||||
let (headers, res) = send_signed_request(
|
||||
let res = send_signed_request::<RedirectUri>(
|
||||
None,
|
||||
"https://sisu.xboxlive.com/authenticate",
|
||||
"/authenticate",
|
||||
|
|
@ -372,16 +466,24 @@ async fn sisu_authenticate(
|
|||
}),
|
||||
key,
|
||||
MinecraftAuthStep::SisuAuthenicate,
|
||||
current_date,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let session_id = headers
|
||||
let session_id = res
|
||||
.headers
|
||||
.get("X-SessionId")
|
||||
.and_then(|x| x.to_str().ok())
|
||||
.ok_or_else(|| MinecraftAuthError::SessionIdError)?
|
||||
.to_string();
|
||||
|
||||
Ok((session_id, res))
|
||||
Ok((
|
||||
session_id,
|
||||
RequestWithDate {
|
||||
date: res.current_date,
|
||||
value: res.body,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -396,11 +498,14 @@ struct OAuthToken {
|
|||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn oauth_token(code: &str, verify: &str) -> Result<OAuthToken, MinecraftAuthError> {
|
||||
async fn oauth_token(
|
||||
code: &str,
|
||||
verify: &str,
|
||||
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthError> {
|
||||
let mut query = HashMap::new();
|
||||
query.insert("client_id", MICROSOFT_CLIENT_ID);
|
||||
query.insert("code", code);
|
||||
query.insert("code_verifier", &*verify);
|
||||
query.insert("code_verifier", verify);
|
||||
query.insert("grant_type", "authorization_code");
|
||||
query.insert("redirect_uri", REDIRECT_URL);
|
||||
query.insert("scope", SCOPES);
|
||||
|
|
@ -418,6 +523,7 @@ async fn oauth_token(code: &str, verify: &str) -> Result<OAuthToken, MinecraftAu
|
|||
})?;
|
||||
|
||||
let status = res.status();
|
||||
let current_date = get_date_header(res.headers());
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
|
|
@ -426,16 +532,24 @@ async fn oauth_token(code: &str, verify: &str) -> Result<OAuthToken, MinecraftAu
|
|||
source,
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::OAuthToken,
|
||||
raw: text,
|
||||
source,
|
||||
status_code: status,
|
||||
let body =
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::OAuthToken,
|
||||
raw: text,
|
||||
source,
|
||||
status_code: status,
|
||||
})?;
|
||||
|
||||
Ok(RequestWithDate {
|
||||
date: current_date,
|
||||
value: body,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn oauth_refresh(refresh_token: &str) -> Result<OAuthToken, MinecraftAuthError> {
|
||||
async fn oauth_refresh(
|
||||
refresh_token: &str,
|
||||
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthError> {
|
||||
let mut query = HashMap::new();
|
||||
query.insert("client_id", MICROSOFT_CLIENT_ID);
|
||||
query.insert("refresh_token", refresh_token);
|
||||
|
|
@ -456,6 +570,7 @@ async fn oauth_refresh(refresh_token: &str) -> Result<OAuthToken, MinecraftAuthE
|
|||
})?;
|
||||
|
||||
let status = res.status();
|
||||
let current_date = get_date_header(res.headers());
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
|
|
@ -464,11 +579,17 @@ async fn oauth_refresh(refresh_token: &str) -> Result<OAuthToken, MinecraftAuthE
|
|||
source,
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::RefreshOAuthToken,
|
||||
raw: text,
|
||||
source,
|
||||
status_code: status,
|
||||
let body =
|
||||
serde_json::from_str(&text).map_err(|source| MinecraftAuthError::DeserializeError {
|
||||
step: MinecraftAuthStep::RefreshOAuthToken,
|
||||
raw: text,
|
||||
source,
|
||||
status_code: status,
|
||||
})?;
|
||||
|
||||
Ok(RequestWithDate {
|
||||
date: current_date,
|
||||
value: body,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -489,8 +610,9 @@ async fn sisu_authorize(
|
|||
access_token: &str,
|
||||
device_token: &str,
|
||||
key: &DeviceKey,
|
||||
) -> Result<SisuAuthorize, MinecraftAuthError> {
|
||||
Ok(send_signed_request(
|
||||
current_date: DateTime<Utc>,
|
||||
) -> Result<RequestWithDate<SisuAuthorize>, MinecraftAuthError> {
|
||||
let res = send_signed_request(
|
||||
None,
|
||||
"https://sisu.xboxlive.com/authorize",
|
||||
"/authorize",
|
||||
|
|
@ -515,9 +637,14 @@ async fn sisu_authorize(
|
|||
}),
|
||||
key,
|
||||
MinecraftAuthStep::SisuAuthorize,
|
||||
current_date,
|
||||
)
|
||||
.await?
|
||||
.1)
|
||||
.await?;
|
||||
|
||||
Ok(RequestWithDate {
|
||||
date: res.current_date,
|
||||
value: res.body,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(key))]
|
||||
|
|
@ -525,8 +652,9 @@ async fn xsts_authorize(
|
|||
authorize: SisuAuthorize,
|
||||
device_token: &str,
|
||||
key: &DeviceKey,
|
||||
) -> Result<DeviceToken, MinecraftAuthError> {
|
||||
Ok(send_signed_request(
|
||||
current_date: DateTime<Utc>,
|
||||
) -> Result<RequestWithDate<DeviceToken>, MinecraftAuthError> {
|
||||
let res = send_signed_request(
|
||||
None,
|
||||
"https://xsts.auth.xboxlive.com/xsts/authorize",
|
||||
"/xsts/authorize",
|
||||
|
|
@ -542,9 +670,14 @@ async fn xsts_authorize(
|
|||
}),
|
||||
key,
|
||||
MinecraftAuthStep::XstsAuthorize,
|
||||
current_date,
|
||||
)
|
||||
.await?
|
||||
.1)
|
||||
.await?;
|
||||
|
||||
Ok(RequestWithDate {
|
||||
date: res.current_date,
|
||||
value: res.body,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -729,6 +862,12 @@ fn device_key() -> Result<DeviceKey, MinecraftAuthError> {
|
|||
})
|
||||
}
|
||||
|
||||
struct SignedRequestResponse<T> {
|
||||
pub headers: reqwest::header::HeaderMap,
|
||||
pub current_date: DateTime<Utc>,
|
||||
pub body: T,
|
||||
}
|
||||
|
||||
async fn send_signed_request<T: serde::de::DeserializeOwned>(
|
||||
authorization: Option<&str>,
|
||||
url: &str,
|
||||
|
|
@ -736,13 +875,14 @@ async fn send_signed_request<T: serde::de::DeserializeOwned>(
|
|||
raw_body: serde_json::Value,
|
||||
key: &DeviceKey,
|
||||
step: MinecraftAuthStep,
|
||||
) -> Result<(reqwest::header::HeaderMap, T), MinecraftAuthError> {
|
||||
current_date: DateTime<Utc>,
|
||||
) -> Result<SignedRequestResponse<T>, MinecraftAuthError> {
|
||||
let auth = authorization
|
||||
.clone()
|
||||
.map_or(Vec::new(), |v| v.as_bytes().to_vec());
|
||||
let body = serde_json::to_vec(&raw_body)
|
||||
.map_err(|source| MinecraftAuthError::SerializeError { step, source })?;
|
||||
let time: u128 = { ((Utc::now().timestamp() as u128) + 11644473600) * 10000000 };
|
||||
let time: u128 = { ((current_date.timestamp() as u128) + 11644473600) * 10000000 };
|
||||
|
||||
use byteorder::WriteBytesExt;
|
||||
let mut buffer = Vec::new();
|
||||
|
|
@ -811,6 +951,7 @@ async fn send_signed_request<T: serde::de::DeserializeOwned>(
|
|||
|
||||
let status = res.status();
|
||||
let headers = res.headers().clone();
|
||||
let current_date = get_date_header(&headers);
|
||||
let body = res
|
||||
.text()
|
||||
.await
|
||||
|
|
@ -823,7 +964,21 @@ async fn send_signed_request<T: serde::de::DeserializeOwned>(
|
|||
source,
|
||||
status_code: status,
|
||||
})?;
|
||||
Ok((headers, body))
|
||||
Ok(SignedRequestResponse {
|
||||
headers,
|
||||
current_date,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
fn get_date_header(headers: &reqwest::header::HeaderMap) -> DateTime<Utc> {
|
||||
headers
|
||||
.get(reqwest::header::DATE)
|
||||
.and_then(|x| x.to_str().ok())
|
||||
.and_then(|x| DateTime::parse_from_rfc2822(x).ok())
|
||||
.map(|x| x.with_timezone(&Utc))
|
||||
.unwrap_or(Utc::now())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue