- also moved tempfile dep to dev-dependencies - removed the noise crate - renamed some things
220 lines
7.0 KiB
Rust
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),
|
|
}
|