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-auth/src | |
| 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-auth/src')
| -rwxr-xr-x | azalea-auth/src/auth.rs | 4 | ||||
| -rwxr-xr-x | azalea-auth/src/game_profile.rs | 98 | ||||
| -rwxr-xr-x | azalea-auth/src/sessionserver.rs | 94 |
3 files changed, 180 insertions, 16 deletions
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<serde_json::Value>, pub capes: Vec<serde_json::Value>, 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<SerializableGameProfile> 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<String>, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SerializableGameProfile { + pub id: Uuid, + pub name: String, + pub properties: Vec<SerializableProfilePropertyValue>, +} + +impl From<GameProfile> 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<String>, +} + +#[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::<SerializableProfilePropertyValue>(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::<ForbiddenError>().await?; match forbidden.error.as_str() { - "InsufficientPrivilegesException" => Err(SessionServerError::MultiplayerDisabled), - "UserBannedException" => Err(SessionServerError::Banned), + "InsufficientPrivilegesException" => { + Err(ClientSessionServerError::MultiplayerDisabled) + } + "UserBannedException" => Err(ClientSessionServerError::Banned), "AuthenticationUnavailableException" => { - Err(SessionServerError::AuthServersUnreachable) + Err(ClientSessionServerError::AuthServersUnreachable) } - "InvalidCredentialsException" => Err(SessionServerError::InvalidSession), - "ForbiddenOperationException" => Err(SessionServerError::ForbiddenOperation), - _ => Err(SessionServerError::Unknown(forbidden.error)), + "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<GameProfile, ServerSessionServerError> { + 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::<ForbiddenError>().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::<SerializableGameProfile>().await?.into()) +} |
