use crate::{ entity::player::ChatMode, net::{bedrock::BedrockClient, java::JavaClient}, server::Server, }; use arc_swap::ArcSwap; use bytes::Bytes; use pumpkin_world::level::SyncChunk; use std::{ net::SocketAddr, num::NonZeroU8, sync::{Arc, atomic::Ordering}, }; use pumpkin_data::translation; use pumpkin_protocol::{BClientPacket, ClientPacket, Property}; use pumpkin_util::{ Hand, ProfileAction, text::TextComponent, version::{BedrockMinecraftVersion, JavaMinecraftVersion}, }; use serde::{Deserialize, Deserializer}; use sha1::Digest; use sha2::Sha256; use tokio::task::JoinHandle; use thiserror::Error; use uuid::Uuid; pub mod authentication; pub mod bedrock; pub mod java; pub mod lan_broadcast; mod proxy; pub mod query; pub mod rcon; #[derive(Deserialize, Debug)] pub struct GameProfile { pub id: Uuid, pub name: String, #[serde(deserialize_with = "from_vec")] pub properties: ArcSwap>, #[serde(rename = "profileActions")] pub profile_actions: Option>, } impl Clone for GameProfile { fn clone(&self) -> Self { Self { id: self.id, name: self.name.clone(), properties: ArcSwap::new(self.properties.load().clone()), profile_actions: self.profile_actions.clone(), } } } fn from_vec<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, { let v = Vec::::deserialize(deserializer)?; Ok(ArcSwap::new(Arc::new(v))) } pub fn offline_uuid(username: &str) -> Result { Uuid::from_slice(&Sha256::digest(username)[..16]) } /// Represents a player's configuration settings. /// /// This struct contains various options that can be customized by the player, affecting their gameplay experience. /// /// **Usage:** /// /// This struct is typically used to store and manage a player's preferences. It can be sent to the server when a player joins or when they change their settings. #[derive(Clone)] pub struct PlayerConfig { /// The player's preferred language. pub locale: String, // 16 /// The maximum distance at which chunks are rendered. pub view_distance: NonZeroU8, /// The player's chat mode settings pub chat_mode: ChatMode, /// Whether chat colors are enabled. pub chat_colors: bool, /// The player's skin configuration options. pub skin_parts: u8, /// The player's dominant hand (left or right). pub main_hand: Hand, /// Whether text filtering is enabled. pub text_filtering: bool, /// Whether the player wants to appear in the server list. pub server_listing: bool, } impl Default for PlayerConfig { fn default() -> Self { Self { locale: "en_us".to_string(), view_distance: NonZeroU8::new(8).unwrap_or(NonZeroU8::MIN), chat_mode: ChatMode::Enabled, chat_colors: true, skin_parts: 0x7F, main_hand: Hand::Right, text_filtering: false, server_listing: false, } } } pub enum PacketHandlerResult { Stop, ReadyToPlay(GameProfile, PlayerConfig), } /// This is just a Wrapper for both Java & Bedrock connections #[expect(clippy::large_enum_variant)] pub enum ClientPlatform { Java(JavaClient), Bedrock(Arc), } impl ClientPlatform { pub fn address(&self) -> SocketAddr { match self { Self::Java(java) => java.address, Self::Bedrock(bedrock) => bedrock.address, } } /// This function should only be used where you know that the client is bedrock! #[inline] #[must_use] pub const fn bedrock(&self) -> Option<&Arc> { if let Self::Bedrock(client) = self { return Some(client); } None } /// This function should only be used where you know that the client is java! #[inline] #[must_use] pub const fn java(&self) -> Option<&JavaClient> { if let Self::Java(client) = self { return Some(client); } None } #[must_use] pub fn closed(&self) -> bool { match self { Self::Java(java) => java.is_closed(), Self::Bedrock(bedrock) => bedrock.is_closed(), } } pub fn java_version(&self) -> JavaMinecraftVersion { match self { Self::Java(java) => java.version.load(), Self::Bedrock(_) => JavaMinecraftVersion::Unknown, } } pub fn bedrock_version(&self) -> BedrockMinecraftVersion { match self { Self::Java(_) => BedrockMinecraftVersion::Unknown, Self::Bedrock(bedrock) => bedrock.version.load(), } } pub fn try_enqueue_packet_data(&self, packet_data: Bytes) { match self { Self::Java(java) => java.try_enqueue_packet_data(packet_data), Self::Bedrock(bedrock) => bedrock.try_enqueue_packet_data(packet_data), } } pub async fn await_close_interrupt(&self) { match self { Self::Java(java) => java.await_close_interrupt().await, Self::Bedrock(bedrock) => bedrock.await_close_interrupt().await, } } pub fn spawn_task(&self, task: F) -> Option> where F: Future + Send + 'static, F::Output: Send + 'static, { match self { Self::Java(java) => java.spawn_task(task), Self::Bedrock(bedrock) => bedrock.spawn_task(task), } } pub async fn enqueue_packet_editioned( &self, je_packet: &J, be_packet: &B, ) { match self { Self::Java(java) => java.enqueue_packet(je_packet).await, Self::Bedrock(bedrock) => bedrock.enqueue_packet(be_packet).await, } } pub fn try_enqueue_packet_editioned( &self, je_packet: &J, be_packet: &B, ) { match self { Self::Java(java) => java.try_enqueue_packet(je_packet), Self::Bedrock(bedrock) => bedrock.try_enqueue_packet(be_packet), } } pub async fn enqueue_packet(&self, packet: &P) { if let Self::Java(java) = self { java.enqueue_packet(packet).await; } } pub async fn enqueue_be_packet(&self, packet: &P) { if let Self::Bedrock(bedrock) = self { bedrock.enqueue_packet(packet).await; } } pub fn try_enqueue_packet(&self, packet: &P) { if let Self::Java(java) = self { java.try_enqueue_packet(packet); } } pub fn try_enqueue_be_packet(&self, packet: &P) { if let Self::Bedrock(bedrock) = self { bedrock.try_enqueue_packet(packet); } } pub fn try_enqueue_spawn_packet(&self, entity: &Arc) { match self { Self::Java(java) => java.try_enqueue_packet(&entity.get_entity().create_spawn_packet()), Self::Bedrock(bedrock) => bedrock.enqueue_spawn_packet(entity.clone()), } } pub async fn enqueue_spawn_packet(&self, entity: &Arc) { match self { Self::Java(java) => entity.send_java_spawn_packet(java).await, Self::Bedrock(bedrock) => entity.send_bedrock_spawn_packet(bedrock).await, } } pub async fn send_chunks(&self, chunks: &[SyncChunk]) { match self { Self::Java(java) => java.send_chunks(chunks).await, Self::Bedrock(bedrock) => bedrock.send_chunks(chunks).await, } } pub async fn send_packet_now(&self, packet: &P) { if let Self::Java(java) = self { java.send_packet_now(packet).await; } } pub async fn send_be_packet_now(&self, packet: &P) { if let Self::Bedrock(bedrock) = self { bedrock.send_game_packet(packet).await; } } pub async fn send_packet_now_editioned( &self, je_packet: &J, be_packet: &B, ) { match self { Self::Java(java) => java.send_packet_now(je_packet).await, Self::Bedrock(bedrock) => bedrock.send_game_packet(be_packet).await, } } pub async fn send_packet_now_data(&self, data: Bytes) { match self { Self::Java(java) => java.send_packet_now_data(data).await, Self::Bedrock(bedrock) => bedrock.enqueue_packet_data(data).await, } } pub async fn kick(&self, reason: DisconnectReason, message: TextComponent) { match self { Self::Java(java) => java.kick(message).await, Self::Bedrock(bedrock) => bedrock.kick(reason, message.get_text()).await, } } } pub async fn can_not_join( profile: &GameProfile, address: &SocketAddr, server: &Server, ) -> Option { const FORMAT_DESCRIPTION: &[time::format_description::FormatItem<'static>] = time::macros::format_description!( "[year]-[month]-[day] at [hour]:[minute]:[second] [offset_hour sign:mandatory]:[offset_minute]" ); let mut banned_players = server.data.banned_player_list.write().await; if let Some(entry) = banned_players.get_entry(profile) { let text = TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_BANNED_REASON, translation::java::MULTIPLAYER_DISCONNECT_BANNED_REASON, [TextComponent::text(entry.reason.clone())], ); return Some(match entry.expires { Some(expires) => text.add_child(TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_BANNED_EXPIRATION, translation::java::MULTIPLAYER_DISCONNECT_BANNED_EXPIRATION, [TextComponent::text( expires.format(FORMAT_DESCRIPTION).unwrap_or_default(), )], )), None => text, }); } drop(banned_players); if server.white_list.load(Ordering::Relaxed) { let ops = server.data.operator_config.read().await; let whitelist = server.data.whitelist_config.read().await; if ops.get_entry(&profile.id).is_none() && !whitelist.is_whitelisted(profile) { return Some(TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_NOT_WHITELISTED, translation::java::MULTIPLAYER_DISCONNECT_NOT_WHITELISTED, &[], )); } } if let Some(entry) = server .data .banned_ip_list .write() .await .get_entry(&address.ip()) { let text = TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_BANNED_IP_REASON, translation::java::MULTIPLAYER_DISCONNECT_BANNED_IP_REASON, [TextComponent::text(entry.reason.clone())], ); return Some(match entry.expires { Some(expires) => text.add_child(TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_BANNED_IP_EXPIRATION, translation::java::MULTIPLAYER_DISCONNECT_BANNED_IP_EXPIRATION, [TextComponent::text( expires.format(FORMAT_DESCRIPTION).unwrap_or_default(), )], )), None => text, }); } None } #[derive(Error, Debug)] pub enum EncryptionError { #[error("failed to decrypt shared secret")] FailedDecrypt, #[error("shared secret has the wrong length")] SharedWrongLength, #[error("encryption is already enabled")] AlreadyEncrypted, } fn is_valid_player_name(name: &str) -> bool { if name.len() > 16 { return false; } !name.chars().any(|c| c.is_control() || c == ' ') } #[derive(Clone, Copy, Debug)] pub enum DisconnectReason { Unknown = 0, CantConnectNoInternet = 1, NoPermissions = 2, UnrecoverableError = 3, ThirdPartyBlocked = 4, ThirdPartyNoInternet = 5, ThirdPartyBadIP = 6, ThirdPartyNoServerOrServerLocked = 7, VersionMismatch = 8, SkinIssue = 9, InviteSessionNotFound = 10, EduLevelSettingsMissing = 11, LocalServerNotFound = 12, LegacyDisconnect = 13, UserLeaveGameAttempted = 14, PlatformLockedSkinsError = 15, RealmsWorldUnassigned = 16, RealmsServerCantConnect = 17, RealmsServerHidden = 18, RealmsServerDisabledBeta = 19, RealmsServerDisabled = 20, CrossPlatformDisabled = 21, CantConnect = 22, SessionNotFound = 23, ClientSettingsIncompatibleWithServer = 24, ServerFull = 25, InvalidPlatformSkin = 26, EditionVersionMismatch = 27, EditionMismatch = 28, LevelNewerThanExeVersion = 29, NoFailOccurred = 30, BannedSkin = 31, Timeout = 32, ServerNotFound = 33, OutdatedServer = 34, OutdatedClient = 35, NoPremiumPlatform = 36, MultiplayerDisabled = 37, NoWiFi = 38, WorldCorruption = 39, NoReason = 40, Disconnected = 41, InvalidPlayer = 42, LoggedInOtherLocation = 43, ServerIdConflict = 44, NotAllowed = 45, NotAuthenticated = 46, InvalidTenant = 47, UnknownPacket = 48, UnexpectedPacket = 49, InvalidCommandRequestPacket = 50, HostSuspended = 51, LoginPacketNoRequest = 52, LoginPacketNoCert = 53, MissingClient = 54, Kicked = 55, KickedForExploit = 56, KickedForIdle = 57, ResourcePackProblem = 58, IncompatiblePack = 59, OutOfStorage = 60, InvalidLevel = 61, DisconnectPacket = 62, BlockMismatch = 63, InvalidHeights = 64, InvalidWidths = 65, ConnectionLost = 66, ZombieConnection = 67, Shutdown = 68, ReasonNotSet = 69, LoadingStateTimeout = 70, ResourcePackLoadingFailed = 71, SearchingForSessionLoadingScreenFailed = 72, NetherNetProtocolVersion = 73, SubsystemStatusError = 74, EmptyAuthFromDiscovery = 75, EmptyUrlFromDiscovery = 76, ExpiredAuthFromDiscovery = 77, UnknownSignalServiceSignInFailure = 78, XBLJoinLobbyFailure = 79, UnspecifiedClientInstanceDisconnection = 80, NetherNetSessionNotFound = 81, NetherNetCreatePeerConnection = 82, NetherNetICE = 83, NetherNetConnectRequest = 84, NetherNetConnectResponse = 85, NetherNetNegotiationTimeout = 86, NetherNetInactivityTimeout = 87, StaleConnectionBeingReplaced = 88, RealmsSessionNotFound = 89, BadPacket = 90, NetherNetFailedToCreateOffer = 91, NetherNetFailedToCreateAnswer = 92, NetherNetFailedToSetLocalDescription = 93, NetherNetFailedToSetRemoteDescription = 94, NetherNetNegotiationTimeoutWaitingForResponse = 95, NetherNetNegotiationTimeoutWaitingForAccept = 96, NetherNetIncomingConnectionIgnored = 97, NetherNetSignalingParsingFailure = 98, NetherNetSignalingUnknownError = 99, NetherNetSignalingUnicastDeliveryFailed = 100, NetherNetSignalingBroadcastDeliveryFailed = 101, NetherNetSignalingGenericDeliveryFailed = 102, EditorMismatchEditorWorld = 103, EditorMismatchVanillaWorld = 104, WorldTransferNotPrimaryClient = 105, RequestServerShutdown = 106, ClientGameSetupCancelled = 107, ClientGameSetupFailed = 108, NoVenue = 109, NetherNetSignalingSigninFailed = 110, SessionAccessDenied = 111, ServiceSigninIssue = 112, NetherNetNoSignalingChannel = 113, NetherNetNotLoggedIn = 114, NetherNetClientSignalingError = 115, SubClientLoginDisabled = 116, DeepLinkTryingToOpenDemoWorldWhileSignedIn = 117, AsyncJoinTaskDenied = 118, RealmsTimelineRequired = 119, GuestWithoutHost = 120, FailedToJoinExperience = 121, NetherNetDataChannelClosed = 122, DiscoveryEnvironmentMismatch = 123, HostWithoutKeys = 124, HostSignedOut = 125, ScriptWatchdogException = 126, ScriptMemoryLimitExceeded = 127, StorageLowDuringGameplay = 128, StorageFullDuringGameplay = 129, LevelStorageCorruption = 130, EditionMismatchVanillaToEdu = 131, EditionMismatchEduToVanilla = 132, EditorMismatchEditorToVanilla = 133, EditorMismatchVanillaToEditor = 134, DenyListed = 135, NonceMissing = 136, NonceNotFound = 137, NonceExpired = 138, NonceNotValid = 139, HostDisconnected = 140, EditorJoinIntentPolicyFailure = 141, NetherNetIdentityNotAllowed = 142, InvalidName = 143, ExpiredToken = 144, HostAcceptsNoTypeOfAuth = 145, NotAuthenticatedFastFail = 146, EditorNotAllowed = 147, } #[cfg(test)] mod tests { use crate::net::is_valid_player_name; /// Test case for a standard, valid English name at max length. #[test] fn valid_max_length_ascii() { let name = "player_name_1234"; // 16 characters (16 bytes) assert!( is_valid_player_name(name), "Max length ASCII name should be valid" ); } /// Test case for a short, valid ASCII name. #[test] fn valid_short_ascii() { let name = "GamerX"; assert!( is_valid_player_name(name), "Short ASCII name should be valid" ); } /// Test case for a name containing allowed punctuation (codepoints 33-126). #[test] fn valid_with_punctuation() { let name = "!-@#$%.^&*_+-="; assert!( is_valid_player_name(name), "Name with valid punctuation should be valid" ); } /// Test case for allowed high-codepoint Unicode characters (like Chinese/CJK). #[test] fn valid_unicode_chinese() { let name = "玩家一号"; // 4 characters, 12 bytes assert!( is_valid_player_name(name), "Chinese characters should be valid" ); } /// Test case for a mix of valid ASCII and Unicode characters. #[test] fn valid_mixed_chars() { let name = "Player_玩家"; // 9 characters assert!( is_valid_player_name(name), "Mixed ASCII and Unicode should be valid" ); } /// Test case for a name that exceeds the 16-byte limit (ASCII). #[test] fn invalid_length_ascii_over() { let name = "this_name_is_too_long"; // 21 characters (21 bytes) assert!( !is_valid_player_name(name), "Name over 16 bytes (ASCII) should be invalid" ); } /// Test case for a name that exceeds the 16-byte limit (Unicode). #[test] fn invalid_length_unicode_over() { let name = "超长玩家名称哈哈"; // 8 Chinese characters * 3 bytes/char = 24 bytes assert!( !is_valid_player_name(name), "Name over 16 bytes (Unicode) should be invalid by byte count" ); } /// Test case for a name containing a standard space (codepoint 32). #[test] fn invalid_contains_space() { let name = "Player Name"; assert!( !is_valid_player_name(name), "Name containing a space should be invalid" ); } /// Test case for an empty string (length 0, but included for completeness). #[test] fn invalid_empty_string() { let name = ""; assert!( is_valid_player_name(name), "Empty string should be valid (length <= 16 and no invalid chars)" ); } /// Test case for a name containing a control character (e.g., Null, codepoint 0). #[test] fn invalid_contains_null() { let name = "Player\0Name"; assert!( !is_valid_player_name(name), "Name containing a null character should be invalid" ); } /// Test case for a name containing a newline character (codepoint 10). #[test] fn invalid_contains_newline() { let name = "Player\nName"; assert!( !is_valid_player_name(name), "Name containing a newline should be invalid" ); } /// Test case for a name containing the DEL control character (codepoint 127). #[test] fn invalid_contains_del() { // DEL character is char::from_u32(127).unwrap() let name = format!("Player{}Name", 127u8 as char); assert!( !is_valid_player_name(&name), "Name containing DEL (127) should be invalid" ); } }