Improve Authentication

This commit is contained in:
Snowiiii 2024-08-07 13:32:48 +02:00
parent d0a8790d7b
commit ea0128c50b
11 changed files with 362 additions and 32 deletions

View File

@ -14,8 +14,10 @@ Pumpkin is currently under heavy development.
## What Pumpkin aims to be: ## What Pumpkin aims to be:
- A fast, efficient, and scalable Minecraft server - A fast, efficient, and scalable Minecraft server
- Use Multi-Threading
- Compatible with the latest Minecraft server version - Compatible with the latest Minecraft server version
- Adherent to Vanilla game mechanics - Adherent to Vanilla game mechanics
- Be Secure and prevent all Exploits found
- Highly configurable with the ability to disable unnecessary features - Highly configurable with the ability to disable unnecessary features
- A platform for plugin development - A platform for plugin development

View File

@ -33,10 +33,7 @@ impl<'a> ClientPacket for CLoginSuccess<'a> {
bytebuf.put_list::<Property>(self.properties, |p, v| { bytebuf.put_list::<Property>(self.properties, |p, v| {
p.put_string(&v.name); p.put_string(&v.name);
p.put_string(&v.value); p.put_string(&v.value);
// has signature ? p.put_option(&v.signature, |p, v| p.put_string(v));
// todo: for some reason we get "got too many bytes error when using a signature"
p.put_bool(false);
// p.put_option(&v.signature, |p,v| p.put_string(v));
}); });
bytebuf.put_bool(self.strict_error_handling); bytebuf.put_bool(self.strict_error_handling);
} }

View File

@ -34,10 +34,7 @@ impl<'a> ClientPacket for CPlayerInfoUpdate<'a> {
p.put_list::<Property>(properties, |p, v| { p.put_list::<Property>(properties, |p, v| {
p.put_string(&v.name); p.put_string(&v.name);
p.put_string(&v.value); p.put_string(&v.value);
// has signature ? p.put_option(&v.signature, |p, v| p.put_string(v));
// todo: for some reason we get "got too many bytes error when using a signature"
p.put_bool(false);
// todo signature
}); });
} }
PlayerAction::InitializeChat(_) => todo!(), PlayerAction::InitializeChat(_) => todo!(),

View File

