diff --git a/azalea-auth/Cargo.toml b/azalea-auth/Cargo.toml index deffe62d..b03ca0f2 100644 --- a/azalea-auth/Cargo.toml +++ b/azalea-auth/Cargo.toml @@ -19,7 +19,7 @@ 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" diff --git a/azalea-auth/src/game_profile.rs b/azalea-auth/src/game_profile.rs index 39cd29e7..e2206837 100755 --- a/azalea-auth/src/game_profile.rs +++ b/azalea-auth/src/game_profile.rs @@ -1,4 +1,5 @@ use azalea_buf::McBuf; +use serde::{de::Visitor, ser::SerializeStruct, Deserialize, Serialize}; use std::collections::HashMap; use uuid::Uuid; @@ -24,3 +25,146 @@ pub struct ProfilePropertyValue { pub value: String, pub signature: Option, } + +impl Serialize for GameProfile { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut serializer = serializer.serialize_struct("GameProfile", 3)?; + serializer.serialize_field("id", &self.uuid)?; + serializer.serialize_field("name", &self.name)?; + serializer.serialize_field( + "properties", + &SerializedProfilePropertyValue::from_map(self.properties.clone()), + )?; + serializer.end() + } +} + +impl<'de> Deserialize<'de> for GameProfile { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + enum Field { + Id, + Name, + Properties, + } + + struct GameProfileVisitor; + impl<'de> Visitor<'de> for GameProfileVisitor { + type Value = GameProfile; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("struct GameProfile") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let id = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; + let name = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + let properties = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?; + Ok(GameProfile { + uuid: id, + name, + properties: SerializedProfilePropertyValue::into_map(properties), + }) + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut id = None; + let mut name = None; + let mut properties = None; + while let Some(key) = map.next_key::()? { + match key { + Field::Id => { + if id.is_some() { + return Err(serde::de::Error::duplicate_field("id")); + } + id = Some(map.next_value()?); + } + Field::Name => { + if name.is_some() { + return Err(serde::de::Error::duplicate_field("name")); + } + name = Some(map.next_value()?); + } + Field::Properties => { + if properties.is_some() { + return Err(serde::de::Error::duplicate_field("properties")); + } + properties = Some(map.next_value()?); + } + } + } + let id = id.ok_or_else(|| serde::de::Error::missing_field("id"))?; + let name = name.ok_or_else(|| serde::de::Error::missing_field("name"))?; + let properties = SerializedProfilePropertyValue::into_map( + properties.ok_or_else(|| serde::de::Error::missing_field("properties"))?, + ); + Ok(GameProfile { + uuid: id, + name, + properties, + }) + } + } + + const FIELDS: &'static [&'static str] = &["id", "name", "properties"]; + deserializer.deserialize_struct("GameProfile", FIELDS, GameProfileVisitor) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SerializedProfilePropertyValue { + pub name: String, + pub value: String, + pub signature: Option, +} + +impl SerializedProfilePropertyValue { + pub fn from_map( + map: HashMap, + ) -> Vec { + let mut list: Vec = Vec::new(); + for (key, value) in map { + list.push(SerializedProfilePropertyValue { + name: key, + value: value.value, + signature: value.signature, + }); + } + list + } + + pub fn into_map( + list: Vec, + ) -> HashMap { + let mut map: HashMap = HashMap::new(); + for value in list { + map.insert( + value.name, + ProfilePropertyValue { + value: value.value, + signature: value.signature, + }, + ); + } + map + } +} diff --git a/azalea-auth/src/sessionserver.rs b/azalea-auth/src/sessionserver.rs index 685cfa25..543991ac 100755 --- a/azalea-auth/src/sessionserver.rs +++ b/azalea-auth/src/sessionserver.rs @@ -4,6 +4,8 @@ use serde_json::json; use thiserror::Error; use uuid::Uuid; +use crate::game_profile::GameProfile; + #[derive(Debug, Error)] pub enum SessionServerError { #[error("Error sending HTTP request to sessionserver: {0}")] @@ -88,3 +90,49 @@ pub async fn join( } } } + +/// 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: &String, + public_key: &[u8], + private_key: &[u8; 16], + ip: Option<&String>, +) -> Result { + let hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data( + "".as_bytes(), + public_key, + private_key, + )); + + let mut url = format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={hash}"); + if let Some(ip) = ip { + url = format!("{url}&ip={ip}"); + } + + let res = reqwest::get(url).await?; + match res.status() { + reqwest::StatusCode::OK => {} + reqwest::StatusCode::NO_CONTENT => { + return Err(SessionServerError::InvalidSession); + } + reqwest::StatusCode::FORBIDDEN => { + return Err(SessionServerError::Unknown( + res.json::().await?.error, + )) + } + status_code => { + // log the headers + log::debug!("Error headers: {:#?}", res.headers()); + let body = res.text().await?; + return Err(SessionServerError::UnexpectedResponse { + status_code: status_code.as_u16(), + body, + }); + } + }; + + Ok(res.json::().await?) +} diff --git a/azalea-protocol/src/connect.rs b/azalea-protocol/src/connect.rs index 48401d03..135942db 100755 --- a/azalea-protocol/src/connect.rs +++ b/azalea-protocol/src/connect.rs @@ -8,6 +8,7 @@ use crate::packets::status::{ClientboundStatusPacket, ServerboundStatusPacket}; use crate::packets::ProtocolPacket; use crate::read::{read_packet, ReadPacketError}; use crate::write::write_packet; +use azalea_auth::game_profile::GameProfile; use azalea_auth::sessionserver::SessionServerError; use azalea_crypto::{Aes128CfbDec, Aes128CfbEnc}; use bytes::BytesMut; @@ -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: &String, + public_key: &[u8], + private_key: &[u8; 16], + ip: Option<&String>, + ) -> 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