Pumpkin/crates/pumpkin/src/net/mod.rs
2026-08-15 18:45:02 +02:00

667 lines
20 KiB
Rust

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<Vec<Property>>,
#[serde(rename = "profileActions")]
pub profile_actions: Option<Vec<ProfileAction>>,
}
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<ArcSwap<Vec<Property>>, D::Error>
where
D: Deserializer<'de>,
{
let v = Vec::<Property>::deserialize(deserializer)?;
Ok(ArcSwap::new(Arc::new(v)))
}
pub fn offline_uuid(username: &str) -> Result<Uuid, uuid::Error> {
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<BedrockClient>),
}
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<BedrockClient>> {
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<F>(&self, task: F) -> Option<JoinHandle<F::Output>>
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<J: ClientPacket, B: BClientPacket>(
&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<J: ClientPacket, B: BClientPacket>(
&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<P: ClientPacket>(&self, packet: &P) {
if let Self::Java(java) = self {
java.enqueue_packet(packet).await;
}
}
pub async fn enqueue_be_packet<P: BClientPacket>(&self, packet: &P) {
if let Self::Bedrock(bedrock) = self {
bedrock.enqueue_packet(packet).await;
}
}
pub fn try_enqueue_packet<P: ClientPacket>(&self, packet: &P) {
if let Self::Java(java) = self {
java.try_enqueue_packet(packet);
}
}
pub fn try_enqueue_be_packet<P: BClientPacket>(&self, packet: &P) {
if let Self::Bedrock(bedrock) = self {
bedrock.try_enqueue_packet(packet);
}
}
pub fn try_enqueue_spawn_packet(&self, entity: &Arc<dyn crate::entity::EntityBase>) {
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<dyn crate::entity::EntityBase>) {
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<P: ClientPacket>(&self, packet: &P) {
if let Self::Java(java) = self {
java.send_packet_now(packet).await;
}
}
pub async fn send_be_packet_now<P: BClientPacket>(&self, packet: &P) {
if let Self::Bedrock(bedrock) = self {
bedrock.send_game_packet(packet).await;
}
}
pub async fn send_packet_now_editioned<J: ClientPacket, B: BClientPacket>(
&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<TextComponent> {
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"
);
}
}