diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | azalea-client/src/account.rs | 296 | ||||
| -rw-r--r-- | azalea-client/src/account/microsoft.rs | 279 | ||||
| -rw-r--r-- | azalea-client/src/account/mod.rs | 152 | ||||
| -rw-r--r-- | azalea-client/src/account/offline.rs | 37 | ||||
| -rw-r--r-- | azalea-client/src/lib.rs | 6 | ||||
| -rw-r--r-- | azalea-client/src/plugins/chat/handler.rs | 9 | ||||
| -rw-r--r-- | azalea-client/src/plugins/chat_signing.rs | 12 | ||||
| -rw-r--r-- | azalea-client/src/plugins/join.rs | 9 | ||||
| -rw-r--r-- | azalea-client/src/plugins/login.rs | 28 | ||||
| -rw-r--r-- | azalea-client/src/plugins/packet/login/events.rs | 2 | ||||
| -rw-r--r-- | azalea-client/src/plugins/packet/login/mod.rs | 2 | ||||
| -rw-r--r-- | azalea/examples/nearest_entity.rs | 2 | ||||
| -rw-r--r-- | azalea/examples/testbot/main.rs | 2 | ||||
| -rw-r--r-- | azalea/src/auto_reconnect.rs | 2 | ||||
| -rw-r--r-- | azalea/src/builder.rs | 2 | ||||
| -rw-r--r-- | azalea/src/client_impl/entity_query.rs | 2 | ||||
| -rw-r--r-- | azalea/src/client_impl/mod.rs | 3 | ||||
| -rw-r--r-- | azalea/src/prelude.rs | 2 | ||||
| -rw-r--r-- | azalea/src/swarm/builder.rs | 2 | ||||
| -rw-r--r-- | azalea/src/swarm/mod.rs | 4 |
21 files changed, 510 insertions, 344 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 41f463b0..27b349cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ is breaking anyways, semantic versioning is not followed. - Re-implement `Client::map_component` and `map_get_component`. - Add an `EntityRef` type to simplify interactions with entities. +- `AccountTrait` was implemented, which allows for custom refresh and join behavior for `Account`s. ### Changed diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs deleted file mode 100644 index 4db19d0e..00000000 --- a/azalea-client/src/account.rs +++ /dev/null @@ -1,296 +0,0 @@ -//! Connect to Minecraft servers. - -use std::sync::Arc; - -#[cfg(feature = "online-mode")] -use azalea_auth::AccessTokenResponse; -#[cfg(feature = "online-mode")] -use azalea_auth::certs::{Certificates, FetchCertificatesError}; -use bevy_ecs::component::Component; -use parking_lot::Mutex; -#[cfg(feature = "online-mode")] -use thiserror::Error; -use uuid::Uuid; - -/// Something that can join Minecraft servers. -/// -/// To join a server using this account, use [`StartJoinServerEvent`] or -/// [`azalea::ClientBuilder`]. -/// -/// This is also an ECS component that is present on our client entities. -/// -/// # Examples -/// -/// ```rust,no_run -/// # use azalea_client::Account; -/// # -/// # #[tokio::main] -/// # async fn main() { -/// let account = Account::microsoft("example@example.com").await; -/// // or Account::offline("example"); -/// # } -/// ``` -/// -/// [`StartJoinServerEvent`]: crate::join::StartJoinServerEvent -/// [`azalea::ClientBuilder`]: https://docs.rs/azalea/latest/azalea/struct.ClientBuilder.html -#[derive(Clone, Component, Debug)] -pub struct Account { - /// The Minecraft username of the account. - pub username: String, - /// The access token for authentication. - /// - /// You can obtain one of these manually from azalea-auth. - /// - /// This is an `Arc<Mutex>` so it can be modified by [`Self::refresh`]. - pub access_token: Option<Arc<Mutex<String>>>, - /// Only required for online-mode accounts. - pub uuid: Option<Uuid>, - - /// The parameters (i.e. email) that were passed for creating this - /// [`Account`]. - /// - /// This is used for automatic reauthentication when we get "Invalid - /// Session" errors. If you don't need that feature (like in - /// offline mode), then you can set this to `AuthOpts::default()`. - pub account_opts: AccountOpts, - - /// The certificates used for chat signing. - /// - /// This is set when you call [`Self::request_certs`], but you only - /// need to if the servers you're joining require it. - #[cfg(feature = "online-mode")] - pub certs: Arc<Mutex<Option<Certificates>>>, -} - -/// The parameters that were passed for creating the associated [`Account`]. -#[derive(Clone, Debug)] -pub enum AccountOpts { - Offline { - username: String, - }, - #[cfg(feature = "online-mode")] - Microsoft { - email: String, - }, - #[cfg(feature = "online-mode")] - MicrosoftWithAccessToken { - msa: Arc<Mutex<azalea_auth::cache::ExpiringValue<AccessTokenResponse>>>, - }, -} - -impl Account { - /// An offline account does not authenticate with Microsoft's servers, and - /// as such can only join offline mode servers. - /// - /// This is useful for testing in LAN worlds. - pub fn offline(username: &str) -> Self { - Self { - username: username.to_owned(), - access_token: None, - uuid: None, - account_opts: AccountOpts::Offline { - username: username.to_owned(), - }, - #[cfg(feature = "online-mode")] - certs: Arc::new(Mutex::new(None)), - } - } - - /// This will create an online-mode account by authenticating with - /// Microsoft's servers. - /// - /// 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 microsoft(cache_key: &str) -> Result<Self, azalea_auth::AuthError> { - Self::microsoft_with_custom_client_id_and_scope(cache_key, None, None).await - } - - /// Similar to [`Account::microsoft`] but you can use your own `client_id` - /// and `scope`. - /// - /// Pass `None` if you want to use default ones. - #[cfg(feature = "online-mode")] - pub async fn microsoft_with_custom_client_id_and_scope( - cache_key: &str, - client_id: Option<&str>, - scope: Option<&str>, - ) -> Result<Self, azalea_auth::AuthError> { - let minecraft_dir = minecraft_folder_path::minecraft_dir().unwrap_or_else(|| { - panic!( - "No {} environment variable found", - minecraft_folder_path::home_env_var() - ) - }); - let auth_result = azalea_auth::auth( - cache_key, - azalea_auth::AuthOpts { - cache_file: Some(minecraft_dir.join("azalea-auth.json")), - client_id, - scope, - ..Default::default() - }, - ) - .await?; - Ok(Self { - username: auth_result.profile.name, - access_token: Some(Arc::new(Mutex::new(auth_result.access_token))), - uuid: Some(auth_result.profile.id), - account_opts: AccountOpts::Microsoft { - email: cache_key.to_owned(), - }, - // we don't do chat signing by default unless the user asks for it - certs: Arc::new(Mutex::new(None)), - }) - } - - /// This will create an online-mode account through - /// [`azalea_auth::get_minecraft_token`] so you can have more control over - /// the authentication process (like doing your own caching or - /// displaying the Microsoft user code to the user in a different way). - /// - /// This will refresh the given token if it's expired. - /// - /// ``` - /// # use azalea_client::Account; - /// # async fn example() -> Result<(), Box<dyn std::error::Error>> { - /// let client = reqwest::Client::new(); - /// - /// let res = azalea_auth::get_ms_link_code(&client, None, None).await?; - /// // Or, `azalea_auth::get_ms_link_code(&client, Some(client_id), None).await?` - /// // if you want to use your own client_id - /// println!( - /// "Go to {} and enter the code {}", - /// res.verification_uri, res.user_code - /// ); - /// let msa = azalea_auth::get_ms_auth_token(&client, res, None).await?; - /// Account::with_microsoft_access_token(msa).await?; - /// # Ok(()) - /// # } - /// ``` - #[cfg(feature = "online-mode")] - pub async fn with_microsoft_access_token( - msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>, - ) -> Result<Self, azalea_auth::AuthError> { - Self::with_microsoft_access_token_and_custom_client_id_and_scope(msa, None, None).await - } - - /// Similar to [`Account::with_microsoft_access_token`] but you can use - /// custom `client_id` and `scope`. - #[cfg(feature = "online-mode")] - pub async fn with_microsoft_access_token_and_custom_client_id_and_scope( - mut msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>, - client_id: Option<&str>, - scope: Option<&str>, - ) -> Result<Self, azalea_auth::AuthError> { - let client = reqwest::Client::new(); - - if msa.is_expired() { - use tracing::trace; - - trace!("refreshing Microsoft auth token"); - msa = azalea_auth::refresh_ms_auth_token( - &client, - &msa.data.refresh_token, - client_id, - scope, - ) - .await?; - } - - let msa_token = &msa.data.access_token; - - let res = azalea_auth::get_minecraft_token(&client, msa_token).await?; - - let profile = azalea_auth::get_profile(&client, &res.minecraft_access_token).await?; - - Ok(Self { - username: profile.name, - access_token: Some(Arc::new(Mutex::new(res.minecraft_access_token))), - uuid: Some(profile.id), - account_opts: AccountOpts::MicrosoftWithAccessToken { - msa: Arc::new(Mutex::new(msa)), - }, - certs: Arc::new(Mutex::new(None)), - }) - } - /// Refresh the access_token for this account to be valid again. - /// - /// This requires the `auth_opts` field to be set correctly (which is done - /// by default if you used the constructor functions). Note that if the - /// Account is offline-mode then this function won't do anything. - #[cfg(feature = "online-mode")] - pub async fn refresh(&self) -> Result<(), azalea_auth::AuthError> { - match &self.account_opts { - // offline mode doesn't need to refresh so just don't do anything lol - AccountOpts::Offline { .. } => Ok(()), - AccountOpts::Microsoft { email } => { - let new_account = Account::microsoft(email).await?; - let access_token_mutex = self.access_token.as_ref().unwrap(); - let new_access_token = new_account.access_token.unwrap().lock().clone(); - *access_token_mutex.lock() = new_access_token; - Ok(()) - } - AccountOpts::MicrosoftWithAccessToken { msa } => { - let msa_value = msa.lock().clone(); - let new_account = Account::with_microsoft_access_token(msa_value).await?; - - let access_token_mutex = self.access_token.as_ref().unwrap(); - let new_access_token = new_account.access_token.unwrap().lock().clone(); - - *access_token_mutex.lock() = new_access_token; - let AccountOpts::MicrosoftWithAccessToken { msa: new_msa } = - new_account.account_opts - else { - unreachable!() - }; - *msa.lock() = new_msa.lock().clone(); - - Ok(()) - } - } - } - - /// Stub function that does nothing when the `online-mode` feature is - /// disabled. - #[cfg(not(feature = "online-mode"))] - pub async fn refresh(&self) -> Result<(), ()> { - Ok(()) - } - - /// Get the UUID of this account. - /// - /// If the `uuid` field is None, the UUID will be determined by using - /// Minecraft's offline-mode UUIDv3 algorithm. - pub fn uuid_or_offline(&self) -> Uuid { - self.uuid - .unwrap_or_else(|| azalea_crypto::offline::generate_uuid(&self.username)) - } -} - -#[cfg(feature = "online-mode")] -#[derive(Debug, Error)] -pub enum RequestCertError { - #[error("Failed to fetch certificates")] - FetchCertificates(#[from] FetchCertificatesError), - #[error("You can't request certificates for an offline account")] - NoAccessToken, -} - -impl Account { - /// Request the certificates used for chat signing and set it in - /// [`Self::certs`]. - #[cfg(feature = "online-mode")] - pub async fn request_certs(&mut self) -> Result<(), RequestCertError> { - let access_token = self - .access_token - .as_ref() - .ok_or(RequestCertError::NoAccessToken)? - .lock() - .clone(); - let certs = azalea_auth::certs::fetch_certificates(&access_token).await?; - *self.certs.lock() = Some(certs); - - Ok(()) - } -} diff --git a/azalea-client/src/account/microsoft.rs b/azalea-client/src/account/microsoft.rs new file mode 100644 index 00000000..22228eb9 --- /dev/null +++ b/azalea-client/src/account/microsoft.rs @@ -0,0 +1,279 @@ +use azalea_auth::{ + AccessTokenResponse, + certs::Certificates, + sessionserver::{self, ClientSessionServerError, SessionServerJoinOpts}, +}; +use parking_lot::Mutex; +use uuid::Uuid; + +use crate::account::{Account, AccountTrait, BoxFuture}; + +/// A type of account that authenticates with Microsoft using Azalea's cache. +/// +/// This type is not intended to be used directly by the user. To actually make +/// an account that authenticates with Microsoft, see [`Account::microsoft`]. +#[derive(Debug)] +pub struct MicrosoftAccount { + cache_key: String, + + username: String, + uuid: Uuid, + + access_token: Mutex<String>, + certs: Mutex<Option<Certificates>>, +} +impl MicrosoftAccount { + // deliberately private, use `Account::microsoft` or + // `Account::microsoft_with_custom_client_id_and_scope` instead. + async fn new( + cache_key: &str, + client_id: Option<&str>, + scope: Option<&str>, + ) -> Result<Self, azalea_auth::AuthError> { + let minecraft_dir = minecraft_folder_path::minecraft_dir().unwrap_or_else(|| { + panic!( + "No {} environment variable found", + minecraft_folder_path::home_env_var() + ) + }); + let auth_result = azalea_auth::auth( + cache_key, + azalea_auth::AuthOpts { + cache_file: Some(minecraft_dir.join("azalea-auth.json")), + client_id, + scope, + ..Default::default() + }, + ) + .await?; + + Ok(Self { + cache_key: cache_key.to_owned(), + username: auth_result.profile.name, + uuid: auth_result.profile.id, + access_token: Mutex::new(auth_result.access_token), + certs: Mutex::new(None), + }) + } +} +impl AccountTrait for MicrosoftAccount { + fn username(&self) -> &str { + &self.username + } + fn uuid(&self) -> Uuid { + self.uuid + } + fn access_token(&self) -> Option<String> { + Some(self.access_token.lock().to_owned()) + } + fn certs(&self) -> Option<azalea_auth::certs::Certificates> { + self.certs.lock().as_ref().cloned() + } + fn set_certs(&self, certs: azalea_auth::certs::Certificates) { + *self.certs.lock() = Some(certs); + } + fn refresh(&self) -> BoxFuture<'_, Result<(), azalea_auth::AuthError>> { + Box::pin(async { + let new_account = MicrosoftAccount::new(&self.cache_key, None, None).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<reqwest::Proxy>, + ) -> BoxFuture<'a, Result<(), ClientSessionServerError>> { + Box::pin(async move { + let access_token = self.access_token.lock().clone(); + sessionserver::join(SessionServerJoinOpts { + access_token: &access_token, + public_key: public_key, + private_key: private_key, + uuid: &self.uuid(), + server_id: &server_id, + proxy, + }) + .await + }) + } +} + +/// A type of account that authenticates using a Microsoft access token that the +/// user directly passes. +/// +/// This does not use Azalea's account cache. +/// +/// This type is not intended to be used directly by the user. To actually make +/// an account that authenticates with Microsoft like this, see +/// [`Account::with_microsoft_access_token`]. +#[derive(Debug)] +pub struct MicrosoftWithAccessTokenAccount { + msa: Mutex<azalea_auth::cache::ExpiringValue<AccessTokenResponse>>, + + username: String, + uuid: Uuid, + + access_token: Mutex<String>, + certs: Mutex<Option<Certificates>>, +} +impl MicrosoftWithAccessTokenAccount { + async fn new( + msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>, + client_id: Option<&str>, + scope: Option<&str>, + ) -> Result<Self, azalea_auth::AuthError> { + let client = reqwest::Client::new(); + + let mut msa = msa.clone(); + + if msa.is_expired() { + use tracing::trace; + + trace!("refreshing Microsoft auth token"); + msa = azalea_auth::refresh_ms_auth_token( + &client, + &msa.data.refresh_token, + client_id, + scope, + ) + .await?; + } + + let msa_token = &msa.data.access_token; + let res = azalea_auth::get_minecraft_token(&client, msa_token).await?; + let profile = azalea_auth::get_profile(&client, &res.minecraft_access_token).await?; + + Ok(Self { + username: profile.name, + access_token: Mutex::new(res.minecraft_access_token), + uuid: profile.id, + msa: Mutex::new(msa), + certs: Mutex::new(None), + }) + } +} +impl AccountTrait for MicrosoftWithAccessTokenAccount { + fn username(&self) -> &str { + &self.username + } + fn uuid(&self) -> Uuid { + self.uuid + } + fn access_token(&self) -> Option<String> { + Some(self.access_token.lock().to_owned()) + } + fn certs(&self) -> Option<azalea_auth::certs::Certificates> { + self.certs.lock().as_ref().cloned() + } + fn set_certs(&self, certs: azalea_auth::certs::Certificates) { + *self.certs.lock() = Some(certs); + } + fn refresh(&self) -> BoxFuture<'_, Result<(), azalea_auth::AuthError>> { + Box::pin(async { + let msa_value = self.msa.lock().clone(); + let new_account = MicrosoftWithAccessTokenAccount::new(msa_value, None, None).await?; + + let new_access_token = new_account.access_token().unwrap(); + + *self.access_token.lock() = new_access_token; + *self.msa.lock() = new_account.msa.lock().clone(); + + Ok(()) + }) + } + fn join<'a>( + &'a self, + public_key: &'a [u8], + private_key: &'a [u8; 16], + server_id: &'a str, + proxy: Option<reqwest::Proxy>, + ) -> BoxFuture<'a, Result<(), ClientSessionServerError>> { + Box::pin(async move { + let access_token = self.access_token.lock().clone(); + sessionserver::join(SessionServerJoinOpts { + access_token: &access_token, + public_key: public_key, + private_key: private_key, + uuid: &self.uuid(), + server_id: &server_id, + proxy, + }) + .await + }) + } +} + +impl Account { + /// This will create an online-mode account by authenticating with + /// Microsoft's servers. + /// + /// 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 microsoft(cache_key: &str) -> Result<Self, azalea_auth::AuthError> { + Self::microsoft_with_custom_client_id_and_scope(cache_key, None, None).await + } + + /// Similar to [`Account::microsoft`] but you can use your own `client_id` + /// and `scope`. + /// + /// Pass `None` if you want to use default ones. + #[cfg(feature = "online-mode")] + pub async fn microsoft_with_custom_client_id_and_scope( + cache_key: &str, + client_id: Option<&str>, + scope: Option<&str>, + ) -> Result<Self, azalea_auth::AuthError> { + MicrosoftAccount::new(cache_key, client_id, scope) + .await + .map(Account::from) + } + + /// This will create an online-mode account through + /// [`azalea_auth::get_minecraft_token`] so you can have more control over + /// the authentication process (like doing your own caching or + /// displaying the Microsoft user code to the user in a different way). + /// + /// This will refresh the given token if it's expired. + /// + /// ``` + /// # use azalea_client::Account; + /// # async fn example() -> Result<(), Box<dyn std::error::Error>> { + /// let client = reqwest::Client::new(); + /// + /// let res = azalea_auth::get_ms_link_code(&client, None, None).await?; + /// // Or, `azalea_auth::get_ms_link_code(&client, Some(client_id), None).await?` + /// // if you want to use your own client_id + /// println!( + /// "Go to {} and enter the code {}", + /// res.verification_uri, res.user_code + /// ); + /// let msa = azalea_auth::get_ms_auth_token(&client, res, None).await?; + /// Account::with_microsoft_access_token(msa).await?; + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "online-mode")] + pub async fn with_microsoft_access_token( + msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>, + ) -> Result<Self, azalea_auth::AuthError> { + Self::with_microsoft_access_token_and_custom_client_id_and_scope(msa, None, None).await + } + + /// Similar to [`Account::with_microsoft_access_token`] but you can use + /// custom `client_id` and `scope`. + #[cfg(feature = "online-mode")] + pub async fn with_microsoft_access_token_and_custom_client_id_and_scope( + msa: azalea_auth::cache::ExpiringValue<AccessTokenResponse>, + client_id: Option<&str>, + scope: Option<&str>, + ) -> Result<Self, azalea_auth::AuthError> { + MicrosoftWithAccessTokenAccount::new(msa, client_id, scope) + .await + .map(Account::from) + } +} diff --git a/azalea-client/src/account/mod.rs b/azalea-client/src/account/mod.rs new file mode 100644 index 00000000..22a099d1 --- /dev/null +++ b/azalea-client/src/account/mod.rs @@ -0,0 +1,152 @@ +//! Connect to Minecraft servers. + +#[cfg(feature = "online-mode")] +pub mod microsoft; +pub mod offline; + +use std::{fmt::Debug, ops::Deref, pin::Pin, sync::Arc}; + +#[cfg(feature = "online-mode")] +use azalea_auth::sessionserver::ClientSessionServerError; +use bevy_ecs::component::Component; +use uuid::Uuid; + +/// Something that can join Minecraft servers. +/// +/// By default, Azalea only supports either authentication with Microsoft +/// (online-mode), or no authentication at all (offline-mode). If you'd like to +/// do authentication in some other way, consider looking at [`AccountTrait`]. +/// +/// To join a server using this account, you can either use +/// [`StartJoinServerEvent`] or `azalea::ClientBuilder`. +/// +/// Note that `Account` is also an ECS component that's present on our client +/// entities. +/// +/// # Examples +/// +/// ```rust,no_run +/// # use azalea_client::Account; +/// # +/// # #[tokio::main] +/// # async fn main() { +/// let account = Account::microsoft("example@example.com").await; +/// // or Account::offline("example"); +/// # } +/// ``` +/// +/// [`StartJoinServerEvent`]: crate::join::StartJoinServerEvent +/// [`azalea::ClientBuilder`]: https://docs.rs/azalea/latest/azalea/struct.ClientBuilder.html +#[derive(Clone, Component, Debug)] +pub struct Account(Arc<dyn AccountTrait>); + +impl Account { + #[deprecated = "moved to `uuid()`."] + pub fn uuid_or_offline(&self) -> Uuid { + self.uuid() + } +} + +pub(crate) type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>; + +/// A trait that all types of accounts implement. +/// +/// This can be used, for example, to join servers with a custom authentication +/// server. +/// +/// Anything that implements [`AccountTrait`] can be converted to an [`Account`] +/// with `.into()`. +/// +/// Consider reading the source code of +/// [`MicrosoftAccount`](microsoft::MicrosoftAccount) for an example of how to +/// implement this. +pub trait AccountTrait: Send + Sync + Debug { + /// Returns the Minecraft username of the account. + fn username(&self) -> &str; + /// Returns the unique identifier for this player. + /// + /// For offline-mode accounts, this UUID is generated by calling + /// [`azalea_crypto::offline::generate_uuid`]. + fn uuid(&self) -> Uuid; + + /// The access token for authentication. + /// + /// You can obtain one of these manually from `azalea-auth`. + fn access_token(&self) -> Option<String>; + + /// Refreshes the access token for this account. + #[cfg(feature = "online-mode")] + fn refresh(&self) -> BoxFuture<'_, Result<(), azalea_auth::AuthError>> { + Box::pin(async { Ok(()) }) + } + /// Refreshes the access token for this account. + /// + /// The `online-mode` feature is disabled, so this won't do anything. + #[cfg(not(feature = "online-mode"))] + fn refresh(&self) -> BoxFuture<Result<(), ()>> { + Box::pin(async { Ok(()) }) + } + + #[cfg(feature = "online-mode")] + fn certs(&self) -> Option<azalea_auth::certs::Certificates> { + None + } + /// Override the chat signing certificates for this account. + /// + /// You can get the certificates needed for this from + /// [`azalea_auth::certs::fetch_certificates`]. You typically don't need to + /// call this yourself, as Azalea will do it for you. + /// + /// For accounts that don't support signing (i.e. offline-mode), this won't + /// do anything. + #[cfg(feature = "online-mode")] + fn set_certs(&self, certs: azalea_auth::certs::Certificates) { + let _ = certs; + } + + /// Typically used to tell Mojang's sessionserver that we are going to join + /// a server. + /// + /// This must be implemented for accounts that can join online-mode servers. + /// + /// This function is called internally by Azalea when the account tries to + /// join a server, but only if [`AccountTrait::access_token`] is `Some`. + #[cfg(feature = "online-mode")] + fn join<'a>( + &'a self, + public_key: &'a [u8], + private_key: &'a [u8; 16], + server_id: &'a str, + proxy: Option<reqwest::Proxy>, + ) -> BoxFuture<'a, Result<(), ClientSessionServerError>> { + let _ = (public_key, private_key, server_id, proxy); + Box::pin(async { Ok(()) }) + } + /// Typically used to tell Mojang's sessionserver that we are going to join + /// a server. + /// + /// The `online-mode` feature is disabled, so this won't do anything. + #[cfg(not(feature = "online-mode"))] + fn join( + &self, + public_key: &[u8], + private_key: &[u8; 16], + server_id: &str, + proxy: Option<reqwest::Proxy>, + ) -> BoxFuture<Result<(), ()>> { + let _ = (public_key, private_key, server_id, proxy); + Box::pin(async { Ok(()) }) + } +} +impl<T: AccountTrait + 'static> From<T> for Account { + fn from(value: T) -> Self { + Account(Arc::new(value)) + } +} +impl Deref for Account { + type Target = dyn AccountTrait; + + fn deref(&self) -> &Self::Target { + &*self.0 + } +} diff --git a/azalea-client/src/account/offline.rs b/azalea-client/src/account/offline.rs new file mode 100644 index 00000000..f0ee49bd --- /dev/null +++ b/azalea-client/src/account/offline.rs @@ -0,0 +1,37 @@ +use uuid::Uuid; + +use crate::account::{Account, AccountTrait}; + +/// A type of account that does not perform any authentication and cannot join +/// online-mode servers. +/// +/// This type is not intended to be used directly by the user. To actually make +/// an offline-mode account, see [`Account::offline`]. +#[derive(Debug)] +pub struct OfflineAccount { + username: String, +} +impl AccountTrait for OfflineAccount { + fn username(&self) -> &str { + &self.username + } + fn uuid(&self) -> Uuid { + azalea_crypto::offline::generate_uuid(&self.username) + } + fn access_token(&self) -> Option<String> { + None + } +} + +impl Account { + /// An offline account does not authenticate with Microsoft's servers, and + /// as such can only join offline mode servers. + /// + /// This is useful for testing in LAN worlds. + pub fn offline(username: &str) -> Self { + OfflineAccount { + username: username.to_owned(), + } + .into() + } +} diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index 6bdc2713..59730bf6 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -2,7 +2,7 @@ #![feature(error_generic_member_access)] #![feature(never_type)] -mod account; +pub mod account; mod client; pub mod local_player; pub mod ping; @@ -13,7 +13,9 @@ mod plugins; #[doc(hidden)] pub mod test_utils; -pub use account::{Account, AccountOpts}; +#[deprecated = "moved to `account::Account`."] +pub type Account = account::Account; + pub use azalea_physics::local_player::{PhysicsState, SprintDirection, WalkDirection}; pub use azalea_protocol::common::client_information::ClientInformation; // Re-export bevy-tasks so plugins can make sure that they're using the same diff --git a/azalea-client/src/plugins/chat/handler.rs b/azalea-client/src/plugins/chat/handler.rs index 626ce7c2..703a9846 100644 --- a/azalea-client/src/plugins/chat/handler.rs +++ b/azalea-client/src/plugins/chat/handler.rs @@ -9,7 +9,7 @@ use bevy_ecs::prelude::*; use super::ChatKind; use crate::packet::game::SendGamePacketEvent; #[cfg(feature = "online-mode")] -use crate::{Account, chat_signing::ChatSigningSession}; +use crate::{account::Account, chat_signing::ChatSigningSession}; /// Send a chat packet to the server of a specific kind (chat message or /// command). Usually you just want [`SendChatEvent`] instead. @@ -99,11 +99,12 @@ pub fn create_signature( ) -> azalea_crypto::signing::MessageSignature { use azalea_crypto::signing::SignChatMessageOptions; - let certs = account.certs.lock(); - let certs = certs.as_ref().expect("certs shouldn't be set back to None"); + let certs = account + .certs() + .expect("certs shouldn't be set back to None"); let signature = azalea_crypto::signing::sign_chat_message(&SignChatMessageOptions { - account_uuid: account.uuid.expect("account must have a uuid"), + account_uuid: account.uuid(), chat_session_uuid: chat_session.session_id, message_index: chat_session.messages_sent, salt, diff --git a/azalea-client/src/plugins/chat_signing.rs b/azalea-client/src/plugins/chat_signing.rs index 8e612f5b..2de9c6b0 100644 --- a/azalea-client/src/plugins/chat_signing.rs +++ b/azalea-client/src/plugins/chat_signing.rs @@ -13,7 +13,7 @@ use tracing::{debug, error}; use uuid::Uuid; use super::{chat, login::IsAuthenticated, packet::game::SendGamePacketEvent}; -use crate::{Account, InGameState}; +use crate::{InGameState, account::Account}; pub struct ChatSigningPlugin; impl Plugin for ChatSigningPlugin { @@ -70,7 +70,7 @@ pub fn poll_request_certs_task( commands.entity(entity).insert(QueuedCertsToSend { certs: certs.clone(), }); - *account.certs.lock() = Some(certs); + account.set_certs(certs); } Err(err) => { error!("Error requesting certs: {err:?}. Retrying in an hour."); @@ -108,8 +108,8 @@ pub fn request_certs_if_needed( continue; } - let certs = account.certs.lock(); - let should_refresh = if let Some(certs) = &*certs { + let certs = account.certs(); + let should_refresh = if let Some(certs) = certs { // certs were already requested and we're waiting for them to refresh // but maybe they weren't sent yet, in which case we still want to send the @@ -122,12 +122,10 @@ pub fn request_certs_if_needed( } else { true }; - drop(certs); - if should_refresh && let Some(access_token) = &account.access_token { + if should_refresh && let Some(access_token) = account.access_token() { let task_pool = IoTaskPool::get(); - let access_token = access_token.lock().clone(); debug!("Started task to fetch certs"); let task = task_pool.spawn(async_compat::Compat::new(async move { azalea_auth::certs::fetch_certificates(&access_token).await diff --git a/azalea-client/src/plugins/join.rs b/azalea-client/src/plugins/join.rs index 058f6c10..c254bc26 100644 --- a/azalea-client/src/plugins/join.rs +++ b/azalea-client/src/plugins/join.rs @@ -20,7 +20,8 @@ use tokio::sync::mpsc; use tracing::{debug, warn}; use crate::{ - Account, LocalPlayerBundle, + LocalPlayerBundle, + account::Account, connection::RawConnection, packet::login::{InLoginState, SendLoginPacketEvent}, }; @@ -94,7 +95,7 @@ pub fn handle_start_join_server_event( connection_query: Query<&RawConnection>, ) { for event in events.read() { - let uuid = event.account.uuid_or_offline(); + let uuid = event.account.uuid(); let entity = if let Some(entity) = entity_uuid_index.get(&uuid) { debug!("Reusing entity {entity:?} for client"); @@ -228,8 +229,8 @@ pub fn poll_create_connection_task( commands.trigger(SendLoginPacketEvent::new( entity, ServerboundHello { - name: account.username.clone(), - profile_id: account.uuid_or_offline(), + name: account.username().to_owned(), + profile_id: account.uuid(), }, )); } diff --git a/azalea-client/src/plugins/login.rs b/azalea-client/src/plugins/login.rs index 5bfefe44..b301d94b 100644 --- a/azalea-client/src/plugins/login.rs +++ b/azalea-client/src/plugins/login.rs @@ -14,7 +14,7 @@ use super::{ connection::RawConnection, packet::login::{ReceiveCustomQueryEvent, ReceiveHelloEvent, SendLoginPacketEvent}, }; -use crate::{Account, join::ConnectOpts}; +use crate::{account::Account, join::ConnectOpts}; /// Some systems that run during the `login` state. pub struct LoginPlugin; @@ -46,7 +46,7 @@ fn handle_receive_hello_event( None }; - let task = task_pool.spawn(auth_with_account(account, packet, connect_opts)); + let task = task_pool.spawn(async { auth_with_account(account, packet, connect_opts).await }); commands.entity(client).insert(AuthTask(task)); } @@ -123,7 +123,7 @@ pub async fn auth_with_account( #[cfg(feature = "online-mode")] if packet.should_authenticate { - let Some(access_token) = &account.access_token else { + if account.access_token().is_none() { // offline mode account, no need to do auth return Ok((key_packet, private_key)); }; @@ -135,26 +135,15 @@ pub async fn auth_with_account( let proxy = proxy.map(Proxy::into); while let Err(err) = { - use azalea_auth::sessionserver::{self, SessionServerJoinOpts}; - - let access_token = access_token.lock().clone(); - - let uuid = &account - .uuid - .expect("Uuid must be present if access token is present."); - let proxy = proxy.clone(); // this is necessary since reqwest usually depends on tokio and we're using // `futures` here - async_compat::Compat::new(sessionserver::join(SessionServerJoinOpts { - access_token: &access_token, - public_key: &packet.public_key, - private_key: &private_key, - uuid, - server_id: &packet.server_id, - proxy, - })) + async_compat::Compat::new(async { + account + .join(&packet.public_key, &private_key, &packet.server_id, proxy) + .await + }) .await } { if attempts >= 2 { @@ -169,6 +158,7 @@ pub async fn auth_with_account( ) { // uh oh, we got an invalid session and have // to reauthenticate now + async_compat::Compat::new(account.refresh()).await?; } else { return Err(err.into()); diff --git a/azalea-client/src/plugins/packet/login/events.rs b/azalea-client/src/plugins/packet/login/events.rs index 52e696cb..5b44e60c 100644 --- a/azalea-client/src/plugins/packet/login/events.rs +++ b/azalea-client/src/plugins/packet/login/events.rs @@ -10,7 +10,7 @@ use bevy_ecs::prelude::*; use tracing::{debug, error}; use super::InLoginState; -use crate::{Account, connection::RawConnection}; +use crate::{account::Account, connection::RawConnection}; #[derive(Clone, Debug, Message)] pub struct ReceiveLoginPacketEvent { diff --git a/azalea-client/src/plugins/packet/login/mod.rs b/azalea-client/src/plugins/packet/login/mod.rs index de44309c..aa630def 100644 --- a/azalea-client/src/plugins/packet/login/mod.rs +++ b/azalea-client/src/plugins/packet/login/mod.rs @@ -17,7 +17,7 @@ use tracing::{debug, error}; use super::as_system; use crate::{ - Account, InConfigState, connection::RawConnection, cookies::RequestCookieEvent, + InConfigState, account::Account, connection::RawConnection, cookies::RequestCookieEvent, disconnect::DisconnectEvent, packet::declare_packet_handlers, player::GameProfileComponent, }; diff --git a/azalea/examples/nearest_entity.rs b/azalea/examples/nearest_entity.rs index a0385917..418297b5 100644 --- a/azalea/examples/nearest_entity.rs +++ b/azalea/examples/nearest_entity.rs @@ -4,7 +4,7 @@ use azalea::{ nearest_entity::EntityFinder, prelude::*, }; -use azalea_client::Account; +use azalea_client::account::Account; use azalea_core::tick::GameTick; use azalea_entity::{ LocalEntity, Position, diff --git a/azalea/examples/testbot/main.rs b/azalea/examples/testbot/main.rs index 9889e250..b2ad0b61 100644 --- a/azalea/examples/testbot/main.rs +++ b/azalea/examples/testbot/main.rs @@ -190,7 +190,7 @@ async fn handle(bot: Client, event: azalea::Event, state: State) -> anyhow::Resu async fn swarm_handle(_swarm: Swarm, event: SwarmEvent, _state: SwarmState) -> anyhow::Result<()> { match &event { SwarmEvent::Disconnect(account, _join_opts) => { - println!("bot got kicked! {}", account.username); + println!("bot got kicked! {}", account.username()); } SwarmEvent::Chat(chat) => { if chat.message().to_string() == "The particle was not visible for anybody" { diff --git a/azalea/src/auto_reconnect.rs b/azalea/src/auto_reconnect.rs index dd353b7a..3ab08f27 100644 --- a/azalea/src/auto_reconnect.rs +++ b/azalea/src/auto_reconnect.rs @@ -11,7 +11,7 @@ use super::{ disconnect::DisconnectEvent, join::{ConnectOpts, ConnectionFailedEvent, StartJoinServerEvent}, }; -use crate::Account; +use crate::account::Account; /// The default delay that Azalea will use for reconnecting our clients. /// diff --git a/azalea/src/builder.rs b/azalea/src/builder.rs index 8151b3a1..db51de63 100644 --- a/azalea/src/builder.rs +++ b/azalea/src/builder.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use azalea_client::{Account, DefaultPlugins}; +use azalea_client::{DefaultPlugins, account::Account}; use azalea_protocol::address::ResolvableAddr; use bevy_app::{AppExit, Plugins}; use bevy_ecs::component::Component; diff --git a/azalea/src/client_impl/entity_query.rs b/azalea/src/client_impl/entity_query.rs index 268eaaf1..44fbe9e8 100644 --- a/azalea/src/client_impl/entity_query.rs +++ b/azalea/src/client_impl/entity_query.rs @@ -189,7 +189,7 @@ impl Client { &self, predicate: impl EntityPredicate<Q, F>, ) -> Option<Entity> { - let instance_name = self.get_component::<InstanceName>()?; + let instance_name = self.get_component::<InstanceName>()?.clone(); predicate.find_any(self.ecs.clone(), &instance_name) } diff --git a/azalea/src/client_impl/mod.rs b/azalea/src/client_impl/mod.rs index e3876f2e..db9ecf91 100644 --- a/azalea/src/client_impl/mod.rs +++ b/azalea/src/client_impl/mod.rs @@ -2,7 +2,8 @@ use std::{collections::HashMap, sync::Arc}; use azalea_auth::game_profile::GameProfile; use azalea_client::{ - Account, DefaultPlugins, + DefaultPlugins, + account::Account, connection::RawConnection, disconnect::DisconnectEvent, join::{ConnectOpts, StartJoinServerEvent}, diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs index d39e50c4..ad895bcb 100644 --- a/azalea/src/prelude.rs +++ b/azalea/src/prelude.rs @@ -1,7 +1,7 @@ //! The Azalea prelude. Things that are necessary for a bare-bones bot are //! re-exported here. -pub use azalea_client::Account; +pub use azalea_client::account::Account; pub use azalea_core::tick::GameTick; pub use bevy_app::AppExit; diff --git a/azalea/src/swarm/builder.rs b/azalea/src/swarm/builder.rs index 4312a485..14c9c290 100644 --- a/azalea/src/swarm/builder.rs +++ b/azalea/src/swarm/builder.rs @@ -8,7 +8,7 @@ use std::{ time::Duration, }; -use azalea_client::{Account, DefaultPlugins, start_ecs_runner}; +use azalea_client::{DefaultPlugins, account::Account, start_ecs_runner}; use azalea_protocol::address::{ResolvableAddr, ResolvedAddr}; use azalea_world::InstanceContainer; use bevy_app::{App, AppExit, Plugins, SubApp}; diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index 691fb354..b1061346 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -12,7 +12,7 @@ use std::sync::{ atomic::{self, AtomicBool}, }; -use azalea_client::{Account, chat::ChatPacket, join::ConnectOpts}; +use azalea_client::{account::Account, chat::ChatPacket, join::ConnectOpts}; use azalea_entity::LocalEntity; use azalea_protocol::address::ResolvedAddr; use azalea_world::InstanceContainer; @@ -165,7 +165,7 @@ impl Swarm { ) -> Client { debug!( "add_with_opts called for account {} with opts {join_opts:?}", - account.username + account.username() ); let mut address = self.address.read().clone(); |
