aboutsummaryrefslogtreecommitdiff
path: root/azalea-auth/src
diff options
context:
space:
mode:
authorEightFactorial <murphkev000@gmail.com>2023-01-21 20:14:23 -0800
committerGitHub <noreply@github.com>2023-01-21 22:14:23 -0600
commit9ee5e71bb13e596248fde000d8717c86276b0ce1 (patch)
treebd6363af53bca9bbd3dede1c7ee59615b94eb107 /azalea-auth/src
parent1059afa6fcf8b2776fd25dac07ed2e76ab48bed3 (diff)
downloadazalea-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-xazalea-auth/src/auth.rs4
-rwxr-xr-xazalea-auth/src/game_profile.rs98
-rwxr-xr-xazalea-auth/src/sessionserver.rs94
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())
+}