1use std::{
18 error::Error,
19 io::Cursor,
20 sync::{Arc, LazyLock},
21};
22
23use azalea_auth::{AuthOpts, sessionserver::ClientSessionServerError};
24use azalea_protocol::{
25 address::ServerAddr,
26 connect::Connection,
27 packets::{
28 self, ClientIntention, PROTOCOL_VERSION, VERSION_NAME,
29 game::{ClientboundGamePacket, ServerboundGamePacket},
30 handshake::{
31 ClientboundHandshakePacket, ServerboundHandshakePacket,
32 s_intention::ServerboundIntention,
33 },
34 login::{
35 ClientboundLoginPacket, ServerboundKey, ServerboundLoginPacket,
36 s_hello::ServerboundHello,
37 },
38 status::{
39 ServerboundStatusPacket,
40 c_pong_response::ClientboundPongResponse,
41 c_status_response::{ClientboundStatusResponse, Players, Version},
42 },
43 },
44 read::ReadPacketError,
45 resolver::resolve_address,
46};
47use futures::FutureExt;
48use parking_lot::Mutex;
49use tokio::{
50 fs::File,
51 io::AsyncWriteExt,
52 net::{TcpListener, TcpStream},
53};
54use tracing::{Level, debug, error, info, warn};
55use uuid::Uuid;
56
57const LISTEN_ADDR: &str = "127.0.0.1:25566";
59const TARGET_ADDR: &str = "localhost";
61const ACCOUNT: &str = "[email protected]";
64
65const PROXY_DESC: &str = "An Azalea Minecraft Proxy";
66static PROXY_FAVICON: LazyLock<Option<String>> = LazyLock::new(|| None);
68static PROXY_VERSION: LazyLock<Version> = LazyLock::new(|| Version {
69 name: VERSION_NAME.to_string(),
70 protocol: PROTOCOL_VERSION,
71});
72const PROXY_PLAYERS: Players = Players {
73 max: 1,
74 online: 0,
75 sample: Vec::new(),
76};
77const PROXY_SECURE_CHAT: Option<bool> = Some(false);
78
79#[tokio::main]
80async fn main() -> eyre::Result<()> {
81 tracing_subscriber::fmt()
82 .with_max_level(Level::DEBUG)
83 .init();
84
85 let listener = TcpListener::bind(LISTEN_ADDR).await?;
87
88 info!("Listening on {LISTEN_ADDR}, proxying to {TARGET_ADDR}");
89
90 loop {
91 let (stream, _) = listener.accept().await?;
93 tokio::spawn(handle_connection(stream));
94 }
95}
96
97async fn handle_connection(stream: TcpStream) -> eyre::Result<()> {
98 stream.set_nodelay(true)?;
99 let ip = stream.peer_addr()?;
100 let mut conn: Connection<ServerboundHandshakePacket, ClientboundHandshakePacket> =
101 Connection::wrap(stream);
102
103 let intent = match conn.read().await {
107 Ok(packet) => match packet {
108 ServerboundHandshakePacket::Intention(packet) => {
109 info!(
110 "New connection from {}, hostname {:?}:{}, version {}, {:?}",
111 ip.ip(),
112 packet.hostname,
113 packet.port,
114 packet.protocol_version,
115 packet.intention
116 );
117 packet
118 }
119 },
120 Err(e) => {
121 let e = e.into();
122 warn!("Error during intent: {e}");
123 return Err(e);
124 }
125 };
126
127 match intent.intention {
128 ClientIntention::Status => {
130 let mut conn = conn.status();
131 loop {
132 match conn.read().await {
133 Ok(p) => match p {
134 ServerboundStatusPacket::StatusRequest(_) => {
135 conn.write(ClientboundStatusResponse {
136 description: PROXY_DESC.into(),
137 favicon: PROXY_FAVICON.clone(),
138 players: PROXY_PLAYERS.clone(),
139 version: PROXY_VERSION.clone(),
140 enforces_secure_chat: PROXY_SECURE_CHAT,
141 })
142 .await?;
143 }
144 ServerboundStatusPacket::PingRequest(p) => {
145 conn.write(ClientboundPongResponse { time: p.time }).await?;
146 break;
147 }
148 },
149 Err(e) => match *e {
150 ReadPacketError::ConnectionClosed => {
151 break;
152 }
153 e => {
154 warn!("Error during status: {e}");
155 return Err(e.into());
156 }
157 },
158 }
159 }
160 }
161 ClientIntention::Login => {
164 let mut conn = conn.login();
165 loop {
166 match conn.read().await {
167 Ok(p) => {
168 if let ServerboundLoginPacket::Hello(hello) = p {
169 info!(
170 "Player \'{}\' from {} logging in with uuid: {}",
171 hello.name,
172 ip.ip(),
173 hello.profile_id.to_string()
174 );
175
176 tokio::spawn(proxy_conn(conn).map(|r| {
177 if let Err(e) = r {
178 error!("Failed to proxy: {e}");
179 }
180 }));
181
182 break;
183 }
184 }
185 Err(e) => match *e {
186 ReadPacketError::ConnectionClosed => {
187 break;
188 }
189 e => {
190 warn!("Error during login: {e}");
191 return Err(e.into());
192 }
193 },
194 }
195 }
196 }
197 ClientIntention::Transfer => {
198 warn!("Client attempted to join via transfer")
199 }
200 }
201
202 Ok(())
203}
204
205async fn proxy_conn(
206 mut client_conn: Connection<ServerboundLoginPacket, ClientboundLoginPacket>,
207) -> Result<(), Box<dyn Error>> {
208 let parsed_target_addr = ServerAddr::try_from(TARGET_ADDR).unwrap();
210 let resolved_target_addr = resolve_address(&parsed_target_addr).await?;
211
212 let mut server_conn = Connection::new(&resolved_target_addr).await?;
213
214 let account = if ACCOUNT.contains('@') {
215 Account::microsoft(ACCOUNT).await?
216 } else {
217 Account::offline(ACCOUNT)
218 };
219 println!("got account: {:?}", account);
220
221 server_conn
222 .write(ServerboundIntention {
223 protocol_version: PROTOCOL_VERSION,
224 hostname: parsed_target_addr.host,
225 port: parsed_target_addr.port,
226 intention: ClientIntention::Login,
227 })
228 .await?;
229 let mut server_conn = server_conn.login();
230
231 server_conn
233 .write(ServerboundHello {
234 name: account.username().to_owned(),
235 profile_id: account.uuid(),
236 })
237 .await?;
238
239 let (server_conn, login_finished) = loop {
240 let packet = server_conn.read().await?;
241
242 println!("got packet: {:?}", packet);
243
244 match packet {
245 ClientboundLoginPacket::Hello(p) => {
246 debug!("Got encryption request");
247 let e = azalea_crypto::encrypt(&p.public_key, &p.challenge).unwrap();
248
249 if let Some(access_token) = account.access_token() {
250 let mut attempts: usize = 1;
253
254 while let Err(e) = {
255 server_conn
256 .authenticate(&access_token, &account.uuid(), e.secret_key, &p, None)
257 .await
258 } {
259 if attempts >= 2 {
260 return Err(e.into());
263 }
264 if matches!(
265 e,
266 ClientSessionServerError::InvalidSession
267 | ClientSessionServerError::ForbiddenOperation
268 ) {
269 account.refresh().await?;
272 } else {
273 return Err(e.into());
274 }
275 attempts += 1;
276 }
277 }
278
279 server_conn
280 .write(ServerboundKey {
281 key_bytes: e.encrypted_public_key,
282 encrypted_challenge: e.encrypted_challenge,
283 })
284 .await?;
285
286 server_conn.set_encryption_key(e.secret_key);
287 }
288 ClientboundLoginPacket::LoginCompression(p) => {
289 debug!("Got compression request {:?}", p.compression_threshold);
290 server_conn.set_compression_threshold(p.compression_threshold);
291 }
292 ClientboundLoginPacket::LoginFinished(p) => {
293 debug!(
294 "Got profile {:?}. handshake is finished and we're now switching to the configuration state",
295 p.game_profile
296 );
297 break (server_conn.config(), p);
299 }
300 ClientboundLoginPacket::LoginDisconnect(p) => {
301 error!("Got disconnect {p:?}");
302 return Err("Disconnected".into());
303 }
304 ClientboundLoginPacket::CustomQuery(p) => {
305 debug!("Got custom query {:?}", p);
306 }
309 ClientboundLoginPacket::CookieRequest(p) => {
310 debug!("Got cookie request {:?}", p);
311
312 server_conn
313 .write(packets::login::ServerboundCookieResponse {
314 key: p.key,
315 payload: None,
317 })
318 .await?;
319 }
320 }
321 };
322
323 println!("got the login_finished: {:?}", login_finished);
325 client_conn.write(login_finished).await?;
326 let client_conn = client_conn.config();
327
328 info!("started direct bridging");
329
330 let listen_raw_reader = client_conn.reader.raw;
332 let listen_raw_writer = client_conn.writer.raw;
333
334 let target_raw_reader = server_conn.reader.raw;
335 let target_raw_writer = server_conn.writer.raw;
336
337 let packet_logs_txt = Arc::new(tokio::sync::Mutex::new(
338 File::create("combined.txt").await.unwrap(),
339 ));
340
341 let packet_logs_txt_clone = packet_logs_txt.clone();
342 let copy_listen_to_target = tokio::spawn(async move {
343 let mut listen_raw_reader = listen_raw_reader;
344 let mut target_raw_writer = target_raw_writer;
345
346 let packet_logs_txt = packet_logs_txt_clone;
347
348 let mut serverbound_parsed_txt = File::create("serverbound.txt").await.unwrap();
349
350 loop {
351 let packet = match listen_raw_reader.read().await {
352 Ok(p) => p,
353 Err(e) => {
354 error!("Error reading packet from listen: {e}");
355 return;
356 }
357 };
358
359 let decoded_packet = azalea_protocol::read::deserialize_packet::<ServerboundGamePacket>(
361 &mut Cursor::new(&packet),
362 );
363
364 if let Ok(decoded_packet) = decoded_packet {
365 let timestamp = chrono::Utc::now();
366 let _ = serverbound_parsed_txt
367 .write_all(format!("{timestamp} {:?}\n", decoded_packet).as_bytes())
368 .await;
369 let _ = packet_logs_txt
370 .lock()
371 .await
372 .write_all(format!("{timestamp} <- {:?}\n", decoded_packet).as_bytes())
373 .await;
374 }
375
376 match target_raw_writer.write(&packet).await {
377 Ok(_) => {}
378 Err(e) => {
379 error!("Error writing packet to target: {e}");
380 return;
381 }
382 }
383 }
384 });
385
386 let (clientbound_tx, mut clientbound_rx) = tokio::sync::mpsc::unbounded_channel::<Box<[u8]>>();
389 let copy_clientbound_to_file = tokio::spawn(async move {
390 let mut clientbound_parsed_txt = File::create("clientbound.txt").await.unwrap();
391
392 loop {
393 let Some(packet) = clientbound_rx.recv().await else {
394 return;
395 };
396
397 let decoded_packet = azalea_protocol::read::deserialize_packet::<ClientboundGamePacket>(
399 &mut Cursor::new(&packet),
400 );
401
402 if let Ok(decoded_packet) = decoded_packet {
403 let timestamp = chrono::Utc::now();
404 let _ = clientbound_parsed_txt
405 .write_all(format!("{timestamp} {decoded_packet:?}\n").as_bytes())
406 .await;
407 let _ = packet_logs_txt
408 .lock()
409 .await
410 .write_all(format!("{timestamp} -> {decoded_packet:?}\n").as_bytes())
411 .await;
412 }
413 }
414 });
415
416 let copy_remote_to_local = tokio::spawn(async move {
417 let mut target_raw_reader = target_raw_reader;
418 let mut listen_raw_writer = listen_raw_writer;
419
420 loop {
421 let packet = match target_raw_reader.read().await {
422 Ok(p) => p,
423 Err(e) => {
424 error!("Error reading packet from target: {e}");
425 return;
426 }
427 };
428
429 clientbound_tx.send(packet.clone()).unwrap();
430
431 match listen_raw_writer.write(&packet).await {
432 Ok(_) => {}
433 Err(e) => {
434 error!("Error writing packet to listen: {e}");
435 return;
436 }
437 }
438 }
439 });
440
441 tokio::try_join!(
442 copy_listen_to_target,
443 copy_remote_to_local,
444 copy_clientbound_to_file
445 )?;
446
447 Ok(())
448}
449
450#[derive(Debug)]
451enum Account {
452 Microsoft {
453 cache_key: String,
454 username: String,
455 uuid: Uuid,
456 access_token: Mutex<String>,
457 },
459 Offline {
460 username: String,
461 },
462}
463impl Account {
464 async fn microsoft(cache_key: &str) -> Result<Self, azalea_auth::AuthError> {
465 let minecraft_dir = minecraft_folder_path::minecraft_dir().unwrap_or_else(|| {
466 panic!(
467 "No {} environment variable found",
468 minecraft_folder_path::home_env_var()
469 )
470 });
471 let cache_file = minecraft_dir.join("azalea-auth.json");
472
473 let auth_result = azalea_auth::auth(
474 cache_key,
475 AuthOpts {
476 cache_file: Some(cache_file),
477 ..Default::default()
478 },
479 )
480 .await?;
481
482 Ok(Self::Microsoft {
483 cache_key: cache_key.to_owned(),
484 username: auth_result.profile.name,
485 uuid: auth_result.profile.id,
486 access_token: Mutex::new(auth_result.access_token),
487 })
489 }
490 fn offline(username: &str) -> Self {
491 Self::Offline {
492 username: username.to_owned(),
493 }
494 }
495
496 async fn refresh(&self) -> Result<(), azalea_auth::AuthError> {
497 match self {
498 Account::Microsoft {
499 cache_key,
500 access_token,
501 ..
502 } => {
503 let acc = Account::microsoft(cache_key).await?;
504 *access_token.lock() = acc.access_token().unwrap();
505 }
506 Account::Offline { .. } => {}
507 }
508
509 Ok(())
510 }
511
512 fn username(&self) -> &str {
513 match self {
514 Account::Microsoft { username, .. } => username,
515 Account::Offline { username } => username,
516 }
517 }
518 fn uuid(&self) -> Uuid {
519 match self {
520 Account::Microsoft { uuid, .. } => *uuid,
521 Account::Offline { username } => azalea_crypto::offline::generate_uuid(username),
522 }
523 }
524 fn access_token(&self) -> Option<String> {
525 match self {
526 Account::Microsoft { access_token, .. } => Some(access_token.lock().to_owned()),
527 Account::Offline { .. } => None,
528 }
529 }
530}