Pumpkin/pumpkin/src/net/authentication.rs
Alexander Medvedev 4bccba7b20 fix some typos
- also moved tempfile dep to dev-dependencies
- removed the noise crate
- renamed some things
2025-04-10 21:21:43 +02:00

220 lines
7.0 KiB
Rust

use std::{collections::HashMap, net::IpAddr};
use base64::{Engine, engine::general_purpose};
use pumpkin_config::{advanced_config, networking::auth::TextureConfig};
use pumpkin_protocol::Property;
use reqwest::{StatusCode, Url};
use rsa::RsaPublicKey;
use rsa::pkcs8::DecodePublicKey;
use serde::Deserialize;
use thiserror::Error;
use uuid::Uuid;
use super::GameProfile;
#[derive(Deserialize, Clone, Debug)]
#[expect(dead_code)]
#[serde(rename_all = "camelCase")]
pub struct ProfileTextures {
timestamp: i64,
profile_id: Uuid,
profile_name: String,
signature_required: 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)]
#[serde(rename_all = "camelCase")]
pub struct JsonPublicKey {
pub public_key: String,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MojangPublicKeys {
pub profile_property_keys: Vec<JsonPublicKey>,
pub player_certificate_keys: Vec<JsonPublicKey>,
pub authentication_keys: Option<Vec<JsonPublicKey>>,
}
const MOJANG_AUTHENTICATION_URL: &str = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}";
const MOJANG_PREVENT_PROXY_AUTHENTICATION_URL: &str = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}";
const MOJANG_SERVICES_URL: &str = "https://api.minecraftservices.com/";
/// Sends a GET request to Mojang's authentication servers to verify a client's Minecraft account.
///
/// **Purpose:**
///
/// This function is used to ensure that a client connecting to the server has a valid, premium Minecraft account. It's a crucial step in preventing unauthorized access and maintaining server security.
///
/// **How it Works:**
///
/// 1. A client with a premium account sends a login request to the Mojang session server.
/// 2. Mojang's servers verify the client's credentials and add the player to the their Servers
/// 3. Now our server will send a Request to the Session servers and check if the Player has joined the Session Server .
///
/// See <https://pumpkinmc.org/developer/authentication.html>
pub async fn authenticate(
username: &str,
server_hash: &str,
ip: &IpAddr,
auth_client: &reqwest::Client,
) -> Result<GameProfile, AuthError> {
let address = if advanced_config()
.networking
.authentication
.prevent_proxy_connections
{
let auth_url = advanced_config()
.networking
.authentication
.prevent_proxy_connection_auth_url
.as_deref()
.unwrap_or(MOJANG_PREVENT_PROXY_AUTHENTICATION_URL);
auth_url
.replace("{username}", username)
.replace("{server_hash}", server_hash)
.replace("{ip}", &ip.to_string())
} else {
let auth_url = advanced_config()
.networking
.authentication
.url
.as_deref()
.unwrap_or(MOJANG_AUTHENTICATION_URL);
auth_url
.replace("{username}", username)
.replace("{server_hash}", server_hash)
};
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))?,
}
let profile: GameProfile = response.json().await.map_err(|_| AuthError::FailedParse)?;
Ok(profile)
}
pub fn validate_textures(property: &Property, config: &TextureConfig) -> Result<(), TextureError> {
let from64 = general_purpose::STANDARD
.decode(&property.value)
.map_err(|e| TextureError::DecodeError(e.to_string()))?;
let textures: ProfileTextures =
serde_json::from_slice(&from64).map_err(|e| TextureError::JSONError(e.to_string()))?;
for texture in textures.textures {
let url =
Url::parse(&texture.1.url).map_err(|e| TextureError::InvalidURL(e.to_string()))?;
is_texture_url_valid(&url, config)?;
}
Ok(())
}
pub fn is_texture_url_valid(url: &Url, config: &TextureConfig) -> Result<(), TextureError> {
let scheme = url.scheme();
if !config
.allowed_url_schemes
.iter()
.any(|allowed_scheme| scheme.ends_with(allowed_scheme))
{
return Err(TextureError::DisallowedUrlScheme(scheme.to_string()));
}
let domain = url.domain().unwrap_or("");
if !config
.allowed_url_domains
.iter()
.any(|allowed_domain| domain.ends_with(allowed_domain))
{
return Err(TextureError::DisallowedUrlDomain(domain.to_string()));
}
Ok(())
}
pub async fn fetch_mojang_public_keys(
auth_client: &reqwest::Client,
) -> Result<Vec<RsaPublicKey>, AuthError> {
let services_url = advanced_config()
.networking
.authentication
.services_url
.as_deref()
.unwrap_or(MOJANG_SERVICES_URL);
let url = format!("{services_url}/publickeys");
let response = auth_client
.get(url)
.send()
.await
.map_err(|_| AuthError::FailedResponse)?;
match response.status() {
StatusCode::OK => {}
StatusCode::NO_CONTENT => Err(AuthError::FailedResponse)?,
other => Err(AuthError::UnknownStatusCode(other))?,
}
let public_keys: MojangPublicKeys =
response.json().await.map_err(|_| AuthError::FailedParse)?;
let as_rsa_keys = public_keys
.player_certificate_keys
.into_iter()
.map(|key| {
let decoded_key = general_purpose::STANDARD
.decode(key.public_key.as_bytes())
.map_err(|_| AuthError::FailedParse)?;
RsaPublicKey::from_public_key_der(&decoded_key).map_err(|_| AuthError::FailedParse)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(as_rsa_keys)
}
#[derive(Error, Debug)]
pub enum AuthError {
#[error("Missing auth client")]
MissingAuthClient,
#[error("Authentication servers are down")]
FailedResponse,
#[error("Failed to verify username")]
UnverifiedUsername,
#[error("You are banned from Authentication servers")]
Banned,
#[error("Texture Error {0}")]
TextureError(TextureError),
#[error("You have disallowed actions from Authentication servers")]
DisallowedAction,
#[error("Failed to parse JSON into Game Profile")]
FailedParse,
#[error("Unknown Status Code {0}")]
UnknownStatusCode(StatusCode),
}
#[derive(Error, Debug)]
pub enum TextureError {
#[error("Invalid URL {0}")]
InvalidURL(String),
#[error("Invalid URL scheme for player texture: {0}")]
DisallowedUrlScheme(String),
#[error("Invalid URL domain for player texture: {0}")]
DisallowedUrlDomain(String),
#[error("Failed to decode base64 player texture: {0}")]
DecodeError(String),
#[error("Failed to parse JSON from player texture: {0}")]
JSONError(String),
}