use std::{collections::HashMap, net::IpAddr}; use base64::{engine::general_purpose, Engine}; use num_bigint::BigInt; use pumpkin_config::{auth::TextureConfig, ADVANCED_CONFIG}; use pumpkin_core::ProfileAction; use pumpkin_protocol::Property; use reqwest::{StatusCode, Url}; use serde::Deserialize; use thiserror::Error; use uuid::Uuid; use crate::server::Server; #[derive(Deserialize, Clone, Debug)] #[expect(non_snake_case)] #[expect(dead_code)] pub struct ProfileTextures { timestamp: i64, profileId: Uuid, profileName: String, signatureRequired: bool, textures: HashMap, } #[derive(Deserialize, Clone, Debug)] #[expect(dead_code)] pub struct Texture { url: String, metadata: Option>, } #[derive(Deserialize, Clone, Debug)] pub struct GameProfile { pub id: Uuid, pub name: String, pub properties: Vec, #[serde(rename = "profileActions")] pub profile_actions: Option>, } pub async fn authenticate( username: &str, server_hash: &str, ip: &IpAddr, server: &mut Server, ) -> Result { assert!(ADVANCED_CONFIG.authentication.enabled); assert!(server.auth_client.is_some()); let address = if ADVANCED_CONFIG.authentication.prevent_proxy_connections { format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}") } else { format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}") }; let auth_client = server .auth_client .as_ref() .ok_or(AuthError::MissingAuthClient)?; let response = auth_client .get(address) .send() .await .map_err(|_| AuthError::FailedResponse)?; match response.status() { StatusCode::OK => {} StatusCode::NO_CONTENT => Err(AuthError::UnverifiedUsername)?, other => Err(AuthError::UnknownStatusCode(other.as_str().to_string()))?, } let profile: GameProfile = response.json().await.map_err(|_| AuthError::FailedParse)?; Ok(profile) } pub fn unpack_textures(property: Property, config: &TextureConfig) { // TODO: no unwrap let from64 = general_purpose::STANDARD.decode(property.value).unwrap(); let textures: ProfileTextures = serde_json::from_slice(&from64).unwrap(); for texture in textures.textures { is_texture_url_valid(Url::parse(&texture.1.url).unwrap(), config); } } pub fn auth_digest(bytes: &[u8]) -> String { BigInt::from_signed_bytes_be(bytes).to_str_radix(16) } pub fn is_texture_url_valid(url: Url, config: &TextureConfig) -> bool { let scheme = url.scheme(); if !config.allowed_url_schemes.contains(&scheme.to_string()) { return false; } let domain = url.domain().unwrap_or(""); if !config.allowed_url_domains.contains(&domain.to_string()) { return false; } true } #[derive(Error, Debug)] pub enum AuthError { #[error("Missing auth client")] MissingAuthClient, #[error("Authentication servers are down")] FailedResponse, #[error("Failed to verify username")] UnverifiedUsername, #[error("Failed to parse JSON into Game Profile")] FailedParse, #[error("Unknown Status Code")] UnknownStatusCode(String), }