diff --git a/core/src/api/logger.rs b/core/src/api/logger.rs index 26d9a441..0f6ff39f 100644 --- a/core/src/api/logger.rs +++ b/core/src/api/logger.rs @@ -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, } +#[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, ) -> crate::Result { 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`] 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, -) -> 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()); + logs: &mut Vec>, +) -> 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, +) -> crate::Result> { + 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::>>()?; - 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 { - 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 { - 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 { 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 = 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))? diff --git a/core/src/error.rs b/core/src/error.rs index e41607dc..46969247 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -123,7 +123,8 @@ pub enum ErrorKind { #[derive(Debug)] pub struct Error { - source: tracing_error::TracedError, + pub raw: std::sync::Arc, + pub source: tracing_error::TracedError>, } impl From for String { @@ -146,8 +147,12 @@ impl std::fmt::Display for Error { impl> From for Error { fn from(source: E) -> Self { + let error = Into::::into(source); + let boxed_error = std::sync::Arc::new(error); + Self { - source: Into::::into(source).in_current_span(), + raw: boxed_error.clone(), + source: boxed_error.in_current_span(), } } } diff --git a/core/src/store/clusters.rs b/core/src/store/clusters.rs index 6a879d1b..ccea989e 100644 --- a/core/src/store/clusters.rs +++ b/core/src/store/clusters.rs @@ -67,6 +67,19 @@ impl ClusterPath { Ok(self) } + /// Validate the cluster and clone the current [`ClusterPath`]. + pub async fn cluster_path(&self) -> crate::Result { + 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 { let path: PathBuf = io::canonicalize(path)?; diff --git a/core/src/store/dirs.rs b/core/src/store/dirs.rs index 0a7b30a7..671ebb5e 100644 --- a/core/src/store/dirs.rs +++ b/core/src/store/dirs.rs @@ -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 { + 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 { diff --git a/core/src/store/minecraft.rs b/core/src/store/minecraft.rs index a4250c50..afb29315 100644 --- a/core/src/store/minecraft.rs +++ b/core/src/store/minecraft.rs @@ -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, + force: bool, + ) -> crate::Result<(DeviceKey, DeviceToken, DateTime, 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 { - 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 { - 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> { + 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> { @@ -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`] attached to it. +struct RequestWithDate { + pub date: DateTime, + 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 { - Ok(send_signed_request( +pub async fn device_token( + key: &DeviceKey, + current_date: DateTime, +) -> Result, 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 Result<(String, RedirectUri), MinecraftAuthError> { + current_date: DateTime, +) -> Result<(String, RequestWithDate), MinecraftAuthError> { // what was microsoft on when writing this?????????? - let (headers, res) = send_signed_request( + let res = send_signed_request::( 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 { +async fn oauth_token( + code: &str, + verify: &str, +) -> Result, 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 Result Result { +async fn oauth_refresh( + refresh_token: &str, +) -> Result, 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 Result Result { - Ok(send_signed_request( + current_date: DateTime, +) -> Result, 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 { - Ok(send_signed_request( + current_date: DateTime, +) -> Result, 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 { }) } +struct SignedRequestResponse { + pub headers: reqwest::header::HeaderMap, + pub current_date: DateTime, + pub body: T, +} + async fn send_signed_request( authorization: Option<&str>, url: &str, @@ -736,13 +875,14 @@ async fn send_signed_request( raw_body: serde_json::Value, key: &DeviceKey, step: MinecraftAuthStep, -) -> Result<(reqwest::header::HeaderMap, T), MinecraftAuthError> { + current_date: DateTime, +) -> Result, 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( 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( source, status_code: status, })?; - Ok((headers, body)) + Ok(SignedRequestResponse { + headers, + current_date, + body, + }) +} + +#[tracing::instrument] +fn get_date_header(headers: &reqwest::header::HeaderMap) -> DateTime { + 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]