@ -1,19 +1,49 @@
use std::net::IpAddr; use std::{collections::HashMap, net::IpAddr};
use base64::{engine::general_purpose, Engine};
use num_bigint::BigInt; use num_bigint::BigInt;
use pumpkin_protocol::Property; use pumpkin_protocol::Property;
use reqwest::StatusCode; use reqwest::{StatusCode, Url};
use serde::Deserialize; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
use crate::server::Server; use crate::{config::auth_config::TextureConfig, server::Server};
#[derive(Deserialize, Clone, Debug)]
#[allow(non_snake_case)]
#[allow(dead_code)]
pub struct ProfileTextures {
timestamp: i64,
profileId: Uuid,
profileName: String,
signatureRequired: bool,
textures: HashMap<String, Texture>,
}
#[derive(Deserialize, Clone, Debug)]
#[allow(non_snake_case)]
#[allow(dead_code)]
pub struct Texture {
url: String,
metadata: Option<HashMap<String, String>>,
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
pub enum ProfileAction {
#[serde(rename = "FORCED_NAME_CHANGE")]
ForcedNameChange,
#[serde(rename = "USING_BANNED_SKIN")]
UsingBannedSkin,
}
#[derive(Deserialize, Clone, Debug)] #[derive(Deserialize, Clone, Debug)]
pub struct GameProfile { pub struct GameProfile {
pub id: Uuid, pub id: Uuid,
pub name: String, pub name: String,
pub properties: Vec<Property>, pub properties: Vec<Property>,
#[serde(rename = "profileActions")]
pub profile_actions: Option<Vec<ProfileAction>>,
} }
pub fn authenticate( pub fn authenticate(
@ -22,8 +52,13 @@ pub fn authenticate(
ip: &IpAddr, ip: &IpAddr,
server: &mut Server, server: &mut Server,
) -> Result<GameProfile, AuthError> { ) -> Result<GameProfile, AuthError> {
assert!(server.advanced_config.authentication.use_authentication);
assert!(server.auth_client.is_some()); assert!(server.auth_client.is_some());
let address = if server.base_config.prevent_proxy_connections { let address = if server
.advanced_config
.authentication
.prevent_proxy_connections
{
format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}") format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}")
} else { } else {
format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}") format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}")
@ -44,10 +79,32 @@ pub fn authenticate(
Ok(profile) 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();
dbg!(&textures);
for texture in textures.textures {
is_texture_url_valid(Url::parse(&texture.1.url).unwrap(), config);
}
}
pub fn auth_digest(bytes: &[u8]) -> String { pub fn auth_digest(bytes: &[u8]) -> String {
BigInt::from_signed_bytes_be(bytes).to_str_radix(16) 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)] #[derive(Error, Debug)]
pub enum AuthError { pub enum AuthError {
#[error("Authentication servers are down")] #[error("Authentication servers are down")]

View File

@ -23,7 +23,10 @@ use crate::{
server::Server, server::Server,
}; };
use super::{authentication::auth_digest, Client, EncryptionError, PlayerConfig}; use super::{
authentication::{auth_digest, unpack_textures},
Client, EncryptionError, PlayerConfig,
};
/// Processes incoming Packets from the Client to the Server /// Processes incoming Packets from the Client to the Server
/// Implements the `Client` Packets /// Implements the `Client` Packets
@ -45,12 +48,15 @@ impl Client {
} }
pub fn handle_login_start(&mut self, server: &mut Server, login_start: SLoginStart) { pub fn handle_login_start(&mut self, server: &mut Server, login_start: SLoginStart) {
// todo: do basic name validation
dbg!("login start"); dbg!("login start");
// default game profile, when no online mode // default game profile, when no online mode
// todo: make offline uuid
self.gameprofile = Some(GameProfile { self.gameprofile = Some(GameProfile {
id: login_start.uuid, id: login_start.uuid,
name: login_start.name, name: login_start.name,
properties: vec![], properties: vec![],
profile_actions: None,
}); });
// todo: check config for encryption // todo: check config for encryption
@ -91,10 +97,40 @@ impl Client {
&ip, &ip,
server, server,
) { ) {
Ok(p) => self.gameprofile = Some(p), Ok(p) => {
// Check if player should join
if let Some(p) = &p.profile_actions {
if !server
.advanced_config
.authentication
.player_profile
.allow_banned_players
{
if !p.is_empty() {
self.kick("Your account can't join");
}
} else {
for allowed in server
.advanced_config
.authentication
.player_profile
.allowed_actions
.clone()
{
if !p.contains(&allowed) {
self.kick("Your account can't join");
}
}
}
}
self.gameprofile = Some(p);
}
Err(e) => self.kick(&e.to_string()), Err(e) => self.kick(&e.to_string()),
} }
} }
for ele in self.gameprofile.as_ref().unwrap().properties.clone() {
unpack_textures(ele, &server.advanced_config.authentication.textures);
}
if let Some(profile) = self.gameprofile.as_ref().cloned() { if let Some(profile) = self.gameprofile.as_ref().cloned() {
let packet = CLoginSuccess::new(profile.id, profile.name, &profile.properties, false); let packet = CLoginSuccess::new(profile.id, profile.name, &profile.properties, false);

View File

@ -38,7 +38,7 @@ use pumpkin_protocol::{
use std::io::Read; use std::io::Read;
use thiserror::Error; use thiserror::Error;
mod authentication; pub mod authentication;
mod client_packet; mod client_packet;
pub mod player_packet; pub mod player_packet;

View File

@ -0,0 +1,93 @@
use serde::{Deserialize, Serialize};
use crate::client::authentication::ProfileAction;
#[derive(Deserialize, Serialize)]
pub struct Authentication {
/// Whether to use Mojang authentication.
pub use_authentication: bool,
/// Prevent proxy connections.
pub prevent_proxy_connections: bool,
/// Player profile handling.
pub player_profile: PlayerProfileConfig,
/// Texture handling.
pub textures: TextureConfig,
}
#[derive(Deserialize, Serialize)]
pub struct PlayerProfileConfig {
/// Allow players flagged by Mojang (banned, forced name change).
pub allow_banned_players: bool,
/// Depends on the value above
pub allowed_actions: Vec<ProfileAction>,
}
impl Default for PlayerProfileConfig {
fn default() -> Self {
Self {
allow_banned_players: false,
allowed_actions: vec![
ProfileAction::ForcedNameChange,
ProfileAction::UsingBannedSkin,
],
}
}
}
#[derive(Deserialize, Serialize)]
pub struct TextureConfig {
/// Whether to use player textures.
pub enabled: bool,
pub allowed_url_schemes: Vec<String>,
pub allowed_url_domains: Vec<String>,
/// Specific texture types.
pub types: TextureTypes,
}
impl Default for TextureConfig {
fn default() -> Self {
Self {
enabled: true,
allowed_url_schemes: vec!["http".into(), "https".into()],
allowed_url_domains: vec![".minecraft.net".into(), ".mojang.com".into()],
types: Default::default(),
}
}
}
#[derive(Deserialize, Serialize)]
pub struct TextureTypes {
/// Use player skins.
pub skin: bool,
/// Use player capes.
pub cape: bool,
/// Use player elytras.
/// (i didn't know myself that there are custom elytras)
pub elytra: bool,
}
impl Default for TextureTypes {
fn default() -> Self {
Self {
skin: true,
cape: true,
elytra: true,
}
}
}
impl Default for Authentication {
fn default() -> Self {
Self {
use_authentication: true,
prevent_proxy_connections: true,
player_profile: Default::default(),
textures: Default::default(),
}
}
}

View File

@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
use crate::{entity::player::GameMode, server::Difficulty}; use crate::{entity::player::GameMode, server::Difficulty};
use super::auth_config::Authentication;
/// Current Config version of the Base Config /// Current Config version of the Base Config
const CURRENT_BASE_VERSION: &str = "1.0.0"; const CURRENT_BASE_VERSION: &str = "1.0.0";
@ -12,19 +14,15 @@ const CURRENT_BASE_VERSION: &str = "1.0.0";
/// This also allows you get some Performance or Resource boosts. /// This also allows you get some Performance or Resource boosts.
/// Important: The Configuration should match Vanilla by default /// Important: The Configuration should match Vanilla by default
pub struct AdvancedConfiguration { pub struct AdvancedConfiguration {
/// Requires Online mode
/// Should player have skins
pub use_skins: bool,
/// Should chat be enabled
pub enable_chat: bool,
pub commands: Commands, pub commands: Commands,
pub authentication: Authentication,
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct Commands { pub struct Commands {
// Are commands from the Console accepted ? /// Are commands from the Console accepted ?
pub use_console: bool, pub use_console: bool,
// todo commands...
} }
impl Default for Commands { impl Default for Commands {
@ -37,8 +35,7 @@ impl Default for Commands {
impl Default for AdvancedConfiguration { impl Default for AdvancedConfiguration {
fn default() -> Self { fn default() -> Self {
Self { Self {
use_skins: true, authentication: Authentication::default(),
enable_chat: true,
commands: Commands::default(), commands: Commands::default(),
} }
} }
@ -74,8 +71,6 @@ pub struct BasicConfiguration {
pub online_mode: bool, pub online_mode: bool,
/// Whether packet encryption is enabled. Required when online mode is enabled. /// Whether packet encryption is enabled. Required when online mode is enabled.
pub encryption: bool, pub encryption: bool,
/// Whether to prevent proxy connections.
pub prevent_proxy_connections: bool,
/// The server's description displayed on the status screen. /// The server's description displayed on the status screen.
pub motd: String, pub motd: String,
/// The default game mode for players. /// The default game mode for players.
@ -99,7 +94,6 @@ impl Default for BasicConfiguration {
hardcore: false, hardcore: false,
online_mode: true, online_mode: true,
encryption: true, encryption: true,
prevent_proxy_connections: true,
motd: "A Blazing fast Pumpkin Server!".to_string(), motd: "A Blazing fast Pumpkin Server!".to_string(),
default_gamemode: GameMode::Survival, default_gamemode: GameMode::Survival,
} }

154
pumpkin/src/config/mod.rs Normal file
View File

@ -0,0 +1,154 @@
use std::path::Path;
use auth_config::Authentication;
use serde::{Deserialize, Serialize};
use crate::{entity::player::GameMode, server::Difficulty};
pub mod auth_config;
/// Current Config version of the Base Config
const CURRENT_BASE_VERSION: &str = "1.0.0";
#[derive(Deserialize, Serialize)]
/// The idea is that Pumpkin should very customizable, You can Enable or Disable Features depning on your needs.
/// This also allows you get some Performance or Resource boosts.
/// Important: The Configuration should match Vanilla by default
pub struct AdvancedConfiguration {
pub commands: Commands,
pub authentication: Authentication,
}
#[derive(Deserialize, Serialize)]
pub struct Commands {
/// Are commands from the Console accepted ?
pub use_console: bool,
// todo commands...
}
impl Default for Commands {
fn default() -> Self {
Self { use_console: true }
}
}
/// Important: The Configuration should match Vanilla by default
impl Default for AdvancedConfiguration {
fn default() -> Self {
Self {
authentication: Authentication::default(),
commands: Commands::default(),
}
}
}
#[derive(Serialize, Deserialize)]
pub struct BasicConfiguration {
/// A version identifier for the configuration format.
pub config_version: String,
/// The address to bind the server to.
pub server_address: String,
/// The port to listen on.
pub server_port: u16,
/// The seed for world generation.
pub seed: String,
/// The maximum number of players allowed on the server.
pub max_players: u32,
/// The maximum view distance for players.
pub view_distance: u8,
/// The maximum simulated view distance.
pub simulation_distance: u8,
/// The path to the resource pack.
pub resource_pack: String,
/// The SHA1 hash of the resource pack.
pub resource_pack_sha1: String,
/// The default game difficulty.
pub default_difficulty: Difficulty,
/// Whether the Nether dimension is enabled.
pub allow_nether: bool,
/// Whether the server is in hardcore mode.
pub hardcore: bool,
/// Whether online mode is enabled. Requires valid Minecraft accounts.
pub online_mode: bool,
/// Whether packet encryption is enabled. Required when online mode is enabled.
pub encryption: bool,
/// The server's description displayed on the status screen.
pub motd: String,
/// The default game mode for players.
pub default_gamemode: GameMode,
}
impl Default for BasicConfiguration {
fn default() -> Self {
Self {
config_version: CURRENT_BASE_VERSION.to_string(),
server_address: "127.0.0.1".to_string(),
server_port: 25565,
seed: "".to_string(),
max_players: 100000,
view_distance: 10,
simulation_distance: 10,
resource_pack: "".to_string(),
resource_pack_sha1: "".to_string(),
default_difficulty: Difficulty::Normal,
allow_nether: true,
hardcore: false,
online_mode: true,
encryption: true,
motd: "A Blazing fast Pumpkin Server!".to_string(),
default_gamemode: GameMode::Survival,
}
}
}
impl AdvancedConfiguration {
pub fn load<P: AsRef<Path>>(path: P) -> AdvancedConfiguration {
if path.as_ref().exists() {
let toml = std::fs::read_to_string(path).expect("Couldn't read configuration");
toml::from_str(toml.as_str()).expect("Couldn't parse, Proberbly old config")
} else {
let config = AdvancedConfiguration::default();
let toml = toml::to_string(&config).expect("Couldn't create toml!");
std::fs::write(path, toml).expect("Couldn't save configuration");
config
}
}
}
impl BasicConfiguration {
pub fn load<P: AsRef<Path>>(path: P) -> BasicConfiguration {
if path.as_ref().exists() {
let toml = std::fs::read_to_string(path).expect("Couldn't read configuration");
toml::from_str(toml.as_str()).expect("Couldn't parse")
} else {
let config = BasicConfiguration::default();
let toml = toml::to_string(&config).expect("Couldn't create toml!");
std::fs::write(path, toml).expect("Couldn't save configuration");
config.validate();
config
}
}
pub fn validate(&self) {
assert_eq!(
self.config_version, CURRENT_BASE_VERSION,
"Config version does not match used Config version. Please update your config"
);
assert!(self.view_distance >= 2, "View distance must be atleast 2");
assert!(
self.view_distance <= 32,
"View distance must be less than 32"
);
if self.online_mode {
assert!(
self.encryption,
"When Online Mode is enabled, Encryption must be enabled"
)
}
assert_eq!(
!self.resource_pack.is_empty(),
!self.resource_pack_sha1.is_empty(),
"Resource Pack path or Sha1 hash is missing"
);
}
}

View File

@ -4,12 +4,12 @@ use std::io::{self};
use client::Client; use client::Client;
use commands::handle_command; use commands::handle_command;
use configuration::AdvancedConfiguration; use config::AdvancedConfiguration;
use std::{collections::HashMap, rc::Rc, thread}; use std::{collections::HashMap, rc::Rc, thread};
use client::interrupted; use client::interrupted;
use configuration::BasicConfiguration; use config::BasicConfiguration;
use server::Server; use server::Server;
// Setup some tokens to allow us to identify which event is for which socket. // Setup some tokens to allow us to identify which event is for which socket.
@ -17,7 +17,7 @@ const SERVER: Token = Token(0);
pub mod client; pub mod client;
pub mod commands; pub mod commands;
pub mod configuration; pub mod config;
pub mod entity; pub mod entity;
pub mod server; pub mod server;
pub mod util; pub mod util;

View File

@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
client::Client, client::Client,
configuration::{AdvancedConfiguration, BasicConfiguration}, config::{AdvancedConfiguration, BasicConfiguration},
entity::player::{GameMode, Player}, entity::player::{GameMode, Player},
}; };