use std::future::Future;
use std::time::Duration;
use chrono::{Duration as ChronoDuration, Utc};
use reqwest::Client;
use crate::auth::{RelyingParty, XblAuthProvider};
use crate::cache::{CachedToken, ExpiryTokenCache, KeyedExpiryTokenCache};
use crate::error::XboxError;
use crate::models::{XstsToken, Xuid};
use crate::people;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
const XUID_CACHE_TTL: ChronoDuration = ChronoDuration::hours(24);
const DEFAULT_XSTS_URL: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";
const DEFAULT_PEOPLEHUB_BASE_URL: &str = "https://peoplehub.xboxlive.com";
#[derive(Debug, Clone)]
pub struct XboxEndpoints {
pub xsts_authorize_url: String,
pub peoplehub_base_url: String,
}
impl Default for XboxEndpoints {
fn default() -> Self {
Self {
xsts_authorize_url: DEFAULT_XSTS_URL.to_string(),
peoplehub_base_url: DEFAULT_PEOPLEHUB_BASE_URL.to_string(),
}
}
}
pub struct XboxClient<P: XblAuthProvider> {
provider: P,
http: Client,
endpoints: XboxEndpoints,
user_token_cache: ExpiryTokenCache<String, XboxError>,
xsts_cache: KeyedExpiryTokenCache<RelyingParty, XstsToken, XboxError>,
xuid_cache: KeyedExpiryTokenCache<String, Xuid, XboxError>,
}
impl<P: XblAuthProvider> XboxClient<P> {
pub fn new(provider: P) -> Self {
Self::with_http_client(provider, Client::new())
}
pub fn with_http_client(provider: P, http: Client) -> Self {
Self::with_endpoints(provider, http, XboxEndpoints::default())
}
pub fn with_endpoints(provider: P, http: Client, endpoints: XboxEndpoints) -> Self {
Self {
provider,
http,
endpoints,
user_token_cache: ExpiryTokenCache::new(),
xsts_cache: KeyedExpiryTokenCache::new(),
xuid_cache: KeyedExpiryTokenCache::new(),
}
}
async fn user_token(&self) -> Result<String, XboxError> {
self.user_token_cache
.get_or_refresh(|| self.provider.user_token())
.await
}
pub async fn xsts_ticket(&self, relying_party: RelyingParty) -> Result<XstsToken, XboxError> {
self.xsts_cache
.get_or_refresh(relying_party, || async {
let user_token = self.user_token().await?;
let ticket = fetch_xsts_ticket(
&self.http,
&self.endpoints.xsts_authorize_url,
&user_token,
relying_party,
)
.await?;
Ok(CachedToken::new(ticket.clone(), ticket.not_after))
})
.await
}
pub async fn gamertag_to_xuid(&self, gamertag: &str) -> Result<Xuid, XboxError> {
let gamertag_owned = gamertag.to_string();
self.xuid_cache
.get_or_refresh(gamertag_owned.clone(), || async {
let xuid = self
.with_single_retry(RelyingParty::XBOX, || async {
let xsts = self.xsts_ticket(RelyingParty::XBOX).await?;
let authorization = xsts
.authorization_header()
.ok_or_else(|| XboxError::PersonNotFound(gamertag_owned.clone()))?;
people::search_gamertag(
&self.http,
&self.endpoints.peoplehub_base_url,
&authorization,
&gamertag_owned,
)
.await
})
.await?;
Ok(CachedToken::new(xuid, Utc::now() + XUID_CACHE_TTL))
})
.await
}
async fn with_single_retry<T, F, Fut>(
&self,
relying_party: RelyingParty,
mut call: F,
) -> Result<T, XboxError>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<T, XboxError>>,
{
match call().await {
Err(err) if err.is_unauthorized() => {
self.xsts_cache.invalidate(&relying_party).await;
self.user_token_cache.invalidate().await;
call().await
}
other => other,
}
}
}
async fn fetch_xsts_ticket(
http: &Client,
url: &str,
user_token: &str,
relying_party: RelyingParty,
) -> Result<XstsToken, XboxError> {
let response = http
.post(url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("x-xbl-contract-version", "1")
.json(&serde_json::json!({
"RelyingParty": relying_party.as_str(),
"TokenType": "JWT",
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [user_token],
}
}))
.timeout(DEFAULT_TIMEOUT)
.send()
.await?;
if !response.status().is_success() {
return Err(XboxError::HttpStatus {
url: url.to_string(),
status: response.status(),
});
}
Ok(response.json::<XstsToken>().await?)
}