Pumpkin/pumpkin/src/client/authentication.rs
2024-09-06 11:20:53 +02:00

112 lines
3.3 KiB
Rust

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<String, Texture>,
}
#[derive(Deserialize, Clone, Debug)]
#[expect(dead_code)]
pub struct Texture {
url: String,
metadata: Option<HashMap<String, String>>,
}
#[derive(Deserialize, Clone, Debug)]
pub struct GameProfile {
pub id: Uuid,
pub name: String,
pub properties: Vec<Property>,
#[serde(rename = "profileActions")]
pub profile_actions: Option<Vec<ProfileAction>>,
}
pub async fn authenticate(
username: &str,
server_hash: &str,
ip: &IpAddr,
server: &mut Server,
) -> Result<GameProfile, AuthError> {
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),
}