From a2933683756ba76d8f2249328cb8f8adc866d271 Mon Sep 17 00:00:00 2001 From: veronoicc <64193056+veronoicc@users.noreply.github.com> Date: Sun, 17 Mar 2024 12:14:55 +0000 Subject: [PATCH 01/10] Add first attempt at generic auth flows, swarm doesn't compile yet --- Cargo.lock | 1 + azalea-auth/Cargo.toml | 1 + azalea-auth/examples/auth.rs | 2 +- azalea-auth/examples/certificates.rs | 2 +- azalea-auth/src/account.rs | 34 +++ azalea-auth/src/cache.rs | 10 +- azalea-auth/src/certs.rs | 66 +---- azalea-auth/src/lib.rs | 6 +- azalea-auth/src/{auth.rs => microsoft.rs} | 337 ++++++++++++++-------- azalea-auth/src/offline.rs | 47 +++ azalea-auth/src/sessionserver.rs | 76 ----- azalea-client/src/account.rs | 234 --------------- azalea-client/src/client.rs | 33 +-- azalea-client/src/lib.rs | 2 - azalea-protocol/src/connect.rs | 14 +- azalea/src/prelude.rs | 3 +- azalea/src/swarm/mod.rs | 20 +- 17 files changed, 335 insertions(+), 553 deletions(-) create mode 100644 azalea-auth/src/account.rs rename azalea-auth/src/{auth.rs => microsoft.rs} (63%) delete mode 100755 azalea-client/src/account.rs diff --git a/Cargo.lock b/Cargo.lock index 6f228d2de..ed634118b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,7 @@ dependencies = [ "azalea-buf", "azalea-crypto", "base64", + "bevy_ecs", "chrono", "env_logger", "md-5", diff --git a/azalea-auth/Cargo.toml b/azalea-auth/Cargo.toml index 72612454f..13bc105b2 100644 --- a/azalea-auth/Cargo.toml +++ b/azalea-auth/Cargo.toml @@ -27,6 +27,7 @@ thiserror = "1.0.57" tokio = { version = "1.36.0", features = ["fs"] } uuid = { version = "1.7.0", features = ["serde", "v3"] } md-5 = "0.10.6" +bevy_ecs = "0.13.0" [dev-dependencies] env_logger = "0.11.2" diff --git a/azalea-auth/examples/auth.rs b/azalea-auth/examples/auth.rs index 350c5b179..fa99765ed 100755 --- a/azalea-auth/examples/auth.rs +++ b/azalea-auth/examples/auth.rs @@ -8,7 +8,7 @@ async fn main() { let auth_result = azalea_auth::auth( "example@example.com", - azalea_auth::AuthOpts { + azalea_auth::MicrosoftAuthOpts { cache_file: Some(cache_file), ..Default::default() }, diff --git a/azalea-auth/examples/certificates.rs b/azalea-auth/examples/certificates.rs index 69a38efeb..cd2904f1c 100755 --- a/azalea-auth/examples/certificates.rs +++ b/azalea-auth/examples/certificates.rs @@ -8,7 +8,7 @@ async fn main() { let auth_result = azalea_auth::auth( "example@example.com", - azalea_auth::AuthOpts { + azalea_auth::MicrosoftAuthOpts { cache_file: Some(cache_file), ..Default::default() }, diff --git a/azalea-auth/src/account.rs b/azalea-auth/src/account.rs new file mode 100644 index 000000000..7172cec6e --- /dev/null +++ b/azalea-auth/src/account.rs @@ -0,0 +1,34 @@ +use std::future::Future; + +use bevy_ecs::component::Component; +use uuid::Uuid; + +use crate::{certs::{Certificates, FetchCertificatesError}, sessionserver::ClientSessionServerError}; + +pub trait Account: Clone + Component { + fn join( + &self, + public_key: &[u8], + private_key: &[u8], + server_id: &str, + ) -> impl Future> + Send { + let server_hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data( + server_id.as_bytes(), + public_key, + private_key, + )); + let uuid = self.get_uuid(); + + self.join_with_server_id_hash(uuid, server_hash) + } + fn join_with_server_id_hash(&self, uuid: Uuid, server_hash: String) -> impl Future> + Send; + + fn fetch_certificates(&self) -> impl Future> + Send; + + fn get_username(&self) -> String; + fn get_uuid(&self) -> Uuid; + + fn is_online(&self) -> bool { + true + } +} \ No newline at end of file diff --git a/azalea-auth/src/cache.rs b/azalea-auth/src/cache.rs index 85d25f939..37cd72a66 100755 --- a/azalea-auth/src/cache.rs +++ b/azalea-auth/src/cache.rs @@ -7,6 +7,8 @@ use thiserror::Error; use tokio::fs::File; use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use crate::{AccessTokenResponse, MinecraftAuthResponse, ProfileResponse, XboxLiveAuth}; + #[derive(Debug, Error)] pub enum CacheError { #[error("Failed to read cache file: {0}")] @@ -23,13 +25,13 @@ pub enum CacheError { pub struct CachedAccount { pub email: String, /// Microsoft auth - pub msa: ExpiringValue, + pub msa: ExpiringValue, /// Xbox Live auth - pub xbl: ExpiringValue, + pub xbl: ExpiringValue, /// Minecraft auth - pub mca: ExpiringValue, + pub mca: ExpiringValue, /// The user's Minecraft profile (i.e. username, UUID, skin) - pub profile: crate::auth::ProfileResponse, + pub profile: ProfileResponse, } #[derive(Deserialize, Serialize, Debug)] diff --git a/azalea-auth/src/certs.rs b/azalea-auth/src/certs.rs index 6214142be..564643de3 100644 --- a/azalea-auth/src/certs.rs +++ b/azalea-auth/src/certs.rs @@ -1,6 +1,5 @@ -use base64::Engine; use chrono::{DateTime, Utc}; -use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey}; +use rsa::RsaPrivateKey; use serde::Deserialize; use thiserror::Error; @@ -12,69 +11,6 @@ pub enum FetchCertificatesError { Pkcs8(#[from] rsa::pkcs8::Error), } -/// Fetch the Mojang-provided key-pair for your player, which is used for -/// cryptographically signing chat messages. -pub async fn fetch_certificates( - minecraft_access_token: &str, -) -> Result { - let client = reqwest::Client::new(); - - let res = client - .post("https://api.minecraftservices.com/player/certificates") - .header("Authorization", format!("Bearer {minecraft_access_token}")) - .send() - .await? - .json::() - .await?; - tracing::trace!("{:?}", res); - - // using RsaPrivateKey::from_pkcs8_pem gives an error with decoding base64 so we - // just decode it ourselves - - // remove the first and last lines of the private key - let private_key_pem_base64 = res - .key_pair - .private_key - .lines() - .skip(1) - .take_while(|line| !line.starts_with('-')) - .collect::(); - let private_key_der = base64::engine::general_purpose::STANDARD - .decode(private_key_pem_base64) - .unwrap(); - - let public_key_pem_base64 = res - .key_pair - .public_key - .lines() - .skip(1) - .take_while(|line| !line.starts_with('-')) - .collect::(); - let public_key_der = base64::engine::general_purpose::STANDARD - .decode(public_key_pem_base64) - .unwrap(); - - // the private key also contains the public key so it's basically a keypair - let private_key = RsaPrivateKey::from_pkcs8_der(&private_key_der).unwrap(); - - let certificates = Certificates { - private_key, - public_key_der, - - signature_v1: base64::engine::general_purpose::STANDARD - .decode(&res.public_key_signature) - .unwrap(), - signature_v2: base64::engine::general_purpose::STANDARD - .decode(&res.public_key_signature_v2) - .unwrap(), - - expires_at: res.expires_at, - refresh_after: res.refreshed_after, - }; - - Ok(certificates) -} - /// A chat signing certificate. #[derive(Clone, Debug)] pub struct Certificates { diff --git a/azalea-auth/src/lib.rs b/azalea-auth/src/lib.rs index 1643bf04f..74a5e8bd6 100755 --- a/azalea-auth/src/lib.rs +++ b/azalea-auth/src/lib.rs @@ -1,10 +1,12 @@ #![doc = include_str!("../README.md")] -mod auth; +pub mod microsoft; pub mod cache; pub mod certs; pub mod game_profile; pub mod offline; pub mod sessionserver; +pub mod account; -pub use auth::*; +pub use microsoft::*; +pub use offline::*; \ No newline at end of file diff --git a/azalea-auth/src/auth.rs b/azalea-auth/src/microsoft.rs similarity index 63% rename from azalea-auth/src/auth.rs rename to azalea-auth/src/microsoft.rs index 7b5846c42..f80840150 100755 --- a/azalea-auth/src/auth.rs +++ b/azalea-auth/src/microsoft.rs @@ -1,9 +1,14 @@ //! Handle Minecraft (Xbox) authentication. -use crate::cache::{self, CachedAccount, ExpiringValue}; +use crate::{account::Account, cache::{self, CachedAccount, ExpiringValue}, certs::{Certificates, CertificatesResponse, FetchCertificatesError}, sessionserver::{ClientSessionServerError, ForbiddenError}}; +use base64::Engine; +use bevy_ecs::component::Component; use chrono::{DateTime, Utc}; +use reqwest::StatusCode; +use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey}; use serde::{Deserialize, Serialize}; use serde_json::json; +use tracing::debug; use std::{ collections::HashMap, path::PathBuf, @@ -13,7 +18,7 @@ use thiserror::Error; use uuid::Uuid; #[derive(Default)] -pub struct AuthOpts { +pub struct MicrosoftAuthOpts { /// 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. @@ -26,8 +31,15 @@ pub struct AuthOpts { pub cache_file: Option, } +#[derive(Clone, Debug, Component)] +pub struct MicrosoftAccount { + pub client: reqwest::Client, + pub access_token: String, + pub profile: ProfileResponse, +} + #[derive(Debug, Error)] -pub enum AuthError { +pub enum MicrosoftAuthError { #[error( "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." @@ -49,142 +61,211 @@ pub enum AuthError { GetXboxLiveAuth(#[from] XboxLiveAuthError), } -/// Authenticate with Microsoft. If the data isn't cached, -/// they'll be asked to go to log into Microsoft in a web page. -/// -/// 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. -/// -/// If you want to use your own code to cache or show the auth code to the user -/// in a different way, use [`get_ms_link_code`], [`get_ms_auth_token`], -/// [`get_minecraft_token`] and [`get_profile`] instead. -pub async fn auth(email: &str, opts: AuthOpts) -> Result { - let cached_account = if let Some(cache_file) = &opts.cache_file { - cache::get_account_in_cache(cache_file, email).await - } else { - None - }; - - if cached_account.is_some() && !cached_account.as_ref().unwrap().mca.is_expired() { - let account = cached_account.as_ref().unwrap(); - // the minecraft auth data is cached and not expired, so we can just - // use that instead of doing auth all over again :) - - Ok(AuthResult { - access_token: account.mca.data.access_token.clone(), - profile: account.profile.clone(), - }) - } else { - let client = reqwest::Client::new(); - let mut msa = if let Some(account) = cached_account { - account.msa +impl MicrosoftAccount { + async fn new(email: &str, opts: MicrosoftAuthOpts) -> Result { + let cached_account = if let Some(cache_file) = &opts.cache_file { + cache::get_account_in_cache(cache_file, email).await } else { - interactive_get_ms_auth_token(&client, email).await? + None }; - if msa.is_expired() { - tracing::trace!("refreshing Microsoft auth token"); - match refresh_ms_auth_token(&client, &msa.data.refresh_token).await { - Ok(new_msa) => msa = new_msa, - Err(e) => { - // can't refresh, ask the user to auth again - tracing::error!("Error refreshing Microsoft auth token: {}", e); - msa = interactive_get_ms_auth_token(&client, email).await?; + + if cached_account.is_some() && !cached_account.as_ref().unwrap().mca.is_expired() { + let account = cached_account.as_ref().unwrap(); + // the minecraft auth data is cached and not expired, so we can just + // use that instead of doing auth all over again :) + + Ok(Self { + client: reqwest::Client::new(), + access_token: account.mca.data.access_token.clone(), + profile: account.profile.clone(), + }) + } else { + let client = reqwest::Client::new(); + let mut msa = if let Some(account) = cached_account { + account.msa + } else { + interactive_get_ms_auth_token(&client, email).await? + }; + if msa.is_expired() { + tracing::trace!("Refreshing Microsoft auth token"); + match refresh_ms_auth_token(&client, &msa.data.refresh_token).await { + Ok(new_msa) => msa = new_msa, + Err(e) => { + // can't refresh, ask the user to auth again + tracing::error!("Error refreshing Microsoft auth token: {}", e); + msa = interactive_get_ms_auth_token(&client, email).await?; + } } } - } - - let msa_token = &msa.data.access_token; - tracing::trace!("Got access token: {msa_token}"); - - let res = get_minecraft_token(&client, msa_token).await?; - - if opts.check_ownership { - let has_game = check_ownership(&client, &res.minecraft_access_token).await?; - if !has_game { - return Err(AuthError::DoesNotOwnGame); - } - } - - let profile: ProfileResponse = get_profile(&client, &res.minecraft_access_token).await?; - - if let Some(cache_file) = opts.cache_file { - if let Err(e) = cache::set_account_in_cache( - &cache_file, - email, - CachedAccount { - email: email.to_string(), - mca: res.mca, - msa, - xbl: res.xbl, - profile: profile.clone(), - }, + + let msa_token = &msa.data.access_token; + tracing::trace!("Got access token: {msa_token}"); + + let xbl = auth_with_xbox_live(&client, msa_token).await?; + + let xsts_token = obtain_xsts_for_minecraft( + &client, + &xbl + .get() + .expect("Xbox Live auth token shouldn't have expired yet") + .token, ) - .await - { - tracing::error!("{}", e); + .await?; + + let mca = auth_with_minecraft(&client, &xbl.data.user_hash, &xsts_token).await?; + + let minecraft_access_token: String = mca + .get() + .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 { + return Err(MicrosoftAuthError::DoesNotOwnGame); + } } + + let profile: ProfileResponse = get_profile(&client, &minecraft_access_token).await?; + + if let Some(cache_file) = opts.cache_file { + if let Err(e) = cache::set_account_in_cache( + &cache_file, + email, + CachedAccount { + email: email.to_string(), + mca, + msa, + xbl, + profile: profile.clone(), + }, + ) + .await + { + tracing::error!("{}", e); + } + } + + Ok(Self { + client, + access_token: minecraft_access_token, + profile, + }) } - - Ok(AuthResult { - access_token: res.minecraft_access_token, - profile, - }) } } -/// Authenticate with Minecraft when we already have a Microsoft auth token. -/// -/// Usually you don't need this since [`auth`] will call it for you, but it's -/// useful if you want more control over what it does. -/// -/// If you don't have a Microsoft auth token, you can get it from -/// [`get_ms_link_code`] and then [`get_ms_auth_token`]. -/// -/// If you got the MSA token from your own app (as opposed to the default -/// Nintendo Switch one), you may have to prepend "d=" to the token. -pub async fn get_minecraft_token( - client: &reqwest::Client, - msa: &str, -) -> Result { - let xbl_auth = auth_with_xbox_live(client, msa).await?; - - let xsts_token = obtain_xsts_for_minecraft( - client, - &xbl_auth - .get() - .expect("Xbox Live auth token shouldn't have expired yet") - .token, - ) - .await?; - - // Minecraft auth - let mca = auth_with_minecraft(client, &xbl_auth.data.user_hash, &xsts_token).await?; - - let minecraft_access_token: String = mca - .get() - .expect("Minecraft auth shouldn't have expired yet") - .access_token - .to_string(); - - Ok(MinecraftTokenResponse { - mca, - xbl: xbl_auth, - minecraft_access_token, - }) -} - -#[derive(Debug)] -pub struct MinecraftTokenResponse { - pub mca: ExpiringValue, - pub xbl: ExpiringValue, - pub minecraft_access_token: String, -} +impl Account for MicrosoftAccount { + async fn join_with_server_id_hash(&self, uuid: Uuid, server_hash: String) -> Result<(), ClientSessionServerError> { + let mut encode_buffer = Uuid::encode_buffer(); + let undashed_uuid = uuid.simple().encode_lower(&mut encode_buffer); + + let data = json!({ + "accessToken": self.access_token, + "selectedProfile": undashed_uuid, + "serverId": server_hash + }); + let res = self.client + .post("https://sessionserver.mojang.com/session/minecraft/join") + .json(&data) + .send() + .await?; + + match res.status() { + StatusCode::NO_CONTENT => Ok(()), + StatusCode::FORBIDDEN => { + let forbidden = res.json::().await?; + match forbidden.error.as_str() { + "InsufficientPrivilegesException" => { + Err(ClientSessionServerError::MultiplayerDisabled) + } + "UserBannedException" => Err(ClientSessionServerError::Banned), + "AuthenticationUnavailableException" => { + Err(ClientSessionServerError::AuthServersUnreachable) + } + "InvalidCredentialsException" => Err(ClientSessionServerError::InvalidSession), + "ForbiddenOperationException" => Err(ClientSessionServerError::ForbiddenOperation), + _ => Err(ClientSessionServerError::Unknown(forbidden.error)), + } + } + StatusCode::TOO_MANY_REQUESTS => Err(ClientSessionServerError::RateLimited), + status_code => { + // log the headers + debug!("Error headers: {:#?}", res.headers()); + let body = res.text().await?; + Err(ClientSessionServerError::UnexpectedResponse { + status_code: status_code.as_u16(), + body, + }) + } + } + } + + fn get_username(&self) -> String { + self.profile.name.clone() + } + + fn get_uuid(&self) -> Uuid { + self.profile.id + } + + async fn fetch_certificates(&self) -> Result { + let res = self.client + .post("https://api.minecraftservices.com/player/certificates") + .header("Authorization", format!("Bearer {}", self.access_token)) + .send() + .await? + .json::() + .await?; + tracing::trace!("{:?}", res); + + // using RsaPrivateKey::from_pkcs8_pem gives an error with decoding base64 so we + // just decode it ourselves + + // remove the first and last lines of the private key + let private_key_pem_base64 = res + .key_pair + .private_key + .lines() + .skip(1) + .take_while(|line| !line.starts_with('-')) + .collect::(); + let private_key_der = base64::engine::general_purpose::STANDARD + .decode(private_key_pem_base64) + .unwrap(); + + let public_key_pem_base64 = res + .key_pair + .public_key + .lines() + .skip(1) + .take_while(|line| !line.starts_with('-')) + .collect::(); + let public_key_der = base64::engine::general_purpose::STANDARD + .decode(public_key_pem_base64) + .unwrap(); + + // the private key also contains the public key so it's basically a keypair + let private_key = RsaPrivateKey::from_pkcs8_der(&private_key_der).unwrap(); + + let certificates = Certificates { + private_key, + public_key_der, + + signature_v1: base64::engine::general_purpose::STANDARD + .decode(&res.public_key_signature) + .unwrap(), + signature_v2: base64::engine::general_purpose::STANDARD + .decode(&res.public_key_signature_v2) + .unwrap(), + + expires_at: res.expires_at, + refresh_after: res.refreshed_after, + }; -#[derive(Debug)] -pub struct AuthResult { - pub access_token: String, - pub profile: ProfileResponse, + Ok(certificates) + } } #[derive(Debug, Deserialize)] diff --git a/azalea-auth/src/offline.rs b/azalea-auth/src/offline.rs index 737555e86..986e6b5b8 100644 --- a/azalea-auth/src/offline.rs +++ b/azalea-auth/src/offline.rs @@ -1,6 +1,53 @@ +use bevy_ecs::component::Component; use md5::{Digest, Md5}; use uuid::Uuid; +use crate::{account::Account, certs::{Certificates, FetchCertificatesError}, sessionserver::ClientSessionServerError}; + +#[derive(Clone, Debug, Component)] +pub struct OfflineAccount { + pub username: String, + pub uuid: Option, +} + +impl OfflineAccount { + pub fn new(username: String) -> Self { + Self { + username, + uuid: None, + } + } + + pub fn with_uuid(username: String, uuid: Uuid) -> Self { + Self { + username, + uuid: Some(uuid), + } + } +} + +impl Account for OfflineAccount { + async fn join_with_server_id_hash(&self, _: Uuid, _: String) -> Result<(), ClientSessionServerError> { + unimplemented!("Offline accounts can't join servers with a session server.") + } + + async fn fetch_certificates(&self) -> Result { + unimplemented!("Offline accounts can't fetch certificates.") + } + + fn get_username(&self) -> String { + self.username.clone() + } + + fn get_uuid(&self) -> Uuid { + self.uuid.unwrap_or_else(|| generate_uuid(&self.username)) + } + + fn is_online(&self) -> bool { + false + } +} + pub fn generate_uuid(username: &str) -> Uuid { uuid::Builder::from_md5_bytes(hash(format!("OfflinePlayer:{username}").as_bytes())).into_uuid() } diff --git a/azalea-auth/src/sessionserver.rs b/azalea-auth/src/sessionserver.rs index 9c73fbf0b..e9ebbe5a5 100755 --- a/azalea-auth/src/sessionserver.rs +++ b/azalea-auth/src/sessionserver.rs @@ -1,11 +1,8 @@ //! Tell Mojang you're joining a multiplayer server. -use once_cell::sync::Lazy; use reqwest::StatusCode; use serde::Deserialize; -use serde_json::json; use thiserror::Error; use tracing::debug; -use uuid::Uuid; use crate::game_profile::{GameProfile, SerializableGameProfile}; @@ -49,79 +46,6 @@ pub struct ForbiddenError { pub path: String, } -static REQWEST_CLIENT: Lazy = Lazy::new(reqwest::Client::new); - -/// Tell Mojang's servers that you are going to join a multiplayer server, -/// which is required to join online-mode servers. The server ID is an empty -/// string. -pub async fn join( - access_token: &str, - public_key: &[u8], - private_key: &[u8], - uuid: &Uuid, - server_id: &str, -) -> Result<(), ClientSessionServerError> { - let client = REQWEST_CLIENT.clone(); - - let server_hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data( - server_id.as_bytes(), - public_key, - private_key, - )); - - join_with_server_id_hash(&client, access_token, uuid, &server_hash).await -} - -pub async fn join_with_server_id_hash( - client: &reqwest::Client, - access_token: &str, - uuid: &Uuid, - server_hash: &str, -) -> Result<(), ClientSessionServerError> { - let mut encode_buffer = Uuid::encode_buffer(); - let undashed_uuid = uuid.simple().encode_lower(&mut encode_buffer); - - let data = json!({ - "accessToken": access_token, - "selectedProfile": undashed_uuid, - "serverId": server_hash - }); - let res = client - .post("https://sessionserver.mojang.com/session/minecraft/join") - .json(&data) - .send() - .await?; - - match res.status() { - StatusCode::NO_CONTENT => Ok(()), - StatusCode::FORBIDDEN => { - let forbidden = res.json::().await?; - match forbidden.error.as_str() { - "InsufficientPrivilegesException" => { - Err(ClientSessionServerError::MultiplayerDisabled) - } - "UserBannedException" => Err(ClientSessionServerError::Banned), - "AuthenticationUnavailableException" => { - Err(ClientSessionServerError::AuthServersUnreachable) - } - "InvalidCredentialsException" => Err(ClientSessionServerError::InvalidSession), - "ForbiddenOperationException" => Err(ClientSessionServerError::ForbiddenOperation), - _ => Err(ClientSessionServerError::Unknown(forbidden.error)), - } - } - StatusCode::TOO_MANY_REQUESTS => Err(ClientSessionServerError::RateLimited), - status_code => { - // log the headers - debug!("Error headers: {:#?}", res.headers()); - let body = res.text().await?; - Err(ClientSessionServerError::UnexpectedResponse { - status_code: status_code.as_u16(), - body, - }) - } - } -} - /// Ask Mojang's servers if the player joining is authenticated. /// Included in the reply is the player's skin and cape. /// The IP field is optional and equivalent to enabling diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs deleted file mode 100755 index 741a07d45..000000000 --- a/azalea-client/src/account.rs +++ /dev/null @@ -1,234 +0,0 @@ -//! Connect to Minecraft servers. - -use std::sync::Arc; - -use azalea_auth::certs::{Certificates, FetchCertificatesError}; -use azalea_auth::AccessTokenResponse; -use bevy_ecs::component::Component; -use parking_lot::Mutex; -use thiserror::Error; -use uuid::Uuid; - -/// Something that can join Minecraft servers. -/// -/// To join a server using this account, use [`Client::join`] or -/// [`azalea::ClientBuilder`]. -/// -/// Note that this is also a component that our clients have. -/// -/// # 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"); -/// # } -/// ``` -/// -/// [`Client::join`]: crate::Client::join -/// [`azalea::ClientBuilder`]: https://docs.rs/azalea/latest/azalea/struct.ClientBuilder.html -#[derive(Clone, Debug, Component)] -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` so it can be modified by [`Self::refresh`]. - pub access_token: Option>>, - /// Only required for online-mode accounts. - pub uuid: Option, - - /// 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. - pub certs: Option, -} - -/// The parameters that were passed for creating the associated [`Account`]. -#[derive(Clone, Debug)] -pub enum AccountOpts { - Offline { - username: String, - }, - Microsoft { - email: String, - }, - MicrosoftWithAccessToken { - msa: Arc>>, - }, -} - -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_string(), - access_token: None, - uuid: None, - account_opts: AccountOpts::Offline { - username: username.to_string(), - }, - certs: None, - } - } - - /// This will create an online-mode account by authenticating with - /// Microsoft's servers. Note that the email given is actually only used as - /// a key for the cache, but it's recommended to use the real email to - /// avoid confusion. - pub async fn microsoft(email: &str) -> Result { - 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( - email, - azalea_auth::AuthOpts { - cache_file: Some(minecraft_dir.join("azalea-auth.json")), - ..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: email.to_string(), - }, - // we don't do chat signing by default unless the user asks for it - certs: 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). - /// - /// Note that this will not refresh the token when it expires. - /// - /// ``` - /// # use azalea_client::Account; - /// # async fn example() -> Result<(), Box> { - /// let client = reqwest::Client::new(); - /// - /// let res = azalea_auth::get_ms_link_code(&client).await?; - /// println!( - /// "Go to {} and enter the code {}", - /// res.verification_uri, res.user_code - /// ); - /// let msa = azalea_auth::get_ms_auth_token(&client, res).await?; - /// Account::with_microsoft_access_token(msa).await?; - /// # Ok(()) - /// # } - /// ``` - pub async fn with_microsoft_access_token( - mut msa: azalea_auth::cache::ExpiringValue, - ) -> Result { - let client = reqwest::Client::new(); - - if msa.is_expired() { - tracing::trace!("refreshing Microsoft auth token"); - msa = azalea_auth::refresh_ms_auth_token(&client, &msa.data.refresh_token).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: 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. - 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(()) - } - } - } - - /// Get the UUID of this account. This will generate an offline-mode UUID - /// by making a hash with the username if the `uuid` field is None. - pub fn uuid_or_offline(&self) -> Uuid { - self.uuid - .unwrap_or_else(|| azalea_auth::offline::generate_uuid(&self.username)) - } -} - -#[derive(Error, Debug)] -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`]. - 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 = Some(certs); - - Ok(()) - } -} diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index af5354159..0579c1ed0 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -21,10 +21,10 @@ use crate::{ raw_connection::RawConnection, respawn::RespawnPlugin, task_pool::TaskPoolPlugin, - Account, PlayerInfo, + PlayerInfo, }; -use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError}; +use azalea_auth::{account::Account, game_profile::GameProfile, sessionserver::ClientSessionServerError}; use azalea_chat::FormattedText; use azalea_core::{position::Vec3, tick::GameTick}; use azalea_entity::{ @@ -105,6 +105,7 @@ pub struct Client { /// /// This as also available from the ECS as [`GameProfileComponent`]. pub profile: GameProfile, + /// The entity for this client in the ECS. pub entity: Entity, @@ -133,7 +134,7 @@ pub enum JoinError { #[error("The given address could not be parsed into a ServerAddress")] InvalidAddress, #[error("Couldn't refresh access token: {0}")] - Auth(#[from] azalea_auth::AuthError), + Auth(#[from] azalea_auth::MicrosoftAuthError), #[error("Disconnected: {reason}")] Disconnect { reason: FormattedText }, } @@ -181,7 +182,7 @@ impl Client { /// } /// ``` pub async fn join( - account: &Account, + account: &impl Account, address: impl TryInto, ) -> Result<(Self, mpsc::UnboundedReceiver), JoinError> { let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?; @@ -209,7 +210,7 @@ impl Client { /// [`start_ecs_runner`]. You'd usually want to use [`Self::join`] instead. pub async fn start_client( ecs_lock: Arc>, - account: &Account, + account: &impl Account, address: &ServerAddress, resolved_address: &SocketAddr, run_schedule_sender: mpsc::UnboundedSender<()>, @@ -220,8 +221,8 @@ impl Client { let mut ecs = ecs_lock.lock(); let entity_uuid_index = ecs.resource::(); - let uuid = account.uuid_or_offline(); - let entity = if let Some(entity) = entity_uuid_index.get(&account.uuid_or_offline()) { + let uuid = account.get_uuid(); + let entity = if let Some(entity) = entity_uuid_index.get(&account.get_uuid()) { debug!("Reusing entity {entity:?} for client"); entity } else { @@ -291,7 +292,7 @@ impl Client { ecs_lock: Arc>, entity: Entity, mut conn: Connection, - account: &Account, + account: &impl Account, address: &ServerAddress, ) -> Result< ( @@ -324,10 +325,8 @@ impl Client { // login conn.write( ServerboundHelloPacket { - name: account.username.clone(), - // TODO: pretty sure this should generate an offline-mode uuid instead of just - // Uuid::default() - profile_id: account.uuid.unwrap_or_default(), + name: account.get_username().clone(), + profile_id: account.get_uuid(), } .get(), ) @@ -353,18 +352,14 @@ impl Client { debug!("Got encryption request"); let e = azalea_crypto::encrypt(&p.public_key, &p.nonce).unwrap(); - if let Some(access_token) = &account.access_token { + if account.is_online() { // keep track of the number of times we tried // authenticating so we can give up after too many let mut attempts: usize = 1; while let Err(e) = { - let access_token = access_token.lock().clone(); conn.authenticate( - &access_token, - &account - .uuid - .expect("Uuid must be present if access token is present."), + account, e.secret_key, &p, ) @@ -382,7 +377,7 @@ impl Client { ) { // uh oh, we got an invalid session and have // to reauthenticate now - account.refresh().await?; + // account.refresh().await?; FIXME: Make this work with microsoft accounts or smth } else { return Err(e.into()); } diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index 41399ce97..396f81f5f 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -8,7 +8,6 @@ #![allow(incomplete_features)] #![feature(error_generic_member_access)] -mod account; pub mod attack; pub mod chat; pub mod chunks; @@ -29,7 +28,6 @@ pub mod raw_connection; pub mod respawn; pub mod task_pool; -pub use account::{Account, AccountOpts}; pub use azalea_protocol::packets::configuration::serverbound_client_information_packet::ClientInformation; pub use client::{ start_ecs_runner, Client, DefaultPlugins, JoinError, JoinedClientBundle, TickBroadcast, diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs index 86b926938..3386ca201 100755 --- a/azalea-protocol/src/connect.rs +++ b/azalea-protocol/src/connect.rs @@ -11,6 +11,7 @@ use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket}; use crate::packets::ProtocolPacket; use crate::read::{deserialize_packet, read_raw_packet, try_read_raw_packet, ReadPacketError}; use crate::write::{serialize_packet, write_raw_packet}; +use azalea_auth::account::Account; use azalea_auth::game_profile::GameProfile; use azalea_auth::sessionserver::{ClientSessionServerError, ServerSessionServerError}; use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc}; @@ -24,7 +25,6 @@ use tokio::io::AsyncWriteExt; use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf, ReuniteError}; use tokio::net::TcpStream; use tracing::{error, info}; -use uuid::Uuid; pub struct RawReadConnection { pub read_stream: OwnedReadHalf, @@ -390,19 +390,11 @@ impl Connection { /// ``` pub async fn authenticate( &self, - access_token: &str, - uuid: &Uuid, + account: &impl Account, private_key: [u8; 16], packet: &ClientboundHelloPacket, ) -> Result<(), ClientSessionServerError> { - azalea_auth::sessionserver::join( - access_token, - &packet.public_key, - &private_key, - uuid, - &packet.server_id, - ) - .await + account.join(&packet.public_key, &private_key, &packet.server_id).await } } diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs index b4d66aaf9..7f0706ba4 100644 --- a/azalea/src/prelude.rs +++ b/azalea/src/prelude.rs @@ -5,7 +5,8 @@ pub use crate::{ bot::BotClientExt, container::ContainerClientExt, pathfinder::PathfinderClientExt, ClientBuilder, }; -pub use azalea_client::{Account, Client, Event}; +pub use azalea_client::{Client, Event}; +pub use azalea_auth::{MicrosoftAccount, OfflineAccount}; // this is necessary to make the macros that reference bevy_ecs work pub use crate::ecs as bevy_ecs; pub use crate::ecs::{component::Component, system::Resource}; diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index 6d3885ef1..cf557e09e 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -4,8 +4,9 @@ mod chat; mod events; pub mod prelude; +use azalea_auth::account::Account; use azalea_client::{ - chat::ChatPacket, start_ecs_runner, Account, Client, DefaultPlugins, Event, JoinError, + chat::ChatPacket, start_ecs_runner, Client, DefaultPlugins, Event, JoinError, }; use azalea_protocol::{resolver, ServerAddress}; use azalea_world::InstanceContainer; @@ -45,14 +46,15 @@ pub struct Swarm { } /// Create a new [`Swarm`]. -pub struct SwarmBuilder +pub struct SwarmBuilder where S: Send + Sync + Clone + Component + 'static, SS: Default + Send + Sync + Clone + Resource + 'static, + A: Account { pub(crate) app: App, /// The accounts that are going to join the server. - pub(crate) accounts: Vec, + pub(crate) accounts: Vec, /// The individual bot states. This must be the same length as `accounts`, /// since each bot gets one state. pub(crate) states: Vec, @@ -233,7 +235,7 @@ where /// By default every account will join at the same time, you can add a delay /// with [`Self::join_delay`]. #[must_use] - pub fn add_accounts(mut self, accounts: Vec) -> Self + pub fn add_accounts(mut self, accounts: Vec) -> Self where S: Default, { @@ -248,7 +250,7 @@ where /// This will make the state for this client be the default, use /// [`Self::add_account_with_state`] to avoid that. #[must_use] - pub fn add_account(self, account: Account) -> Self + pub fn add_account(self, account: impl Account) -> Self where S: Default, { @@ -257,7 +259,7 @@ where /// Add an account with a custom initial state. Use just /// [`Self::add_account`] to use the Default implementation for the state. #[must_use] - pub fn add_account_with_state(mut self, account: Account, state: S) -> Self { + pub fn add_account_with_state(mut self, account: impl Account, state: S) -> Self { self.accounts.push(account); self.states.push(state); self @@ -522,7 +524,7 @@ impl Swarm { /// Returns an `Err` if the bot could not do a handshake successfully. pub async fn add( &mut self, - account: &Account, + account: &impl Account, state: S, ) -> Result { let address = self.address.read().clone(); @@ -575,7 +577,7 @@ impl Swarm { /// seconds and doubling up to 15 seconds. pub async fn add_and_retry_forever( &mut self, - account: &Account, + account: &impl Account, state: S, ) -> Client { let mut disconnects = 0; @@ -586,7 +588,7 @@ impl Swarm { disconnects += 1; let delay = (Duration::from_secs(5) * 2u32.pow(disconnects.min(16))) .min(Duration::from_secs(15)); - let username = account.username.clone(); + let username = account.get_username().clone(); error!("Error joining as {username}: {e}. Waiting {delay:?} and trying again."); tokio::time::sleep(delay).await; } From 6306763f0fa038c9fe8cb3fb74b2774ab5a27b5f Mon Sep 17 00:00:00 2001 From: veronoicc <64193056+veronoicc@users.noreply.github.com> Date: Sun, 17 Mar 2024 14:06:27 +0000 Subject: [PATCH 02/10] This is VERY likely not a good way to do it, but it works for now --- azalea-auth/src/account.rs | 2 +- azalea/src/lib.rs | 23 ++++++------- azalea/src/swarm/chat.rs | 16 +++++---- azalea/src/swarm/mod.rs | 68 ++++++++++++++++++++------------------ 4 files changed, 56 insertions(+), 53 deletions(-) diff --git a/azalea-auth/src/account.rs b/azalea-auth/src/account.rs index 7172cec6e..8c962eb15 100644 --- a/azalea-auth/src/account.rs +++ b/azalea-auth/src/account.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use crate::{certs::{Certificates, FetchCertificatesError}, sessionserver::ClientSessionServerError}; -pub trait Account: Clone + Component { +pub trait Account: Send + Sync + Clone + Component { fn join( &self, public_key: &[u8], diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index 84c215d54..0cf2f64ed 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -73,19 +73,20 @@ pub enum StartError { /// # Ok(()) /// # } /// ``` -pub struct ClientBuilder +pub struct ClientBuilder where S: Default + Send + Sync + Clone + Component + 'static, + A: Send + Sync + Clone + auth::account::Account + 'static, { /// Internally, ClientBuilder is just a wrapper over SwarmBuilder since it's /// technically just a subset of it so we can avoid duplicating code this /// way. - swarm: SwarmBuilder, + swarm: SwarmBuilder, } -impl ClientBuilder { +impl ClientBuilder where A: Send + Sync + Clone + auth::account::Account + 'static, { /// Start building a client that can join the world. #[must_use] - pub fn new() -> ClientBuilder { + pub fn new() -> ClientBuilder { Self::new_without_plugins() .add_plugins(DefaultPlugins) .add_plugins(DefaultBotPlugins) @@ -115,7 +116,7 @@ impl ClientBuilder { /// # } /// ``` #[must_use] - pub fn new_without_plugins() -> ClientBuilder { + pub fn new_without_plugins() -> ClientBuilder { Self { swarm: SwarmBuilder::new_without_plugins(), } @@ -138,7 +139,7 @@ impl ClientBuilder { /// } /// ``` #[must_use] - pub fn set_handler(self, handler: HandleFn) -> ClientBuilder + pub fn set_handler(self, handler: HandleFn) -> ClientBuilder where S: Default + Send + Sync + Clone + Component + 'static, Fut: Future> + Send + 'static, @@ -148,9 +149,10 @@ impl ClientBuilder { } } } -impl ClientBuilder +impl ClientBuilder where S: Default + Send + Sync + Clone + Component + 'static, + A: Send + Sync + Clone + auth::account::Account + 'static, { /// Set the client state instead of initializing defaults. #[must_use] @@ -180,7 +182,7 @@ where /// [`ServerAddress`]: azalea_protocol::ServerAddress pub async fn start( mut self, - account: Account, + account: A, address: impl TryInto, ) -> Result { self.swarm.accounts = vec![account]; @@ -190,11 +192,6 @@ where self.swarm.start(address).await } } -impl Default for ClientBuilder { - fn default() -> Self { - Self::new() - } -} /// A marker that can be used in place of a State in [`ClientBuilder`] or /// [`SwarmBuilder`]. You probably don't need to use this manually since the diff --git a/azalea/src/swarm/chat.rs b/azalea/src/swarm/chat.rs index 7425293bf..052596edb 100644 --- a/azalea/src/swarm/chat.rs +++ b/azalea/src/swarm/chat.rs @@ -19,6 +19,7 @@ use crate::ecs::{ schedule::IntoSystemConfigs, system::{Commands, Query, Res, ResMut, Resource}, }; +use azalea_auth::account::Account; use azalea_client::chat::{ChatPacket, ChatReceivedEvent}; use bevy_app::{App, Plugin, Update}; use bevy_ecs::prelude::Event; @@ -27,13 +28,16 @@ use std::collections::VecDeque; use super::{Swarm, SwarmEvent}; #[derive(Clone)] -pub struct SwarmChatPlugin; -impl Plugin for SwarmChatPlugin { +pub struct SwarmChatPlugin where A: Send + Sync + Clone + Account + 'static { + pub account: std::marker::PhantomData, +} + +impl Plugin for SwarmChatPlugin where A: Send + Sync + Clone + Account + 'static { fn build(&self, app: &mut App) { app.add_event::() .add_systems( Update, - (chat_listener, update_min_index_and_shrink_queue).chain(), + (chat_listener, update_min_index_and_shrink_queue::).chain(), ) .insert_resource(GlobalChatState { chat_queue: VecDeque::new(), @@ -113,12 +117,12 @@ fn chat_listener( } } -fn update_min_index_and_shrink_queue( +fn update_min_index_and_shrink_queue( query: Query<&ClientChatState>, mut global_chat_state: ResMut, mut events: EventReader, - swarm: Option>, -) { + swarm: Option>>, +) where A: Send + Sync + Clone + Account + 'static { for event in events.read() { if let Some(swarm) = &swarm { // it should also work if Swarm isn't present (so the tests don't need it) diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index cf557e09e..3dcd7564d 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -14,7 +14,7 @@ use bevy_app::{App, PluginGroup, PluginGroupBuilder, Plugins}; use bevy_ecs::{component::Component, entity::Entity, system::Resource, world::World}; use futures::future::{join_all, BoxFuture}; use parking_lot::{Mutex, RwLock}; -use std::{collections::HashMap, future::Future, net::SocketAddr, sync::Arc, time::Duration}; +use std::{collections::HashMap, future::Future, marker::PhantomData, net::SocketAddr, sync::Arc, time::Duration}; use tokio::sync::mpsc; use tracing::error; @@ -28,7 +28,7 @@ use crate::{BoxHandleFn, DefaultBotPlugins, HandleFn, NoState, StartError}; /// The `S` type parameter is the type of the state for individual bots. /// It's used to make the [`Swarm::add`] function work. #[derive(Clone, Resource)] -pub struct Swarm { +pub struct Swarm where A: Send + Sync + Clone + Account + 'static, { pub ecs_lock: Arc>, bots: Arc>>, @@ -40,7 +40,7 @@ pub struct Swarm { pub instance_container: Arc>, bots_tx: mpsc::UnboundedSender<(Option, Client)>, - swarm_tx: mpsc::UnboundedSender, + swarm_tx: mpsc::UnboundedSender>, run_schedule_sender: mpsc::UnboundedSender<()>, } @@ -50,7 +50,7 @@ pub struct SwarmBuilder where S: Send + Sync + Clone + Component + 'static, SS: Default + Send + Sync + Clone + Resource + 'static, - A: Account + A: Send + Sync + Clone + Account + 'static, { pub(crate) app: App, /// The accounts that are going to join the server. @@ -64,7 +64,7 @@ where pub(crate) handler: Option>, /// The function that's called every time the swarm receives a /// [`SwarmEvent`]. - pub(crate) swarm_handler: Option>, + pub(crate) swarm_handler: Option>, /// How long we should wait between each bot joining the server. Set to /// None to have every bot connect at the same time. None is different than @@ -72,14 +72,14 @@ where /// the previous one to be ready. pub(crate) join_delay: Option, } -impl SwarmBuilder { +impl SwarmBuilder where A: Send + Sync + Clone + Account + 'static { /// Start creating the swarm. #[must_use] - pub fn new() -> SwarmBuilder { + pub fn new() -> SwarmBuilder { Self::new_without_plugins() .add_plugins(DefaultPlugins) .add_plugins(DefaultBotPlugins) - .add_plugins(DefaultSwarmPlugins) + .add_plugins(DefaultSwarmPlugins { _account: PhantomData:: }) } /// [`Self::new`] but without adding the plugins by default. This is useful @@ -108,7 +108,7 @@ impl SwarmBuilder { /// # } /// ``` #[must_use] - pub fn new_without_plugins() -> SwarmBuilder { + pub fn new_without_plugins() -> SwarmBuilder { SwarmBuilder { // we create the app here so plugins can add onto it. // the schedules won't run until [`Self::start`] is called. @@ -123,9 +123,10 @@ impl SwarmBuilder { } } -impl SwarmBuilder +impl SwarmBuilder where SS: Default + Send + Sync + Clone + Resource + 'static, + A: Send + Sync + Clone + Account + 'static, { /// Set the function that's called every time a bot receives an [`Event`]. /// This is the way to handle normal per-bot events. @@ -154,7 +155,7 @@ where /// # } /// ``` #[must_use] - pub fn set_handler(self, handler: HandleFn) -> SwarmBuilder + pub fn set_handler(self, handler: HandleFn) -> SwarmBuilder where Fut: Future> + Send + 'static, S: Send + Sync + Clone + Component + Default + 'static, @@ -170,9 +171,10 @@ where } } -impl SwarmBuilder +impl SwarmBuilder where S: Send + Sync + Clone + Component + 'static, + A: Send + Sync + Clone + Account + 'static { /// Set the function that's called every time the swarm receives a /// [`SwarmEvent`]. This is the way to handle global swarm events. @@ -202,10 +204,11 @@ where /// } /// ``` #[must_use] - pub fn set_swarm_handler(self, handler: SwarmHandleFn) -> SwarmBuilder + pub fn set_swarm_handler(self, handler: SwarmHandleFn) -> SwarmBuilder where SS: Default + Send + Sync + Clone + Resource + 'static, Fut: Future> + Send + 'static, + A: Send + Sync + Clone + Account + 'static, { SwarmBuilder { handler: self.handler, @@ -221,10 +224,11 @@ where } } -impl SwarmBuilder +impl SwarmBuilder where S: Send + Sync + Clone + Component + 'static, SS: Default + Send + Sync + Clone + Resource + 'static, + A: Send + Sync + Clone + Account + 'static, { /// Add a vec of [`Account`]s to the swarm. /// @@ -235,7 +239,7 @@ where /// By default every account will join at the same time, you can add a delay /// with [`Self::join_delay`]. #[must_use] - pub fn add_accounts(mut self, accounts: Vec) -> Self + pub fn add_accounts(mut self, accounts: Vec) -> Self where S: Default, { @@ -250,7 +254,7 @@ where /// This will make the state for this client be the default, use /// [`Self::add_account_with_state`] to avoid that. #[must_use] - pub fn add_account(self, account: impl Account) -> Self + pub fn add_account(self, account: A) -> Self where S: Default, { @@ -259,7 +263,7 @@ where /// Add an account with a custom initial state. Use just /// [`Self::add_account`] to use the Default implementation for the state. #[must_use] - pub fn add_account_with_state(mut self, account: impl Account, state: S) -> Self { + pub fn add_account_with_state(mut self, account: A, state: S) -> Self { self.accounts.push(account); self.states.push(state); self @@ -427,15 +431,10 @@ where } } -impl Default for SwarmBuilder { - fn default() -> Self { - Self::new() - } -} /// An event about something that doesn't have to do with a single bot. #[derive(Clone, Debug)] -pub enum SwarmEvent { +pub enum SwarmEvent where A: Send + Sync + Clone + Account + 'static, { /// All the bots in the swarm have successfully joined the server. Login, /// The swarm was created. This is only fired once, and it's guaranteed to @@ -445,14 +444,14 @@ pub enum SwarmEvent { /// /// You can implement an auto-reconnect by calling [`Swarm::add`] /// with the account from this event. - Disconnect(Box), + Disconnect(Box), /// At least one bot received a chat message. Chat(ChatPacket), } -pub type SwarmHandleFn = fn(Swarm, SwarmEvent, SS) -> Fut; -pub type BoxSwarmHandleFn = - Box BoxFuture<'static, Result<(), anyhow::Error>> + Send>; +pub type SwarmHandleFn = fn(Swarm, SwarmEvent, SS) -> Fut; +pub type BoxSwarmHandleFn = + Box, SwarmEvent, SS) -> BoxFuture<'static, Result<(), anyhow::Error>> + Send>; /// Make a bot [`Swarm`]. /// @@ -515,7 +514,7 @@ pub type BoxSwarmHandleFn = /// Ok(()) /// } -impl Swarm { +impl Swarm where A: Send + Sync + Clone + Account + 'static, { /// Add a new account to the swarm. You can remove it later by calling /// [`Client::disconnect`]. /// @@ -560,7 +559,7 @@ impl Swarm { } cloned_bots.lock().remove(&bot.entity); let account = cloned_bot - .get_component::() + .get_component::() .expect("bot is missing required Account component"); swarm_tx .send(SwarmEvent::Disconnect(Box::new(account))) @@ -597,7 +596,7 @@ impl Swarm { } } -impl IntoIterator for Swarm { +impl IntoIterator for Swarm where A: Send + Sync + Clone + Account + 'static, { type Item = Client; type IntoIter = std::vec::IntoIter; @@ -626,12 +625,15 @@ impl IntoIterator for Swarm { /// This plugin group will add all the default plugins necessary for swarms to /// work. -pub struct DefaultSwarmPlugins; +pub struct DefaultSwarmPlugins where A: Send + Sync + Clone + Account + 'static { + _account: PhantomData, + +} -impl PluginGroup for DefaultSwarmPlugins { +impl PluginGroup for DefaultSwarmPlugins where A: Send + Sync + Clone + Account + 'static { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::() - .add(chat::SwarmChatPlugin) + .add(chat::SwarmChatPlugin { account: PhantomData:: }) .add(events::SwarmPlugin) } } From e33f22eb2f18b27875040ac854a0884224633ed8 Mon Sep 17 00:00:00 2001 From: veronoicc <64193056+veronoicc@users.noreply.github.com> Date: Sun, 17 Mar 2024 15:22:42 +0000 Subject: [PATCH 03/10] Add devcontainer.json --- .devcontainer/devcontainer.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..989657ca6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,3 @@ +{ + "image": "mcr.microsoft.com/devcontainers/rust", +} \ No newline at end of file From 75fbb0f5616673fb662c72eba474bc25a9178d3a Mon Sep 17 00:00:00 2001 From: veronoicc <64193056+veronoicc@users.noreply.github.com> Date: Sun, 17 Mar 2024 21:18:05 +0000 Subject: [PATCH 04/10] Revert "This is VERY likely not a good way to do it, but it works for now" This reverts commit 6306763f0fa038c9fe8cb3fb74b2774ab5a27b5f. --- azalea-auth/src/account.rs | 2 +- azalea/src/lib.rs | 23 +++++++------ azalea/src/swarm/chat.rs | 16 ++++----- azalea/src/swarm/mod.rs | 68 ++++++++++++++++++-------------------- 4 files changed, 53 insertions(+), 56 deletions(-) diff --git a/azalea-auth/src/account.rs b/azalea-auth/src/account.rs index 8c962eb15..7172cec6e 100644 --- a/azalea-auth/src/account.rs +++ b/azalea-auth/src/account.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use crate::{certs::{Certificates, FetchCertificatesError}, sessionserver::ClientSessionServerError}; -pub trait Account: Send + Sync + Clone + Component { +pub trait Account: Clone + Component { fn join( &self, public_key: &[u8], diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index 0cf2f64ed..84c215d54 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -73,20 +73,19 @@ pub enum StartError { /// # Ok(()) /// # } /// ``` -pub struct ClientBuilder +pub struct ClientBuilder where S: Default + Send + Sync + Clone + Component + 'static, - A: Send + Sync + Clone + auth::account::Account + 'static, { /// Internally, ClientBuilder is just a wrapper over SwarmBuilder since it's /// technically just a subset of it so we can avoid duplicating code this /// way. - swarm: SwarmBuilder, + swarm: SwarmBuilder, } -impl ClientBuilder where A: Send + Sync + Clone + auth::account::Account + 'static, { +impl ClientBuilder { /// Start building a client that can join the world. #[must_use] - pub fn new() -> ClientBuilder { + pub fn new() -> ClientBuilder { Self::new_without_plugins() .add_plugins(DefaultPlugins) .add_plugins(DefaultBotPlugins) @@ -116,7 +115,7 @@ impl ClientBuilder where A: Send + Sync + Clone + auth::account:: /// # } /// ``` #[must_use] - pub fn new_without_plugins() -> ClientBuilder { + pub fn new_without_plugins() -> ClientBuilder { Self { swarm: SwarmBuilder::new_without_plugins(), } @@ -139,7 +138,7 @@ impl ClientBuilder where A: Send + Sync + Clone + auth::account:: /// } /// ``` #[must_use] - pub fn set_handler(self, handler: HandleFn) -> ClientBuilder + pub fn set_handler(self, handler: HandleFn) -> ClientBuilder where S: Default + Send + Sync + Clone + Component + 'static, Fut: Future> + Send + 'static, @@ -149,10 +148,9 @@ impl ClientBuilder where A: Send + Sync + Clone + auth::account:: } } } -impl ClientBuilder +impl ClientBuilder where S: Default + Send + Sync + Clone + Component + 'static, - A: Send + Sync + Clone + auth::account::Account + 'static, { /// Set the client state instead of initializing defaults. #[must_use] @@ -182,7 +180,7 @@ where /// [`ServerAddress`]: azalea_protocol::ServerAddress pub async fn start( mut self, - account: A, + account: Account, address: impl TryInto, ) -> Result { self.swarm.accounts = vec![account]; @@ -192,6 +190,11 @@ where self.swarm.start(address).await } } +impl Default for ClientBuilder { + fn default() -> Self { + Self::new() + } +} /// A marker that can be used in place of a State in [`ClientBuilder`] or /// [`SwarmBuilder`]. You probably don't need to use this manually since the diff --git a/azalea/src/swarm/chat.rs b/azalea/src/swarm/chat.rs index 052596edb..7425293bf 100644 --- a/azalea/src/swarm/chat.rs +++ b/azalea/src/swarm/chat.rs @@ -19,7 +19,6 @@ use crate::ecs::{ schedule::IntoSystemConfigs, system::{Commands, Query, Res, ResMut, Resource}, }; -use azalea_auth::account::Account; use azalea_client::chat::{ChatPacket, ChatReceivedEvent}; use bevy_app::{App, Plugin, Update}; use bevy_ecs::prelude::Event; @@ -28,16 +27,13 @@ use std::collections::VecDeque; use super::{Swarm, SwarmEvent}; #[derive(Clone)] -pub struct SwarmChatPlugin where A: Send + Sync + Clone + Account + 'static { - pub account: std::marker::PhantomData, -} - -impl Plugin for SwarmChatPlugin where A: Send + Sync + Clone + Account + 'static { +pub struct SwarmChatPlugin; +impl Plugin for SwarmChatPlugin { fn build(&self, app: &mut App) { app.add_event::() .add_systems( Update, - (chat_listener, update_min_index_and_shrink_queue::).chain(), + (chat_listener, update_min_index_and_shrink_queue).chain(), ) .insert_resource(GlobalChatState { chat_queue: VecDeque::new(), @@ -117,12 +113,12 @@ fn chat_listener( } } -fn update_min_index_and_shrink_queue( +fn update_min_index_and_shrink_queue( query: Query<&ClientChatState>, mut global_chat_state: ResMut, mut events: EventReader, - swarm: Option>>, -) where A: Send + Sync + Clone + Account + 'static { + swarm: Option>, +) { for event in events.read() { if let Some(swarm) = &swarm { // it should also work if Swarm isn't present (so the tests don't need it) diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index 3dcd7564d..cf557e09e 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -14,7 +14,7 @@ use bevy_app::{App, PluginGroup, PluginGroupBuilder, Plugins}; use bevy_ecs::{component::Component, entity::Entity, system::Resource, world::World}; use futures::future::{join_all, BoxFuture}; use parking_lot::{Mutex, RwLock}; -use std::{collections::HashMap, future::Future, marker::PhantomData, net::SocketAddr, sync::Arc, time::Duration}; +use std::{collections::HashMap, future::Future, net::SocketAddr, sync::Arc, time::Duration}; use tokio::sync::mpsc; use tracing::error; @@ -28,7 +28,7 @@ use crate::{BoxHandleFn, DefaultBotPlugins, HandleFn, NoState, StartError}; /// The `S` type parameter is the type of the state for individual bots. /// It's used to make the [`Swarm::add`] function work. #[derive(Clone, Resource)] -pub struct Swarm where A: Send + Sync + Clone + Account + 'static, { +pub struct Swarm { pub ecs_lock: Arc>, bots: Arc>>, @@ -40,7 +40,7 @@ pub struct Swarm where A: Send + Sync + Clone + Account + 'static, { pub instance_container: Arc>, bots_tx: mpsc::UnboundedSender<(Option, Client)>, - swarm_tx: mpsc::UnboundedSender>, + swarm_tx: mpsc::UnboundedSender, run_schedule_sender: mpsc::UnboundedSender<()>, } @@ -50,7 +50,7 @@ pub struct SwarmBuilder where S: Send + Sync + Clone + Component + 'static, SS: Default + Send + Sync + Clone + Resource + 'static, - A: Send + Sync + Clone + Account + 'static, + A: Account { pub(crate) app: App, /// The accounts that are going to join the server. @@ -64,7 +64,7 @@ where pub(crate) handler: Option>, /// The function that's called every time the swarm receives a /// [`SwarmEvent`]. - pub(crate) swarm_handler: Option>, + pub(crate) swarm_handler: Option>, /// How long we should wait between each bot joining the server. Set to /// None to have every bot connect at the same time. None is different than @@ -72,14 +72,14 @@ where /// the previous one to be ready. pub(crate) join_delay: Option, } -impl SwarmBuilder where A: Send + Sync + Clone + Account + 'static { +impl SwarmBuilder { /// Start creating the swarm. #[must_use] - pub fn new() -> SwarmBuilder { + pub fn new() -> SwarmBuilder { Self::new_without_plugins() .add_plugins(DefaultPlugins) .add_plugins(DefaultBotPlugins) - .add_plugins(DefaultSwarmPlugins { _account: PhantomData:: }) + .add_plugins(DefaultSwarmPlugins) } /// [`Self::new`] but without adding the plugins by default. This is useful @@ -108,7 +108,7 @@ impl SwarmBuilder where A: Send + Sync + Clone + Ac /// # } /// ``` #[must_use] - pub fn new_without_plugins() -> SwarmBuilder { + pub fn new_without_plugins() -> SwarmBuilder { SwarmBuilder { // we create the app here so plugins can add onto it. // the schedules won't run until [`Self::start`] is called. @@ -123,10 +123,9 @@ impl SwarmBuilder where A: Send + Sync + Clone + Ac } } -impl SwarmBuilder +impl SwarmBuilder where SS: Default + Send + Sync + Clone + Resource + 'static, - A: Send + Sync + Clone + Account + 'static, { /// Set the function that's called every time a bot receives an [`Event`]. /// This is the way to handle normal per-bot events. @@ -155,7 +154,7 @@ where /// # } /// ``` #[must_use] - pub fn set_handler(self, handler: HandleFn) -> SwarmBuilder + pub fn set_handler(self, handler: HandleFn) -> SwarmBuilder where Fut: Future> + Send + 'static, S: Send + Sync + Clone + Component + Default + 'static, @@ -171,10 +170,9 @@ where } } -impl SwarmBuilder +impl SwarmBuilder where S: Send + Sync + Clone + Component + 'static, - A: Send + Sync + Clone + Account + 'static { /// Set the function that's called every time the swarm receives a /// [`SwarmEvent`]. This is the way to handle global swarm events. @@ -204,11 +202,10 @@ where /// } /// ``` #[must_use] - pub fn set_swarm_handler(self, handler: SwarmHandleFn) -> SwarmBuilder + pub fn set_swarm_handler(self, handler: SwarmHandleFn) -> SwarmBuilder where SS: Default + Send + Sync + Clone + Resource + 'static, Fut: Future> + Send + 'static, - A: Send + Sync + Clone + Account + 'static, { SwarmBuilder { handler: self.handler, @@ -224,11 +221,10 @@ where } } -impl SwarmBuilder +impl SwarmBuilder where S: Send + Sync + Clone + Component + 'static, SS: Default + Send + Sync + Clone + Resource + 'static, - A: Send + Sync + Clone + Account + 'static, { /// Add a vec of [`Account`]s to the swarm. /// @@ -239,7 +235,7 @@ where /// By default every account will join at the same time, you can add a delay /// with [`Self::join_delay`]. #[must_use] - pub fn add_accounts(mut self, accounts: Vec) -> Self + pub fn add_accounts(mut self, accounts: Vec) -> Self where S: Default, { @@ -254,7 +250,7 @@ where /// This will make the state for this client be the default, use /// [`Self::add_account_with_state`] to avoid that. #[must_use] - pub fn add_account(self, account: A) -> Self + pub fn add_account(self, account: impl Account) -> Self where S: Default, { @@ -263,7 +259,7 @@ where /// Add an account with a custom initial state. Use just /// [`Self::add_account`] to use the Default implementation for the state. #[must_use] - pub fn add_account_with_state(mut self, account: A, state: S) -> Self { + pub fn add_account_with_state(mut self, account: impl Account, state: S) -> Self { self.accounts.push(account); self.states.push(state); self @@ -431,10 +427,15 @@ where } } +impl Default for SwarmBuilder { + fn default() -> Self { + Self::new() + } +} /// An event about something that doesn't have to do with a single bot. #[derive(Clone, Debug)] -pub enum SwarmEvent where A: Send + Sync + Clone + Account + 'static, { +pub enum SwarmEvent { /// All the bots in the swarm have successfully joined the server. Login, /// The swarm was created. This is only fired once, and it's guaranteed to @@ -444,14 +445,14 @@ pub enum SwarmEvent where A: Send + Sync + Clone + Account + 'static, { /// /// You can implement an auto-reconnect by calling [`Swarm::add`] /// with the account from this event. - Disconnect(Box), + Disconnect(Box), /// At least one bot received a chat message. Chat(ChatPacket), } -pub type SwarmHandleFn = fn(Swarm, SwarmEvent, SS) -> Fut; -pub type BoxSwarmHandleFn = - Box, SwarmEvent, SS) -> BoxFuture<'static, Result<(), anyhow::Error>> + Send>; +pub type SwarmHandleFn = fn(Swarm, SwarmEvent, SS) -> Fut; +pub type BoxSwarmHandleFn = + Box BoxFuture<'static, Result<(), anyhow::Error>> + Send>; /// Make a bot [`Swarm`]. /// @@ -514,7 +515,7 @@ pub type BoxSwarmHandleFn = /// Ok(()) /// } -impl Swarm where A: Send + Sync + Clone + Account + 'static, { +impl Swarm { /// Add a new account to the swarm. You can remove it later by calling /// [`Client::disconnect`]. /// @@ -559,7 +560,7 @@ impl Swarm where A: Send + Sync + Clone + Account + 'static, { } cloned_bots.lock().remove(&bot.entity); let account = cloned_bot - .get_component::() + .get_component::() .expect("bot is missing required Account component"); swarm_tx .send(SwarmEvent::Disconnect(Box::new(account))) @@ -596,7 +597,7 @@ impl Swarm where A: Send + Sync + Clone + Account + 'static, { } } -impl IntoIterator for Swarm where A: Send + Sync + Clone + Account + 'static, { +impl IntoIterator for Swarm { type Item = Client; type IntoIter = std::vec::IntoIter; @@ -625,15 +626,12 @@ impl IntoIterator for Swarm where A: Send + Sync + Clone + Account + 'stat /// This plugin group will add all the default plugins necessary for swarms to /// work. -pub struct DefaultSwarmPlugins where A: Send + Sync + Clone + Account + 'static { - _account: PhantomData, - -} +pub struct DefaultSwarmPlugins; -impl PluginGroup for DefaultSwarmPlugins where A: Send + Sync + Clone + Account + 'static { +impl PluginGroup for DefaultSwarmPlugins { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::() - .add(chat::SwarmChatPlugin { account: PhantomData:: }) + .add(chat::SwarmChatPlugin) .add(events::SwarmPlugin) } } From 3f1e3b6abefd742f47164cc8b2c7f55f3f344716 Mon Sep 17 00:00:00 2001 From: veronoicc <64193056+veronoicc@users.noreply.github.com> Date: Sun, 17 Mar 2024 22:05:17 +0000 Subject: [PATCH 05/10] It compiles once again!!! --- Cargo.lock | 5 +++-- azalea-auth/Cargo.toml | 3 ++- azalea-auth/src/account.rs | 22 ++++++++++++++-------- azalea-auth/src/microsoft.rs | 4 +++- azalea-auth/src/offline.rs | 5 +++-- azalea-client/src/client.rs | 24 ++++++++++++------------ azalea-protocol/src/connect.rs | 7 ++++--- azalea/src/lib.rs | 7 +++++-- azalea/src/swarm/mod.rs | 27 ++++++++++++++------------- 9 files changed, 60 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed634118b..a595066f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,9 +179,9 @@ checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" dependencies = [ "proc-macro2", "quote", @@ -238,6 +238,7 @@ dependencies = [ name = "azalea-auth" version = "0.9.0" dependencies = [ + "async-trait", "azalea-buf", "azalea-crypto", "base64", diff --git a/azalea-auth/Cargo.toml b/azalea-auth/Cargo.toml index 13bc105b2..70ecdeba3 100644 --- a/azalea-auth/Cargo.toml +++ b/azalea-auth/Cargo.toml @@ -28,7 +28,8 @@ tokio = { version = "1.36.0", features = ["fs"] } uuid = { version = "1.7.0", features = ["serde", "v3"] } md-5 = "0.10.6" bevy_ecs = "0.13.0" +async-trait = "0.1.78" [dev-dependencies] env_logger = "0.11.2" -tokio = { version = "1.36.0", features = ["full"] } +tokio = { version = "1.36.0", features = ["full"] } \ No newline at end of file diff --git a/azalea-auth/src/account.rs b/azalea-auth/src/account.rs index 7172cec6e..78f2f62f6 100644 --- a/azalea-auth/src/account.rs +++ b/azalea-auth/src/account.rs @@ -1,17 +1,20 @@ -use std::future::Future; +use std::{future::Future, sync::Arc}; + +use async_trait::async_trait; use bevy_ecs::component::Component; use uuid::Uuid; use crate::{certs::{Certificates, FetchCertificatesError}, sessionserver::ClientSessionServerError}; -pub trait Account: Clone + Component { - fn join( +#[async_trait] +pub trait Account: std::fmt::Debug + Send + Sync + 'static { + async fn join( &self, public_key: &[u8], private_key: &[u8], server_id: &str, - ) -> impl Future> + Send { + ) -> Result<(), ClientSessionServerError> { let server_hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data( server_id.as_bytes(), public_key, @@ -19,11 +22,11 @@ pub trait Account: Clone + Component { )); let uuid = self.get_uuid(); - self.join_with_server_id_hash(uuid, server_hash) + self.join_with_server_id_hash(uuid, server_hash).await } - fn join_with_server_id_hash(&self, uuid: Uuid, server_hash: String) -> impl Future> + Send; + async fn join_with_server_id_hash(&self, uuid: Uuid, server_hash: String) -> Result<(), ClientSessionServerError>; - fn fetch_certificates(&self) -> impl Future> + Send; + async fn fetch_certificates(&self) -> Result; fn get_username(&self) -> String; fn get_uuid(&self) -> Uuid; @@ -31,4 +34,7 @@ pub trait Account: Clone + Component { fn is_online(&self) -> bool { true } -} \ No newline at end of file +} + +#[derive(Clone, Debug, Component)] +pub struct BoxedAccount(pub Arc); \ No newline at end of file diff --git a/azalea-auth/src/microsoft.rs b/azalea-auth/src/microsoft.rs index f80840150..fe5e93343 100755 --- a/azalea-auth/src/microsoft.rs +++ b/azalea-auth/src/microsoft.rs @@ -1,6 +1,7 @@ //! Handle Minecraft (Xbox) authentication. use crate::{account::Account, cache::{self, CachedAccount, ExpiringValue}, certs::{Certificates, CertificatesResponse, FetchCertificatesError}, sessionserver::{ClientSessionServerError, ForbiddenError}}; +use async_trait::async_trait; use base64::Engine; use bevy_ecs::component::Component; use chrono::{DateTime, Utc}; @@ -31,7 +32,7 @@ pub struct MicrosoftAuthOpts { pub cache_file: Option, } -#[derive(Clone, Debug, Component)] +#[derive(Clone, Debug)] pub struct MicrosoftAccount { pub client: reqwest::Client, pub access_token: String, @@ -156,6 +157,7 @@ impl MicrosoftAccount { } } +#[async_trait] impl Account for MicrosoftAccount { async fn join_with_server_id_hash(&self, uuid: Uuid, server_hash: String) -> Result<(), ClientSessionServerError> { let mut encode_buffer = Uuid::encode_buffer(); diff --git a/azalea-auth/src/offline.rs b/azalea-auth/src/offline.rs index 986e6b5b8..4e6dd42fc 100644 --- a/azalea-auth/src/offline.rs +++ b/azalea-auth/src/offline.rs @@ -1,10 +1,10 @@ -use bevy_ecs::component::Component; +use async_trait::async_trait; use md5::{Digest, Md5}; use uuid::Uuid; use crate::{account::Account, certs::{Certificates, FetchCertificatesError}, sessionserver::ClientSessionServerError}; -#[derive(Clone, Debug, Component)] +#[derive(Clone, Debug)] pub struct OfflineAccount { pub username: String, pub uuid: Option, @@ -26,6 +26,7 @@ impl OfflineAccount { } } +#[async_trait] impl Account for OfflineAccount { async fn join_with_server_id_hash(&self, _: Uuid, _: String) -> Result<(), ClientSessionServerError> { unimplemented!("Offline accounts can't join servers with a session server.") diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 0579c1ed0..879fbd803 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -24,7 +24,7 @@ use crate::{ PlayerInfo, }; -use azalea_auth::{account::Account, game_profile::GameProfile, sessionserver::ClientSessionServerError}; +use azalea_auth::{account::{Account, BoxedAccount}, game_profile::GameProfile, sessionserver::ClientSessionServerError}; use azalea_chat::FormattedText; use azalea_core::{position::Vec3, tick::GameTick}; use azalea_entity::{ @@ -182,7 +182,7 @@ impl Client { /// } /// ``` pub async fn join( - account: &impl Account, + account: impl Account, address: impl TryInto, ) -> Result<(Self, mpsc::UnboundedReceiver), JoinError> { let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?; @@ -198,7 +198,7 @@ impl Client { Self::start_client( ecs_lock, - account, + BoxedAccount(Arc::new(account)), &address, &resolved_address, run_schedule_sender, @@ -210,7 +210,7 @@ impl Client { /// [`start_ecs_runner`]. You'd usually want to use [`Self::join`] instead. pub async fn start_client( ecs_lock: Arc>, - account: &impl Account, + account: BoxedAccount, address: &ServerAddress, resolved_address: &SocketAddr, run_schedule_sender: mpsc::UnboundedSender<()>, @@ -221,8 +221,8 @@ impl Client { let mut ecs = ecs_lock.lock(); let entity_uuid_index = ecs.resource::(); - let uuid = account.get_uuid(); - let entity = if let Some(entity) = entity_uuid_index.get(&account.get_uuid()) { + let uuid = account.0.get_uuid(); + let entity = if let Some(entity) = entity_uuid_index.get(&account.0.get_uuid()) { debug!("Reusing entity {entity:?} for client"); entity } else { @@ -235,7 +235,7 @@ impl Client { }; // add the Account to the entity now so plugins can access it earlier - ecs.entity_mut(entity).insert(account.to_owned()); + ecs.entity_mut(entity).insert(account.clone()); entity }; @@ -292,7 +292,7 @@ impl Client { ecs_lock: Arc>, entity: Entity, mut conn: Connection, - account: &impl Account, + account: BoxedAccount, address: &ServerAddress, ) -> Result< ( @@ -325,8 +325,8 @@ impl Client { // login conn.write( ServerboundHelloPacket { - name: account.get_username().clone(), - profile_id: account.get_uuid(), + name: account.0.get_username().clone(), + profile_id: account.0.get_uuid(), } .get(), ) @@ -352,14 +352,14 @@ impl Client { debug!("Got encryption request"); let e = azalea_crypto::encrypt(&p.public_key, &p.nonce).unwrap(); - if account.is_online() { + if account.0.is_online() { // keep track of the number of times we tried // authenticating so we can give up after too many let mut attempts: usize = 1; while let Err(e) = { conn.authenticate( - account, + account.clone(), e.secret_key, &p, ) diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs index 3386ca201..f4b0433be 100755 --- a/azalea-protocol/src/connect.rs +++ b/azalea-protocol/src/connect.rs @@ -11,7 +11,7 @@ use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket}; use crate::packets::ProtocolPacket; use crate::read::{deserialize_packet, read_raw_packet, try_read_raw_packet, ReadPacketError}; use crate::write::{serialize_packet, write_raw_packet}; -use azalea_auth::account::Account; +use azalea_auth::account::{Account, BoxedAccount}; use azalea_auth::game_profile::GameProfile; use azalea_auth::sessionserver::{ClientSessionServerError, ServerSessionServerError}; use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc}; @@ -20,6 +20,7 @@ use std::fmt::Debug; use std::io::Cursor; use std::marker::PhantomData; use std::net::SocketAddr; +use std::sync::Arc; use thiserror::Error; use tokio::io::AsyncWriteExt; use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf, ReuniteError}; @@ -390,11 +391,11 @@ impl Connection { /// ``` pub async fn authenticate( &self, - account: &impl Account, + account: BoxedAccount, private_key: [u8; 16], packet: &ClientboundHelloPacket, ) -> Result<(), ClientSessionServerError> { - account.join(&packet.public_key, &private_key, &packet.server_id).await + account.0.join(&packet.public_key, &private_key, &packet.server_id).await } } diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index 84c215d54..e58d81190 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -15,7 +15,10 @@ pub mod pathfinder; pub mod prelude; pub mod swarm; +use std::sync::Arc; + use app::Plugins; +use auth::account::{Account, BoxedAccount}; pub use azalea_auth as auth; pub use azalea_block as blocks; pub use azalea_brigadier as brigadier; @@ -180,10 +183,10 @@ where /// [`ServerAddress`]: azalea_protocol::ServerAddress pub async fn start( mut self, - account: Account, + account: impl Account, address: impl TryInto, ) -> Result { - self.swarm.accounts = vec![account]; + self.swarm.accounts = vec![BoxedAccount(Arc::new(account))]; if self.swarm.states.is_empty() { self.swarm.states = vec![S::default()]; } diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index cf557e09e..485fce89b 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -4,7 +4,7 @@ mod chat; mod events; pub mod prelude; -use azalea_auth::account::Account; +use azalea_auth::account::{Account, BoxedAccount}; use azalea_client::{ chat::ChatPacket, start_ecs_runner, Client, DefaultPlugins, Event, JoinError, }; @@ -45,16 +45,16 @@ pub struct Swarm { run_schedule_sender: mpsc::UnboundedSender<()>, } + /// Create a new [`Swarm`]. -pub struct SwarmBuilder +pub struct SwarmBuilder where S: Send + Sync + Clone + Component + 'static, SS: Default + Send + Sync + Clone + Resource + 'static, - A: Account { pub(crate) app: App, /// The accounts that are going to join the server. - pub(crate) accounts: Vec, + pub(crate) accounts: Vec, /// The individual bot states. This must be the same length as `accounts`, /// since each bot gets one state. pub(crate) states: Vec, @@ -260,7 +260,7 @@ where /// [`Self::add_account`] to use the Default implementation for the state. #[must_use] pub fn add_account_with_state(mut self, account: impl Account, state: S) -> Self { - self.accounts.push(account); + self.accounts.push(BoxedAccount(Arc::new(account))); self.states.push(state); self } @@ -361,7 +361,7 @@ where tokio::spawn(async move { if let Some(join_delay) = join_delay { // if there's a join delay, then join one by one - for (account, state) in accounts.iter().zip(states) { + for (account, state) in accounts.iter().cloned().zip(states) { swarm_clone.add_and_retry_forever(account, state).await; tokio::time::sleep(join_delay).await; } @@ -371,6 +371,7 @@ where join_all( accounts .iter() + .cloned() .zip(states) .map(move |(account, state)| async { swarm_borrow @@ -445,7 +446,7 @@ pub enum SwarmEvent { /// /// You can implement an auto-reconnect by calling [`Swarm::add`] /// with the account from this event. - Disconnect(Box), + Disconnect(BoxedAccount), /// At least one bot received a chat message. Chat(ChatPacket), } @@ -524,7 +525,7 @@ impl Swarm { /// Returns an `Err` if the bot could not do a handshake successfully. pub async fn add( &mut self, - account: &impl Account, + account: BoxedAccount, state: S, ) -> Result { let address = self.address.read().clone(); @@ -560,10 +561,10 @@ impl Swarm { } cloned_bots.lock().remove(&bot.entity); let account = cloned_bot - .get_component::() + .get_component::() .expect("bot is missing required Account component"); swarm_tx - .send(SwarmEvent::Disconnect(Box::new(account))) + .send(SwarmEvent::Disconnect(account)) .unwrap(); }); @@ -577,18 +578,18 @@ impl Swarm { /// seconds and doubling up to 15 seconds. pub async fn add_and_retry_forever( &mut self, - account: &impl Account, + account: BoxedAccount, state: S, ) -> Client { let mut disconnects = 0; loop { - match self.add(account, state.clone()).await { + match self.add(account.clone(), state.clone()).await { Ok(bot) => return bot, Err(e) => { disconnects += 1; let delay = (Duration::from_secs(5) * 2u32.pow(disconnects.min(16))) .min(Duration::from_secs(15)); - let username = account.get_username().clone(); + let username = account.0.get_username(); error!("Error joining as {username}: {e}. Waiting {delay:?} and trying again."); tokio::time::sleep(delay).await; } From e38be86b061cc672ad6cf70f91dbc7f21869961c Mon Sep 17 00:00:00 2001 From: veronoicc <64193056+veronoicc@users.noreply.github.com> Date: Sun, 17 Mar 2024 22:06:45 +0000 Subject: [PATCH 06/10] `cargo fmt` --- azalea-auth/src/account.rs | 18 ++++--- azalea-auth/src/lib.rs | 6 +-- azalea-auth/src/microsoft.rs | 50 ++++++++++++------- azalea-auth/src/offline.rs | 12 ++++- azalea-client/src/client.rs | 20 ++++---- .../src/parse_macro.rs | 2 +- azalea-protocol/src/connect.rs | 5 +- azalea/src/prelude.rs | 2 +- azalea/src/swarm/mod.rs | 9 +--- 9 files changed, 74 insertions(+), 50 deletions(-) diff --git a/azalea-auth/src/account.rs b/azalea-auth/src/account.rs index 78f2f62f6..befb00447 100644 --- a/azalea-auth/src/account.rs +++ b/azalea-auth/src/account.rs @@ -1,11 +1,13 @@ use std::{future::Future, sync::Arc}; - use async_trait::async_trait; use bevy_ecs::component::Component; use uuid::Uuid; -use crate::{certs::{Certificates, FetchCertificatesError}, sessionserver::ClientSessionServerError}; +use crate::{ + certs::{Certificates, FetchCertificatesError}, + sessionserver::ClientSessionServerError, +}; #[async_trait] pub trait Account: std::fmt::Debug + Send + Sync + 'static { @@ -14,17 +16,21 @@ pub trait Account: std::fmt::Debug + Send + Sync + 'static { public_key: &[u8], private_key: &[u8], server_id: &str, - ) -> Result<(), ClientSessionServerError> { + ) -> Result<(), ClientSessionServerError> { let server_hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data( server_id.as_bytes(), public_key, private_key, )); let uuid = self.get_uuid(); - + self.join_with_server_id_hash(uuid, server_hash).await } - async fn join_with_server_id_hash(&self, uuid: Uuid, server_hash: String) -> Result<(), ClientSessionServerError>; + async fn join_with_server_id_hash( + &self, + uuid: Uuid, + server_hash: String, + ) -> Result<(), ClientSessionServerError>; async fn fetch_certificates(&self) -> Result; @@ -37,4 +43,4 @@ pub trait Account: std::fmt::Debug + Send + Sync + 'static { } #[derive(Clone, Debug, Component)] -pub struct BoxedAccount(pub Arc); \ No newline at end of file +pub struct BoxedAccount(pub Arc); diff --git a/azalea-auth/src/lib.rs b/azalea-auth/src/lib.rs index 74a5e8bd6..ebcb129a9 100755 --- a/azalea-auth/src/lib.rs +++ b/azalea-auth/src/lib.rs @@ -1,12 +1,12 @@ #![doc = include_str!("../README.md")] -pub mod microsoft; +pub mod account; pub mod cache; pub mod certs; pub mod game_profile; +pub mod microsoft; pub mod offline; pub mod sessionserver; -pub mod account; pub use microsoft::*; -pub use offline::*; \ No newline at end of file +pub use offline::*; diff --git a/azalea-auth/src/microsoft.rs b/azalea-auth/src/microsoft.rs index fe5e93343..3c2c9b530 100755 --- a/azalea-auth/src/microsoft.rs +++ b/azalea-auth/src/microsoft.rs @@ -1,6 +1,11 @@ //! Handle Minecraft (Xbox) authentication. -use crate::{account::Account, cache::{self, CachedAccount, ExpiringValue}, certs::{Certificates, CertificatesResponse, FetchCertificatesError}, sessionserver::{ClientSessionServerError, ForbiddenError}}; +use crate::{ + account::Account, + cache::{self, CachedAccount, ExpiringValue}, + certs::{Certificates, CertificatesResponse, FetchCertificatesError}, + sessionserver::{ClientSessionServerError, ForbiddenError}, +}; use async_trait::async_trait; use base64::Engine; use bevy_ecs::component::Component; @@ -9,13 +14,13 @@ use reqwest::StatusCode; use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey}; use serde::{Deserialize, Serialize}; use serde_json::json; -use tracing::debug; use std::{ collections::HashMap, path::PathBuf, time::{Instant, SystemTime, UNIX_EPOCH}, }; use thiserror::Error; +use tracing::debug; use uuid::Uuid; #[derive(Default)] @@ -69,12 +74,12 @@ impl MicrosoftAccount { } else { None }; - + if cached_account.is_some() && !cached_account.as_ref().unwrap().mca.is_expired() { let account = cached_account.as_ref().unwrap(); // the minecraft auth data is cached and not expired, so we can just // use that instead of doing auth all over again :) - + Ok(Self { client: reqwest::Client::new(), access_token: account.mca.data.access_token.clone(), @@ -98,16 +103,15 @@ impl MicrosoftAccount { } } } - + let msa_token = &msa.data.access_token; tracing::trace!("Got access token: {msa_token}"); - + let xbl = auth_with_xbox_live(&client, msa_token).await?; let xsts_token = obtain_xsts_for_minecraft( &client, - &xbl - .get() + &xbl.get() .expect("Xbox Live auth token shouldn't have expired yet") .token, ) @@ -120,16 +124,16 @@ impl MicrosoftAccount { .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 { return Err(MicrosoftAuthError::DoesNotOwnGame); } } - + let profile: ProfileResponse = get_profile(&client, &minecraft_access_token).await?; - + if let Some(cache_file) = opts.cache_file { if let Err(e) = cache::set_account_in_cache( &cache_file, @@ -147,7 +151,7 @@ impl MicrosoftAccount { tracing::error!("{}", e); } } - + Ok(Self { client, access_token: minecraft_access_token, @@ -159,7 +163,11 @@ impl MicrosoftAccount { #[async_trait] impl Account for MicrosoftAccount { - async fn join_with_server_id_hash(&self, uuid: Uuid, server_hash: String) -> Result<(), ClientSessionServerError> { + async fn join_with_server_id_hash( + &self, + uuid: Uuid, + server_hash: String, + ) -> Result<(), ClientSessionServerError> { let mut encode_buffer = Uuid::encode_buffer(); let undashed_uuid = uuid.simple().encode_lower(&mut encode_buffer); @@ -168,7 +176,8 @@ impl Account for MicrosoftAccount { "selectedProfile": undashed_uuid, "serverId": server_hash }); - let res = self.client + let res = self + .client .post("https://sessionserver.mojang.com/session/minecraft/join") .json(&data) .send() @@ -187,7 +196,9 @@ impl Account for MicrosoftAccount { Err(ClientSessionServerError::AuthServersUnreachable) } "InvalidCredentialsException" => Err(ClientSessionServerError::InvalidSession), - "ForbiddenOperationException" => Err(ClientSessionServerError::ForbiddenOperation), + "ForbiddenOperationException" => { + Err(ClientSessionServerError::ForbiddenOperation) + } _ => Err(ClientSessionServerError::Unknown(forbidden.error)), } } @@ -203,17 +214,18 @@ impl Account for MicrosoftAccount { } } } - + fn get_username(&self) -> String { self.profile.name.clone() } - + fn get_uuid(&self) -> Uuid { self.profile.id } - + async fn fetch_certificates(&self) -> Result { - let res = self.client + let res = self + .client .post("https://api.minecraftservices.com/player/certificates") .header("Authorization", format!("Bearer {}", self.access_token)) .send() diff --git a/azalea-auth/src/offline.rs b/azalea-auth/src/offline.rs index 4e6dd42fc..65a9cd623 100644 --- a/azalea-auth/src/offline.rs +++ b/azalea-auth/src/offline.rs @@ -2,7 +2,11 @@ use async_trait::async_trait; use md5::{Digest, Md5}; use uuid::Uuid; -use crate::{account::Account, certs::{Certificates, FetchCertificatesError}, sessionserver::ClientSessionServerError}; +use crate::{ + account::Account, + certs::{Certificates, FetchCertificatesError}, + sessionserver::ClientSessionServerError, +}; #[derive(Clone, Debug)] pub struct OfflineAccount { @@ -28,7 +32,11 @@ impl OfflineAccount { #[async_trait] impl Account for OfflineAccount { - async fn join_with_server_id_hash(&self, _: Uuid, _: String) -> Result<(), ClientSessionServerError> { + async fn join_with_server_id_hash( + &self, + _: Uuid, + _: String, + ) -> Result<(), ClientSessionServerError> { unimplemented!("Offline accounts can't join servers with a session server.") } diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 879fbd803..94e2c7f52 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -24,7 +24,11 @@ use crate::{ PlayerInfo, }; -use azalea_auth::{account::{Account, BoxedAccount}, game_profile::GameProfile, sessionserver::ClientSessionServerError}; +use azalea_auth::{ + account::{Account, BoxedAccount}, + game_profile::GameProfile, + sessionserver::ClientSessionServerError, +}; use azalea_chat::FormattedText; use azalea_core::{position::Vec3, tick::GameTick}; use azalea_entity::{ @@ -357,14 +361,9 @@ impl Client { // authenticating so we can give up after too many let mut attempts: usize = 1; - while let Err(e) = { - conn.authenticate( - account.clone(), - e.secret_key, - &p, - ) - .await - } { + while let Err(e) = + { conn.authenticate(account.clone(), e.secret_key, &p).await } + { if attempts >= 2 { // if this is the second attempt and we failed // both times, give up @@ -377,7 +376,8 @@ impl Client { ) { // uh oh, we got an invalid session and have // to reauthenticate now - // account.refresh().await?; FIXME: Make this work with microsoft accounts or smth + // account.refresh().await?; FIXME: Make this + // work with microsoft accounts or smth } else { return Err(e.into()); } diff --git a/azalea-inventory/azalea-inventory-macros/src/parse_macro.rs b/azalea-inventory/azalea-inventory-macros/src/parse_macro.rs index 20c2ddc30..de1ec9723 100644 --- a/azalea-inventory/azalea-inventory-macros/src/parse_macro.rs +++ b/azalea-inventory/azalea-inventory-macros/src/parse_macro.rs @@ -1,5 +1,5 @@ use syn::{ - braced, + braced, parse::{Parse, ParseStream, Result}, Ident, LitInt, Token, }; diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs index f4b0433be..7b18580aa 100755 --- a/azalea-protocol/src/connect.rs +++ b/azalea-protocol/src/connect.rs @@ -395,7 +395,10 @@ impl Connection { private_key: [u8; 16], packet: &ClientboundHelloPacket, ) -> Result<(), ClientSessionServerError> { - account.0.join(&packet.public_key, &private_key, &packet.server_id).await + account + .0 + .join(&packet.public_key, &private_key, &packet.server_id) + .await } } diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs index 7f0706ba4..a799db882 100644 --- a/azalea/src/prelude.rs +++ b/azalea/src/prelude.rs @@ -5,8 +5,8 @@ pub use crate::{ bot::BotClientExt, container::ContainerClientExt, pathfinder::PathfinderClientExt, ClientBuilder, }; -pub use azalea_client::{Client, Event}; pub use azalea_auth::{MicrosoftAccount, OfflineAccount}; +pub use azalea_client::{Client, Event}; // this is necessary to make the macros that reference bevy_ecs work pub use crate::ecs as bevy_ecs; pub use crate::ecs::{component::Component, system::Resource}; diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index 485fce89b..bd51eda3c 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -5,9 +5,7 @@ mod events; pub mod prelude; use azalea_auth::account::{Account, BoxedAccount}; -use azalea_client::{ - chat::ChatPacket, start_ecs_runner, Client, DefaultPlugins, Event, JoinError, -}; +use azalea_client::{chat::ChatPacket, start_ecs_runner, Client, DefaultPlugins, Event, JoinError}; use azalea_protocol::{resolver, ServerAddress}; use azalea_world::InstanceContainer; use bevy_app::{App, PluginGroup, PluginGroupBuilder, Plugins}; @@ -45,7 +43,6 @@ pub struct Swarm { run_schedule_sender: mpsc::UnboundedSender<()>, } - /// Create a new [`Swarm`]. pub struct SwarmBuilder where @@ -563,9 +560,7 @@ impl Swarm { let account = cloned_bot .get_component::() .expect("bot is missing required Account component"); - swarm_tx - .send(SwarmEvent::Disconnect(account)) - .unwrap(); + swarm_tx.send(SwarmEvent::Disconnect(account)).unwrap(); }); Ok(bot) From fb4f30de29b0a445365ca4a3bfd3448e08b61312 Mon Sep 17 00:00:00 2001 From: veronoicc <64193056+veronoicc@users.noreply.github.com> Date: Sun, 17 Mar 2024 22:15:47 +0000 Subject: [PATCH 07/10] Examples work now --- azalea-auth/examples/auth.rs | 4 +++- azalea-auth/examples/auth_manual.rs | 22 +++++++++++++++++++-- azalea-auth/examples/certificates.rs | 6 ++++-- azalea-auth/src/account.rs | 29 +++++++++++++++++++++++++++- azalea-auth/src/microsoft.rs | 9 ++++----- azalea-auth/src/offline.rs | 8 ++++---- azalea-protocol/src/connect.rs | 3 +-- azalea/examples/echo.rs | 2 +- azalea/examples/nearest_entity.rs | 4 ++-- azalea/examples/steal.rs | 2 +- azalea/examples/testbot/main.rs | 7 ++++--- 11 files changed, 72 insertions(+), 24 deletions(-) diff --git a/azalea-auth/examples/auth.rs b/azalea-auth/examples/auth.rs index fa99765ed..97d1d5ad7 100755 --- a/azalea-auth/examples/auth.rs +++ b/azalea-auth/examples/auth.rs @@ -1,12 +1,14 @@ use std::path::PathBuf; +use azalea_auth::MicrosoftAccount; + #[tokio::main] async fn main() { env_logger::init(); let cache_file = PathBuf::from("example_cache.json"); - let auth_result = azalea_auth::auth( + let auth_result = MicrosoftAccount::new( "example@example.com", azalea_auth::MicrosoftAuthOpts { cache_file: Some(cache_file), diff --git a/azalea-auth/examples/auth_manual.rs b/azalea-auth/examples/auth_manual.rs index 6a9510a8f..318a97f68 100755 --- a/azalea-auth/examples/auth_manual.rs +++ b/azalea-auth/examples/auth_manual.rs @@ -27,6 +27,24 @@ async fn auth() -> Result> { res.verification_uri, res.user_code ); let msa = azalea_auth::get_ms_auth_token(&client, res).await?; - let auth_result = azalea_auth::get_minecraft_token(&client, &msa.data.access_token).await?; - Ok(azalea_auth::get_profile(&client, &auth_result.minecraft_access_token).await?) + let xbl_auth = azalea_auth::auth_with_xbox_live(&client, &msa.data.access_token).await?; + + let xsts_token = azalea_auth::obtain_xsts_for_minecraft( + &client, + &xbl_auth + .get() + .expect("Xbox Live auth token shouldn't have expired yet") + .token, + ) + .await?; + + // Minecraft auth + let mca = azalea_auth::auth_with_minecraft(&client, &xbl_auth.data.user_hash, &xsts_token).await?; + + let minecraft_access_token: String = mca + .get() + .expect("Minecraft auth shouldn't have expired yet") + .access_token + .to_string(); + Ok(azalea_auth::get_profile(&client, &minecraft_access_token).await?) } diff --git a/azalea-auth/examples/certificates.rs b/azalea-auth/examples/certificates.rs index cd2904f1c..53d5e0b64 100755 --- a/azalea-auth/examples/certificates.rs +++ b/azalea-auth/examples/certificates.rs @@ -1,12 +1,14 @@ use std::path::PathBuf; +use azalea_auth::{account::Account, MicrosoftAccount}; + #[tokio::main] async fn main() { env_logger::init(); let cache_file = PathBuf::from("example_cache.json"); - let auth_result = azalea_auth::auth( + let auth_result = MicrosoftAccount::new( "example@example.com", azalea_auth::MicrosoftAuthOpts { cache_file: Some(cache_file), @@ -16,7 +18,7 @@ async fn main() { .await .unwrap(); - let certs = azalea_auth::certs::fetch_certificates(&auth_result.access_token) + let certs = auth_result.fetch_certificates() .await .unwrap(); diff --git a/azalea-auth/src/account.rs b/azalea-auth/src/account.rs index befb00447..dc322b306 100644 --- a/azalea-auth/src/account.rs +++ b/azalea-auth/src/account.rs @@ -1,4 +1,4 @@ -use std::{future::Future, sync::Arc}; +use std::sync::Arc; use async_trait::async_trait; use bevy_ecs::component::Component; @@ -44,3 +44,30 @@ pub trait Account: std::fmt::Debug + Send + Sync + 'static { #[derive(Clone, Debug, Component)] pub struct BoxedAccount(pub Arc); + +#[async_trait] +impl Account for BoxedAccount { + async fn join_with_server_id_hash( + &self, + uuid: Uuid, + server_hash: String, + ) -> Result<(), ClientSessionServerError> { + self.0.join_with_server_id_hash(uuid, server_hash).await + } + + async fn fetch_certificates(&self) -> Result { + self.0.fetch_certificates().await + } + + fn get_username(&self) -> String { + self.0.get_username() + } + + fn get_uuid(&self) -> Uuid { + self.0.get_uuid() + } + + fn is_online(&self) -> bool { + self.0.is_online() + } +} diff --git a/azalea-auth/src/microsoft.rs b/azalea-auth/src/microsoft.rs index 3c2c9b530..e837e0e89 100755 --- a/azalea-auth/src/microsoft.rs +++ b/azalea-auth/src/microsoft.rs @@ -8,7 +8,6 @@ use crate::{ }; use async_trait::async_trait; use base64::Engine; -use bevy_ecs::component::Component; use chrono::{DateTime, Utc}; use reqwest::StatusCode; use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey}; @@ -68,7 +67,7 @@ pub enum MicrosoftAuthError { } impl MicrosoftAccount { - async fn new(email: &str, opts: MicrosoftAuthOpts) -> Result { + pub async fn new(email: &str, opts: MicrosoftAuthOpts) -> Result { let cached_account = if let Some(cache_file) = &opts.cache_file { cache::get_account_in_cache(cache_file, email).await } else { @@ -508,7 +507,7 @@ pub enum XboxLiveAuthError { InvalidExpiryDate(String), } -async fn auth_with_xbox_live( +pub async fn auth_with_xbox_live( client: &reqwest::Client, access_token: &str, ) -> Result, XboxLiveAuthError> { @@ -559,7 +558,7 @@ pub enum MinecraftXstsAuthError { Http(#[from] reqwest::Error), } -async fn obtain_xsts_for_minecraft( +pub async fn obtain_xsts_for_minecraft( client: &reqwest::Client, xbl_auth_token: &str, ) -> Result { @@ -589,7 +588,7 @@ pub enum MinecraftAuthError { Http(#[from] reqwest::Error), } -async fn auth_with_minecraft( +pub async fn auth_with_minecraft( client: &reqwest::Client, user_hash: &str, xsts_token: &str, diff --git a/azalea-auth/src/offline.rs b/azalea-auth/src/offline.rs index 65a9cd623..6ed6d5216 100644 --- a/azalea-auth/src/offline.rs +++ b/azalea-auth/src/offline.rs @@ -15,16 +15,16 @@ pub struct OfflineAccount { } impl OfflineAccount { - pub fn new(username: String) -> Self { + pub fn new(username: impl ToString) -> Self { Self { - username, + username: username.to_string(), uuid: None, } } - pub fn with_uuid(username: String, uuid: Uuid) -> Self { + pub fn with_uuid(username: impl ToString, uuid: Uuid) -> Self { Self { - username, + username: username.to_string(), uuid: Some(uuid), } } diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs index 7b18580aa..5d79620a8 100755 --- a/azalea-protocol/src/connect.rs +++ b/azalea-protocol/src/connect.rs @@ -11,7 +11,7 @@ use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket}; use crate::packets::ProtocolPacket; use crate::read::{deserialize_packet, read_raw_packet, try_read_raw_packet, ReadPacketError}; use crate::write::{serialize_packet, write_raw_packet}; -use azalea_auth::account::{Account, BoxedAccount}; +use azalea_auth::account::BoxedAccount; use azalea_auth::game_profile::GameProfile; use azalea_auth::sessionserver::{ClientSessionServerError, ServerSessionServerError}; use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc}; @@ -20,7 +20,6 @@ use std::fmt::Debug; use std::io::Cursor; use std::marker::PhantomData; use std::net::SocketAddr; -use std::sync::Arc; use thiserror::Error; use tokio::io::AsyncWriteExt; use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf, ReuniteError}; diff --git a/azalea/examples/echo.rs b/azalea/examples/echo.rs index 01390982d..f6b4dec08 100755 --- a/azalea/examples/echo.rs +++ b/azalea/examples/echo.rs @@ -4,7 +4,7 @@ use azalea::prelude::*; #[tokio::main] async fn main() { - let account = Account::offline("bot"); + let account = OfflineAccount::new("bot"); // or let account = Account::microsoft("email").await.unwrap(); ClientBuilder::new() diff --git a/azalea/examples/nearest_entity.rs b/azalea/examples/nearest_entity.rs index d50e6c467..87d46be41 100644 --- a/azalea/examples/nearest_entity.rs +++ b/azalea/examples/nearest_entity.rs @@ -1,7 +1,7 @@ use azalea::nearest_entity::EntityFinder; use azalea::ClientBuilder; use azalea::{Bot, LookAtEvent}; -use azalea_client::Account; +use azalea_auth::OfflineAccount; use azalea_core::tick::GameTick; use azalea_entity::metadata::{ItemItem, Player}; use azalea_entity::{EyeHeight, LocalEntity, Position}; @@ -14,7 +14,7 @@ use bevy_ecs::{ #[tokio::main] async fn main() { - let account = Account::offline("bot"); + let account = OfflineAccount::new("bot"); ClientBuilder::new() .add_plugins(LookAtStuffPlugin) diff --git a/azalea/examples/steal.rs b/azalea/examples/steal.rs index c6ab46397..7b6b917b0 100644 --- a/azalea/examples/steal.rs +++ b/azalea/examples/steal.rs @@ -8,7 +8,7 @@ use std::sync::Arc; #[tokio::main] async fn main() { - let account = Account::offline("bot"); + let account = OfflineAccount::new("bot"); // or let bot = Account::microsoft("email").await.unwrap(); ClientBuilder::new() diff --git a/azalea/examples/testbot/main.rs b/azalea/examples/testbot/main.rs index 86395b7e6..a57a84775 100644 --- a/azalea/examples/testbot/main.rs +++ b/azalea/examples/testbot/main.rs @@ -21,6 +21,7 @@ use azalea::brigadier::command_dispatcher::CommandDispatcher; use azalea::ecs::prelude::*; use azalea::prelude::*; use azalea::swarm::prelude::*; +use azalea_auth::account::Account; use commands::{register_commands, CommandSource}; use parking_lot::Mutex; use std::sync::Arc; @@ -61,7 +62,7 @@ async fn main() { }); } - let account = Account::offline(USERNAME); + let account = OfflineAccount::new(USERNAME); let mut commands = CommandDispatcher::new(); register_commands(&mut commands); @@ -182,9 +183,9 @@ async fn swarm_handle( ) -> anyhow::Result<()> { match &event { SwarmEvent::Disconnect(account) => { - println!("bot got kicked! {}", account.username); + println!("bot got kicked! {}", account.get_username()); tokio::time::sleep(Duration::from_secs(5)).await; - swarm.add_and_retry_forever(account, State::default()).await; + swarm.add_and_retry_forever(account.clone(), State::default()).await; } SwarmEvent::Chat(chat) => { if chat.message().to_string() == "The particle was not visible for anybody" { From 5e3706df0bf11145d4eb1e1f622cb1c91bd5ad8a Mon Sep 17 00:00:00 2001 From: veronoicc <64193056+veronoicc@users.noreply.github.com> Date: Sun, 17 Mar 2024 23:01:36 +0000 Subject: [PATCH 08/10] Switch to having the `BoxedAccount` struct in azalea-client, this makes it so that azalea-auth doesnt need to depend on the ecs anymore --- Cargo.lock | 1 - azalea-auth/Cargo.toml | 1 - azalea-auth/src/account.rs | 35 +------------------------------ azalea-client/src/client.rs | 38 ++++++++++++++++++++++++++++++---- azalea-client/src/lib.rs | 2 +- azalea-protocol/src/connect.rs | 6 +++--- azalea/src/lib.rs | 2 +- azalea/src/swarm/mod.rs | 4 ++-- 8 files changed, 42 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a595066f5..f52491716 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -242,7 +242,6 @@ dependencies = [ "azalea-buf", "azalea-crypto", "base64", - "bevy_ecs", "chrono", "env_logger", "md-5", diff --git a/azalea-auth/Cargo.toml b/azalea-auth/Cargo.toml index 70ecdeba3..a769d8a8c 100644 --- a/azalea-auth/Cargo.toml +++ b/azalea-auth/Cargo.toml @@ -27,7 +27,6 @@ thiserror = "1.0.57" tokio = { version = "1.36.0", features = ["fs"] } uuid = { version = "1.7.0", features = ["serde", "v3"] } md-5 = "0.10.6" -bevy_ecs = "0.13.0" async-trait = "0.1.78" [dev-dependencies] diff --git a/azalea-auth/src/account.rs b/azalea-auth/src/account.rs index dc322b306..f7cc7d1b1 100644 --- a/azalea-auth/src/account.rs +++ b/azalea-auth/src/account.rs @@ -1,7 +1,4 @@ -use std::sync::Arc; - use async_trait::async_trait; -use bevy_ecs::component::Component; use uuid::Uuid; use crate::{ @@ -40,34 +37,4 @@ pub trait Account: std::fmt::Debug + Send + Sync + 'static { fn is_online(&self) -> bool { true } -} - -#[derive(Clone, Debug, Component)] -pub struct BoxedAccount(pub Arc); - -#[async_trait] -impl Account for BoxedAccount { - async fn join_with_server_id_hash( - &self, - uuid: Uuid, - server_hash: String, - ) -> Result<(), ClientSessionServerError> { - self.0.join_with_server_id_hash(uuid, server_hash).await - } - - async fn fetch_certificates(&self) -> Result { - self.0.fetch_certificates().await - } - - fn get_username(&self) -> String { - self.0.get_username() - } - - fn get_uuid(&self) -> Uuid { - self.0.get_uuid() - } - - fn is_online(&self) -> bool { - self.0.is_online() - } -} +} \ No newline at end of file diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 94e2c7f52..a0fff3e49 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -24,10 +24,9 @@ use crate::{ PlayerInfo, }; +use async_trait::async_trait; use azalea_auth::{ - account::{Account, BoxedAccount}, - game_profile::GameProfile, - sessionserver::ClientSessionServerError, + account::Account, certs::{Certificates, FetchCertificatesError}, game_profile::GameProfile, sessionserver::ClientSessionServerError }; use azalea_chat::FormattedText; use azalea_core::{position::Vec3, tick::GameTick}; @@ -143,6 +142,37 @@ pub enum JoinError { Disconnect { reason: FormattedText }, } +#[derive(Clone, Debug, Component)] +pub struct BoxedAccount(pub Arc); + +#[async_trait] +impl Account for BoxedAccount { + async fn join_with_server_id_hash( + &self, + uuid: Uuid, + server_hash: String, + ) -> Result<(), ClientSessionServerError> { + self.0.join_with_server_id_hash(uuid, server_hash).await + } + + async fn fetch_certificates(&self) -> Result { + self.0.fetch_certificates().await + } + + fn get_username(&self) -> String { + self.0.get_username() + } + + fn get_uuid(&self) -> Uuid { + self.0.get_uuid() + } + + fn is_online(&self) -> bool { + self.0.is_online() + } +} + + impl Client { /// Create a new client from the given [`GameProfile`], ECS Entity, ECS /// World, and schedule runner function. @@ -362,7 +392,7 @@ impl Client { let mut attempts: usize = 1; while let Err(e) = - { conn.authenticate(account.clone(), e.secret_key, &p).await } + { conn.authenticate(account.0.clone(), e.secret_key, &p).await } { if attempts >= 2 { // if this is the second attempt and we failed diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index 396f81f5f..c705354f3 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -30,7 +30,7 @@ pub mod task_pool; pub use azalea_protocol::packets::configuration::serverbound_client_information_packet::ClientInformation; pub use client::{ - start_ecs_runner, Client, DefaultPlugins, JoinError, JoinedClientBundle, TickBroadcast, + start_ecs_runner, Client, DefaultPlugins, JoinError, JoinedClientBundle, TickBroadcast, BoxedAccount, }; pub use events::Event; pub use local_player::{GameProfileComponent, InstanceHolder, TabList}; diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs index 5d79620a8..6ad4b05d4 100755 --- a/azalea-protocol/src/connect.rs +++ b/azalea-protocol/src/connect.rs @@ -11,7 +11,7 @@ use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket}; use crate::packets::ProtocolPacket; use crate::read::{deserialize_packet, read_raw_packet, try_read_raw_packet, ReadPacketError}; use crate::write::{serialize_packet, write_raw_packet}; -use azalea_auth::account::BoxedAccount; +use azalea_auth::account::Account; use azalea_auth::game_profile::GameProfile; use azalea_auth::sessionserver::{ClientSessionServerError, ServerSessionServerError}; use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc}; @@ -20,6 +20,7 @@ use std::fmt::Debug; use std::io::Cursor; use std::marker::PhantomData; use std::net::SocketAddr; +use std::sync::Arc; use thiserror::Error; use tokio::io::AsyncWriteExt; use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf, ReuniteError}; @@ -390,12 +391,11 @@ impl Connection { /// ``` pub async fn authenticate( &self, - account: BoxedAccount, + account: Arc, private_key: [u8; 16], packet: &ClientboundHelloPacket, ) -> Result<(), ClientSessionServerError> { account - .0 .join(&packet.public_key, &private_key, &packet.server_id) .await } diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index e58d81190..e166717db 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -18,7 +18,7 @@ pub mod swarm; use std::sync::Arc; use app::Plugins; -use auth::account::{Account, BoxedAccount}; +use auth::account::Account; pub use azalea_auth as auth; pub use azalea_block as blocks; pub use azalea_brigadier as brigadier; diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index bd51eda3c..e782a0f42 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -4,8 +4,8 @@ mod chat; mod events; pub mod prelude; -use azalea_auth::account::{Account, BoxedAccount}; -use azalea_client::{chat::ChatPacket, start_ecs_runner, Client, DefaultPlugins, Event, JoinError}; +use azalea_auth::account::Account; +use azalea_client::{chat::ChatPacket, start_ecs_runner, BoxedAccount, Client, DefaultPlugins, Event, JoinError}; use azalea_protocol::{resolver, ServerAddress}; use azalea_world::InstanceContainer; use bevy_app::{App, PluginGroup, PluginGroupBuilder, Plugins}; From 60c84e92d20f0ebdd7f4207b2dabfe8107202cfd Mon Sep 17 00:00:00 2001 From: veronoicc <64193056+veronoicc@users.noreply.github.com> Date: Sun, 17 Mar 2024 23:08:28 +0000 Subject: [PATCH 09/10] Make stuff not panick when using certain functions on an offline account --- azalea-auth/src/account.rs | 6 +--- azalea-auth/src/microsoft.rs | 4 +-- azalea-auth/src/offline.rs | 10 ++----- azalea-client/src/client.rs | 56 ++++++++++++++++-------------------- 4 files changed, 31 insertions(+), 45 deletions(-) diff --git a/azalea-auth/src/account.rs b/azalea-auth/src/account.rs index f7cc7d1b1..77bc8e11c 100644 --- a/azalea-auth/src/account.rs +++ b/azalea-auth/src/account.rs @@ -29,12 +29,8 @@ pub trait Account: std::fmt::Debug + Send + Sync + 'static { server_hash: String, ) -> Result<(), ClientSessionServerError>; - async fn fetch_certificates(&self) -> Result; + async fn fetch_certificates(&self) -> Result, FetchCertificatesError>; fn get_username(&self) -> String; fn get_uuid(&self) -> Uuid; - - fn is_online(&self) -> bool { - true - } } \ No newline at end of file diff --git a/azalea-auth/src/microsoft.rs b/azalea-auth/src/microsoft.rs index e837e0e89..5a294f9ee 100755 --- a/azalea-auth/src/microsoft.rs +++ b/azalea-auth/src/microsoft.rs @@ -222,7 +222,7 @@ impl Account for MicrosoftAccount { self.profile.id } - async fn fetch_certificates(&self) -> Result { + async fn fetch_certificates(&self) -> Result, FetchCertificatesError> { let res = self .client .post("https://api.minecraftservices.com/player/certificates") @@ -277,7 +277,7 @@ impl Account for MicrosoftAccount { refresh_after: res.refreshed_after, }; - Ok(certificates) + Ok(Some(certificates)) } } diff --git a/azalea-auth/src/offline.rs b/azalea-auth/src/offline.rs index 6ed6d5216..5f1e799ae 100644 --- a/azalea-auth/src/offline.rs +++ b/azalea-auth/src/offline.rs @@ -37,11 +37,11 @@ impl Account for OfflineAccount { _: Uuid, _: String, ) -> Result<(), ClientSessionServerError> { - unimplemented!("Offline accounts can't join servers with a session server.") + Ok(()) } - async fn fetch_certificates(&self) -> Result { - unimplemented!("Offline accounts can't fetch certificates.") + async fn fetch_certificates(&self) -> Result, FetchCertificatesError> { + Ok(None) } fn get_username(&self) -> String { @@ -51,10 +51,6 @@ impl Account for OfflineAccount { fn get_uuid(&self) -> Uuid { self.uuid.unwrap_or_else(|| generate_uuid(&self.username)) } - - fn is_online(&self) -> bool { - false - } } pub fn generate_uuid(username: &str) -> Uuid { diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index a0fff3e49..708a34f28 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -155,7 +155,7 @@ impl Account for BoxedAccount { self.0.join_with_server_id_hash(uuid, server_hash).await } - async fn fetch_certificates(&self) -> Result { + async fn fetch_certificates(&self) -> Result, FetchCertificatesError> { self.0.fetch_certificates().await } @@ -166,10 +166,6 @@ impl Account for BoxedAccount { fn get_uuid(&self) -> Uuid { self.0.get_uuid() } - - fn is_online(&self) -> bool { - self.0.is_online() - } } @@ -386,33 +382,31 @@ impl Client { debug!("Got encryption request"); let e = azalea_crypto::encrypt(&p.public_key, &p.nonce).unwrap(); - if account.0.is_online() { - // keep track of the number of times we tried - // authenticating so we can give up after too many - let mut attempts: usize = 1; - - while let Err(e) = - { conn.authenticate(account.0.clone(), e.secret_key, &p).await } - { - if attempts >= 2 { - // if this is the second attempt and we failed - // both times, give up - return Err(e.into()); - } - if matches!( - e, - ClientSessionServerError::InvalidSession - | ClientSessionServerError::ForbiddenOperation - ) { - // uh oh, we got an invalid session and have - // to reauthenticate now - // account.refresh().await?; FIXME: Make this - // work with microsoft accounts or smth - } else { - return Err(e.into()); - } - attempts += 1; + // keep track of the number of times we tried + // authenticating so we can give up after too many + let mut attempts: usize = 1; + + while let Err(e) = + { conn.authenticate(account.0.clone(), e.secret_key, &p).await } + { + if attempts >= 2 { + // if this is the second attempt and we failed + // both times, give up + return Err(e.into()); + } + if matches!( + e, + ClientSessionServerError::InvalidSession + | ClientSessionServerError::ForbiddenOperation + ) { + // uh oh, we got an invalid session and have + // to reauthenticate now + // account.refresh().await?; FIXME: Make this + // work with microsoft accounts or smth + } else { + return Err(e.into()); } + attempts += 1; } conn.write( From 7c5a78f27607f72f5338ea76ebfda55802f322b1 Mon Sep 17 00:00:00 2001 From: veronoicc Date: Wed, 22 May 2024 11:47:50 +0200 Subject: [PATCH 10/10] `cargo fmt` and use type parameters instead of impl parameters to allow for turbofish --- azalea-auth/examples/auth_manual.rs | 3 ++- azalea-auth/examples/certificates.rs | 4 +--- azalea-auth/src/account.rs | 2 +- azalea-client/src/client.rs | 15 ++++++++++----- azalea-client/src/lib.rs | 3 ++- azalea/examples/testbot/main.rs | 4 +++- azalea/src/lib.rs | 9 ++++++--- azalea/src/swarm/mod.rs | 15 +++++++++++---- 8 files changed, 36 insertions(+), 19 deletions(-) diff --git a/azalea-auth/examples/auth_manual.rs b/azalea-auth/examples/auth_manual.rs index 318a97f68..a661e162d 100755 --- a/azalea-auth/examples/auth_manual.rs +++ b/azalea-auth/examples/auth_manual.rs @@ -39,7 +39,8 @@ async fn auth() -> Result> { .await?; // Minecraft auth - let mca = azalea_auth::auth_with_minecraft(&client, &xbl_auth.data.user_hash, &xsts_token).await?; + let mca = + azalea_auth::auth_with_minecraft(&client, &xbl_auth.data.user_hash, &xsts_token).await?; let minecraft_access_token: String = mca .get() diff --git a/azalea-auth/examples/certificates.rs b/azalea-auth/examples/certificates.rs index 53d5e0b64..b6b6d43a2 100755 --- a/azalea-auth/examples/certificates.rs +++ b/azalea-auth/examples/certificates.rs @@ -18,9 +18,7 @@ async fn main() { .await .unwrap(); - let certs = auth_result.fetch_certificates() - .await - .unwrap(); + let certs = auth_result.fetch_certificates().await.unwrap(); println!("{certs:?}"); } diff --git a/azalea-auth/src/account.rs b/azalea-auth/src/account.rs index 77bc8e11c..e50f47a2c 100644 --- a/azalea-auth/src/account.rs +++ b/azalea-auth/src/account.rs @@ -33,4 +33,4 @@ pub trait Account: std::fmt::Debug + Send + Sync + 'static { fn get_username(&self) -> String; fn get_uuid(&self) -> Uuid; -} \ No newline at end of file +} diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 708a34f28..9e290b1c3 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -26,7 +26,10 @@ use crate::{ use async_trait::async_trait; use azalea_auth::{ - account::Account, certs::{Certificates, FetchCertificatesError}, game_profile::GameProfile, sessionserver::ClientSessionServerError + account::Account, + certs::{Certificates, FetchCertificatesError}, + game_profile::GameProfile, + sessionserver::ClientSessionServerError, }; use azalea_chat::FormattedText; use azalea_core::{position::Vec3, tick::GameTick}; @@ -168,7 +171,6 @@ impl Account for BoxedAccount { } } - impl Client { /// Create a new client from the given [`GameProfile`], ECS Entity, ECS /// World, and schedule runner function. @@ -211,10 +213,13 @@ impl Client { /// Ok(()) /// } /// ``` - pub async fn join( - account: impl Account, + pub async fn join( + account: A, address: impl TryInto, - ) -> Result<(Self, mpsc::UnboundedReceiver), JoinError> { + ) -> Result<(Self, mpsc::UnboundedReceiver), JoinError> + where + A: Account, + { let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?; let resolved_address = resolver::resolve_address(&address).await?; diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index c705354f3..32781bcc1 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -30,7 +30,8 @@ pub mod task_pool; pub use azalea_protocol::packets::configuration::serverbound_client_information_packet::ClientInformation; pub use client::{ - start_ecs_runner, Client, DefaultPlugins, JoinError, JoinedClientBundle, TickBroadcast, BoxedAccount, + start_ecs_runner, BoxedAccount, Client, DefaultPlugins, JoinError, JoinedClientBundle, + TickBroadcast, }; pub use events::Event; pub use local_player::{GameProfileComponent, InstanceHolder, TabList}; diff --git a/azalea/examples/testbot/main.rs b/azalea/examples/testbot/main.rs index a57a84775..55f97ea2e 100644 --- a/azalea/examples/testbot/main.rs +++ b/azalea/examples/testbot/main.rs @@ -185,7 +185,9 @@ async fn swarm_handle( SwarmEvent::Disconnect(account) => { println!("bot got kicked! {}", account.get_username()); tokio::time::sleep(Duration::from_secs(5)).await; - swarm.add_and_retry_forever(account.clone(), State::default()).await; + swarm + .add_and_retry_forever(account.clone(), State::default()) + .await; } SwarmEvent::Chat(chat) => { if chat.message().to_string() == "The particle was not visible for anybody" { diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index e166717db..1a81ef418 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -181,11 +181,14 @@ where /// to a Minecraft server. /// /// [`ServerAddress`]: azalea_protocol::ServerAddress - pub async fn start( + pub async fn start( mut self, - account: impl Account, + account: A, address: impl TryInto, - ) -> Result { + ) -> Result + where + A: Account, + { self.swarm.accounts = vec![BoxedAccount(Arc::new(account))]; if self.swarm.states.is_empty() { self.swarm.states = vec![S::default()]; diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index e782a0f42..50df3502c 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -5,7 +5,9 @@ mod events; pub mod prelude; use azalea_auth::account::Account; -use azalea_client::{chat::ChatPacket, start_ecs_runner, BoxedAccount, Client, DefaultPlugins, Event, JoinError}; +use azalea_client::{ + chat::ChatPacket, start_ecs_runner, BoxedAccount, Client, DefaultPlugins, Event, JoinError, +}; use azalea_protocol::{resolver, ServerAddress}; use azalea_world::InstanceContainer; use bevy_app::{App, PluginGroup, PluginGroupBuilder, Plugins}; @@ -232,9 +234,10 @@ where /// By default every account will join at the same time, you can add a delay /// with [`Self::join_delay`]. #[must_use] - pub fn add_accounts(mut self, accounts: Vec) -> Self + pub fn add_accounts(mut self, accounts: Vec) -> Self where S: Default, + A: Account, { for account in accounts { self = self.add_account(account); @@ -247,16 +250,20 @@ where /// This will make the state for this client be the default, use /// [`Self::add_account_with_state`] to avoid that. #[must_use] - pub fn add_account(self, account: impl Account) -> Self + pub fn add_account(self, account: A) -> Self where S: Default, + A: Account, { self.add_account_with_state(account, S::default()) } /// Add an account with a custom initial state. Use just /// [`Self::add_account`] to use the Default implementation for the state. #[must_use] - pub fn add_account_with_state(mut self, account: impl Account, state: S) -> Self { + pub fn add_account_with_state(mut self, account: A, state: S) -> Self + where + A: Account, + { self.accounts.push(BoxedAccount(Arc::new(account))); self.states.push(state); self