deepslate 0.3.1

A high-performance Minecraft server proxy written in Rust.
Documentation
//! Mojang session server authentication.

use std::time::Instant;

use deepslate_protocol::types::GameProfile;
use serde::Deserialize;
use tracing::debug;

use crate::metrics;

/// The Mojang session server `hasJoined` endpoint URL.
const HAS_JOINED_URL: &str = "https://sessionserver.mojang.com/session/minecraft/hasJoined";

/// Response from the Mojang `hasJoined` endpoint.
#[derive(Debug, Deserialize)]
struct MojangProfile {
    id: String,
    name: String,
    #[serde(default)]
    properties: Vec<MojangProperty>,
}

/// A property in the Mojang profile response.
#[derive(Debug, Deserialize)]
struct MojangProperty {
    name: String,
    value: String,
    signature: Option<String>,
}

/// Errors that can occur during Mojang authentication.
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
    /// HTTP request failed.
    #[error("HTTP request failed: {0}")]
    Http(#[from] reqwest::Error),

    /// Player is not authenticated (204 response).
    #[error("player is not authenticated with Mojang")]
    NotAuthenticated,

    /// Unexpected response from Mojang.
    #[error("unexpected response from session server: HTTP {0}")]
    UnexpectedStatus(u16),

    /// Failed to parse the UUID from the response.
    #[error("invalid UUID in response: {0}")]
    InvalidUuid(String),
}

/// Verify a player's session with the Mojang session server.
///
/// This performs the `hasJoined` check that confirms the player has authenticated
/// with Mojang and joined with the expected server ID hash.
///
/// # Errors
///
/// Returns `AuthError` if the request fails, the player is not authenticated,
/// or the response is malformed.
pub async fn verify_player(
    client: &reqwest::Client,
    username: &str,
    server_id: &str,
) -> Result<GameProfile, AuthError> {
    debug!(username, "verifying player with Mojang session server");

    let request_start = Instant::now();
    let response = client
        .get(HAS_JOINED_URL)
        .query(&[("username", username), ("serverId", server_id)])
        .send()
        .await?;
    metrics::histogram!(
        "auth_mojang_duration_seconds",
        request_start.elapsed().as_secs_f64()
    );

    match response.status().as_u16() {
        200 => {}
        204 => return Err(AuthError::NotAuthenticated),
        status => return Err(AuthError::UnexpectedStatus(status)),
    }

    let profile: MojangProfile = response.json().await?;

    // Mojang returns UUID without dashes — parse it
    let uuid =
        uuid::Uuid::parse_str(&profile.id).map_err(|e| AuthError::InvalidUuid(e.to_string()))?;

    Ok(GameProfile {
        id: uuid,
        name: profile.name,
        properties: profile
            .properties
            .into_iter()
            .map(|p| deepslate_protocol::types::ProfileProperty {
                name: p.name,
                value: p.value,
                signature: p.signature,
            })
            .collect(),
    })
}