azalea_client/plugins/
login.rs1#[cfg(feature = "online-mode")]
2use azalea_auth::sessionserver::ClientSessionServerError;
3use azalea_protocol::{
4 connect::Proxy,
5 packets::login::{ClientboundHello, ServerboundCustomQueryAnswer, ServerboundKey},
6};
7use bevy_app::prelude::*;
8use bevy_ecs::prelude::*;
9use bevy_tasks::{IoTaskPool, Task, futures_lite::future};
10use thiserror::Error;
11use tracing::{debug, error, trace, warn};
12
13use super::{
14 connection::RawConnection,
15 packet::login::{ReceiveCustomQueryEvent, ReceiveHelloEvent, SendLoginPacketEvent},
16};
17use crate::{Account, join::ConnectOpts};
18
19pub struct LoginPlugin;
21impl Plugin for LoginPlugin {
22 fn build(&self, app: &mut App) {
23 app.add_observer(handle_receive_hello_event)
24 .add_systems(Update, (poll_auth_task, reply_to_custom_queries));
25 }
26}
27
28fn handle_receive_hello_event(
29 receive_hello: On<ReceiveHelloEvent>,
30 mut commands: Commands,
31 query: Query<&ConnectOpts>,
32) {
33 let task_pool = IoTaskPool::get();
34
35 let account = receive_hello.account.clone();
36 let packet = receive_hello.packet.clone();
37 let client = receive_hello.entity;
38
39 let connect_opts = if let Ok(opts) = query.get(client) {
43 opts.sessionserver_proxy.clone()
44 } else {
45 warn!("ConnectOpts component missing on a client ({client}) that got ReceiveHelloEvent");
46 None
47 };
48
49 let task = task_pool.spawn(auth_with_account(account, packet, connect_opts));
50 commands.entity(client).insert(AuthTask(task));
51}
52
53#[derive(Component)]
56pub struct IsAuthenticated;
57
58pub fn poll_auth_task(
59 mut commands: Commands,
60 mut query: Query<(Entity, &mut AuthTask, &mut RawConnection)>,
61) {
62 for (entity, mut auth_task, mut raw_conn) in query.iter_mut() {
63 if let Some(poll_res) = future::block_on(future::poll_once(&mut auth_task.0)) {
64 debug!("Finished auth");
65 commands
66 .entity(entity)
67 .remove::<AuthTask>()
68 .insert(IsAuthenticated);
69 match poll_res {
70 Ok((packet, private_key)) => {
71 if let Err(e) = raw_conn.write(packet) {
76 error!("Error sending key packet: {e:?}");
77 }
78 if let Some(net_conn) = raw_conn.net_conn() {
79 net_conn.set_encryption_key(private_key);
80 }
81 }
82 Err(err) => {
83 error!("Error during authentication: {err:?}");
84 }
85 }
86 }
87 }
88}
89
90type PrivateKey = [u8; 16];
91
92#[derive(Component)]
93pub struct AuthTask(Task<Result<(ServerboundKey, PrivateKey), AuthWithAccountError>>);
94
95#[derive(Debug, Error)]
96pub enum AuthWithAccountError {
97 #[error("Failed to encrypt the challenge from the server for {0:?}")]
98 Encryption(ClientboundHello),
99 #[cfg(feature = "online-mode")]
100 #[error("{0}")]
101 SessionServer(#[from] ClientSessionServerError),
102 #[cfg(feature = "online-mode")]
103 #[error("Couldn't refresh access token: {0}")]
104 Auth(#[from] azalea_auth::AuthError),
105}
106
107pub async fn auth_with_account(
108 account: Account,
109 packet: ClientboundHello,
110 proxy: Option<Proxy>,
111) -> Result<(ServerboundKey, PrivateKey), AuthWithAccountError> {
112 let Ok(encrypt_res) = azalea_crypto::encrypt(&packet.public_key, &packet.challenge) else {
113 return Err(AuthWithAccountError::Encryption(packet));
114 };
115 let key_packet = ServerboundKey {
116 key_bytes: encrypt_res.encrypted_public_key,
117 encrypted_challenge: encrypt_res.encrypted_challenge,
118 };
119 let private_key = encrypt_res.secret_key;
120
121 #[cfg(not(feature = "online-mode"))]
122 let _ = (account, proxy);
123
124 #[cfg(feature = "online-mode")]
125 if packet.should_authenticate {
126 let Some(access_token) = &account.access_token else {
127 return Ok((key_packet, private_key));
129 };
130
131 let mut attempts: usize = 1;
134
135 let proxy = proxy.map(Proxy::into);
136
137 while let Err(err) = {
138 use azalea_auth::sessionserver::{self, SessionServerJoinOpts};
139
140 let access_token = access_token.lock().clone();
141
142 let uuid = &account
143 .uuid
144 .expect("Uuid must be present if access token is present.");
145
146 let proxy = proxy.clone();
147
148 async_compat::Compat::new(sessionserver::join(SessionServerJoinOpts {
151 access_token: &access_token,
152 public_key: &packet.public_key,
153 private_key: &private_key,
154 uuid,
155 server_id: &packet.server_id,
156 proxy,
157 }))
158 .await
159 } {
160 if attempts >= 2 {
161 return Err(err.into());
164 }
165 if matches!(
166 err,
167 ClientSessionServerError::InvalidSession
168 | ClientSessionServerError::ForbiddenOperation
169 ) {
170 async_compat::Compat::new(account.refresh()).await?;
173 } else {
174 return Err(err.into());
175 }
176 attempts += 1;
177 }
178 }
179
180 Ok((key_packet, private_key))
181}
182
183pub fn reply_to_custom_queries(
184 mut commands: Commands,
185 mut events: MessageReader<ReceiveCustomQueryEvent>,
186) {
187 for event in events.read() {
188 trace!("Maybe replying to custom query: {event:?}");
189 if event.disabled {
190 continue;
191 }
192
193 commands.trigger(SendLoginPacketEvent::new(
194 event.entity,
195 ServerboundCustomQueryAnswer {
196 transaction_id: event.packet.transaction_id,
197 data: None,
198 },
199 ));
200 }
201}