From 9ee5e71bb13e596248fde000d8717c86276b0ce1 Mon Sep 17 00:00:00 2001 From: EightFactorial Date: Sat, 21 Jan 2023 20:14:23 -0800 Subject: [PATCH] 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 --- Cargo.lock | 172 ++++++++++++++- azalea-auth/Cargo.toml | 8 +- azalea-auth/src/auth.rs | 4 +- azalea-auth/src/game_profile.rs | 98 ++++++++- azalea-auth/src/sessionserver.rs | 96 +++++++-- azalea-client/src/account.rs | 4 +- azalea-client/src/chat.rs | 4 +- azalea-client/src/client.rs | 8 +- azalea-protocol/Cargo.toml | 6 + azalea-protocol/examples/handshake_proxy.rs | 225 ++++++++++++++++++++ azalea-protocol/src/connect.rs | 69 +++++- azalea/Cargo.toml | 4 +- 12 files changed, 656 insertions(+), 42 deletions(-) create mode 100644 azalea-protocol/examples/handshake_proxy.rs diff --git a/Cargo.lock b/Cargo.lock index 9b655ab0..c47a3e25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,7 +97,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -121,7 +121,7 @@ dependencies = [ "azalea-physics", "azalea-protocol", "azalea-world", - "env_logger", + "env_logger 0.10.0", "futures", "log", "nohash-hasher", @@ -140,7 +140,7 @@ dependencies = [ "azalea-buf", "azalea-crypto", "chrono", - "env_logger", + "env_logger 0.9.3", "log", "num-bigint", "reqwest", @@ -300,6 +300,7 @@ dependencies = [ name = "azalea-protocol" version = "0.5.0" dependencies = [ + "anyhow", "async-compression", "async-recursion", "azalea-auth", @@ -319,11 +320,14 @@ dependencies = [ "futures", "futures-util", "log", + "once_cell", "serde", "serde_json", "thiserror", "tokio", "tokio-util", + "tracing", + "tracing-subscriber", "trust-dns-resolver", "uuid", ] @@ -415,7 +419,7 @@ dependencies = [ "anyhow", "azalea", "azalea-protocol", - "env_logger", + "env_logger 0.9.3", "parking_lot", "rand", "tokio", @@ -711,6 +715,40 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fastrand" version = "1.8.0" @@ -928,6 +966,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "http" version = "0.2.8" @@ -1054,12 +1101,34 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-lifetimes" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" +dependencies = [ + "libc", + "windows-sys 0.42.0", +] + [[package]] name = "ipnet" version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" +[[package]] +name = "is-terminal" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" +dependencies = [ + "hermit-abi 0.2.6", + "io-lifetimes", + "rustix", + "windows-sys 0.42.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1108,6 +1177,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + [[package]] name = "lock_api" version = "0.4.9" @@ -1217,6 +1292,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num" version = "0.4.0" @@ -1310,7 +1395,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", ] @@ -1325,9 +1410,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "oorandom" @@ -1380,6 +1465,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1650,6 +1741,20 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "rustix" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.42.0", +] + [[package]] name = "ryu" version = "1.0.11" @@ -1768,6 +1873,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -1888,6 +2002,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -2004,6 +2127,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -2110,6 +2259,15 @@ name = "uuid" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +dependencies = [ + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "vcpkg" diff --git a/azalea-auth/Cargo.toml b/azalea-auth/Cargo.toml index deffe62d..5de305d5 100644 --- a/azalea-auth/Cargo.toml +++ b/azalea-auth/Cargo.toml @@ -9,8 +9,8 @@ version = "0.5.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -azalea-buf = {path = "../azalea-buf", version = "^0.5.0" } -azalea-crypto = {path = "../azalea-crypto", version = "^0.5.0" } +azalea-buf = {path = "../azalea-buf", version = "^0.5.0"} +azalea-crypto = {path = "../azalea-crypto", version = "^0.5.0"} chrono = {version = "0.4.22", default-features = false} log = "0.4.17" num-bigint = "0.4.3" @@ -19,8 +19,8 @@ serde = {version = "1.0.145", features = ["derive"]} serde_json = "1.0.86" thiserror = "1.0.37" tokio = {version = "1.23.1", features = ["fs"]} -uuid = "^1.1.2" +uuid = {version = "^1.1.2", features = ["serde"]} [dev-dependencies] -env_logger = "0.9.1" +env_logger = "0.9.3" tokio = {version = "1.23.1", features = ["full"]} diff --git a/azalea-auth/src/auth.rs b/azalea-auth/src/auth.rs index bed15d74..e668a947 100755 --- a/azalea-auth/src/auth.rs +++ b/azalea-auth/src/auth.rs @@ -10,6 +10,7 @@ use std::{ time::{Instant, SystemTime, UNIX_EPOCH}, }; use thiserror::Error; +use uuid::Uuid; #[derive(Default)] pub struct AuthOpts { @@ -209,8 +210,7 @@ pub struct GameOwnershipItem { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ProfileResponse { - // todo: make the id a uuid - pub id: String, + pub id: Uuid, pub name: String, pub skins: Vec, pub capes: Vec, diff --git a/azalea-auth/src/game_profile.rs b/azalea-auth/src/game_profile.rs index 39cd29e7..bdd6cda5 100755 --- a/azalea-auth/src/game_profile.rs +++ b/azalea-auth/src/game_profile.rs @@ -1,8 +1,9 @@ use azalea_buf::McBuf; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use uuid::Uuid; -#[derive(McBuf, Debug, Clone, Default)] +#[derive(McBuf, Debug, Clone, Default, Eq, PartialEq)] pub struct GameProfile { pub uuid: Uuid, pub name: String, @@ -19,8 +20,101 @@ impl GameProfile { } } -#[derive(McBuf, Debug, Clone)] +impl From for GameProfile { + fn from(value: SerializableGameProfile) -> Self { + let mut properties = HashMap::new(); + for value in value.properties { + properties.insert( + value.name, + ProfilePropertyValue { + value: value.value, + signature: value.signature, + }, + ); + } + Self { + uuid: value.id, + name: value.name, + properties, + } + } +} + +#[derive(McBuf, Debug, Clone, Eq, PartialEq)] pub struct ProfilePropertyValue { pub value: String, pub signature: Option, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SerializableGameProfile { + pub id: Uuid, + pub name: String, + pub properties: Vec, +} + +impl From for SerializableGameProfile { + fn from(value: GameProfile) -> Self { + let mut properties = Vec::new(); + for (key, value) in value.properties { + properties.push(SerializableProfilePropertyValue { + name: key, + value: value.value, + signature: value.signature, + }); + } + Self { + id: value.uuid, + name: value.name, + properties, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SerializableProfilePropertyValue { + pub name: String, + pub value: String, + pub signature: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_game_profile() { + let json = r#"{ + "id": "f1a2b3c4-d5e6-f7a8-b9c0-d1e2f3a4b5c6", + "name": "Notch", + "properties": [ + { + "name": "qwer", + "value": "asdf", + "signature": "zxcv" + } + ] + }"#; + let profile = GameProfile::from( + serde_json::from_str::(json).unwrap(), + ); + assert_eq!( + profile, + GameProfile { + uuid: Uuid::parse_str("f1a2b3c4-d5e6-f7a8-b9c0-d1e2f3a4b5c6").unwrap(), + name: "Notch".to_string(), + properties: { + let mut map = HashMap::new(); + map.insert( + "asdf".to_string(), + ProfilePropertyValue { + value: "qwer".to_string(), + signature: Some("zxcv".to_string()), + }, + ); + map + }, + } + ); + } +} diff --git a/azalea-auth/src/sessionserver.rs b/azalea-auth/src/sessionserver.rs index 685cfa25..502ae098 100755 --- a/azalea-auth/src/sessionserver.rs +++ b/azalea-auth/src/sessionserver.rs @@ -1,11 +1,15 @@ //! Tell Mojang you're joining a multiplayer server. +use log::debug; +use reqwest::StatusCode; use serde::Deserialize; use serde_json::json; use thiserror::Error; use uuid::Uuid; +use crate::game_profile::{GameProfile, SerializableGameProfile}; + #[derive(Debug, Error)] -pub enum SessionServerError { +pub enum ClientSessionServerError { #[error("Error sending HTTP request to sessionserver: {0}")] HttpError(#[from] reqwest::Error), #[error("Multiplayer is not enabled for this account")] @@ -24,6 +28,18 @@ pub enum SessionServerError { UnexpectedResponse { status_code: u16, body: String }, } +#[derive(Debug, Error)] +pub enum ServerSessionServerError { + #[error("Error sending HTTP request to sessionserver: {0}")] + HttpError(#[from] reqwest::Error), + #[error("Invalid or expired session")] + InvalidSession, + #[error("Unexpected response from sessionserver (status code {status_code}): {body}")] + UnexpectedResponse { status_code: u16, body: String }, + #[error("Unknown sessionserver error: {0}")] + Unknown(String), +} + #[derive(Deserialize)] pub struct ForbiddenError { pub error: String, @@ -39,7 +55,7 @@ pub async fn join( private_key: &[u8], uuid: &Uuid, server_id: &str, -) -> Result<(), SessionServerError> { +) -> Result<(), ClientSessionServerError> { let client = reqwest::Client::new(); let server_hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data( @@ -63,28 +79,82 @@ pub async fn join( .await?; match res.status() { - reqwest::StatusCode::NO_CONTENT => Ok(()), - reqwest::StatusCode::FORBIDDEN => { + StatusCode::NO_CONTENT => Ok(()), + StatusCode::FORBIDDEN => { let forbidden = res.json::().await?; match forbidden.error.as_str() { - "InsufficientPrivilegesException" => Err(SessionServerError::MultiplayerDisabled), - "UserBannedException" => Err(SessionServerError::Banned), - "AuthenticationUnavailableException" => { - Err(SessionServerError::AuthServersUnreachable) + "InsufficientPrivilegesException" => { + Err(ClientSessionServerError::MultiplayerDisabled) } - "InvalidCredentialsException" => Err(SessionServerError::InvalidSession), - "ForbiddenOperationException" => Err(SessionServerError::ForbiddenOperation), - _ => Err(SessionServerError::Unknown(forbidden.error)), + "UserBannedException" => Err(ClientSessionServerError::Banned), + "AuthenticationUnavailableException" => { + Err(ClientSessionServerError::AuthServersUnreachable) + } + "InvalidCredentialsException" => Err(ClientSessionServerError::InvalidSession), + "ForbiddenOperationException" => Err(ClientSessionServerError::ForbiddenOperation), + _ => Err(ClientSessionServerError::Unknown(forbidden.error)), } } status_code => { // log the headers - log::debug!("Error headers: {:#?}", res.headers()); + debug!("Error headers: {:#?}", res.headers()); let body = res.text().await?; - Err(SessionServerError::UnexpectedResponse { + 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 +/// 'prevent-proxy-connections' in server.properties +pub async fn serverside_auth( + username: &str, + public_key: &[u8], + private_key: &[u8; 16], + ip: Option<&str>, +) -> Result { + let hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data( + "".as_bytes(), + public_key, + private_key, + )); + + let url = reqwest::Url::parse_with_params( + "https://sessionserver.mojang.com/session/minecraft/hasJoined", + if let Some(ip) = ip { + vec![("username", username), ("serverId", &hash), ("ip", ip)] + } else { + vec![("username", username), ("serverId", &hash)] + }, + ) + .expect("URL should always be valid"); + + let res = reqwest::get(url).await?; + + match res.status() { + StatusCode::OK => {} + StatusCode::NO_CONTENT => { + return Err(ServerSessionServerError::InvalidSession); + } + StatusCode::FORBIDDEN => { + return Err(ServerSessionServerError::Unknown( + res.json::().await?.error, + )) + } + status_code => { + // log the headers + debug!("Error headers: {:#?}", res.headers()); + let body = res.text().await?; + return Err(ServerSessionServerError::UnexpectedResponse { + status_code: status_code.as_u16(), + body, + }); + } + }; + + Ok(res.json::().await?.into()) +} diff --git a/azalea-client/src/account.rs b/azalea-client/src/account.rs index d1c20cc8..79feb1a7 100755 --- a/azalea-client/src/account.rs +++ b/azalea-client/src/account.rs @@ -31,7 +31,7 @@ pub struct Account { /// 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, + pub uuid: Option, /// The parameters (i.e. email) that were passed for creating this /// [`Account`]. This is used to for automatic reauthentication when we get @@ -85,7 +85,7 @@ impl Account { Ok(Self { username: auth_result.profile.name, access_token: Some(Arc::new(Mutex::new(auth_result.access_token))), - uuid: Some(Uuid::parse_str(&auth_result.profile.id).expect("Invalid UUID")), + uuid: Some(auth_result.profile.id), auth_opts: AuthOpts::Microsoft { email: email.to_string(), }, diff --git a/azalea-client/src/chat.rs b/azalea-client/src/chat.rs index 44ad8a71..de71f586 100755 --- a/azalea-client/src/chat.rs +++ b/azalea-client/src/chat.rs @@ -77,8 +77,8 @@ impl ChatPacket { /// player-sent chat message, this will be None. pub fn uuid(&self) -> Option { match self { - ChatPacket::System(_) => return None, - ChatPacket::Player(m) => return Some(m.sender), + ChatPacket::System(_) => None, + ChatPacket::Player(m) => Some(m.sender), } } diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index dbb1b71e..125facda 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -1,6 +1,6 @@ pub use crate::chat::ChatPacket; use crate::{movement::WalkDirection, plugins::PluginStates, Account, PlayerInfo}; -use azalea_auth::{game_profile::GameProfile, sessionserver::SessionServerError}; +use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError}; use azalea_chat::Component; use azalea_core::{ChunkPos, ResourceLocation, Vec3}; use azalea_protocol::{ @@ -141,7 +141,7 @@ pub enum JoinError { #[error("{0}")] Io(#[from] io::Error), #[error("{0}")] - SessionServer(#[from] azalea_auth::sessionserver::SessionServerError), + SessionServer(#[from] azalea_auth::sessionserver::ClientSessionServerError), #[error("The given address could not be parsed into a ServerAddress")] InvalidAddress, #[error("Couldn't refresh access token: {0}")] @@ -315,8 +315,8 @@ impl Client { } if matches!( e, - SessionServerError::InvalidSession - | SessionServerError::ForbiddenOperation + ClientSessionServerError::InvalidSession + | ClientSessionServerError::ForbiddenOperation ) { // uh oh, we got an invalid session and have // to reauthenticate now 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," +static PROXY_FAVICON: Lazy> = Lazy::new(|| None); + +static PROXY_VERSION: Lazy = 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 = Some(false); +const PROXY_SECURE_CHAT: Option = 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 = + 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> { + 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 = + 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 { 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 { } } +impl Connection { + /// Change our state from handshake to login. This is the state that is used + /// for logging in. + pub fn login(self) -> Connection { + 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 { + Connection::from(self) + } +} + +impl Connection { + /// 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 { + 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 { + 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 From for T" so we do this instead impl Connection @@ -390,4 +448,9 @@ where }, } } + + /// Convert from a `Connection` into a `TcpStream`. Useful for servers. + pub fn unwrap(self) -> Result { + self.reader.read_stream.reunite(self.writer.write_stream) + } } diff --git a/azalea/Cargo.toml b/azalea/Cargo.toml index 7c82a00c..d3fba18f 100644 --- a/azalea/Cargo.toml +++ b/azalea/Cargo.toml @@ -32,6 +32,4 @@ tokio = "^1.23.1" uuid = "1.2.2" [dev-dependencies] -anyhow = "^1.0.65" -env_logger = "^0.9.1" -tokio = "^1.23.1" +env_logger = "^0.10.0"