mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 14:26:04 +00:00
caching*
refreshing microsoft auth tokens isn't implemented yet, also i haven't tested it
This commit is contained in:
parent
cb001fa341
commit
fe1fb2dbfd
5 changed files with 273 additions and 75 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
@ -119,6 +119,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"azalea-buf",
|
"azalea-buf",
|
||||||
"azalea-crypto",
|
"azalea-crypto",
|
||||||
|
"chrono",
|
||||||
"log",
|
"log",
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
@ -429,9 +430,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.19"
|
version = "0.4.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
@ -818,7 +819,7 @@ checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.10.2+wasi-snapshot-preview1",
|
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2096,9 +2097,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.10.2+wasi-snapshot-preview1"
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
|
|
|
@ -9,16 +9,17 @@ version = "0.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.65"
|
anyhow = "1.0.65"
|
||||||
azalea-buf = { path = "../azalea-buf", version = "^0.1.0" }
|
azalea-buf = {path = "../azalea-buf", version = "^0.1.0"}
|
||||||
|
azalea-crypto = {path = "../azalea-crypto", version = "^0.1.0"}
|
||||||
|
chrono = {version = "0.4.22", default-features = false}
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
num-bigint = "0.4.3"
|
num-bigint = "0.4.3"
|
||||||
reqwest = { version = "0.11.12", features = ["json"] }
|
reqwest = {version = "0.11.12", features = ["json"]}
|
||||||
serde = { version = "1.0.145", features = ["derive"] }
|
serde = {version = "1.0.145", features = ["derive"]}
|
||||||
serde_json = "1.0.86"
|
serde_json = "1.0.86"
|
||||||
|
thiserror = "1.0.37"
|
||||||
tokio = "1.21.2"
|
tokio = "1.21.2"
|
||||||
uuid = "^1.1.2"
|
uuid = "^1.1.2"
|
||||||
azalea-crypto = { path = "../azalea-crypto", version = "^0.1.0" }
|
|
||||||
thiserror = "1.0.37"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.21.2", features = ["full"] }
|
tokio = {version = "1.21.2", features = ["full"]}
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
//! Handle Minecraft (Xbox) authentication.
|
//! Handle Minecraft (Xbox) authentication.
|
||||||
|
|
||||||
|
use crate::cache::{self, CachedAccount, ExpiringValue};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use serde::Deserialize;
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{collections::HashMap, time::Instant};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
path::PathBuf,
|
||||||
|
time::{Instant, SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct AuthOpts {
|
pub struct AuthOpts {
|
||||||
|
@ -15,38 +20,94 @@ pub struct AuthOpts {
|
||||||
// /// Whether we should get the Minecraft profile data (i.e. username, uuid,
|
// /// Whether we should get the Minecraft profile data (i.e. username, uuid,
|
||||||
// /// skin, etc) for the player.
|
// /// skin, etc) for the player.
|
||||||
// pub get_profile: bool,
|
// pub get_profile: bool,
|
||||||
|
/// The directory to store the cache in. If this is not set, caching is not
|
||||||
|
/// done.
|
||||||
|
pub cache_file: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticate with authenticate with Microsoft. If the data isn't cached,
|
/// Authenticate with authenticate with Microsoft. If the data isn't cached,
|
||||||
/// they'll be asked to go to log into Microsoft in a web page.
|
/// they'll be asked to go to log into Microsoft in a web page.
|
||||||
pub async fn auth(opts: Option<AuthOpts>) -> anyhow::Result<AuthResult> {
|
///
|
||||||
|
/// The email is technically only used as a cache key, so it *could* be
|
||||||
|
/// anything. You should just have it be the actual email so it's not confusing
|
||||||
|
/// though, and in case the Microsoft API does start providing the real email.
|
||||||
|
pub async fn auth(email: &str, opts: Option<AuthOpts>) -> anyhow::Result<AuthResult> {
|
||||||
let opts = opts.unwrap_or_default();
|
let opts = opts.unwrap_or_default();
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let cached_account = if let Some(cache_file) = &opts.cache_file && let Some(account) = cache::get_account_in_cache(&cache_file, email).await {
|
||||||
|
Some(account)
|
||||||
|
} else { None };
|
||||||
|
|
||||||
let auth_token_res = interactive_get_auth_token(&client).await?;
|
// these two MUST be set by the end, since we return them in AuthResult
|
||||||
// TODO: cache this
|
let profile: ProfileResponse;
|
||||||
println!("Got access token: {}", auth_token_res.access_token);
|
let minecraft_access_token: String;
|
||||||
|
|
||||||
let xbl_auth = auth_with_xbox_live(&client, &auth_token_res.access_token).await?;
|
if let Some(account) = &cached_account && !account.mca.is_expired() {
|
||||||
|
// the minecraft auth data is cached and not expired, so we can just
|
||||||
|
// use that instead of doing auth all over again :)
|
||||||
|
profile = account.profile.clone();
|
||||||
|
minecraft_access_token = account.mca.data.access_token.clone();
|
||||||
|
} else {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let mut msa = if let Some(account) = cached_account {
|
||||||
|
account.msa
|
||||||
|
} else {
|
||||||
|
interactive_get_auth_token(&client).await?
|
||||||
|
};
|
||||||
|
if msa.is_expired() {
|
||||||
|
todo!("refresh msa token");
|
||||||
|
}
|
||||||
|
let ms_access_token = &msa.data.access_token;
|
||||||
|
println!("Got access token: {}", ms_access_token);
|
||||||
|
|
||||||
let xsts_token = obtain_xsts_for_minecraft(&client, &xbl_auth).await?;
|
let xbl_auth = auth_with_xbox_live(&client, &ms_access_token).await?;
|
||||||
|
|
||||||
let minecraft_access_token =
|
let xsts_token = obtain_xsts_for_minecraft(
|
||||||
auth_with_minecraft(&client, &xbl_auth.user_hash, &xsts_token).await?;
|
&client,
|
||||||
|
&xbl_auth
|
||||||
|
.get()
|
||||||
|
.expect("Xbox Live auth token shouldn't have expired yet")
|
||||||
|
.token,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
if opts.check_ownership {
|
// Minecraft auth
|
||||||
let has_game = check_ownership(&client, &minecraft_access_token).await?;
|
let mca = auth_with_minecraft(&client, &xbl_auth.data.user_hash, &xsts_token).await?;
|
||||||
if !has_game {
|
|
||||||
panic!(
|
minecraft_access_token = (&mca)
|
||||||
"The Minecraft API is indicating that you don't own the game. \
|
.get()
|
||||||
If you're using Xbox Game Pass, set `check_ownership` to false in the auth options."
|
.expect("Minecraft auth shouldn't have expired yet")
|
||||||
);
|
.access_token
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if opts.check_ownership {
|
||||||
|
let has_game = check_ownership(&client, &minecraft_access_token).await?;
|
||||||
|
if !has_game {
|
||||||
|
panic!(
|
||||||
|
"The Minecraft API is indicating that you don't own the game. \
|
||||||
|
If you're using Xbox Game Pass, set `check_ownership` to false in the auth options."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profile = get_profile(&client, &minecraft_access_token).await?;
|
||||||
|
|
||||||
|
if let Some(cache_file) = opts.cache_file {
|
||||||
|
cache::set_account_in_cache(
|
||||||
|
&cache_file,
|
||||||
|
email,
|
||||||
|
CachedAccount {
|
||||||
|
email: email.to_string(),
|
||||||
|
mca,
|
||||||
|
msa,
|
||||||
|
xbl: xbl_auth,
|
||||||
|
profile: profile.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let profile = get_profile(&client, &minecraft_access_token).await?;
|
|
||||||
|
|
||||||
Ok(AuthResult {
|
Ok(AuthResult {
|
||||||
access_token: minecraft_access_token,
|
access_token: minecraft_access_token,
|
||||||
profile,
|
profile,
|
||||||
|
@ -69,7 +130,7 @@ pub struct DeviceCodeResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct AccessTokenResponse {
|
pub struct AccessTokenResponse {
|
||||||
token_type: String,
|
token_type: String,
|
||||||
expires_in: u64,
|
expires_in: u64,
|
||||||
|
@ -90,13 +151,14 @@ pub struct XboxLiveAuthResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Just the important data
|
/// Just the important data
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct XboxLiveAuth {
|
pub struct XboxLiveAuth {
|
||||||
token: String,
|
token: String,
|
||||||
user_hash: String,
|
user_hash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct MinecraftAuthResponse {
|
pub struct MinecraftAuthResponse {
|
||||||
username: String,
|
username: String,
|
||||||
roles: Vec<String>,
|
roles: Vec<String>,
|
||||||
|
@ -120,7 +182,7 @@ pub struct GameOwnershipItem {
|
||||||
signature: String,
|
signature: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct ProfileResponse {
|
pub struct ProfileResponse {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -131,7 +193,7 @@ pub struct ProfileResponse {
|
||||||
/// Asks the user to go to a webpage and log in with Microsoft.
|
/// Asks the user to go to a webpage and log in with Microsoft.
|
||||||
async fn interactive_get_auth_token(
|
async fn interactive_get_auth_token(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
) -> anyhow::Result<AccessTokenResponse> {
|
) -> anyhow::Result<ExpiringValue<AccessTokenResponse>> {
|
||||||
// nintendo switch (real)
|
// nintendo switch (real)
|
||||||
let client_id = "00000000441cc96b";
|
let client_id = "00000000441cc96b";
|
||||||
|
|
||||||
|
@ -152,15 +214,13 @@ async fn interactive_get_auth_token(
|
||||||
res.verification_uri, res.user_code
|
res.verification_uri, res.user_code
|
||||||
);
|
);
|
||||||
|
|
||||||
let access_token_response: AccessTokenResponse;
|
let login_expires_at = Instant::now() + std::time::Duration::from_secs(res.expires_in);
|
||||||
|
|
||||||
let expire_time = Instant::now() + std::time::Duration::from_secs(res.expires_in);
|
while Instant::now() < login_expires_at {
|
||||||
|
|
||||||
while Instant::now() < expire_time {
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(res.interval)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(res.interval)).await;
|
||||||
|
|
||||||
println!("trying");
|
println!("trying");
|
||||||
if let Ok(res) = client
|
if let Ok(access_token_response) = client
|
||||||
.post(format!(
|
.post(format!(
|
||||||
"https://login.live.com/oauth20_token.srf?client_id={}",
|
"https://login.live.com/oauth20_token.srf?client_id={}",
|
||||||
client_id
|
client_id
|
||||||
|
@ -175,8 +235,15 @@ async fn interactive_get_auth_token(
|
||||||
.json::<AccessTokenResponse>()
|
.json::<AccessTokenResponse>()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
access_token_response = res;
|
let expires_at = SystemTime::now()
|
||||||
return Ok(access_token_response);
|
+ std::time::Duration::from_secs(access_token_response.expires_in);
|
||||||
|
return Ok(ExpiringValue {
|
||||||
|
data: access_token_response,
|
||||||
|
expires_at: expires_at
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("Time went backwards")
|
||||||
|
.as_secs(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +253,7 @@ async fn interactive_get_auth_token(
|
||||||
async fn auth_with_xbox_live(
|
async fn auth_with_xbox_live(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
access_token: &str,
|
access_token: &str,
|
||||||
) -> anyhow::Result<XboxLiveAuth> {
|
) -> anyhow::Result<ExpiringValue<XboxLiveAuth>> {
|
||||||
let auth_json = json!({
|
let auth_json = json!({
|
||||||
"Properties": {
|
"Properties": {
|
||||||
"AuthMethod": "RPS",
|
"AuthMethod": "RPS",
|
||||||
|
@ -219,15 +286,28 @@ async fn auth_with_xbox_live(
|
||||||
.await?;
|
.await?;
|
||||||
println!("got res: {:?}", res);
|
println!("got res: {:?}", res);
|
||||||
|
|
||||||
Ok(XboxLiveAuth {
|
// not_after looks like 2020-12-21T19:52:08.4463796Z
|
||||||
token: res.token,
|
let expires_at = DateTime::parse_from_rfc3339(&res.not_after)
|
||||||
user_hash: res.display_claims["xui"].get(0).unwrap()["uhs"].clone(),
|
.map_err(|_| {
|
||||||
|
anyhow!(
|
||||||
|
"Failed to parse not_after date from Xbox Live: {}",
|
||||||
|
res.not_after
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.with_timezone(&Utc)
|
||||||
|
.timestamp() as u64;
|
||||||
|
Ok(ExpiringValue {
|
||||||
|
data: XboxLiveAuth {
|
||||||
|
token: res.token,
|
||||||
|
user_hash: res.display_claims["xui"].get(0).unwrap()["uhs"].clone(),
|
||||||
|
},
|
||||||
|
expires_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn obtain_xsts_for_minecraft(
|
async fn obtain_xsts_for_minecraft(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
xbl_auth: &XboxLiveAuth,
|
xbl_auth_token: &str,
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
let res = client
|
let res = client
|
||||||
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
|
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
|
||||||
|
@ -235,7 +315,7 @@ async fn obtain_xsts_for_minecraft(
|
||||||
.json(&json!({
|
.json(&json!({
|
||||||
"Properties": {
|
"Properties": {
|
||||||
"SandboxId": "RETAIL",
|
"SandboxId": "RETAIL",
|
||||||
"UserTokens": [xbl_auth.token]
|
"UserTokens": [xbl_auth_token.to_string()]
|
||||||
},
|
},
|
||||||
"RelyingParty": "rp://api.minecraftservices.com/",
|
"RelyingParty": "rp://api.minecraftservices.com/",
|
||||||
"TokenType": "JWT"
|
"TokenType": "JWT"
|
||||||
|
@ -253,7 +333,7 @@ async fn auth_with_minecraft(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
user_hash: &str,
|
user_hash: &str,
|
||||||
xsts_token: &str,
|
xsts_token: &str,
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<ExpiringValue<MinecraftAuthResponse>> {
|
||||||
let res = client
|
let res = client
|
||||||
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
|
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
|
||||||
.header("Accept", "application/json")
|
.header("Accept", "application/json")
|
||||||
|
@ -266,7 +346,12 @@ async fn auth_with_minecraft(
|
||||||
.await?;
|
.await?;
|
||||||
println!("{:?}", res);
|
println!("{:?}", res);
|
||||||
|
|
||||||
Ok(res.access_token)
|
let expires_at = SystemTime::now() + std::time::Duration::from_secs(res.expires_in);
|
||||||
|
Ok(ExpiringValue {
|
||||||
|
data: res,
|
||||||
|
// to seconds since epoch
|
||||||
|
expires_at: expires_at.duration_since(UNIX_EPOCH).unwrap().as_secs(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_ownership(
|
async fn check_ownership(
|
||||||
|
|
|
@ -1,13 +1,109 @@
|
||||||
//! Cache auth information
|
//! Cache auth information
|
||||||
|
|
||||||
// pub fn get_auth_token() -> Option<AccessTokenResponse> {
|
use serde::{Deserialize, Serialize};
|
||||||
// let mut cache = CACHE.lock().unwrap();
|
use std::path::Path;
|
||||||
// if cache.auth_token.is_none() {
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
// return None;
|
use thiserror::Error;
|
||||||
// }
|
use tokio::fs::File;
|
||||||
// let auth_token = cache.auth_token.as_ref().unwrap();
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
// if auth_token.expires_in < SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() {
|
|
||||||
// return None;
|
#[derive(Debug, Error)]
|
||||||
// }
|
pub enum CacheError {
|
||||||
// Some(auth_token.clone())
|
#[error("Failed to read cache file")]
|
||||||
// }
|
ReadError(std::io::Error),
|
||||||
|
#[error("Failed to write cache file")]
|
||||||
|
WriteError(std::io::Error),
|
||||||
|
#[error("Failed to parse cache file")]
|
||||||
|
ParseError(serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct CachedAccount {
|
||||||
|
pub email: String,
|
||||||
|
/// Microsoft auth
|
||||||
|
pub msa: ExpiringValue<crate::auth::AccessTokenResponse>,
|
||||||
|
/// Xbox Live auth
|
||||||
|
pub xbl: ExpiringValue<crate::auth::XboxLiveAuth>,
|
||||||
|
/// Minecraft auth
|
||||||
|
pub mca: ExpiringValue<crate::auth::MinecraftAuthResponse>,
|
||||||
|
/// The user's Minecraft profile (i.e. username, UUID, skin)
|
||||||
|
pub profile: crate::auth::ProfileResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct ExpiringValue<T> {
|
||||||
|
/// Seconds since the UNIX epoch
|
||||||
|
pub expires_at: u64,
|
||||||
|
pub data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ExpiringValue<T> {
|
||||||
|
pub fn is_expired(&self) -> bool {
|
||||||
|
self.expires_at
|
||||||
|
< SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the data if it's not expired, otherwise return `None`
|
||||||
|
pub fn get(&self) -> Option<&T> {
|
||||||
|
if self.is_expired() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&self.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_entire_cache(cache_file: &Path) -> Result<Vec<CachedAccount>, CacheError> {
|
||||||
|
let mut cache: Vec<CachedAccount> = Vec::new();
|
||||||
|
if cache_file.exists() {
|
||||||
|
let cache_file = File::open(cache_file)
|
||||||
|
.await
|
||||||
|
.map_err(CacheError::ReadError)?;
|
||||||
|
// read the file into a string
|
||||||
|
let mut cache_file = tokio::io::BufReader::new(cache_file);
|
||||||
|
let mut contents = String::new();
|
||||||
|
cache_file
|
||||||
|
.read_to_string(&mut contents)
|
||||||
|
.await
|
||||||
|
.map_err(CacheError::ReadError)?;
|
||||||
|
cache = serde_json::from_str(&contents).map_err(CacheError::ParseError)?;
|
||||||
|
}
|
||||||
|
Ok(cache)
|
||||||
|
}
|
||||||
|
async fn set_entire_cache(cache_file: &Path, cache: Vec<CachedAccount>) -> Result<(), CacheError> {
|
||||||
|
let cache_file = File::create(cache_file)
|
||||||
|
.await
|
||||||
|
.map_err(CacheError::WriteError)?;
|
||||||
|
let mut cache_file = tokio::io::BufWriter::new(cache_file);
|
||||||
|
let cache = serde_json::to_string(&cache).map_err(CacheError::ParseError)?;
|
||||||
|
cache_file
|
||||||
|
.write_all(cache.as_bytes())
|
||||||
|
.await
|
||||||
|
.map_err(CacheError::WriteError)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets cached data for the given email.
|
||||||
|
///
|
||||||
|
/// Technically it doesn't actually have to be an email since it's only the
|
||||||
|
/// cache key. I considered using usernames or UUIDs as the cache key, but
|
||||||
|
/// usernames change and no one has their UUID memorized.
|
||||||
|
pub async fn get_account_in_cache(cache_file: &Path, email: &str) -> Option<CachedAccount> {
|
||||||
|
let cache = get_entire_cache(cache_file).await.unwrap_or_default();
|
||||||
|
cache.into_iter().find(|account| account.email == email)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_account_in_cache(
|
||||||
|
cache_file: &Path,
|
||||||
|
email: &str,
|
||||||
|
account: CachedAccount,
|
||||||
|
) -> Result<(), CacheError> {
|
||||||
|
let mut cache = get_entire_cache(cache_file).await.unwrap_or_default();
|
||||||
|
cache.retain(|account| account.email != email);
|
||||||
|
cache.push(account);
|
||||||
|
set_entire_cache(cache_file, cache).await
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
#![feature(let_chains)]
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
mod cache;
|
||||||
pub mod game_profile;
|
pub mod game_profile;
|
||||||
pub mod sessionserver;
|
pub mod sessionserver;
|
||||||
|
|
||||||
|
@ -7,17 +10,6 @@ use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{collections::HashMap, time::Instant};
|
use std::{collections::HashMap, time::Instant};
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct AuthOpts {
|
|
||||||
/// Whether we should check if the user actually owns the game. This will
|
|
||||||
/// fail if the user has Xbox Game Pass! Note that this isn't really
|
|
||||||
/// necessary, since getting the user profile will check this anyways.
|
|
||||||
pub check_ownership: bool,
|
|
||||||
// /// Whether we should get the Minecraft profile data (i.e. username, uuid,
|
|
||||||
// /// skin, etc) for the player.
|
|
||||||
// pub get_profile: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ask the user to authenticate with Microsoft and return the user's Minecraft
|
/// Ask the user to authenticate with Microsoft and return the user's Minecraft
|
||||||
/// access token. There's caching, so if the user has already authenticated
|
/// access token. There's caching, so if the user has already authenticated
|
||||||
/// before then they won't have to log in manually again.
|
/// before then they won't have to log in manually again.
|
||||||
|
@ -50,6 +42,29 @@ pub async fn auth(opts: AuthOpts) -> anyhow::Result<String> {
|
||||||
Ok(minecraft_access_token)
|
Ok(minecraft_access_token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AuthOpts {
|
||||||
|
/// Whether we should check if the user actually owns the game. This will
|
||||||
|
/// fail if the user has Xbox Game Pass! Note that this isn't really
|
||||||
|
/// necessary, since getting the user profile will check this anyways.
|
||||||
|
pub check_ownership: bool,
|
||||||
|
// /// Whether we should get the Minecraft profile data (i.e. username, uuid,
|
||||||
|
// /// skin, etc) for the player.
|
||||||
|
// pub get_profile: bool,
|
||||||
|
/// The file to store cached data in. If this is `None`, then no
|
||||||
|
/// caching will be done.
|
||||||
|
pub cache_dir: Option<std::path::PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AuthOpts {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
check_ownership: true,
|
||||||
|
// get_profile: true,
|
||||||
|
cache_dir: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct DeviceCodeResponse {
|
pub struct DeviceCodeResponse {
|
||||||
user_code: String,
|
user_code: String,
|
||||||
|
@ -260,7 +275,7 @@ async fn auth_with_minecraft(
|
||||||
Ok(res.access_token)
|
Ok(res.access_token)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_ownership(
|
pub async fn check_ownership(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
minecraft_access_token: &str,
|
minecraft_access_token: &str,
|
||||||
) -> anyhow::Result<bool> {
|
) -> anyhow::Result<bool> {
|
||||||
|
@ -281,7 +296,7 @@ async fn check_ownership(
|
||||||
Ok(!res.items.is_empty())
|
Ok(!res.items.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_profile(
|
pub async fn get_profile(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
minecraft_access_token: &str,
|
minecraft_access_token: &str,
|
||||||
) -> anyhow::Result<ProfileResponse> {
|
) -> anyhow::Result<ProfileResponse> {
|
||||||
|
|
Loading…
Add table
Reference in a new issue