diff options
| author | EightFactorial <murphkev000@gmail.com> | 2023-01-21 20:14:23 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-01-21 22:14:23 -0600 |
| commit | 9ee5e71bb13e596248fde000d8717c86276b0ce1 (patch) | |
| tree | bd6363af53bca9bbd3dede1c7ee59615b94eb107 /azalea-protocol | |
| parent | 1059afa6fcf8b2776fd25dac07ed2e76ab48bed3 (diff) | |
| download | azalea-drasl-9ee5e71bb13e596248fde000d8717c86276b0ce1.tar.xz | |
Server functions and proxy example (#59)
* A couple useful things for servers
* Add proxy example
* Use Uuid's serde feature
* Add const options to proxy example
* Example crates go in dev-dependencies
* Warn instead of error
* Log address on login
* Requested changes
* add a test for deserializing game profile + random small changes
Co-authored-by: mat <github@matdoes.dev>
Diffstat (limited to 'azalea-protocol')
| -rw-r--r-- | azalea-protocol/Cargo.toml | 6 | ||||
| -rw-r--r-- | azalea-protocol/examples/handshake_proxy.rs | 225 | ||||
| -rwxr-xr-x | azalea-protocol/src/connect.rs | 69 |
3 files changed, 297 insertions, 3 deletions
diff --git a/azalea-protocol/Cargo.toml b/azalea-protocol/Cargo.toml index 6da763bd..9066cdc3 100644 --- a/azalea-protocol/Cargo.toml +++ b/azalea-protocol/Cargo.toml @@ -40,3 +40,9 @@ uuid = "1.1.2" connecting = [] default = ["packets"] packets = ["connecting", "dep:async-compression", "dep:azalea-core"] + +[dev-dependencies] +anyhow = "^1.0.65" +tracing = "^0.1.36" +tracing-subscriber = "^0.3.15" +once_cell = "1.17.0"
\ No newline at end of file diff --git a/azalea-protocol/examples/handshake_proxy.rs b/azalea-protocol/examples/handshake_proxy.rs new file mode 100644 index 00000000..5a4b4b6a --- /dev/null +++ b/azalea-protocol/examples/handshake_proxy.rs @@ -0,0 +1,225 @@ +//! A "simple" server that gets login information and proxies connections. +//! After login all connections are encrypted and Azalea cannot read them. + +use azalea_protocol::{ + connect::Connection, + packets::{ + handshake::{ + client_intention_packet::ClientIntentionPacket, ClientboundHandshakePacket, + ServerboundHandshakePacket, + }, + login::{serverbound_hello_packet::ServerboundHelloPacket, ServerboundLoginPacket}, + status::{ + clientbound_pong_response_packet::ClientboundPongResponsePacket, + clientbound_status_response_packet::{ + ClientboundStatusResponsePacket, Players, Version, + }, + ServerboundStatusPacket, + }, + ConnectionProtocol, PROTOCOL_VERSION, + }, + read::ReadPacketError, +}; +use futures::FutureExt; +use log::{error, info, warn}; +use once_cell::sync::Lazy; +use std::error::Error; +use tokio::{ + io::{self, AsyncWriteExt}, + net::{TcpListener, TcpStream}, +}; +use tracing::Level; + +const LISTEN_ADDR: &str = "127.0.0.1:25566"; +const PROXY_ADDR: &str = "173.205.80.60:25565"; + +const PROXY_DESC: &str = "An Azalea Minecraft Proxy"; + +// String must be formatted like "data:image/png;base64,<data>" +static PROXY_FAVICON: Lazy<Option<String>> = Lazy::new(|| None); + +static PROXY_VERSION: Lazy<Version> = Lazy::new(|| Version { + name: "1.19.3".to_string(), + protocol: PROTOCOL_VERSION as i32, +}); + +const PROXY_PLAYERS: Players = Players { + max: 1, + online: 0, + sample: Vec::new(), +}; + +const PROXY_PREVIEWS_CHAT: Option<bool> = Some(false); +const PROXY_SECURE_CHAT: Option<bool> = Some(false); + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt().with_max_level(Level::INFO).init(); + + // Bind to an address and port + let listener = TcpListener::bind(LISTEN_ADDR).await?; + loop { + // When a connection is made, pass it off to another thread + let (stream, _) = listener.accept().await?; + tokio::spawn(handle_connection(stream)); + } +} + +async fn handle_connection(stream: TcpStream) -> anyhow::Result<()> { + stream.set_nodelay(true)?; + let ip = stream.peer_addr()?; + let mut conn: Connection<ServerboundHandshakePacket, ClientboundHandshakePacket> = + Connection::wrap(stream); + + // The first packet sent from a client is the intent packet. + // This specifies whether the client is pinging + // the server or is going to join the game. + let intent = match conn.read().await { + Ok(packet) => match packet { + ServerboundHandshakePacket::ClientIntention(packet) => { + info!( + "New connection: {0}, Version {1}, {2:?}", + ip.ip(), + packet.protocol_version, + packet.intention + ); + packet + } + }, + Err(e) => { + let e = e.into(); + warn!("Error during intent: {e}"); + return Err(e); + } + }; + + match intent.intention { + // If the client is pinging the proxy, + // reply with the information below. + ConnectionProtocol::Status => { + let mut conn = conn.status(); + loop { + match conn.read().await { + Ok(p) => match p { + ServerboundStatusPacket::StatusRequest(_) => { + conn.write( + ClientboundStatusResponsePacket { + description: PROXY_DESC.into(), + favicon: PROXY_FAVICON.clone(), + players: PROXY_PLAYERS.clone(), + version: PROXY_VERSION.clone(), + previews_chat: PROXY_PREVIEWS_CHAT, + enforces_secure_chat: PROXY_SECURE_CHAT, + } + .get(), + ) + .await?; + } + ServerboundStatusPacket::PingRequest(p) => { + conn.write(ClientboundPongResponsePacket { time: p.time }.get()) + .await?; + break; + } + }, + Err(e) => match *e { + ReadPacketError::ConnectionClosed => { + break; + } + e => { + warn!("Error during status: {e}"); + return Err(e.into()); + } + }, + } + } + } + // If the client intends to join the proxy, + // wait for them to send the `Hello` packet to + // log their username and uuid, then forward the + // connection along to the proxy target. + ConnectionProtocol::Login => { + let mut conn = conn.login(); + loop { + match conn.read().await { + Ok(p) => { + if let ServerboundLoginPacket::Hello(hello) = p { + info!( + "Player \'{0}\' from {1} logging in with uuid: {2}", + hello.name, + ip.ip(), + if let Some(id) = hello.profile_id { + id.to_string() + } else { + "".to_string() + } + ); + + tokio::spawn(transfer(conn.unwrap()?, intent, hello).map(|r| { + if let Err(e) = r { + error!("Failed to proxy: {e}"); + } + })); + + break; + } + } + Err(e) => match *e { + ReadPacketError::ConnectionClosed => { + break; + } + e => { + warn!("Error during login: {e}"); + return Err(e.into()); + } + }, + } + } + } + _ => { + warn!("Client provided weird intent: {:?}", intent.intention); + } + } + + Ok(()) +} + +async fn transfer( + mut inbound: TcpStream, + intent: ClientIntentionPacket, + hello: ServerboundHelloPacket, +) -> Result<(), Box<dyn Error>> { + let outbound = TcpStream::connect(PROXY_ADDR).await?; + let name = hello.name.clone(); + outbound.set_nodelay(true)?; + + // Repeat the intent and hello packet + // recieved earlier to the proxy target + let mut outbound_conn: Connection<ClientboundHandshakePacket, ServerboundHandshakePacket> = + Connection::wrap(outbound); + outbound_conn.write(intent.get()).await?; + + let mut outbound_conn = outbound_conn.login(); + outbound_conn.write(hello.get()).await?; + + let mut outbound = outbound_conn.unwrap()?; + + // Split the incoming and outgoing connections in + // halves and handle each pair on separate threads. + let (mut ri, mut wi) = inbound.split(); + let (mut ro, mut wo) = outbound.split(); + + let client_to_server = async { + io::copy(&mut ri, &mut wo).await?; + wo.shutdown().await + }; + + let server_to_client = async { + io::copy(&mut ro, &mut wi).await?; + wi.shutdown().await + }; + + tokio::try_join!(client_to_server, server_to_client)?; + info!("Player \'{name}\' left the game"); + + Ok(()) +} diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs index 48401d03..149ea95d 100755 --- a/azalea-protocol/src/connect.rs +++ b/azalea-protocol/src/connect.rs @@ -8,7 +8,8 @@ use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket}; use crate::packets::ProtocolPacket; use crate::read::{read_packet, ReadPacketError}; use crate::write::write_packet; -use azalea_auth::sessionserver::SessionServerError; +use azalea_auth::game_profile::GameProfile; +use azalea_auth::sessionserver::{ClientSessionServerError, ServerSessionServerError}; use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc}; use bytes::BytesMut; use log::{error, info}; @@ -17,7 +18,7 @@ use std::marker::PhantomData; use std::net::SocketAddr; use thiserror::Error; use tokio::io::AsyncWriteExt; -use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; +use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf, ReuniteError}; use tokio::net::TcpStream; use uuid::Uuid; @@ -327,7 +328,7 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> { uuid: &Uuid, private_key: [u8; 16], packet: &ClientboundHelloPacket, - ) -> Result<(), SessionServerError> { + ) -> Result<(), ClientSessionServerError> { azalea_auth::sessionserver::join( access_token, &packet.public_key, @@ -339,6 +340,63 @@ impl Connection<ClientboundLoginPacket, ServerboundLoginPacket> { } } +impl Connection<ServerboundHandshakePacket, ClientboundHandshakePacket> { + /// Change our state from handshake to login. This is the state that is used + /// for logging in. + pub fn login(self) -> Connection<ServerboundLoginPacket, ClientboundLoginPacket> { + Connection::from(self) + } + + /// Change our state from handshake to status. This is the state that is + /// used for pinging the server. + pub fn status(self) -> Connection<ServerboundStatusPacket, ClientboundStatusPacket> { + Connection::from(self) + } +} + +impl Connection<ServerboundLoginPacket, ClientboundLoginPacket> { + /// Set our compression threshold, i.e. the maximum size that a packet is + /// allowed to be without getting compressed. If you set it to less than 0 + /// then compression gets disabled. + pub fn set_compression_threshold(&mut self, threshold: i32) { + // if you pass a threshold of less than 0, compression is disabled + if threshold >= 0 { + self.reader.compression_threshold = Some(threshold as u32); + self.writer.compression_threshold = Some(threshold as u32); + } else { + self.reader.compression_threshold = None; + self.writer.compression_threshold = None; + } + } + + /// Set the encryption key that is used to encrypt and decrypt packets. It's + /// the same for both reading and writing. + pub fn set_encryption_key(&mut self, key: [u8; 16]) { + let (enc_cipher, dec_cipher) = azalea_crypto::create_cipher(&key); + self.reader.dec_cipher = Some(dec_cipher); + self.writer.enc_cipher = Some(enc_cipher); + } + + /// Change our state from login to game. This is the state that's used when + /// the client is actually in the game. + pub fn game(self) -> Connection<ServerboundGamePacket, ClientboundGamePacket> { + Connection::from(self) + } + + /// Verify connecting clients have authenticated with Minecraft's servers. + /// This must happen after the client sends a `ServerboundLoginPacket::Key` + /// packet. + pub async fn authenticate( + &self, + username: &str, + public_key: &[u8], + private_key: &[u8; 16], + ip: Option<&str>, + ) -> Result<GameProfile, ServerSessionServerError> { + azalea_auth::sessionserver::serverside_auth(username, public_key, private_key, ip).await + } +} + // rust doesn't let us implement From because allegedly it conflicts with // `core`'s "impl<T> From<T> for T" so we do this instead impl<R1, W1> Connection<R1, W1> @@ -390,4 +448,9 @@ where }, } } + + /// Convert from a `Connection` into a `TcpStream`. Useful for servers. + pub fn unwrap(self) -> Result<TcpStream, ReuniteError> { + self.reader.read_stream.reunite(self.writer.write_stream) + } } |
