use std::path::PathBuf; use azalea_auth::{ certs::Certificates, sessionserver::{self, ClientSessionServerError, SessionServerJoinOpts}, yggdrasil::{YggdrasilAuthError, YggdrasilAuthOpts, yggdrasil_auth}, }; use parking_lot::Mutex; use uuid::Uuid; use crate::account::{Account, AccountTrait, BoxFuture}; fn default_cache_file() -> PathBuf { let minecraft_dir = minecraft_folder_path::minecraft_dir().unwrap_or_else(|| { panic!( "No {} environment variable found", minecraft_folder_path::home_env_var() ) }); minecraft_dir.join("azalea-auth-yggdrasil.json") } fn default_auth_opts() -> YggdrasilAuthOpts { YggdrasilAuthOpts { cache_file: Some(default_cache_file()), } } #[derive(Debug, Clone)] pub struct Backend { pub auth: String, pub session: String, pub player: Option, } impl Backend { pub fn new_drasl(base_url: &str, certs: bool) -> Self { Self { auth: format!("{base_url}/auth"), session: format!("{base_url}/session"), player: certs.then(|| format!("{base_url}/player")), } } } /// A type of account that authenticates with an Yggdrasil server using Azalea's /// cache. /// /// This type is not intended to be used directly by the user. To actually make /// an account that authenticates with an Yggdrasil server, see /// [`Account::yggdrasil`] or [`Account::yggdrasil_with_opts`]. #[derive(Debug)] pub struct YggdrasilAccount { auth_opts: YggdrasilAuthOpts, backend: Backend, username: String, password: Option, uuid: Uuid, access_token: Mutex, certs: Mutex>, } impl YggdrasilAccount { // deliberately private, use `Account::yggdrasil` or // `Account::yggdrasil_with_opts` instead. async fn new( username: String, password: Option, backend: Backend, auth_opts: YggdrasilAuthOpts, ) -> Result { let auth_result = yggdrasil_auth( &username, password.as_deref(), &backend.auth, auth_opts.clone(), ) .await?; Ok(Self { username, password, uuid: auth_result.user.id, access_token: Mutex::new(auth_result.access_token), certs: Mutex::new(None), backend, auth_opts, }) } } impl AccountTrait for YggdrasilAccount { fn username(&self) -> &str { &self.username } fn uuid(&self) -> Uuid { self.uuid } fn access_token(&self) -> Option { Some(self.access_token.lock().to_owned()) } fn certs(&self) -> Option { self.certs.lock().as_ref().cloned() } fn set_certs(&self, certs: azalea_auth::certs::Certificates) { *self.certs.lock() = Some(certs); } fn certs_backend(&self) -> Option<&str> { self.backend.player.as_deref() } fn refresh(&self) -> BoxFuture<'_, Result<(), azalea_auth::AuthError>> { Box::pin(async { let new_account = YggdrasilAccount::new( self.username.clone(), self.password.clone(), self.backend.clone(), self.auth_opts.clone(), ) .await?; let new_access_token = new_account.access_token().unwrap(); *self.access_token.lock() = new_access_token; Ok(()) }) } fn join<'a>( &'a self, public_key: &'a [u8], private_key: &'a [u8; 16], server_id: &'a str, proxy: Option, ) -> BoxFuture<'a, Result<(), ClientSessionServerError>> { Box::pin(async move { let access_token = self.access_token.lock().clone(); sessionserver::join_with_backend_url( SessionServerJoinOpts { access_token: &access_token, public_key, private_key, uuid: &self.uuid(), server_id, proxy, }, &self.backend.session, ) .await }) } } impl Account { /// This will create an online-mode account by authenticating with /// an Yggdrasil server. /// /// The cache key is used for avoiding having to log in every time. This is /// typically set to the account email, but it can be any string. #[cfg(feature = "online-mode")] pub async fn yggdrasil(username: String, backend: Backend) -> Result { YggdrasilAccount::new(username, None, backend, default_auth_opts()) .await .map(Account::from) } #[cfg(feature = "online-mode")] pub async fn yggdrasil_with_password( username: String, password: String, backend: Backend, ) -> Result { YggdrasilAccount::new(username, Some(password), backend, default_auth_opts()) .await .map(Account::from) } /// Similar to [`Account::yggdrasil`] but you can pass custom auth options /// (including the cache file location). /// /// For a custom cache directory, set /// `auth_opts.cache_file = /// Some(custom_dir.join("azalea-auth-yggdrasil.json"))`. /// /// If `auth_opts.cache_file` is `None`, it will default to Azalea's /// standard cache file (`~/.minecraft/azalea-auth-yggdrasil.json`) to match /// [`Account::yggdrasil`]. #[cfg(feature = "online-mode")] pub async fn yggdrasil_with_opts( username: String, password: Option, backend: Backend, auth_opts: YggdrasilAuthOpts, ) -> Result { YggdrasilAccount::new(username, password, backend, auth_opts) .await .map(Account::from) } }